on master help sign in
Merge branch 'master' into patch-1
ecb054aMarkus Heiser
.dir-locals.elnew file+133
@@ -0,0 +1,133 @@
1+;;; .dir-locals.el
2+;;
3+;; If you get ``*** EPC Error ***`` (even after a jedi:install-server) in your
4+;; emacs session, mostly you have jedi-mode enabled but the python enviroment is
5+;; missed. The python environment has to be next to the
6+;; ``<repo>/.dir-locals.el`` in::
7+;;
8+;; ./local/py3
9+;;
10+;; In Emacs, some buffer locals are referencing the project environment:
11+;;
12+;; - prj-root --> <repo>/
13+;; - python-environment-directory --> <repo>/local
14+;; - python-environment-default-root-name --> py3
15+;; - python-shell-virtualenv-root --> <repo>/local/py3
16+;; When this variable is set with the path of the virtualenv to use,
17+;; `process-environment' and `exec-path' get proper values in order to run
18+;; shells inside the specified virtualenv, example::
19+;; (setq python-shell-virtualenv-root "/path/to/env/")
20+;;
21+;; To setup such an environment build target 'pyenv' or 'pyenvinstall'::
22+;;
23+;; $ make pyenvinstall
24+;;
25+;; Alternatively create the virtualenv, source it and install jedi + epc
26+;; (required by `emacs-jedi <https://tkf.github.io/emacs-jedi>`_)::
27+;;
28+;; $ virtualenv --python=python3 "--no-site-packages" ./local/py3
29+;; ...
30+;; $ source ./local/py3/bin/activate
31+;; (py3)$ # now install into the activated 'py3' environment ..
32+;; (py3)$ pip install jedi epc
33+;; ...
34+;;
35+;; Here is what also I found useful to add to my .emacs::
36+;;
37+;; (global-set-key [f6] 'flycheck-mode)
38+;; (add-hook 'python-mode-hook 'my:python-mode-hook)
39+;;
40+;; (defun my:python-mode-hook ()
41+;; (add-to-list 'company-backends 'company-jedi)
42+;; (require 'jedi-core)
43+;; (jedi:setup)
44+;; (define-key python-mode-map (kbd "C-c C-d") 'jedi:show-doc)
45+;; (define-key python-mode-map (kbd "M-.") 'jedi:goto-definition)
46+;; (define-key python-mode-map (kbd "M-,") 'jedi:goto-definition-pop-marker)
47+;; )
48+;;
49+
50+((nil
51+ . ((fill-column . 80)
52+ ))
53+ (python-mode
54+ . ((indent-tabs-mode . nil)
55+
56+ ;; project root folder is where the `.dir-locals.el' is located
57+ (eval . (setq-local
58+ prj-root (locate-dominating-file default-directory ".dir-locals.el")))
59+
60+ (eval . (setq-local
61+ python-environment-directory (expand-file-name "./local" prj-root)))
62+
63+ ;; use 'py3' enviroment as default
64+ (eval . (setq-local
65+ python-environment-default-root-name "py3"))
66+
67+ (eval . (setq-local
68+ python-shell-virtualenv-root
69+ (concat python-environment-directory
70+ "/"
71+ python-environment-default-root-name)))
72+
73+ ;; python-shell-virtualenv-path is obsolete, use python-shell-virtualenv-root!
74+ ;; (eval . (setq-local
75+ ;; python-shell-virtualenv-path python-shell-virtualenv-root))
76+
77+ (eval . (setq-local
78+ python-shell-interpreter
79+ (expand-file-name "bin/python" python-shell-virtualenv-root)))
80+
81+ (eval . (setq-local
82+ python-environment-virtualenv
83+ (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
84+ ;;"--system-site-packages"
85+ "--quiet")))
86+
87+ (eval . (setq-local
88+ pylint-command
89+ (expand-file-name "bin/pylint" python-shell-virtualenv-root)))
90+
91+ ;; pylint will find the '.pylintrc' file next to the CWD
92+ ;; https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options
93+ (eval . (setq-local
94+ flycheck-pylintrc ".pylintrc"))
95+
96+ ;; flycheck & other python stuff should use the local py3 environment
97+ (eval . (setq-local
98+ flycheck-python-pylint-executable python-shell-interpreter))
99+
100+ ;; use 'M-x jedi:show-setup-info' and 'M-x epc:controller' to inspect jedi server
101+
102+ ;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-root -- You
103+ ;; can specify a full path instead of a name (relative path). In that case,
104+ ;; python-environment-directory is ignored and Python virtual environment
105+ ;; is created at the specified path.
106+ (eval . (setq-local jedi:environment-root python-shell-virtualenv-root))
107+
108+ ;; https://tkf.github.io/emacs-jedi/latest/#jedi:server-command
109+ (eval .(setq-local
110+ jedi:server-command
111+ (list python-shell-interpreter
112+ jedi:server-script)
113+ ))
114+
115+ ;; jedi:environment-virtualenv --> see above 'python-environment-virtualenv'
116+ ;; is set buffer local! No need to setup jedi:environment-virtualenv:
117+ ;;
118+ ;; Virtualenv command to use. A list of string. If it is nil,
119+ ;; python-environment-virtualenv is used instead. You must set non-nil
120+ ;; value to jedi:environment-root in order to make this setting work.
121+ ;;
122+ ;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-virtualenv
123+ ;;
124+ ;; (eval . (setq-local
125+ ;; jedi:environment-virtualenv
126+ ;; (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
127+ ;; ;;"--python"
128+ ;; ;;"/usr/bin/python3.4"
129+ ;; )))
130+
131+ ;; jedi:server-args
132+
133+ )))
.gitignore+6
@@ -18,3 +18,9 @@ setup.cfg
18node_modules/
18node_modules/
19
19
20.tx/
20.tx/
21+
22+build/
23+dist/
24+local/
25+gh-pages/
26+searx.egg-info/
.pylintrcnew file+444
@@ -0,0 +1,444 @@
1+# -*- coding: utf-8; mode: conf -*-
2+# lint Python modules using external checkers.
3+#
4+# This is the main checker controlling the other ones and the reports
5+# generation. It is itself both a raw checker and an astng checker in order
6+# to:
7+# * handle message activation / deactivation at the module level
8+# * handle some basic but necessary stats'data (number of classes, methods...)
9+#
10+[MASTER]
11+
12+# A comma-separated list of package or module names from where C extensions may
13+# be loaded. Extensions are loading into the active Python interpreter and may
14+# run arbitrary code
15+extension-pkg-whitelist=
16+
17+# Add files or directories to the blacklist. They should be base names, not
18+# paths.
19+ignore=CVS, .git, .svn
20+
21+# Add files or directories matching the regex patterns to the blacklist. The
22+# regex matches against base names, not paths.
23+ignore-patterns=
24+
25+# Python code to execute, usually for sys.path manipulation such as
26+# pygtk.require().
27+#init-hook=
28+
29+# Use multiple processes to speed up Pylint.
30+jobs=1
31+
32+# List of plugins (as comma separated values of python modules names) to load,
33+# usually to register additional checkers.
34+load-plugins=
35+
36+# Pickle collected data for later comparisons.
37+persistent=yes
38+
39+# Specify a configuration file.
40+#rcfile=
41+
42+# Allow loading of arbitrary C extensions. Extensions are imported into the
43+# active Python interpreter and may run arbitrary code.
44+unsafe-load-any-extension=no
45+
46+
47+[MESSAGES CONTROL]
48+
49+# Only show warnings with the listed confidence levels. Leave empty to show
50+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
51+confidence=
52+
53+# Disable the message, report, category or checker with the given id(s). You
54+# can either give multiple identifiers separated by comma (,) or put this
55+# option multiple times (only on the command line, not in the configuration
56+# file where it should appear only once).You can also use "--disable=all" to
57+# disable everything first and then reenable specific checks. For example, if
58+# you want to run only the similarities checker, you can use "--disable=all
59+# --enable=similarities". If you want to run only the classes checker, but have
60+# no Warning level messages displayed, use"--disable=all --enable=classes
61+# --disable=W"
62+disable=bad-whitespace, duplicate-code
63+
64+# Enable the message, report, category or checker with the given id(s). You can
65+# either give multiple identifier separated by comma (,) or put this option
66+# multiple time (only on the command line, not in the configuration file where
67+# it should appear only once). See also the "--disable" option for examples.
68+enable=
69+
70+
71+[REPORTS]
72+
73+# Python expression which should return a note less than 10 (10 is the highest
74+# note). You have access to the variables errors warning, statement which
75+# respectively contain the number of errors / warnings messages and the total
76+# number of statements analyzed. This is used by the global evaluation report
77+# (RP0004).
78+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
79+
80+# Template used to display messages. This is a python new-style format string
81+# used to format the message information. See doc for all details
82+
83+# HINT: do not set this here, use argument --msg-template=...
84+#msg-template={path}:{line}: [{msg_id}({symbol}),{obj}] {msg}
85+
86+# Set the output format. Available formats are text, parseable, colorized, json
87+# and msvs (visual studio).You can also give a reporter class, eg
88+# mypackage.mymodule.MyReporterClass.
89+
90+# HINT: do not set this here, use argument --output-format=...
91+#output-format=text
92+
93+# Tells whether to display a full report or only the messages
94+reports=no
95+
96+# Activate the evaluation score.
97+score=yes
98+
99+
100+[REFACTORING]
101+
102+# Maximum number of nested blocks for function / method body
103+max-nested-blocks=5
104+
105+
106+[BASIC]
107+
108+# List of builtins function names that should not be used, separated by a comma
109+bad-functions=map,filter,apply,input
110+
111+# Naming hint for argument names
112+argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
113+
114+# Regular expression matching correct argument names
115+argument-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*))$
116+
117+# Naming hint for attribute names
118+attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
119+
120+# Regular expression matching correct attribute names
121+attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*)|([A-Z0-9_]*))$
122+
123+# Bad variable names which should always be refused, separated by a comma
124+bad-names=foo,bar,baz,toto,tutu,tata
125+
126+# Naming hint for class attribute names
127+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
128+
129+# Regular expression matching correct class attribute names
130+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
131+
132+# Naming hint for class names
133+class-name-hint=[A-Z_][a-zA-Z0-9]+$
134+
135+# Regular expression matching correct class names
136+class-rgx=[A-Z_][a-zA-Z0-9]+$
137+
138+# Naming hint for constant names
139+const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
140+
141+# Regular expression matching correct constant names
142+const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$
143+#const-rgx=[f]?[A-Z_][a-zA-Z0-9_]{2,30}$
144+
145+# Minimum line length for functions/classes that require docstrings, shorter
146+# ones are exempt.
147+docstring-min-length=-1
148+
149+# Naming hint for function names
150+function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
151+
152+# Regular expression matching correct function names
153+function-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*))$
154+
155+# Good variable names which should always be accepted, separated by a comma
156+good-names=i,j,k,ex,Run,_,log,cfg,id
157+
158+# Include a hint for the correct naming format with invalid-name
159+include-naming-hint=no
160+
161+# Naming hint for inline iteration names
162+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
163+
164+# Regular expression matching correct inline iteration names
165+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
166+
167+# Naming hint for method names
168+method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
169+
170+# Regular expression matching correct method names
171+method-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*))$
172+
173+# Naming hint for module names
174+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
175+
176+# Regular expression matching correct module names
177+#module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
178+module-rgx=([a-z_][a-z0-9_]*)$
179+
180+# Colon-delimited sets of names that determine each other's naming style when
181+# the name regexes allow several styles.
182+name-group=
183+
184+# Regular expression which should only match function or class names that do
185+# not require a docstring.
186+no-docstring-rgx=^_
187+
188+# List of decorators that produce properties, such as abc.abstractproperty. Add
189+# to this list to register other decorators that produce valid properties.
190+property-classes=abc.abstractproperty
191+
192+# Naming hint for variable names
193+variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
194+
195+# Regular expression matching correct variable names
196+variable-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*)|([a-z]))$
197+
198+
199+[FORMAT]
200+
201+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
202+expected-line-ending-format=
203+
204+# Regexp for a line that is allowed to be longer than the limit.
205+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
206+
207+# Number of spaces of indent required inside a hanging or continued line.
208+indent-after-paren=4
209+
210+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
211+# tab).
212+indent-string=' '
213+
214+# Maximum number of characters on a single line.
215+max-line-length=120
216+
217+# Maximum number of lines in a module
218+max-module-lines=2000
219+
220+# List of optional constructs for which whitespace checking is disabled. `dict-
221+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
222+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
223+# `empty-line` allows space-only lines.
224+no-space-check=trailing-comma,dict-separator
225+
226+# Allow the body of a class to be on the same line as the declaration if body
227+# contains single statement.No config file found, using default configuration
228+
229+single-line-class-stmt=no
230+
231+# Allow the body of an if to be on the same line as the test if there is no
232+# else.
233+single-line-if-stmt=no
234+
235+
236+[LOGGING]
237+
238+# Logging modules to check that the string format arguments are in logging
239+# function parameter format
240+logging-modules=logging
241+
242+
243+[MISCELLANEOUS]
244+
245+# List of note tags to take in consideration, separated by a comma.
246+notes=FIXME,XXX,TODO
247+
248+
249+[SIMILARITIES]
250+
251+# Ignore comments when computing similarities.
252+ignore-comments=yes
253+
254+# Ignore docstrings when computing similarities.
255+ignore-docstrings=yes
256+
257+# Ignore imports when computing similarities.
258+ignore-imports=no
259+
260+# Minimum lines number of a similarity.
261+min-similarity-lines=4
262+
263+
264+[SPELLING]
265+
266+# Spelling dictionary name. Available dictionaries: none. To make it working
267+# install python-enchant package.
268+spelling-dict=
269+
270+# List of comma separated words that should not be checked.
271+spelling-ignore-words=
272+
273+# A path to a file that contains private dictionary; one word per line.
274+spelling-private-dict-file=
275+
276+# Tells whether to store unknown words to indicated private dictionary in
277+# --spelling-private-dict-file option instead of raising a message.
278+spelling-store-unknown-words=no
279+
280+
281+[TYPECHECK]
282+
283+# List of decorators that produce context managers, such as
284+# contextlib.contextmanager. Add to this list to register other decorators that
285+# produce valid context managers.
286+contextmanager-decorators=contextlib.contextmanager
287+
288+# List of members which are set dynamically and missed by pylint inference
289+# system, and so shouldn't trigger E1101 when accessed. Python regular
290+# expressions are accepted.
291+generated-members=
292+
293+# Tells whether missing members accessed in mixin class should be ignored. A
294+# mixin class is detected if its name ends with "mixin" (case insensitive).
295+ignore-mixin-members=yes
296+
297+# This flag controls whether pylint should warn about no-member and similar
298+# checks whenever an opaque object is returned when inferring. The inference
299+# can return multiple potential results while evaluating a Python object, but
300+# some branches might not be evaluated, which results in partial inference. In
301+# that case, it might be useful to still emit no-member and other checks for
302+# the rest of the inferred objects.
303+ignore-on-opaque-inference=yes
304+
305+# List of class names for which member attributes should not be checked (useful
306+# for classes with dynamically set attributes). This supports the use of
307+# qualified names.
308+ignored-classes=optparse.Values,thread._local,_thread._local
309+
310+# List of module names for which member attributes should not be checked
311+# (useful for modules/projects where namespaces are manipulated during runtime
312+# and thus existing member attributes cannot be deduced by static analysis. It
313+# supports qualified module names, as well as Unix pattern matching.
314+ignored-modules=
315+
316+# Show a hint with possible names when a member name was not found. The aspect
317+# of finding the hint is based on edit distance.
318+missing-member-hint=yes
319+
320+# The minimum edit distance a name should have in order to be considered a
321+# similar match for a missing member name.
322+missing-member-hint-distance=1
323+
324+# The total number of similar names that should be taken in consideration when
325+# showing a hint for a missing member.
326+missing-member-max-choices=1
327+
328+
329+[VARIABLES]
330+
331+# List of additional names supposed to be defined in builtins. Remember that
332+# you should avoid to define new builtins when possible.
333+additional-builtins=
334+
335+# Tells whether unused global variables should be treated as a violation.
336+allow-global-unused-variables=yes
337+
338+# List of strings which can identify a callback function by name. A callback
339+# name must start or end with one of those strings.
340+callbacks=cb_,_cb
341+
342+# A regular expression matching the name of dummy variables (i.e. expectedly
343+# not used).
344+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
345+
346+# Argument names that match this expression will be ignored. Default to name
347+# with leading underscore
348+ignored-argument-names=_.*|^ignored_|^unused_
349+
350+# Tells whether we should check for unused import in __init__ files.
351+init-import=no
352+
353+# List of qualified module names which can have objects that can redefine
354+# builtins.
355+redefining-builtins-modules=six.moves,future.builtins
356+
357+
358+[CLASSES]
359+
360+# List of method names used to declare (i.e. assign) instance attributes.
361+defining-attr-methods=__init__,__new__,setUp
362+
363+# List of member names, which should be excluded from the protected access
364+# warning.
365+exclude-protected=_asdict,_fields,_replace,_source,_make
366+
367+# List of valid names for the first argument in a class method.
368+valid-classmethod-first-arg=cls
369+
370+# List of valid names for the first argument in a metaclass class method.
371+valid-metaclass-classmethod-first-arg=mcs
372+
373+
374+[DESIGN]
375+
376+# Maximum number of arguments for function / method
377+max-args=8
378+
379+# Maximum number of attributes for a class (see R0902).
380+max-attributes=20
381+
382+# Maximum number of boolean expressions in a if statement
383+max-bool-expr=5
384+
385+# Maximum number of branch for function / method body
386+max-branches=12
387+
388+# Maximum number of locals for function / method body
389+max-locals=20
390+
391+# Maximum number of parents for a class (see R0901).
392+max-parents=7
393+
394+# Maximum number of public methods for a class (see R0904).
395+max-public-methods=20
396+
397+# Maximum number of return / yield for function / method body
398+max-returns=6
399+
400+# Maximum number of statements in function / method body
401+max-statements=50
402+
403+# Minimum number of public methods for a class (see R0903).
404+min-public-methods=2
405+
406+
407+[IMPORTS]
408+
409+# Allow wildcard imports from modules that define __all__.
410+allow-wildcard-with-all=no
411+
412+# Analyse import fallback blocks. This can be used to support both Python 2 and
413+# 3 compatible code, which means that the block might have code that exists
414+# only in one or another interpreter, leading to false positives when analysed.
415+analyse-fallback-blocks=no
416+
417+# Deprecated modules which should not be used, separated by a comma
418+deprecated-modules=optparse,tkinter.tix
419+
420+# Create a graph of external dependencies in the given file (report RP0402 must
421+# not be disabled)
422+ext-import-graph=
423+
424+# Create a graph of every (i.e. internal and external) dependencies in the
425+# given file (report RP0402 must not be disabled)
426+import-graph=
427+
428+# Create a graph of internal dependencies in the given file (report RP0402 must
429+# not be disabled)
430+int-import-graph=
431+
432+# Force import order to recognize a module as part of the standard
433+# compatibility libraries.
434+known-standard-library=
435+
436+# Force import order to recognize a module as part of a third party library.
437+known-third-party=enchant
438+
439+
440+[EXCEPTIONS]
441+
442+# Exceptions that will emit a warning when being caught. Defaults to
443+# "Exception"
444+overgeneral-exceptions=Exception
AUTHORS.rst+3−1
@@ -1,4 +1,4 @@
1−Searx was created by Adam Tauber and is maintained by Adam Tauber, Alexandre Flament and Noémi Ványi.
1+Searx was created by Adam Tauber and is maintained by Adam Tauber, Alexandre Flament, Noémi Ványi, @pofilo and Markus Heiser.
2
2
3Major contributing authors:
3Major contributing authors:
4
4
@@ -9,6 +9,8 @@ Major contributing authors:
9- @Cqoicebordel
9- @Cqoicebordel
10- Noémi Ványi
10- Noémi Ványi
11- Marc Abonce Seguin @a01200356
11- Marc Abonce Seguin @a01200356
12+- @pofilo
13+- Markus Heiser @return42
12
14
13People who have submitted patches/translates, reported bugs, consulted features or
15People who have submitted patches/translates, reported bugs, consulted features or
14generally made searx better:
16generally made searx better:
Dockerfile+18−16
@@ -1,32 +1,36 @@
1FROM alpine:3.10
1FROM alpine:3.10
2+ENTRYPOINT ["/sbin/tini","--","/usr/local/searx/dockerfiles/docker-entrypoint.sh"]
3+EXPOSE 8080
4+VOLUME /etc/searx
5+VOLUME /var/log/uwsgi
2
6
3−ARG VERSION_GITCOMMIT=unknow
7+ARG VERSION_GITCOMMIT=unknown
4−ARG SEARX_GIT_VERSION=unknow
8+ARG SEARX_GIT_VERSION=unknown
5
9
6−ARG SEARX_GID=1000
10+ARG SEARX_GID=977
7−ARG SEARX_UID=1000
11+ARG SEARX_UID=977
12+
13+RUN addgroup -g ${SEARX_GID} searx && \
14+ adduser -u ${SEARX_UID} -D -h /usr/local/searx -s /bin/sh -G searx searx
8
15
9ARG TIMESTAMP_SETTINGS=0
16ARG TIMESTAMP_SETTINGS=0
10ARG TIMESTAMP_UWSGI=0
17ARG TIMESTAMP_UWSGI=0
11ARG LABEL_VCS_REF=
18ARG LABEL_VCS_REF=
12ARG LABEL_VCS_URL=
19ARG LABEL_VCS_URL=
13
20
21+ENV INSTANCE_NAME=searx \
22+ AUTOCOMPLETE= \
14−ENV BASE_URL= \
23+ BASE_URL= \
15 MORTY_KEY= \
24 MORTY_KEY= \
16 MORTY_URL=
25 MORTY_URL=
17−EXPOSE 8080
18−VOLUME /etc/searx
19−VOLUME /var/log/uwsgi
20
26
21WORKDIR /usr/local/searx
27WORKDIR /usr/local/searx
22
28
23−RUN addgroup -g ${SEARX_GID} searx && \
24− adduser -u ${SEARX_UID} -D -h /usr/local/searx -s /bin/sh -G searx searx
25
29
26COPY requirements.txt ./requirements.txt
30COPY requirements.txt ./requirements.txt
27
31
28−RUN apk -U upgrade \
32+RUN apk upgrade --no-cache \
29− && apk add -t build-dependencies \
33+ && apk add --no-cache -t build-dependencies \
30 build-base \
34 build-base \
31 py3-setuptools \
35 py3-setuptools \
32 python3-dev \
36 python3-dev \
@@ -36,7 +40,7 @@ RUN apk -U upgrade \
36 openssl-dev \
40 openssl-dev \
37 tar \
41 tar \
38 git \
42 git \
39− && apk add \
43+ && apk add --no-cache \
40 ca-certificates \
44 ca-certificates \
41 su-exec \
45 su-exec \
42 python3 \
46 python3 \
@@ -48,8 +52,7 @@ RUN apk -U upgrade \
48 uwsgi-python3 \
52 uwsgi-python3 \
49 && pip3 install --upgrade pip \
53 && pip3 install --upgrade pip \
50 && pip3 install --no-cache -r requirements.txt \
54 && pip3 install --no-cache -r requirements.txt \
51− && apk del build-dependencies \
55+ && apk del build-dependencies
52− && rm -f /var/cache/apk/*
53
56
54COPY --chown=searx:searx . .
57COPY --chown=searx:searx . .
55
58
@@ -60,7 +63,6 @@ RUN su searx -c "/usr/bin/python3 -m compileall -q searx"; \
60 echo "VERSION_STRING = VERSION_STRING + \"-$VERSION_GITCOMMIT\"" >> /usr/local/searx/searx/version.py; \
63 echo "VERSION_STRING = VERSION_STRING + \"-$VERSION_GITCOMMIT\"" >> /usr/local/searx/searx/version.py; \
61 fi
64 fi
62
65
63−ENTRYPOINT ["/sbin/tini","--","/usr/local/searx/dockerfiles/docker-entrypoint.sh"]
64
66
65# Keep this argument at the end since it change each time
67# Keep this argument at the end since it change each time
66ARG LABEL_DATE=
68ARG LABEL_DATE=
Makefilenew file+85
@@ -0,0 +1,85 @@
1+# -*- coding: utf-8; mode: makefile-gmake -*-
2+
3+export GIT_URL=https://github.com/asciimoo/searx
4+export SEARX_URL=https://searx.me
5+export DOCS_URL=https://asciimoo.github.io/searx
6+
7+PYOBJECTS = searx
8+DOC = docs
9+PY_SETUP_EXTRAS ?= \[test\]
10+
11+include utils/makefile.include
12+include utils/makefile.python
13+include utils/makefile.sphinx
14+
15+all: clean install
16+
17+PHONY += help
18+help:
19+ @echo ' test - run developer tests'
20+ @echo ' docs - build documentation'
21+ @echo ' docs-live - autobuild HTML documentation while editing'
22+ @echo ' run - run developer instance'
23+ @echo ' install - developer install (./local)'
24+ @echo ' uninstall - uninstall (./local)'
25+ @echo ' gh-pages - build docs & deploy on gh-pages branch'
26+ @echo ''
27+ @$(MAKE) -s -f utils/makefile.include make-help
28+ @echo ''
29+ @$(MAKE) -s -f utils/makefile.python python-help
30+
31+PHONY += install
32+install: pyenvinstall
33+
34+PHONY += uninstall
35+uninstall: pyenvuninstall
36+
37+PHONY += clean
38+clean: pyclean
39+ $(call cmd,common_clean)
40+
41+PHONY += run
42+run: pyenvinstall
43+ $(Q) ( \
44+ sed -i -e "s/debug : False/debug : True/g" ./searx/settings.yml ; \
45+ sleep 2 ; \
46+ xdg-open http://127.0.0.1:8888/ ; \
47+ sleep 3 ; \
48+ sed -i -e "s/debug : True/debug : False/g" ./searx/settings.yml ; \
49+ ) &
50+ $(PY_ENV)/bin/python ./searx/webapp.py
51+
52+# docs
53+# ----
54+
55+PHONY += docs
56+docs: pyenvinstall sphinx-doc
57+ $(call cmd,sphinx,html,docs,docs)
58+
59+PHONY += docs-live
60+docs-live: pyenvinstall sphinx-live
61+ $(call cmd,sphinx_autobuild,html,docs,docs)
62+
63+$(GH_PAGES)::
64+ @echo "doc available at --> $(DOCS_URL)"
65+
66+# test
67+# ----
68+
69+PHONY += test test.pylint test.pep8 test.unit test.robot
70+
71+# TODO: balance linting with pylint
72+test: test.pep8 test.unit test.robot
73+ - make pylint
74+
75+test.pep8: pyenvinstall
76+ $(PY_ENV_ACT); ./manage.sh pep8_check
77+
78+test.unit: pyenvinstall
79+ $(PY_ENV_ACT); ./manage.sh unit_tests
80+
81+test.robot: pyenvinstall
82+ $(PY_ENV_ACT); ./manage.sh install_geckodriver
83+ $(PY_ENV_ACT); ./manage.sh robot_tests
84+
85+.PHONY: $(PHONY)
dockerfiles/docker-entrypoint.sh+4
@@ -29,6 +29,8 @@ do
29 printf " -f Always update on the configuration files (existing files are renamed with the .old suffix)\n"
29 printf " -f Always update on the configuration files (existing files are renamed with the .old suffix)\n"
30 printf " Without this option, new configuration files are copied with the .new suffix\n"
30 printf " Without this option, new configuration files are copied with the .new suffix\n"
31 printf "\nEnvironment variables:\n\n"
31 printf "\nEnvironment variables:\n\n"
32+ printf " INSTANCE_NAME settings.yml : general.instance_name\n"
33+ printf " AUTOCOMPLETE settings.yml : search.autocomplete\n"
32 printf " BASE_URL settings.yml : server.base_url\n"
34 printf " BASE_URL settings.yml : server.base_url\n"
33 printf " MORTY_URL settings.yml : result_proxy.url\n"
35 printf " MORTY_URL settings.yml : result_proxy.url\n"
34 printf " MORTY_KEY settings.yml : result_proxy.key\n"
36 printf " MORTY_KEY settings.yml : result_proxy.key\n"
@@ -53,6 +55,8 @@ patch_searx_settings() {
53
55
54 # update settings.yml
56 # update settings.yml
55 sed -i -e "s|base_url : False|base_url : ${BASE_URL}|g" \
57 sed -i -e "s|base_url : False|base_url : ${BASE_URL}|g" \
58+ -e "s/instance_name : \"searx\"/instance_name : \"${INSTANCE_NAME}\"/g" \
59+ -e "s/autocomplete : \"\"/autocomplete : \"${AUTOCOMPLETE}\"/g" \
56 -e "s/ultrasecretkey/$(openssl rand -hex 32)/g" \
60 -e "s/ultrasecretkey/$(openssl rand -hex 32)/g" \
57 "${CONF}"
61 "${CONF}"
58
62
docs/_themes/searx/static/searx.cssnew file+30
@@ -0,0 +1,30 @@
1+@import url("pocoo.css");
2+
3+a, a.reference, a.footnote-reference {
4+ color: #004b6b;
5+ border-color: #004b6b;
6+}
7+
8+a:hover {
9+ color: #6d4100;
10+ border-color: #6d4100;
11+}
12+
13+p.version-warning {
14+ background-color: #004b6b;
15+}
16+
17+div.sidebar {
18+ background-color: whitesmoke;
19+ border-color: lightsteelblue;
20+ border-radius: 3pt;
21+}
22+
23+p.sidebar-title, .sidebar p {
24+ margin: 6pt;
25+}
26+
27+.sidebar li {
28+ list-style-type: disclosure-closed;
29+}
30+
docs/_themes/searx/theme.confnew file+6
@@ -0,0 +1,6 @@
1+[theme]
2+inherit = pocoo
3+stylesheet = searx.css
4+
5+[options]
6+touch_icon =
docs/admin/api.rstnew file+96
@@ -0,0 +1,96 @@
1+.. _adminapi:
2+
3+==================
4+Administration API
5+==================
6+
7+Get configuration data
8+======================
9+
10+.. code:: http
11+
12+ GET /config HTTP/1.1
13+
14+Sample response
15+---------------
16+
17+.. code:: json
18+
19+ {
20+ "autocomplete": "",
21+ "categories": [
22+ "map",
23+ "it",
24+ "images",
25+ ],
26+ "default_locale": "",
27+ "default_theme": "oscar",
28+ "engines": [
29+ {
30+ "categories": [
31+ "map"
32+ ],
33+ "enabled": true,
34+ "name": "openstreetmap",
35+ "shortcut": "osm"
36+ },
37+ {
38+ "categories": [
39+ "it"
40+ ],
41+ "enabled": true,
42+ "name": "arch linux wiki",
43+ "shortcut": "al"
44+ },
45+ {
46+ "categories": [
47+ "images"
48+ ],
49+ "enabled": true,
50+ "name": "google images",
51+ "shortcut": "goi"
52+ },
53+ {
54+ "categories": [
55+ "it"
56+ ],
57+ "enabled": false,
58+ "name": "bitbucket",
59+ "shortcut": "bb"
60+ },
61+ ],
62+ "instance_name": "searx",
63+ "locales": {
64+ "de": "Deutsch (German)",
65+ "en": "English",
66+ "eo": "Esperanto (Esperanto)",
67+ },
68+ "plugins": [
69+ {
70+ "enabled": true,
71+ "name": "HTTPS rewrite"
72+ },
73+ {
74+ "enabled": false,
75+ "name": "Vim-like hotkeys"
76+ }
77+ ],
78+ "safe_search": 0
79+ }
80+
81+
82+Embed search bar
83+================
84+
85+The search bar can be embedded into websites. Just paste the example into the
86+HTML of the site. URL of the searx instance and values are customizable.
87+
88+.. code:: html
89+
90+ <form method="post" action="https://searx.me/">
91+ <!-- search --> <input type="text" name="q" />
92+ <!-- categories --> <input type="hidden" name="categories" value="general,social media" />
93+ <!-- language --> <input type="hidden" name="lang" value="all" />
94+ <!-- locale --> <input type="hidden" name="locale" value="en" />
95+ <!-- date filter --> <input type="hidden" name="time_range" value="month" />
96+ </form>
docs/admin/filtron.rstnew file+148
@@ -0,0 +1,148 @@
1+==========================
2+How to protect an instance
3+==========================
4+
5+Searx depens on external search services. To avoid the abuse of these services
6+it is advised to limit the number of requests processed by searx.
7+
8+An application firewall, ``filtron`` solves exactly this problem. Information
9+on how to install it can be found at the `project page of filtron
10+<https://github.com/asciimoo/filtron>`__.
11+
12+
13+Sample configuration of filtron
14+===============================
15+
16+An example configuration can be find below. This configuration limits the access
17+of:
18+
19+- scripts or applications (roboagent limit)
20+- webcrawlers (botlimit)
21+- IPs which send too many requests (IP limit)
22+- too many json, csv, etc. requests (rss/json limit)
23+- the same UserAgent of if too many requests (useragent limit)
24+
25+.. code:: json
26+
27+ [{
28+ "name":"search request",
29+ "filters":[
30+ "Param:q",
31+ "Path=^(/|/search)$"
32+ ],
33+ "interval":"<time-interval-in-sec (int)>",
34+ "limit":"<max-request-number-in-interval (int)>",
35+ "subrules":[
36+ {
37+ "name":"roboagent limit",
38+ "interval":"<time-interval-in-sec (int)>",
39+ "limit":"<max-request-number-in-interval (int)>",
40+ "filters":[
41+ "Header:User-Agent=(curl|cURL|Wget|python-requests|Scrapy|FeedFetcher|Go-http-client)"
42+ ],
43+ "actions":[
44+ {
45+ "name":"block",
46+ "params":{
47+ "message":"Rate limit exceeded"
48+ }
49+ }
50+ ]
51+ },
52+ {
53+ "name":"botlimit",
54+ "limit":0,
55+ "stop":true,
56+ "filters":[
57+ "Header:User-Agent=(Googlebot|bingbot|Baiduspider|yacybot|YandexMobileBot|YandexBot|Yahoo! Slurp|MJ12bot|AhrefsBot|archive.org_bot|msnbot|MJ12bot|SeznamBot|linkdexbot|Netvibes|SMTBot|zgrab|James BOT)"
58+ ],
59+ "actions":[
60+ {
61+ "name":"block",
62+ "params":{
63+ "message":"Rate limit exceeded"
64+ }
65+ }
66+ ]
67+ },
68+ {
69+ "name":"IP limit",
70+ "interval":"<time-interval-in-sec (int)>",
71+ "limit":"<max-request-number-in-interval (int)>",
72+ "stop":true,
73+ "aggregations":[
74+ "Header:X-Forwarded-For"
75+ ],
76+ "actions":[
77+ {
78+ "name":"block",
79+ "params":{
80+ "message":"Rate limit exceeded"
81+ }
82+ }
83+ ]
84+ },
85+ {
86+ "name":"rss/json limit",
87+ "interval":"<time-interval-in-sec (int)>",
88+ "limit":"<max-request-number-in-interval (int)>",
89+ "stop":true,
90+ "filters":[
91+ "Param:format=(csv|json|rss)"
92+ ],
93+ "actions":[
94+ {
95+ "name":"block",
96+ "params":{
97+ "message":"Rate limit exceeded"
98+ }
99+ }
100+ ]
101+ },
102+ {
103+ "name":"useragent limit",
104+ "interval":"<time-interval-in-sec (int)>",
105+ "limit":"<max-request-number-in-interval (int)>",
106+ "aggregations":[
107+ "Header:User-Agent"
108+ ],
109+ "actions":[
110+ {
111+ "name":"block",
112+ "params":{
113+ "message":"Rate limit exceeded"
114+ }
115+ }
116+ ]
117+ }
118+ ]
119+ }]
120+
121+
122+
123+Route request through filtron
124+=============================
125+
126+Filtron can be started using the following command:
127+
128+.. code:: sh
129+
130+ $ filtron -rules rules.json
131+
132+It listens on ``127.0.0.1:4004`` and forwards filtered requests to
133+``127.0.0.1:8888`` by default.
134+
135+Use it along with ``nginx`` with the following example configuration.
136+
137+.. code:: nginx
138+
139+ location / {
140+ proxy_set_header Host $http_host;
141+ proxy_set_header X-Real-IP $remote_addr;
142+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
143+ proxy_set_header X-Scheme $scheme;
144+ proxy_pass http://127.0.0.1:4004/;
145+ }
146+
147+Requests are coming from port 4004 going through filtron and then forwarded to
148+port 8888 where a searx is being run.
docs/admin/index.rstnew file+11
@@ -0,0 +1,11 @@
1+===========================
2+Administrator documentation
3+===========================
4+
5+.. toctree::
6+ :maxdepth: 1
7+
8+ installation
9+ api
10+ filtron
11+ morty
docs/admin/installation.rstnew file+341
@@ -0,0 +1,341 @@
1+.. _installation:
2+
3+============
4+Installation
5+============
6+
7+.. contents::
8+ :depth: 3
9+
10+Basic installation
11+==================
12+
13+Step by step installation for Debian/Ubuntu with virtualenv. For Ubuntu, be sure
14+to have enable universe repository.
15+
16+Install packages:
17+
18+.. code:: sh
19+
20+ $ sudo -H apt-get install \
21+ git build-essential libxslt-dev \
22+ python-dev python-virtualenv python-babel \
23+ zlib1g-dev libffi-dev libssl-dev
24+
25+Install searx:
26+
27+.. code:: sh
28+
29+ cd /usr/local
30+ sudo -H git clone https://github.com/asciimoo/searx.git
31+ sudo -H useradd searx -d /usr/local/searx
32+ sudo -H chown searx:searx -R /usr/local/searx
33+
34+Install dependencies in a virtualenv:
35+
36+.. code:: sh
37+
38+ cd /usr/local/searx
39+ sudo -H -u searx -i
40+
41+.. code:: sh
42+
43+ (searx)$ virtualenv searx-ve
44+ (searx)$ . ./searx-ve/bin/activate
45+ (searx)$ ./manage.sh update_packages
46+
47+Configuration
48+==============
49+
50+.. code:: sh
51+
52+ sed -i -e "s/ultrasecretkey/`openssl rand -hex 16`/g" searx/settings.yml
53+
54+Edit searx/settings.yml if necessary.
55+
56+Check
57+=====
58+
59+Start searx:
60+
61+.. code:: sh
62+
63+ python searx/webapp.py
64+
65+Go to http://localhost:8888
66+
67+If everything works fine, disable the debug option in settings.yml:
68+
69+.. code:: sh
70+
71+ sed -i -e "s/debug : True/debug : False/g" searx/settings.yml
72+
73+At this point searx is not demonized ; uwsgi allows this.
74+
75+You can exit the virtualenv and the searx user bash (enter exit command
76+twice).
77+
78+uwsgi
79+=====
80+
81+Install packages:
82+
83+.. code:: sh
84+
85+ sudo -H apt-get install \
86+ uwsgi uwsgi-plugin-python
87+
88+Create the configuration file ``/etc/uwsgi/apps-available/searx.ini`` with this
89+content:
90+
91+.. code:: ini
92+
93+ [uwsgi]
94+ # Who will run the code
95+ uid = searx
96+ gid = searx
97+
98+ # disable logging for privacy
99+ disable-logging = true
100+
101+ # Number of workers (usually CPU count)
102+ workers = 4
103+
104+ # The right granted on the created socket
105+ chmod-socket = 666
106+
107+ # Plugin to use and interpretor config
108+ single-interpreter = true
109+ master = true
110+ plugin = python
111+ lazy-apps = true
112+ enable-threads = true
113+
114+ # Module to import
115+ module = searx.webapp
116+
117+ # Virtualenv and python path
118+ virtualenv = /usr/local/searx/searx-ve/
119+ pythonpath = /usr/local/searx/
120+ chdir = /usr/local/searx/searx/
121+
122+Activate the uwsgi application and restart:
123+
124+.. code:: sh
125+
126+ cd /etc/uwsgi/apps-enabled
127+ ln -s ../apps-available/searx.ini
128+ /etc/init.d/uwsgi restart
129+
130+Web server
131+==========
132+
133+with nginx
134+----------
135+
136+If nginx is not installed (uwsgi will not work with the package
137+nginx-light):
138+
139+.. code:: sh
140+
141+ sudo -H apt-get install nginx
142+
143+Hosted at /
144+~~~~~~~~~~~
145+
146+Create the configuration file ``/etc/nginx/sites-available/searx`` with this
147+content:
148+
149+.. code:: nginx
150+
151+ server {
152+ listen 80;
153+ server_name searx.example.com;
154+ root /usr/local/searx;
155+
156+ location / {
157+ include uwsgi_params;
158+ uwsgi_pass unix:/run/uwsgi/app/searx/socket;
159+ }
160+ }
161+
162+Create a symlink to sites-enabled:
163+
164+.. code:: sh
165+
166+ sudo -H ln -s /etc/nginx/sites-available/searx /etc/nginx/sites-enabled/searx
167+
168+Restart service:
169+
170+.. code:: sh
171+
172+ sudo -H service nginx restart
173+ sudo -H service uwsgi restart
174+
175+from subdirectory URL (/searx)
176+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
177+
178+Add this configuration in the server config file
179+``/etc/nginx/sites-enabled/default``:
180+
181+.. code:: nginx
182+
183+ location = /searx { rewrite ^ /searx/; }
184+ location /searx {
185+ try_files $uri @searx;
186+ }
187+ location @searx {
188+ uwsgi_param SCRIPT_NAME /searx;
189+ include uwsgi_params;
190+ uwsgi_modifier1 30;
191+ uwsgi_pass unix:/run/uwsgi/app/searx/socket;
192+ }
193+
194+
195+**OR** using reverse proxy (Please, note that reverse proxy advised to be used
196+in case of single-user or low-traffic instances.)
197+
198+.. code:: nginx
199+
200+ location /searx {
201+ proxy_pass http://127.0.0.1:8888;
202+ proxy_set_header Host $host;
203+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
204+ proxy_set_header X-Scheme $scheme;
205+ proxy_set_header X-Script-Name /searx;
206+ proxy_buffering off;
207+ }
208+
209+
210+Enable ``base_url`` in ``searx/settings.yml``
211+
212+.. code:: yaml
213+
214+ base_url : http://your.domain.tld/searx/
215+
216+Restart service:
217+
218+.. code:: sh
219+
220+ sudo -H service nginx restart
221+ sudo -H service uwsgi restart
222+
223+disable logs
224+^^^^^^^^^^^^
225+
226+for better privacy you can disable nginx logs about searx.
227+
228+how to proceed: below ``uwsgi_pass`` in ``/etc/nginx/sites-available/default``
229+add:
230+
231+.. code:: nginx
232+
233+ access_log /dev/null;
234+ error_log /dev/null;
235+
236+Restart service:
237+
238+.. code:: sh
239+
240+ sudo -H service nginx restart
241+
242+with apache
243+-----------
244+
245+Add wsgi mod:
246+
247+.. code:: sh
248+
249+ sudo -H apt-get install libapache2-mod-uwsgi
250+ sudo -H a2enmod uwsgi
251+
252+Add this configuration in the file ``/etc/apache2/apache2.conf``:
253+
254+.. code:: apache
255+
256+ <Location />
257+ Options FollowSymLinks Indexes
258+ SetHandler uwsgi-handler
259+ uWSGISocket /run/uwsgi/app/searx/socket
260+ </Location>
261+
262+Note that if your instance of searx is not at the root, you should change
263+``<Location />`` by the location of your instance, like ``<Location /searx>``.
264+
265+Restart Apache:
266+
267+.. code:: sh
268+
269+ sudo -H /etc/init.d/apache2 restart
270+
271+disable logs
272+~~~~~~~~~~~~
273+
274+For better privacy you can disable Apache logs.
275+
276+.. warning::
277+
278+ You can only disable logs for the whole (virtual) server not for a specific
279+ path.
280+
281+Go back to ``/etc/apache2/apache2.conf`` and above ``<Location />`` add:
282+
283+.. code:: apache
284+
285+ CustomLog /dev/null combined
286+
287+Restart Apache:
288+
289+.. code:: sh
290+
291+ sudo -H /etc/init.d/apache2 restart
292+
293+How to update
294+=============
295+
296+.. code:: sh
297+
298+ cd /usr/local/searx
299+ sudo -H -u searx -i
300+
301+.. code:: sh
302+
303+ (searx)$ . ./searx-ve/bin/activate
304+ (searx)$ git stash
305+ (searx)$ git pull origin master
306+ (searx)$ git stash apply
307+ (searx)$ ./manage.sh update_packages
308+
309+.. code:: sh
310+
311+ sudo -H service uwsgi restart
312+
313+Docker
314+======
315+
316+Make sure you have installed Docker. For instance, you can deploy searx like this:
317+
318+.. code:: sh
319+
320+ docker pull wonderfall/searx
321+ docker run -d --name searx -p $PORT:8888 wonderfall/searx
322+
323+Go to ``http://localhost:$PORT``.
324+
325+See https://hub.docker.com/r/wonderfall/searx/ for more informations. It's also
326+possible to build searx from the embedded Dockerfile.
327+
328+.. code:: sh
329+
330+ git clone https://github.com/asciimoo/searx.git
331+ cd searx
332+ docker build -t whatever/searx .
333+
334+References
335+==========
336+
337+* https://about.okhin.fr/posts/Searx/ with some additions
338+
339+* How to: `Setup searx in a couple of hours with a free SSL certificate
340+ <https://www.reddit.com/r/privacytoolsIO/comments/366kvn/how_to_setup_your_own_privacy_respecting_search/>`__
341+
docs/admin/morty.rstnew file+26
@@ -0,0 +1,26 @@
1+=========================
2+How to setup result proxy
3+=========================
4+
5+.. _morty: https://github.com/asciimoo/morty
6+.. _morty's README: https://github.com/asciimoo/morty
7+
8+By default searx can only act as an image proxy for result images, but it is
9+possible to proxify all the result URLs with an external service, morty_.
10+
11+To use this feature, morty has to be installed and activated in searx's
12+``settings.yml``.
13+
14+Add the following snippet to your ``settings.yml`` and restart searx:
15+
16+.. code:: yaml
17+
18+ result_proxy:
19+ url : http://127.0.0.1:3000/
20+ key : your_morty_proxy_key
21+
22+``url``
23+ Is the address of the running morty service.
24+
25+``key``
26+ Is an optional argument, see `morty's README`_ for more information.
docs/blog/admin.rstnew file+43
@@ -0,0 +1,43 @@
1+=============================================================
2+Searx admin interface
3+=============================================================
4+
5+.. _searx-admin: https://github.com/kvch/searx-admin#searx-admin
6+.. _NLnet Foundation: https://nlnet.nl/
7+
8+ manage your instance from your browser
9+
10+.. sidebar:: Installation
11+
12+ Installation guide can be found in the repository of searx-admin_.
13+
14+One of the biggest advantages of searx is being extremely customizable. But at
15+first it can be daunting to newcomers. A barrier of taking advantage of this
16+feature is our ugly settings file which is sometimes hard to understand and
17+edit.
18+
19+To make self-hosting searx more accessible a new tool is introduced, called
20+``searx-admin``. It is a web application which is capable of managing your
21+instance and manipulating its settings via a web UI. It aims to replace editing
22+of ``settings.yml`` for less experienced administrators or people who prefer
23+graphical admin interfaces.
24+
25+.. figure:: searx-admin-engines.png
26+ :alt: Screenshot of engine list
27+
28+ Configuration page of engines
29+
30+Since ``searx-admin`` acts as a supervisor for searx, we have decided to
31+implement it as a standalone tool instead of part of searx. Another reason for
32+making it a standalone tool is that the codebase and dependencies of searx
33+should not grow because of a fully optional feature, which does not affect
34+existing instances.
35+
36+
37+Acknowledgements
38+================
39+
40+This development was sponsored by `NLnet Foundation`_.
41+
42+| Happy hacking.
43+| kvch // 2017.08.22 21:25
docs/blog/index.rstnew file+10
@@ -0,0 +1,10 @@
1+====
2+Blog
3+====
4+
5+.. toctree::
6+ :maxdepth: 1
7+
8+ python3
9+ admin
10+ intro-offline
docs/blog/intro-offline.rstnew file+77
@@ -0,0 +1,77 @@
1+===============================
2+Preparation for offline engines
3+===============================
4+
5+Offline engines
6+===============
7+
8+To extend the functionality of searx, offline engines are going to be
9+introduced. An offline engine is an engine which does not need Internet
10+connection to perform a search and does not use HTTP to communicate.
11+
12+Offline engines can be configured as online engines, by adding those to the
13+`engines` list of :origin:`settings.yml <searx/settings.yml>`. Thus, searx
14+finds the engine file and imports it.
15+
16+Example skeleton for the new engines:
17+
18+.. code:: python
19+
20+ from subprocess import PIPE, Popen
21+
22+ categories = ['general']
23+ offline = True
24+
25+ def init(settings):
26+ pass
27+
28+ def search(query, params):
29+ process = Popen(['ls', query], stdout=PIPE)
30+ return_code = process.wait()
31+ if return_code != 0:
32+ raise RuntimeError('non-zero return code', return_code)
33+
34+ results = []
35+ line = process.stdout.readline()
36+ while line:
37+ result = parse_line(line)
38+ results.append(results)
39+
40+ line = process.stdout.readline()
41+
42+ return results
43+
44+
45+Development progress
46+====================
47+
48+First, a proposal has been created as a Github issue. Then it was moved to the
49+wiki as a design document. You can read it here: :wiki:`Offline-engines`.
50+
51+In this development step, searx core was prepared to accept and perform offline
52+searches. Offline search requests are scheduled together with regular offline
53+requests.
54+
55+As offline searches can return arbitrary results depending on the engine, the
56+current result templates were insufficient to present such results. Thus, a new
57+template is introduced which is caplable of presenting arbitrary key value pairs
58+as a table. You can check out the pull request for more details see
59+:pull:`1700`.
60+
61+Next steps
62+==========
63+
64+Today, it is possible to create/run an offline engine. However, it is going to be publicly available for everyone who knows the searx instance. So the next step is to introduce token based access for engines. This way administrators are able to limit the access to private engines.
65+
66+Acknowledgement
67+===============
68+
69+This development was sponsored by `Search and Discovery Fund`_ of `NLnet Foundation`_ .
70+
71+.. _Search and Discovery Fund: https://nlnet.nl/discovery
72+.. _NLnet Foundation: https://nlnet.nl/
73+
74+
75+| Happy hacking.
76+| kvch // 2019.10.21 17:03
77+
docs/blog/python3.rstnew file+68
@@ -0,0 +1,68 @@
1+============================
2+Introducing Python 3 support
3+============================
4+
5+.. _Python 2.7 clock: https://pythonclock.org/
6+
7+.. sidebar:: Python 2.7 to 3 upgrade
8+
9+ This chapter exists of historical reasons. Python 2.7 release schedule ends
10+ (`Python 2.7 clock`_) after 11 years Python 3 exists
11+
12+As most operation systems are coming with Python3 installed by default. So it is
13+time for searx to support Python3. But don't worry support of Python2.7 won't be
14+dropped.
15+
16+.. image:: searxpy3.png
17+ :scale: 50 %
18+ :alt: hurray
19+ :align: center
20+
21+
22+How to run searx using Python 3
23+===============================
24+
25+Please make sure that you run at least Python 3.5.
26+
27+To run searx, first a Python3 virtualenv should be created. After entering the
28+virtualenv, dependencies must be installed. Then run searx with python3 instead
29+of the usual python command.
30+
31+.. code:: sh
32+
33+ virtualenv -p python3 venv3
34+ source venv3/bin/activate
35+ pip3 install -r requirements.txt
36+ python3 searx/webapp.py
37+
38+
39+If you want to run searx using Python2.7, you don't have to do anything
40+differently as before.
41+
42+Fun facts
43+=========
44+
45+- 115 files were changed when implementing the support for both Python versions.
46+
47+- All of the dependencies was compatible except for the robotframework used for
48+ browser tests. Thus, these tests were migrated to splinter. So from now on
49+ both versions are being tested on Travis and can be tested locally.
50+
51+If you found bugs
52+=================
53+
54+Please open an issue on `GitHub`_. Make sure that you mention your Python
55+version in your issue, so we can investigate it properly.
56+
57+.. _GitHub: https://github.com/asciimoo/searx/issues
58+
59+Acknowledgment
60+==============
61+
62+This development was sponsored by `NLnet Foundation`_.
63+
64+.. _NLnet Foundation: https://nlnet.nl/
65+
66+
67+| Happy hacking.
68+| kvch // 2017.05.13 22:57
docs/blog/searx-admin-engines.pngnew file

Binary file not shown.

docs/blog/searxpy3.pngnew file

Binary file not shown.

docs/conf.pynew file+83
@@ -0,0 +1,83 @@
1+# -*- coding: utf-8 -*-
2+
3+import sys, os
4+from searx.version import VERSION_STRING
5+from pallets_sphinx_themes import ProjectLink
6+
7+GIT_URL = os.environ.get("GIT_URL", "https://github.com/asciimoo/searx")
8+SEARX_URL = os.environ.get("SEARX_URL", "https://searx.me")
9+DOCS_URL = os.environ.get("DOCS_URL", "https://asciimoo.github.io/searx/")
10+
11+# Project --------------------------------------------------------------
12+
13+project = u'searx'
14+copyright = u'2015-2019, Adam Tauber, Noémi Ványi'
15+author = u'Adam Tauber'
16+release, version = VERSION_STRING, VERSION_STRING
17+
18+# General --------------------------------------------------------------
19+
20+master_doc = "index"
21+source_suffix = '.rst'
22+
23+# usage:: lorem :patch:`f373169` ipsum
24+extlinks = {}
25+
26+# upstream links
27+extlinks['wiki'] = ('https://github.com/asciimoo/searx/wiki/%s', ' ')
28+extlinks['pull'] = ('https://github.com/asciimoo/searx/pull/%s', 'PR ')
29+
30+# links to custom brand
31+extlinks['origin'] = (GIT_URL + '/blob/master/%s', 'git://')
32+extlinks['patch'] = (GIT_URL + '/commit/%s', '#')
33+extlinks['search'] = (SEARX_URL + '/%s', '#')
34+extlinks['docs'] = (DOCS_URL + '/%s', 'docs: ')
35+
36+extensions = [
37+ 'sphinx.ext.extlinks',
38+ 'sphinx.ext.viewcode',
39+ "sphinx.ext.autodoc",
40+ "sphinx.ext.intersphinx",
41+ "pallets_sphinx_themes",
42+ "sphinx_issues", # https://github.com/sloria/sphinx-issues/blob/master/README.rst
43+]
44+
45+intersphinx_mapping = {
46+ "python": ("https://docs.python.org/3/", None),
47+ # "flask": ("https://flask.palletsprojects.com/", None),
48+ # "werkzeug": ("https://werkzeug.palletsprojects.com/", None),
49+ # "jinja": ("https://jinja.palletsprojects.com/", None),
50+}
51+
52+issues_github_path = "asciimoo/searx"
53+
54+# HTML -----------------------------------------------------------------
55+
56+sys.path.append(os.path.abspath('_themes'))
57+
58+html_theme_path = ['_themes']
59+html_theme = "searx"
60+
61+html_theme_options = {"index_sidebar_logo": True}
62+html_context = {
63+ "project_links": [
64+ ProjectLink("Source", GIT_URL),
65+ ProjectLink("Wiki", "https://github.com/asciimoo/searx/wiki"),
66+ ProjectLink("Public instances", "https://github.com/asciimoo/searx/wiki/Searx-instances"),
67+ ProjectLink("Twitter", "https://twitter.com/Searx_engine"),
68+ ]
69+}
70+html_sidebars = {
71+ "**": ["project.html", "relations.html", "searchbox.html"],
72+}
73+singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]}
74+html_static_path = ["static"]
75+html_logo = "static/img/searx_logo_small.png"
76+html_title = "Searx Documentation ({})".format("Searx-{}.tex".format(VERSION_STRING))
77+html_show_sourcelink = False
78+
79+# LaTeX ----------------------------------------------------------------
80+
81+latex_documents = [
82+ (master_doc, "searx-{}.tex".format(VERSION_STRING), html_title, author, "manual")
83+]
docs/dev/contribution_guide.rstnew file+147
@@ -0,0 +1,147 @@
1+=================
2+How to contribute
3+=================
4+
5+Prime directives: Privacy, Hackability
6+======================================
7+
8+Searx has two prime directives, **privacy-by-design and hackability** . The
9+hackability comes in three levels:
10+
11+- support of search engines
12+- plugins to alter search behaviour
13+- hacking searx itself
14+
15+Note the lack of "world domination" among the directives. Searx has no
16+intention of wide mass-adoption, rounded corners, etc. The prime directive
17+"privacy" deserves a separate chapter, as it's quite uncommon unfortunately.
18+
19+Privacy-by-design
20+-----------------
21+
22+Searx was born out of the need for a **privacy-respecting** search tool which
23+can be extended easily to maximize both, its search and its privacy protecting
24+capabilities.
25+
26+A few widely used features work differently or turned off by default or not
27+implemented at all **as a consequence of privacy-by-design**.
28+
29+If a feature reduces the privacy preserving aspects of searx, it should be
30+switched off by default or should not implemented at all. There are plenty of
31+search engines already providing such features. If a feature reduces the
32+protection of searx, users must be informed about the effect of choosing to
33+enable it. Features that protect privacy but differ from the expectations of
34+the user should also be explained.
35+
36+Also, if you think that something works weird with searx, it's might be because
37+of the tool you use is designed in a way to interfere with the privacy respect.
38+Submitting a bugreport to the vendor of the tool that misbehaves might be a good
39+feedback to reconsider the disrespect to its customers (e.g. ``GET`` vs ``POST``
40+requests in various browsers).
41+
42+Remember the other prime directive of searx is to be hackable, so if the above
43+privacy concerns do not fancy you, simply fork it.
44+
45+ *Happy hacking.*
46+
47+Code
48+====
49+
50+.. _PEP8: https://www.python.org/dev/peps/pep-0008/
51+
52+
53+In order to submit a patch, please follow the steps below:
54+
55+- Follow coding conventions.
56+
57+ - PEP8_ standards apply, except the convention of line length
58+ - Maximum line length is 120 characters
59+
60+- Check if your code breaks existing tests. If so, update the tests or fix your
61+ code.
62+
63+- If your code can be unit-tested, add unit tests.
64+
65+- Add yourself to the :origin:`AUTHORS.rst` file.
66+
67+- Create a pull request.
68+
69+For more help on getting started with searx development, see :ref:`devquickstart`.
70+
71+
72+Translation
73+===========
74+
75+Translation currently takes place on :ref:`transifex <translation>`.
76+
77+.. caution::
78+
79+ Please, do not update translation files in the repo.
80+
81+
82+Documentation
83+=============
84+
85+.. _Sphinx: http://www.sphinx-doc.org
86+.. _reST: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
87+
88+.. sidebar:: The reST sources
89+
90+ has been moved from ``gh-branch`` into ``master`` (:origin:`docs`).
91+
92+The documentation is built using Sphinx_. So in order to be able to generate
93+the required files, you have to install it on your system. Much easier, use
94+Makefile our targets.
95+
96+Here is an example which makes a complete rebuild:
97+
98+.. code:: sh
99+
100+ $ make docs-clean docs
101+ ...
102+ The HTML pages are in dist/docs.
103+
104+
105+live build
106+----------
107+
108+.. sidebar:: docs-clean
109+
110+ It is recommended to assert a complete rebuild before deploying (use
111+ ``docs-clean``).
112+
113+Live build is like WYSIWYG, If you want to edit the documentation, its
114+recommended to use. The Makefile target ``docs-live`` builds the docs, opens URL
115+in your favorite browser and rebuilds every time a reST file has been changed.
116+
117+.. code:: sh
118+
119+ $ make docs-live
120+ ...
121+ The HTML pages are in dist/docs.
122+ ... Serving on http://0.0.0.0:8080
123+ ... Start watching changes
124+
125+
126+
127+deploy on github.io
128+-------------------
129+
130+To deploy documentation at :docs:`github.io <.>` use Makefile target
131+``gh-pages``, which will builds the documentation, clones searx into a sub
132+folder ``gh-pages``, cleans it, copies the doc build into and runs all the
133+needed git add, commit and push:
134+
135+.. code:: sh
136+
137+ $ make docs-clean gh-pages
138+ ...
139+ SPHINX docs --> file://<...>/dist/docs
140+ The HTML pages are in dist/docs.
141+ ...
142+ Cloning into 'gh-pages' ...
143+ ...
144+ cd gh-pages; git checkout gh-pages >/dev/null
145+ Switched to a new branch 'gh-pages'
146+ ...
147+ doc available at --> https://asciimoo.github.io/searx
docs/dev/engine_overview.rstnew file+265
@@ -0,0 +1,265 @@
1+
2+.. _engines-dev:
3+
4+===============
5+Engine overview
6+===============
7+
8+.. _metasearch-engine: https://en.wikipedia.org/wiki/Metasearch_engine
9+
10+searx is a metasearch-engine_, so it uses different search engines to provide
11+better results.
12+
13+Because there is no general search API which could be used for every search
14+engine, an adapter has to be built between searx and the external search
15+engines. Adapters are stored under the folder :origin:`searx/engines`.
16+
17+.. contents::
18+ :depth: 3
19+ :backlinks: entry
20+
21+general engine configuration
22+============================
23+
24+It is required to tell searx the type of results the engine provides. The
25+arguments can be set in the engine file or in the settings file
26+(normally ``settings.yml``). The arguments in the settings file override
27+the ones in the engine file.
28+
29+It does not matter if an option is stored in the engine file or in the
30+settings. However, the standard way is the following:
31+
32+
33+engine file
34+-----------
35+
36+======================= =========== ===========================================
37+argument type information
38+======================= =========== ===========================================
39+categories list pages, in which the engine is working
40+paging boolean support multible pages
41+language_support boolean support language choosing
42+time_range_support boolean support search time range
43+offline boolean engine runs offline
44+======================= =========== ===========================================
45+
46+
47+settings.yml
48+------------
49+
50+======================= =========== ===========================================
51+argument type information
52+======================= =========== ===========================================
53+name string name of search-engine
54+engine string name of searx-engine
55+ (filename without ``.py``)
56+shortcut string shortcut of search-engine
57+timeout string specific timeout for search-engine
58+======================= =========== ===========================================
59+
60+
61+overrides
62+---------
63+
64+A few of the options have default values in the engine, but are often
65+overwritten by the settings. If ``None`` is assigned to an option in the engine
66+file, it has to be redefined in the settings, otherwise searx will not start
67+with that engine.
68+
69+The naming of overrides is arbitrary. But the recommended overrides are the
70+following:
71+
72+======================= =========== ===========================================
73+argument type information
74+======================= =========== ===========================================
75+base_url string base-url, can be overwritten to use same
76+ engine on other URL
77+number_of_results int maximum number of results per request
78+language string ISO code of language and country like en_US
79+api_key string api-key if required by engine
80+======================= =========== ===========================================
81+
82+example code
83+------------
84+
85+.. code:: python
86+
87+ # engine dependent config
88+ categories = ['general']
89+ paging = True
90+ language_support = True
91+
92+
93+making a request
94+================
95+
96+To perform a search an URL have to be specified. In addition to specifying an
97+URL, arguments can be passed to the query.
98+
99+passed arguments
100+----------------
101+
102+These arguments can be used to construct the search query. Furthermore,
103+parameters with default value can be redefined for special purposes.
104+
105+====================== ============ ========================================================================
106+argument type default-value, information
107+====================== ============ ========================================================================
108+url string ``''``
109+method string ``'GET'``
110+headers set ``{}``
111+data set ``{}``
112+cookies set ``{}``
113+verify boolean ``True``
114+headers.User-Agent string a random User-Agent
115+category string current category, like ``'general'``
116+started datetime current date-time
117+pageno int current pagenumber
118+language string specific language code like ``'en_US'``, or ``'all'`` if unspecified
119+====================== ============ ========================================================================
120+
121+parsed arguments
122+----------------
123+
124+The function ``def request(query, params):`` always returns the ``params``
125+variable. Inside searx, the following paramters can be used to specify a search
126+request:
127+
128+============ =========== =========================================================
129+argument type information
130+============ =========== =========================================================
131+url string requested url
132+method string HTTP request method
133+headers set HTTP header information
134+data set HTTP data information (parsed if ``method != 'GET'``)
135+cookies set HTTP cookies
136+verify boolean Performing SSL-Validity check
137+============ =========== =========================================================
138+
139+
140+example code
141+------------
142+
143+.. code:: python
144+
145+ # search-url
146+ base_url = 'https://example.com/'
147+ search_string = 'search?{query}&page={page}'
148+
149+ # do search-request
150+ def request(query, params):
151+ search_path = search_string.format(
152+ query=urlencode({'q': query}),
153+ page=params['pageno'])
154+
155+ params['url'] = base_url + search_path
156+
157+ return params
158+
159+
160+returned results
161+================
162+
163+Searx is able to return results of different media-types. Currently the
164+following media-types are supported:
165+
166+- default_
167+- images_
168+- videos_
169+- torrent_
170+- map_
171+
172+To set another media-type as default, the parameter ``template`` must be set to
173+the desired type.
174+
175+default
176+-------
177+
178+========================= =====================================================
179+result-parameter information
180+========================= =====================================================
181+url string, url of the result
182+title string, title of the result
183+content string, general result-text
184+publishedDate :py:class:`datetime.datetime`, time of publish
185+========================= =====================================================
186+
187+images
188+------
189+
190+To use this template, the parameter:
191+
192+========================= =====================================================
193+result-parameter information
194+========================= =====================================================
195+template is set to ``images.html``
196+url string, url to the result site
197+title string, title of the result *(partly implemented)*
198+content *(partly implemented)*
199+publishedDate :py:class:`datetime.datetime`,
200+ time of publish *(partly implemented)*
201+img\_src string, url to the result image
202+thumbnail\_src string, url to a small-preview image
203+========================= =====================================================
204+
205+videos
206+------
207+
208+========================= =====================================================
209+result-parameter information
210+========================= =====================================================
211+template is set to ``videos.html``
212+url string, url of the result
213+title string, title of the result
214+content *(not implemented yet)*
215+publishedDate :py:class:`datetime.datetime`, time of publish
216+thumbnail string, url to a small-preview image
217+========================= =====================================================
218+
219+torrent
220+-------
221+
222+.. _magnetlink: https://en.wikipedia.org/wiki/Magnet_URI_scheme
223+
224+========================= =====================================================
225+result-parameter information
226+========================= =====================================================
227+template is set to ``torrent.html``
228+url string, url of the result
229+title string, title of the result
230+content string, general result-text
231+publishedDate :py:class:`datetime.datetime`,
232+ time of publish *(not implemented yet)*
233+seed int, number of seeder
234+leech int, number of leecher
235+filesize int, size of file in bytes
236+files int, number of files
237+magnetlink string, magnetlink_ of the result
238+torrentfile string, torrentfile of the result
239+========================= =====================================================
240+
241+
242+map
243+---
244+
245+========================= =====================================================
246+result-parameter information
247+========================= =====================================================
248+url string, url of the result
249+title string, title of the result
250+content string, general result-text
251+publishedDate :py:class:`datetime.datetime`, time of publish
252+latitude latitude of result (in decimal format)
253+longitude longitude of result (in decimal format)
254+boundingbox boundingbox of result (array of 4. values
255+ ``[lat-min, lat-max, lon-min, lon-max]``)
256+geojson geojson of result (http://geojson.org)
257+osm.type type of osm-object (if OSM-Result)
258+osm.id id of osm-object (if OSM-Result)
259+address.name name of object
260+address.road street name of object
261+address.house_number house number of object
262+address.locality city, place of object
263+address.postcode postcode of object
264+address.country country of object
265+========================= =====================================================
docs/dev/index.rstnew file+13
@@ -0,0 +1,13 @@
1+=======================
2+Developer documentation
3+=======================
4+
5+.. toctree::
6+ :maxdepth: 1
7+
8+ quickstart
9+ contribution_guide
10+ engine_overview
11+ search_api
12+ plugins
13+ translation
docs/dev/plugins.rstnew file+48
@@ -0,0 +1,48 @@
1+=======
2+Plugins
3+=======
4+
5+Plugins can extend or replace functionality of various components of searx.
6+
7+Example plugin
8+==============
9+
10+.. code:: python
11+
12+ name = 'Example plugin'
13+ description = 'This plugin extends the suggestions with the word "example"'
14+ default_on = False # disabled by default
15+
16+ js_dependencies = tuple() # optional, list of static js files
17+ css_dependencies = tuple() # optional, list of static css files
18+
19+
20+ # attach callback to the post search hook
21+ # request: flask request object
22+ # ctx: the whole local context of the post search hook
23+ def post_search(request, ctx):
24+ ctx['search'].suggestions.add('example')
25+ return True
26+
27+Plugin entry points
28+===================
29+
30+Entry points (hooks) define when a plugin runs. Right now only three hooks are
31+implemented. So feel free to implement a hook if it fits the behaviour of your
32+plugin.
33+
34+Pre search hook
35+---------------
36+
37+Runs BEFORE the search request. Function to implement: ``pre_search``
38+
39+Post search hook
40+----------------
41+
42+Runs AFTER the search request. Function to implement: ``post_search``
43+
44+Result hook
45+-----------
46+
47+Runs when a new result is added to the result list. Function to implement:
48+``on_result``
docs/dev/quickstart.rstnew file+110
@@ -0,0 +1,110 @@
1+.. _devquickstart:
2+
3+======================
4+Development Quickstart
5+======================
6+
7+This quickstart guide gets your environment set up with searx. Furthermore, it
8+gives a short introduction to the ``manage.sh`` script.
9+
10+How to setup your development environment
11+=========================================
12+
13+First, clone the source code of searx to the desired folder. In this case the
14+source is cloned to ``~/myprojects/searx``. Then create and activate the
15+searx-ve virtualenv and install the required packages using manage.sh.
16+
17+.. code:: sh
18+
19+ cd ~/myprojects
20+ git clone https://github.com/asciimoo/searx.git
21+ cd searx
22+ virtualenv searx-ve
23+ . ./searx-ve/bin/activate
24+ ./manage.sh update_dev_packages
25+
26+
27+How to run tests
28+================
29+
30+Tests can be run using the ``manage.sh`` script. Following tests and checks are
31+available:
32+
33+- Unit tests
34+- Selenium tests
35+- PEP8 validation
36+- Unit test coverage check
37+
38+For example unit tests are run with the command below:
39+
40+.. code:: sh
41+
42+ ./manage.sh unit_tests
43+
44+For further test options, please consult the help of the ``manage.sh`` script.
45+
46+
47+How to compile styles and javascript
48+====================================
49+
50+.. _less: http://lesscss.org/
51+.. _NodeJS: https://nodejs.org
52+
53+How to build styles
54+-------------------
55+
56+Less_ is required to build the styles of searx. Less_ can be installed using
57+either NodeJS_ or Apt.
58+
59+.. code:: sh
60+
61+ sudo -H apt-get install nodejs
62+ sudo -H npm install -g less
63+
64+OR
65+
66+.. code:: sh
67+
68+ sudo -H apt-get install node-less
69+
70+After satisfying the requirements styles can be build using ``manage.sh``
71+
72+.. code:: sh
73+
74+ ./manage.sh styles
75+
76+
77+How to build the source of the oscar theme
78+==========================================
79+
80+.. _grunt: https://gruntjs.com/
81+
82+Grunt_ must be installed in order to build the javascript sources. It depends on
83+NodeJS, so first Node has to be installed.
84+
85+.. code:: sh
86+
87+ sudo -H apt-get install nodejs
88+ sudo -H npm install -g grunt-cli
89+
90+After installing grunt, the files can be built using the following command:
91+
92+.. code:: sh
93+
94+ ./manage.sh grunt_build
95+
96+
97+Tips for debugging/development
98+==============================
99+
100+Turn on debug logging
101+ Whether you are working on a new engine or trying to eliminate a bug, it is
102+ always a good idea to turn on debug logging. When debug logging is enabled a
103+ stack trace appears, instead of the cryptic ``Internal Server Error``
104+ message. It can be turned on by setting ``debug: False`` to ``debug: True`` in
105+ :origin:`settings.yml <searx/settings.yml>`.
106+
107+Run ``./manage.sh tests`` before creating a PR.
108+ Failing build on Travis is common because of PEP8 checks. So a new commit
109+ must be created containing these format fixes. This phase can be skipped if
110+ ``./manage.sh tests`` is run locally before creating a PR.
docs/dev/search_api.rstnew file+112
@@ -0,0 +1,112 @@
1+==========
2+Search API
3+==========
4+
5+The search supports both ``GET`` and ``POST``.
6+
7+Furthermore, two enpoints ``/`` and ``/search`` are available for querying.
8+
9+
10+``GET /``
11+
12+``GET /search``
13+
14+Parameters
15+==========
16+
17+``q`` : required
18+ The search query. This string is passed to external search services. Thus,
19+ searx supports syntax of each search service. For example, ``site:github.com
20+ searx`` is a valid query for Google. However, if simply the query above is
21+ passed to any search engine which does not filter its results based on this
22+ syntax, you might not get the results you wanted.
23+
24+ See more at :ref:`search-syntax`
25+
26+``categories`` : optional
27+ Comma separated list, specifies the active search categories
28+
29+``engines``: optional
30+ Comma separated list, specifies the active search engines.
31+
32+``lang``: default ``all``
33+ Code of the language.
34+
35+``pageno``: default ``1``
36+ Search page number.
37+
38+``time_range``: optional
39+ [ ``day``, ``month``, ``year`` ]
40+
41+ Time range of search for engines which support it. See if an engine supports
42+ time range search in the preferences page of an instance.
43+
44+``format``: optional
45+ [ ``json``, ``csv``, ``rss`` ]
46+
47+ Output format of results.
48+
49+``results_on_new_tab``: default ``0``
50+ [ ``0``, ``1`` ]
51+
52+ Open search results on new tab.
53+
54+``image_proxy``: default ``False``
55+ [ ``True``, ``False`` ]
56+
57+ Proxy image results through searx.
58+
59+``autocomplete``: default *empty*
60+ [ ``google``, ``dbpedia``, ``duckduckgo``, ``startpage``, ``wikipedia`` ]
61+
62+ Service which completes words as you type.
63+
64+``safesearch``: default ``None``
65+ [ ``0``, ``1``, ``None`` ]
66+
67+ Filter search results of engines which support safe search. See if an engine
68+ supports safe search in the preferences page of an instance.
69+
70+``theme``: default ``oscar``
71+ [ ``oscar``, ``simple``, ``legacy``, ``pix-art``, ``courgette`` ]
72+
73+ Theme of instance.
74+
75+ Please note, available themes depend on an instance. It is possible that an
76+ instance administrator deleted, created or renamed themes on his/her instance.
77+ See the available options in the preferences page of the instance.
78+
79+``oscar-style``: default ``logicodev``
80+ [ ``pointhi``, ``logicodev`` ]
81+
82+ Style of Oscar theme. It is only parsed if the theme of an instance is
83+ ``oscar``.
84+
85+ Please note, available styles depend on an instance. It is possible that an
86+ instance administrator deleted, created or renamed styles on his/her
87+ instance. See the available options in the preferences page of the instance.
88+
89+``enabled_plugins``: optional
90+ List of enabled plugins.
91+
92+ :default: ``HTTPS_rewrite``, ``Self_Informations``,
93+ ``Search_on_category_select``, ``Tracker_URL_remover``
94+
95+ :values: [ ``DOAI_rewrite``, ``HTTPS_rewrite``, ``Infinite_scroll``,
96+ ``Vim-like_hotkeys``, ``Self_Informations``, ``Tracker_URL_remover``,
97+ ``Search_on_category_select`` ]
98+
99+``disabled_plugins``: optional
100+ List of disabled plugins.
101+
102+ :default: ``DOAI_rewrite``, ``Infinite_scroll``, ``Vim-like_hotkeys``
103+ :values: ``DOAI_rewrite``, ``HTTPS_rewrite``, ``Infinite_scroll``,
104+ ``Vim-like_hotkeys``, ``Self_Informations``, ``Tracker_URL_remover``,
105+ ``Search_on_category_select``
106+
107+``enabled_engines``: optional : *all* :origin:`engines <searx/engines>`
108+ List of enabled engines.
109+
110+``disabled_engines``: optional : *all* :origin:`engines <searx/engines>`
111+ List of disabled engines.
112+
docs/dev/translation.rstnew file+71
@@ -0,0 +1,71 @@
1+.. _translation:
2+
3+===========
4+Translation
5+===========
6+
7+.. _searx@transifex: https://www.transifex.com/asciimoo/searx/
8+
9+Translation currently takes place on `searx@transifex`_
10+
11+Requirements
12+============
13+
14+* Transifex account
15+* Installed CLI tool of Transifex
16+
17+Init Transifex project
18+======================
19+
20+After installing ``transifex`` using pip, run the following command to
21+initialize the project.
22+
23+.. code:: sh
24+
25+ tx init # Transifex instance: https://www.transifex.com/asciimoo/searx/
26+
27+
28+After ``$HOME/.transifexrc`` is created, get a Transifex API key and insert it
29+into the configuration file.
30+
31+Create a configuration file for ``tx`` named ``$HOME/.tx/config``.
32+
33+.. code:: ini
34+
35+ [main]
36+ host = https://www.transifex.com
37+ [searx.messagespo]
38+ file_filter = searx/translations/<lang>/LC_MESSAGES/messages.po
39+ source_file = messages.pot
40+ source_lang = en
41+ type = PO
42+
43+
44+Then run ``tx set``:
45+
46+.. code:: shell
47+
48+ tx set --auto-local -r searx.messagespo 'searx/translations/<lang>/LC_MESSAGES/messages.po' \
49+ --source-lang en --type PO --source-file messages.pot --execute
50+
51+
52+Update translations
53+===================
54+
55+To retrieve the latest translations, pull it from Transifex.
56+
57+.. code:: sh
58+
59+ tx pull -a
60+
61+Then check the new languages. If strings translated are not enough, delete those
62+folders, because those should not be compiled. Call the command below to compile
63+the ``.po`` files.
64+
65+.. code:: shell
66+
67+ pybabel compile -d searx/translations
68+
69+
70+After the compilation is finished commit the ``.po`` and ``.mo`` files and
71+create a PR.
docs/index.rstnew file+32
@@ -0,0 +1,32 @@
1+================
2+Welcome to searx
3+================
4+
5+Search without being tracked.
6+
7+.. sidebar:: Features
8+
9+ - Self hosted
10+ - No user tracking
11+ - No user profiling
12+ - About 70 supported search engines
13+ - Easy integration with any search engine
14+ - Cookies are not used by default
15+ - Secure, encrypted connections (HTTPS/SSL)
16+ - Hosted by organizations, such as *La Quadrature du Net*, which promote
17+ digital rights
18+
19+Searx is a free internet metasearch engine which aggregates results from more
20+than 70 search services. Users are neither tracked nor profiled. Additionally,
21+searx can be used over Tor for online anonymity.
22+
23+Get started with searx by using one of the :wiki:`Searx-instances`. If you
24+don't trust anyone, you can set up your own, see :ref:`installation`.
25+
26+.. toctree::
27+ :maxdepth: 2
28+
29+ user/index
30+ admin/index
31+ dev/index
32+ blog/index
docs/static/img/searx_logo_small.pngnew file

Binary file not shown.

docs/user/index.rstnew file+9
@@ -0,0 +1,9 @@
1+==================
2+User documentation
3+==================
4+
5+.. toctree::
6+ :maxdepth: 1
7+
8+ search_syntax
9+ own-instance
docs/user/own-instance.rstnew file+77
@@ -0,0 +1,77 @@
1+===========================
2+Why use a private instance?
3+===========================
4+
5+"Is it worth to run my own instance?" is a common question among searx users.
6+Before answering this question, see what options a searx user has.
7+
8+Public instances are open to everyone who has access to its URL. Usually, these
9+are operated by unknown parties (from the users' point of view). Private
10+instances can be used by a select group of people. It is for example a searx of
11+group of friends or a company which can be accessed through VPN. Also it can be
12+single user one which runs on the user's laptop.
13+
14+To gain more insight on how these instances work let's dive into how searx
15+protects its users.
16+
17+How does searx protect privacy?
18+===============================
19+
20+Searx protects the privacy of its users in multiple ways regardless of the type
21+of the instance (private, public). Removal of private data from search requests
22+comes in three forms:
23+
24+ 1. removal of private data from requests going to search services
25+ 2. not forwarding anything from a third party services through search services
26+ (e.g. advertisement)
27+ 3. removal of private data from requests going to the result pages
28+
29+Removing private data means not sending cookies to external search engines and
30+generating a random browser profile for every request. Thus, it does not matter
31+if a public or private instance handles the request, because it is anonymized in
32+both cases. IP addresses will be the IP of the instance. But searx can be
33+configured to use proxy or Tor. `Result proxy
34+<https://github.com/asciimoo/morty>`__ is supported, too.
35+
36+Searx does not serve ads or tracking content unlike most search services. So
37+private data is not forwarded to third parties who might monetize it. Besides
38+protecting users from search services, both referring page and search query are
39+hidden from visited result pages.
40+
41+
42+What are the consequences of using public instances?
43+----------------------------------------------------
44+
45+If someone uses a public instance, he/she has to trust the administrator of that
46+instance. This means that the user of the public instance does not know whether
47+his/her requests are logged, aggregated and sent or sold to a third party.
48+
49+Also, public instances without proper protection are more vulnerable to abusing
50+the search service, In this case the external service in exchange returns
51+CAPTCHAs or bans the IP of the instance. Thus, search requests return less
52+results.
53+
54+I see. What about private instances?
55+------------------------------------
56+
57+If users run their own instances, everything is in their control: the source
58+code, logging settings and private data. Unknown instance administrators do not
59+have to be trusted.
60+
61+Furthermore, as the default settings of their instance is editable, there is no
62+need to use cookies to tailor searx to their needs. So preferences will not be
63+reset to defaults when clearing browser cookies. As settings are stored on
64+their computer, it will not be accessible to others as long as their computer is
65+not compromised.
66+
67+Conclusion
68+==========
69+
70+Always use an instance which is operated by people you trust. The privacy
71+features of searx are available to users no matter what kind of instance they
72+use.
73+
74+If someone is on the go or just wants to try searx for the first time public
75+instances are the best choices. Additionally, public instance are making a
76+world a better place, because those who cannot or do not want to run an
77+instance, have access to a privacy respecting search service.
docs/user/search_syntax.rstnew file+42
@@ -0,0 +1,42 @@
1+
2+.. _search-syntax:
3+
4+=============
5+Search syntax
6+=============
7+
8+Searx allows you to modify the default categories, engines and search language
9+via the search query.
10+
11+Prefix ``!``
12+ to set Category/engine
13+
14+Prefix: ``:``
15+ to set language
16+
17+Prefix: ``?``
18+ to add engines and categories to the currently selected categories
19+
20+Abbrevations of the engines and languages are also accepted. Engine/category
21+modifiers are chainable and inclusive (e.g. with :search:`!it !ddg !wp qwer
22+<?q=%21it%20%21ddg%20%21wp%20qwer>` search in IT category **and** duckduckgo
23+**and** wikipedia for ``qwer``).
24+
25+See the :search:`/preferences page <preferences>` for the list of engines,
26+categories and languages.
27+
28+Examples
29+========
30+
31+Search in wikipedia for ``qwer``:
32+
33+- :search:`!wp qwer <?q=%21wp%20qwer>` or
34+- :search:`!wikipedia qwer :search:<?q=%21wikipedia%20qwer>`
35+
36+Image search:
37+
38+- :search:`!images Cthulhu <?q=%21images%20Cthulhu>`
39+
40+Custom language in wikipedia:
41+
42+- :search:`:hu !wp hackerspace <?q=%3Ahu%20%21wp%20hackerspace>`
manage.sh+8−2
@@ -18,12 +18,12 @@ ACTION="$1"
18update_packages() {
18update_packages() {
19 pip install --upgrade pip
19 pip install --upgrade pip
20 pip install --upgrade setuptools
20 pip install --upgrade setuptools
21− pip install -r "$BASE_DIR/requirements.txt"
21+ pip install -Ur "$BASE_DIR/requirements.txt"
22}
22}
23
23
24update_dev_packages() {
24update_dev_packages() {
25 update_packages
25 update_packages
26− pip install -r "$BASE_DIR/requirements-dev.txt"
26+ pip install -Ur "$BASE_DIR/requirements-dev.txt"
27}
27}
28
28
29install_geckodriver() {
29install_geckodriver() {
@@ -70,6 +70,11 @@ locales() {
70 pybabel compile -d "$SEARX_DIR/translations"
70 pybabel compile -d "$SEARX_DIR/translations"
71}
71}
72
72
73+update_useragents() {
74+ echo '[!] Updating user agent versions'
75+ python utils/fetch_firefox_version.py
76+}
77+
73pep8_check() {
78pep8_check() {
74 echo '[!] Running pep8 check'
79 echo '[!] Running pep8 check'
75 # ignored rules:
80 # ignored rules:
@@ -246,6 +251,7 @@ Commands
246 update_dev_packages - Check & update development and production dependency changes
251 update_dev_packages - Check & update development and production dependency changes
247 install_geckodriver - Download & install geckodriver if not already installed (required for robot_tests)
252 install_geckodriver - Download & install geckodriver if not already installed (required for robot_tests)
248 npm_packages - Download & install npm dependencies
253 npm_packages - Download & install npm dependencies
254+ update_useragents - Update useragents.json with the most recent versions of Firefox
249
255
250 Build
256 Build
251 -----
257 -----
requirements-dev.txt+3
@@ -1,3 +1,6 @@
1+pallets-sphinx-themes
2+Sphinx
3+sphinx-issues
1mock==2.0.0
4mock==2.0.0
2nose2[coverage_plugin]
5nose2[coverage_plugin]
3cov-core==1.15.0
6cov-core==1.15.0
searx/data/useragents.json+8−7
@@ -1,14 +1,15 @@
1{
1{
2− "ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
3 "versions": [
2 "versions": [
4− "61.0.1",
3+ "70.0.1",
5− "61.0",
4+ "70.0",
5+ "69.0.3",
6− "60.0.2",
6+ "69.0.2",
7− "60.0.1",
7+ "69.0.1",
8− "60.0"
8+ "69.0"
9 ],
9 ],
10 "os": [
10 "os": [
11 "Windows NT 10; WOW64",
11 "Windows NT 10; WOW64",
12 "X11; Linux x86_64"
12 "X11; Linux x86_64"
13− ]
13+ ],
14+ "ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}"
14}
15}
searx/engines/__init__.py+15−12
@@ -27,7 +27,7 @@ from json import loads
27from requests import get
27from requests import get
28from searx import settings
28from searx import settings
29from searx import logger
29from searx import logger
30−from searx.utils import load_module, match_language
30+from searx.utils import load_module, match_language, get_engine_from_settings
31
31
32
32
33logger = logger.getChild('engines')
33logger = logger.getChild('engines')
@@ -53,7 +53,8 @@ engine_default_args = {'paging': False,
53 'disabled': False,
53 'disabled': False,
54 'suspend_end_time': 0,
54 'suspend_end_time': 0,
55 'continuous_errors': 0,
55 'continuous_errors': 0,
56− 'time_range_support': False}
56+ 'time_range_support': False,
57+ 'offline': False}
57
58
58
59
59def load_engine(engine_data):
60def load_engine(engine_data):
@@ -128,14 +129,16 @@ def load_engine(engine_data):
128 engine.stats = {
129 engine.stats = {
129 'result_count': 0,
130 'result_count': 0,
130 'search_count': 0,
131 'search_count': 0,
131− 'page_load_time': 0,
132− 'page_load_count': 0,
133 'engine_time': 0,
132 'engine_time': 0,
134 'engine_time_count': 0,
133 'engine_time_count': 0,
135 'score_count': 0,
134 'score_count': 0,
136 'errors': 0
135 'errors': 0
137 }
136 }
138
137
138+ if not engine.offline:
139+ engine.stats['page_load_time'] = 0
140+ engine.stats['page_load_count'] = 0
141+
139 for category_name in engine.categories:
142 for category_name in engine.categories:
140 categories.setdefault(category_name, []).append(engine)
143 categories.setdefault(category_name, []).append(engine)
141
144
@@ -173,11 +176,6 @@ def get_engines_stats():
173 results_num = \
176 results_num = \
174 engine.stats['result_count'] / float(engine.stats['search_count'])
177 engine.stats['result_count'] / float(engine.stats['search_count'])
175
178
176− if engine.stats['page_load_count'] != 0:
177− load_times = engine.stats['page_load_time'] / float(engine.stats['page_load_count']) # noqa
178− else:
179− load_times = 0
180−
181 if engine.stats['engine_time_count'] != 0:
179 if engine.stats['engine_time_count'] != 0:
182 this_engine_time = engine.stats['engine_time'] / float(engine.stats['engine_time_count']) # noqa
180 this_engine_time = engine.stats['engine_time'] / float(engine.stats['engine_time_count']) # noqa
183 else:
181 else:
@@ -189,14 +187,19 @@ def get_engines_stats():
189 else:
187 else:
190 score = score_per_result = 0.0
188 score = score_per_result = 0.0
191
189
190+ if not engine.offline:
191+ load_times = 0
192+ if engine.stats['page_load_count'] != 0:
193+ load_times = engine.stats['page_load_time'] / float(engine.stats['page_load_count']) # noqa
192− max_pageload = max(load_times, max_pageload)
194+ max_pageload = max(load_times, max_pageload)
195+ pageloads.append({'avg': load_times, 'name': engine.name})
196+
193 max_engine_times = max(this_engine_time, max_engine_times)
197 max_engine_times = max(this_engine_time, max_engine_times)
194 max_results = max(results_num, max_results)
198 max_results = max(results_num, max_results)
195 max_score = max(score, max_score)
199 max_score = max(score, max_score)
196 max_score_per_result = max(score_per_result, max_score_per_result)
200 max_score_per_result = max(score_per_result, max_score_per_result)
197 max_errors = max(max_errors, engine.stats['errors'])
201 max_errors = max(max_errors, engine.stats['errors'])
198
202
199− pageloads.append({'avg': load_times, 'name': engine.name})
200 engine_times.append({'avg': this_engine_time, 'name': engine.name})
203 engine_times.append({'avg': this_engine_time, 'name': engine.name})
201 results.append({'avg': results_num, 'name': engine.name})
204 results.append({'avg': results_num, 'name': engine.name})
202 scores.append({'avg': score, 'name': engine.name})
205 scores.append({'avg': score, 'name': engine.name})
@@ -255,7 +258,7 @@ def initialize_engines(engine_list):
255 load_engines(engine_list)
258 load_engines(engine_list)
256
259
257 def engine_init(engine_name, init_fn):
260 def engine_init(engine_name, init_fn):
258− init_fn()
261+ init_fn(get_engine_from_settings(engine_name))
259 logger.debug('%s engine: Initialized', engine_name)
262 logger.debug('%s engine: Initialized', engine_name)
260
263
261 for engine_name, engine in engines.items():
264 for engine_name, engine in engines.items():
searx/engines/arxiv.py+1
@@ -17,6 +17,7 @@ from searx.url_utils import urlencode
17
17
18
18
19categories = ['science']
19categories = ['science']
20+paging = True
20
21
21base_url = 'http://export.arxiv.org/api/query?search_query=all:'\
22base_url = 'http://export.arxiv.org/api/query?search_query=all:'\
22 + '{query}&start={offset}&max_results={number_of_results}'
23 + '{query}&start={offset}&max_results={number_of_results}'
searx/engines/bing.py+10−10
@@ -18,7 +18,7 @@ from lxml import html
18from searx import logger, utils
18from searx import logger, utils
19from searx.engines.xpath import extract_text
19from searx.engines.xpath import extract_text
20from searx.url_utils import urlencode
20from searx.url_utils import urlencode
21−from searx.utils import match_language, gen_useragent
21+from searx.utils import match_language, gen_useragent, eval_xpath
22
22
23logger = logger.getChild('bing engine')
23logger = logger.getChild('bing engine')
24
24
@@ -65,11 +65,11 @@ def response(resp):
65
65
66 dom = html.fromstring(resp.text)
66 dom = html.fromstring(resp.text)
67 # parse results
67 # parse results
68− for result in dom.xpath('//div[@class="sa_cc"]'):
68+ for result in eval_xpath(dom, '//div[@class="sa_cc"]'):
69− link = result.xpath('.//h3/a')[0]
69+ link = eval_xpath(result, './/h3/a')[0]
70 url = link.attrib.get('href')
70 url = link.attrib.get('href')
71 title = extract_text(link)
71 title = extract_text(link)
72− content = extract_text(result.xpath('.//p'))
72+ content = extract_text(eval_xpath(result, './/p'))
73
73
74 # append result
74 # append result
75 results.append({'url': url,
75 results.append({'url': url,
@@ -77,11 +77,11 @@ def response(resp):
77 'content': content})
77 'content': content})
78
78
79 # parse results again if nothing is found yet
79 # parse results again if nothing is found yet
80− for result in dom.xpath('//li[@class="b_algo"]'):
80+ for result in eval_xpath(dom, '//li[@class="b_algo"]'):
81− link = result.xpath('.//h2/a')[0]
81+ link = eval_xpath(result, './/h2/a')[0]
82 url = link.attrib.get('href')
82 url = link.attrib.get('href')
83 title = extract_text(link)
83 title = extract_text(link)
84− content = extract_text(result.xpath('.//p'))
84+ content = extract_text(eval_xpath(result, './/p'))
85
85
86 # append result
86 # append result
87 results.append({'url': url,
87 results.append({'url': url,
@@ -89,7 +89,7 @@ def response(resp):
89 'content': content})
89 'content': content})
90
90
91 try:
91 try:
92− result_len_container = "".join(dom.xpath('//span[@class="sb_count"]/text()'))
92+ result_len_container = "".join(eval_xpath(dom, '//span[@class="sb_count"]/text()'))
93 result_len_container = utils.to_string(result_len_container)
93 result_len_container = utils.to_string(result_len_container)
94 if "-" in result_len_container:
94 if "-" in result_len_container:
95 # Remove the part "from-to" for paginated request ...
95 # Remove the part "from-to" for paginated request ...
@@ -113,9 +113,9 @@ def response(resp):
113def _fetch_supported_languages(resp):
113def _fetch_supported_languages(resp):
114 supported_languages = []
114 supported_languages = []
115 dom = html.fromstring(resp.text)
115 dom = html.fromstring(resp.text)
116− options = dom.xpath('//div[@id="limit-languages"]//input')
116+ options = eval_xpath(dom, '//div[@id="limit-languages"]//input')
117 for option in options:
117 for option in options:
118− code = option.xpath('./@id')[0].replace('_', '-')
118+ code = eval_xpath(option, './@id')[0].replace('_', '-')
119 if code == 'nb':
119 if code == 'nb':
120 code = 'no'
120 code = 'no'
121 supported_languages.append(code)
121 supported_languages.append(code)
searx/engines/deviantart.py+22−25
@@ -24,7 +24,7 @@ time_range_support = True
24
24
25# search-url
25# search-url
26base_url = 'https://www.deviantart.com/'
26base_url = 'https://www.deviantart.com/'
27−search_url = base_url + 'browse/all/?offset={offset}&{query}'
27+search_url = base_url + 'search?page={page}&{query}'
28time_range_url = '&order={range}'
28time_range_url = '&order={range}'
29
29
30time_range_dict = {'day': 11,
30time_range_dict = {'day': 11,
@@ -37,9 +37,7 @@ def request(query, params):
37 if params['time_range'] and params['time_range'] not in time_range_dict:
37 if params['time_range'] and params['time_range'] not in time_range_dict:
38 return params
38 return params
39
39
40− offset = (params['pageno'] - 1) * 24
41−
42− params['url'] = search_url.format(offset=offset,
40+ params['url'] = search_url.format(page=params['pageno'],
43 query=urlencode({'q': query}))
41 query=urlencode({'q': query}))
44 if params['time_range'] in time_range_dict:
42 if params['time_range'] in time_range_dict:
45 params['url'] += time_range_url.format(range=time_range_dict[params['time_range']])
43 params['url'] += time_range_url.format(range=time_range_dict[params['time_range']])
@@ -57,28 +55,27 @@ def response(resp):
57
55
58 dom = html.fromstring(resp.text)
56 dom = html.fromstring(resp.text)
59
57
60− regex = re.compile(r'\/200H\/')
61−
62 # parse results
58 # parse results
63− for result in dom.xpath('.//span[@class="thumb wide"]'):
59+ for row in dom.xpath('//div[contains(@data-hook, "content_row")]'):
60+ for result in row.xpath('./div'):
64− link = result.xpath('.//a[@class="torpedo-thumb-link"]')[0]
61+ link = result.xpath('.//a[@data-hook="deviation_link"]')[0]
65− url = link.attrib.get('href')
62+ url = link.attrib.get('href')
66− title = extract_text(result.xpath('.//span[@class="title"]'))
63+ title = link.attrib.get('title')
67− thumbnail_src = link.xpath('.//img')[0].attrib.get('src')
64+ thumbnail_src = result.xpath('.//img')[0].attrib.get('src')
68− img_src = regex.sub('/', thumbnail_src)
65+ img_src = thumbnail_src
69−
66+
70− # http to https, remove domain sharding
67+ # http to https, remove domain sharding
71− thumbnail_src = re.sub(r"https?://(th|fc)\d+.", "https://th01.", thumbnail_src)
68+ thumbnail_src = re.sub(r"https?://(th|fc)\d+.", "https://th01.", thumbnail_src)
72− thumbnail_src = re.sub(r"http://", "https://", thumbnail_src)
69+ thumbnail_src = re.sub(r"http://", "https://", thumbnail_src)
73−
70+
74− url = re.sub(r"http://(.*)\.deviantart\.com/", "https://\\1.deviantart.com/", url)
71+ url = re.sub(r"http://(.*)\.deviantart\.com/", "https://\\1.deviantart.com/", url)
75−
72+
76− # append result
73+ # append result
77− results.append({'url': url,
74+ results.append({'url': url,
78− 'title': title,
75+ 'title': title,
79− 'img_src': img_src,
76+ 'img_src': img_src,
80− 'thumbnail_src': thumbnail_src,
77+ 'thumbnail_src': thumbnail_src,
81− 'template': 'images.html'})
78+ 'template': 'images.html'})
82
79
83 # return results
80 # return results
84 return results
81 return results
searx/engines/dictzone.py+4−4
@@ -11,7 +11,7 @@
11
11
12import re
12import re
13from lxml import html
13from lxml import html
14−from searx.utils import is_valid_lang
14+from searx.utils import is_valid_lang, eval_xpath
15from searx.url_utils import urljoin
15from searx.url_utils import urljoin
16
16
17categories = ['general']
17categories = ['general']
@@ -47,14 +47,14 @@ def response(resp):
47
47
48 dom = html.fromstring(resp.text)
48 dom = html.fromstring(resp.text)
49
49
50− for k, result in enumerate(dom.xpath(results_xpath)[1:]):
50+ for k, result in enumerate(eval_xpath(dom, results_xpath)[1:]):
51 try:
51 try:
52− from_result, to_results_raw = result.xpath('./td')
52+ from_result, to_results_raw = eval_xpath(result, './td')
53 except:
53 except:
54 continue
54 continue
55
55
56 to_results = []
56 to_results = []
57− for to_result in to_results_raw.xpath('./p/a'):
57+ for to_result in eval_xpath(to_results_raw, './p/a'):
58 t = to_result.text_content()
58 t = to_result.text_content()
59 if t.strip():
59 if t.strip():
60 to_results.append(to_result.text_content())
60 to_results.append(to_result.text_content())
searx/engines/digg.py+12−24
@@ -15,7 +15,8 @@ import string
15from dateutil import parser
15from dateutil import parser
16from json import loads
16from json import loads
17from lxml import html
17from lxml import html
18−from searx.url_utils import quote_plus
18+from searx.url_utils import urlencode
19+from datetime import datetime
19
20
20# engine dependent config
21# engine dependent config
21categories = ['news', 'social media']
22categories = ['news', 'social media']
@@ -23,7 +24,7 @@ paging = True
23
24
24# search-url
25# search-url
25base_url = 'https://digg.com/'
26base_url = 'https://digg.com/'
26−search_url = base_url + 'api/search/{query}.json?position={position}&format=html'
27+search_url = base_url + 'api/search/?{query}&from={position}&size=20&format=html'
27
28
28# specific xpath variables
29# specific xpath variables
29results_xpath = '//article'
30results_xpath = '//article'
@@ -38,9 +39,9 @@ digg_cookie_chars = string.ascii_uppercase + string.ascii_lowercase +\
38
39
39# do search-request
40# do search-request
40def request(query, params):
41def request(query, params):
41− offset = (params['pageno'] - 1) * 10
42+ offset = (params['pageno'] - 1) * 20
42 params['url'] = search_url.format(position=offset,
43 params['url'] = search_url.format(position=offset,
43− query=quote_plus(query))
44+ query=urlencode({'q': query}))
44 params['cookies']['frontend.auid'] = ''.join(random.choice(
45 params['cookies']['frontend.auid'] = ''.join(random.choice(
45 digg_cookie_chars) for _ in range(22))
46 digg_cookie_chars) for _ in range(22))
46 return params
47 return params
@@ -52,30 +53,17 @@ def response(resp):
52
53
53 search_result = loads(resp.text)
54 search_result = loads(resp.text)
54
55
55− if 'html' not in search_result or search_result['html'] == '':
56− return results
57−
58− dom = html.fromstring(search_result['html'])
59−
60 # parse results
56 # parse results
61− for result in dom.xpath(results_xpath):
57+ for result in search_result['mapped']:
62− url = result.attrib.get('data-contenturl')
63− thumbnail = result.xpath('.//img')[0].attrib.get('src')
64− title = ''.join(result.xpath(title_xpath))
65− content = ''.join(result.xpath(content_xpath))
66− pubdate = result.xpath(pubdate_xpath)[0].attrib.get('datetime')
67− publishedDate = parser.parse(pubdate)
68−
69− # http to https
70− thumbnail = thumbnail.replace("http://static.digg.com", "https://static.digg.com")
71
58
59+ published = datetime.strptime(result['created']['ISO'], "%Y-%m-%d %H:%M:%S")
72 # append result
60 # append result
73− results.append({'url': url,
61+ results.append({'url': result['url'],
74− 'title': title,
62+ 'title': result['title'],
75− 'content': content,
63+ 'content': result['excerpt'],
76 'template': 'videos.html',
64 'template': 'videos.html',
77− 'publishedDate': publishedDate,
65+ 'publishedDate': published,
78− 'thumbnail': thumbnail})
66+ 'thumbnail': result['images']['thumbImage']})
79
67
80 # return results
68 # return results
81 return results
69 return results
searx/engines/doku.py+8−7
@@ -11,6 +11,7 @@
11
11
12from lxml.html import fromstring
12from lxml.html import fromstring
13from searx.engines.xpath import extract_text
13from searx.engines.xpath import extract_text
14+from searx.utils import eval_xpath
14from searx.url_utils import urlencode
15from searx.url_utils import urlencode
15
16
16# engine dependent config
17# engine dependent config
@@ -45,16 +46,16 @@ def response(resp):
45
46
46 # parse results
47 # parse results
47 # Quickhits
48 # Quickhits
48− for r in doc.xpath('//div[@class="search_quickresult"]/ul/li'):
49+ for r in eval_xpath(doc, '//div[@class="search_quickresult"]/ul/li'):
49 try:
50 try:
50− res_url = r.xpath('.//a[@class="wikilink1"]/@href')[-1]
51+ res_url = eval_xpath(r, './/a[@class="wikilink1"]/@href')[-1]
51 except:
52 except:
52 continue
53 continue
53
54
54 if not res_url:
55 if not res_url:
55 continue
56 continue
56
57
57− title = extract_text(r.xpath('.//a[@class="wikilink1"]/@title'))
58+ title = extract_text(eval_xpath(r, './/a[@class="wikilink1"]/@title'))
58
59
59 # append result
60 # append result
60 results.append({'title': title,
61 results.append({'title': title,
@@ -62,13 +63,13 @@ def response(resp):
62 'url': base_url + res_url})
63 'url': base_url + res_url})
63
64
64 # Search results
65 # Search results
65− for r in doc.xpath('//dl[@class="search_results"]/*'):
66+ for r in eval_xpath(doc, '//dl[@class="search_results"]/*'):
66 try:
67 try:
67 if r.tag == "dt":
68 if r.tag == "dt":
68− res_url = r.xpath('.//a[@class="wikilink1"]/@href')[-1]
69+ res_url = eval_xpath(r, './/a[@class="wikilink1"]/@href')[-1]
69− title = extract_text(r.xpath('.//a[@class="wikilink1"]/@title'))
70+ title = extract_text(eval_xpath(r, './/a[@class="wikilink1"]/@title'))
70 elif r.tag == "dd":
71 elif r.tag == "dd":
71− content = extract_text(r.xpath('.'))
72+ content = extract_text(eval_xpath(r, '.'))
72
73
73 # append result
74 # append result
74 results.append({'title': title,
75 results.append({'title': title,
searx/engines/duckduckgo.py+30−13
@@ -18,7 +18,7 @@ from json import loads
18from searx.engines.xpath import extract_text
18from searx.engines.xpath import extract_text
19from searx.poolrequests import get
19from searx.poolrequests import get
20from searx.url_utils import urlencode
20from searx.url_utils import urlencode
21−from searx.utils import match_language
21+from searx.utils import match_language, eval_xpath
22
22
23# engine dependent config
23# engine dependent config
24categories = ['general']
24categories = ['general']
@@ -65,21 +65,36 @@ def get_region_code(lang, lang_list=[]):
65
65
66
66
67def request(query, params):
67def request(query, params):
68− if params['time_range'] and params['time_range'] not in time_range_dict:
68+ if params['time_range'] not in (None, 'None', '') and params['time_range'] not in time_range_dict:
69 return params
69 return params
70
70
71 offset = (params['pageno'] - 1) * 30
71 offset = (params['pageno'] - 1) * 30
72
72
73 region_code = get_region_code(params['language'], supported_languages)
73 region_code = get_region_code(params['language'], supported_languages)
74− if region_code:
74+ params['url'] = 'https://duckduckgo.com/html/'
75− params['url'] = url.format(
75+ if params['pageno'] > 1:
76− query=urlencode({'q': query, 'kl': region_code}), offset=offset, dc_param=offset)
76+ params['method'] = 'POST'
77+ params['data']['q'] = query
78+ params['data']['s'] = offset
79+ params['data']['dc'] = 30
80+ params['data']['nextParams'] = ''
81+ params['data']['v'] = 'l'
82+ params['data']['o'] = 'json'
83+ params['data']['api'] = '/d.js'
84+ if params['time_range'] in time_range_dict:
85+ params['data']['df'] = time_range_dict[params['time_range']]
86+ if region_code:
87+ params['data']['kl'] = region_code
77 else:
88 else:
89+ if region_code:
78− params['url'] = url.format(
90+ params['url'] = url.format(
91+ query=urlencode({'q': query, 'kl': region_code}), offset=offset, dc_param=offset)
92+ else:
93+ params['url'] = url.format(
79− query=urlencode({'q': query}), offset=offset, dc_param=offset)
94+ query=urlencode({'q': query}), offset=offset, dc_param=offset)
80
95
81− if params['time_range'] in time_range_dict:
96+ if params['time_range'] in time_range_dict:
82− params['url'] += time_range_url.format(range=time_range_dict[params['time_range']])
97+ params['url'] += time_range_url.format(range=time_range_dict[params['time_range']])
83
98
84 return params
99 return params
85
100
@@ -91,17 +106,19 @@ def response(resp):
91 doc = fromstring(resp.text)
106 doc = fromstring(resp.text)
92
107
93 # parse results
108 # parse results
94− for r in doc.xpath(result_xpath):
109+ for i, r in enumerate(eval_xpath(doc, result_xpath)):
110+ if i >= 30:
111+ break
95 try:
112 try:
96− res_url = r.xpath(url_xpath)[-1]
113+ res_url = eval_xpath(r, url_xpath)[-1]
97 except:
114 except:
98 continue
115 continue
99
116
100 if not res_url:
117 if not res_url:
101 continue
118 continue
102
119
103− title = extract_text(r.xpath(title_xpath))
120+ title = extract_text(eval_xpath(r, title_xpath))
104− content = extract_text(r.xpath(content_xpath))
121+ content = extract_text(eval_xpath(r, content_xpath))
105
122
106 # append result
123 # append result
107 results.append({'title': title,
124 results.append({'title': title,
searx/engines/duckduckgo_definitions.py+16−3
@@ -1,3 +1,14 @@
1+"""
2+DuckDuckGo (definitions)
3+
4+- `Instant Answer API`_
5+- `DuckDuckGo query`_
6+
7+.. _Instant Answer API: https://duckduckgo.com/api
8+.. _DuckDuckGo query: https://api.duckduckgo.com/?q=DuckDuckGo&format=json&pretty=1
9+
10+"""
11+
1import json
12import json
2from lxml import html
13from lxml import html
3from re import compile
14from re import compile
@@ -25,7 +36,8 @@ def result_to_text(url, text, htmlResult):
25def request(query, params):
36def request(query, params):
26 params['url'] = url.format(query=urlencode({'q': query}))
37 params['url'] = url.format(query=urlencode({'q': query}))
27 language = match_language(params['language'], supported_languages, language_aliases)
38 language = match_language(params['language'], supported_languages, language_aliases)
28− params['headers']['Accept-Language'] = language.split('-')[0]
39+ language = language.split('-')[0]
40+ params['headers']['Accept-Language'] = language
29 return params
41 return params
30
42
31
43
@@ -43,8 +55,9 @@ def response(resp):
43
55
44 # add answer if there is one
56 # add answer if there is one
45 answer = search_res.get('Answer', '')
57 answer = search_res.get('Answer', '')
46− if answer != '':
58+ if answer:
59+ if search_res.get('AnswerType', '') not in ['calc']:
47− results.append({'answer': html_to_text(answer)})
60+ results.append({'answer': html_to_text(answer)})
48
61
49 # add infobox
62 # add infobox
50 if 'Definition' in search_res:
63 if 'Definition' in search_res:
searx/engines/duden.py+8−7
@@ -11,6 +11,7 @@
11from lxml import html, etree
11from lxml import html, etree
12import re
12import re
13from searx.engines.xpath import extract_text
13from searx.engines.xpath import extract_text
14+from searx.utils import eval_xpath
14from searx.url_utils import quote, urljoin
15from searx.url_utils import quote, urljoin
15from searx import logger
16from searx import logger
16
17
@@ -52,9 +53,9 @@ def response(resp):
52 dom = html.fromstring(resp.text)
53 dom = html.fromstring(resp.text)
53
54
54 try:
55 try:
55− number_of_results_string = re.sub('[^0-9]', '', dom.xpath(
56+ number_of_results_string =\
57+ re.sub('[^0-9]', '',
56− '//a[@class="active" and contains(@href,"/suchen/dudenonline")]/span/text()')[0]
58+ eval_xpath(dom, '//a[@class="active" and contains(@href,"/suchen/dudenonline")]/span/text()')[0])
57− )
58
59
59 results.append({'number_of_results': int(number_of_results_string)})
60 results.append({'number_of_results': int(number_of_results_string)})
60
61
@@ -62,12 +63,12 @@ def response(resp):
62 logger.debug("Couldn't read number of results.")
63 logger.debug("Couldn't read number of results.")
63 pass
64 pass
64
65
65− for result in dom.xpath('//section[not(contains(@class, "essay"))]'):
66+ for result in eval_xpath(dom, '//section[not(contains(@class, "essay"))]'):
66 try:
67 try:
67− url = result.xpath('.//h2/a')[0].get('href')
68+ url = eval_xpath(result, './/h2/a')[0].get('href')
68 url = urljoin(base_url, url)
69 url = urljoin(base_url, url)
69− title = result.xpath('string(.//h2/a)').strip()
70+ title = eval_xpath(result, 'string(.//h2/a)').strip()
70− content = extract_text(result.xpath('.//p'))
71+ content = extract_text(eval_xpath(result, './/p'))
71 # append result
72 # append result
72 results.append({'url': url,
73 results.append({'url': url,
73 'title': title,
74 'title': title,
searx/engines/framalibre.py+4−1
@@ -10,7 +10,10 @@
10 @parse url, title, content, thumbnail, img_src
10 @parse url, title, content, thumbnail, img_src
11"""
11"""
12
12
13+try:
13−from cgi import escape
14+ from cgi import escape
15+except:
16+ from html import escape
14from lxml import html
17from lxml import html
15from searx.engines.xpath import extract_text
18from searx.engines.xpath import extract_text
16from searx.url_utils import urljoin, urlencode
19from searx.url_utils import urljoin, urlencode
searx/engines/gigablast.py+30−13
@@ -14,7 +14,9 @@ import random
14from json import loads
14from json import loads
15from time import time
15from time import time
16from lxml.html import fromstring
16from lxml.html import fromstring
17+from searx.poolrequests import get
17from searx.url_utils import urlencode
18from searx.url_utils import urlencode
19+from searx.utils import eval_xpath
18
20
19# engine dependent config
21# engine dependent config
20categories = ['general']
22categories = ['general']
@@ -30,13 +32,9 @@ search_string = 'search?{query}'\
30 '&c=main'\
32 '&c=main'\
31 '&s={offset}'\
33 '&s={offset}'\
32 '&format=json'\
34 '&format=json'\
33− '&qh=0'\
35+ '&langcountry={lang}'\
34− '&qlang={lang}'\
35 '&ff={safesearch}'\
36 '&ff={safesearch}'\
36− '&rxiec={rxieu}'\
37− '&ulse={ulse}'\
38− '&rand={rxikd}' # current unix timestamp
37+ '&rand={rxikd}'
39−
40# specific xpath variables
38# specific xpath variables
41results_xpath = '//response//result'
39results_xpath = '//response//result'
42url_xpath = './/url'
40url_xpath = './/url'
@@ -45,9 +43,26 @@ content_xpath = './/sum'
45
43
46supported_languages_url = 'https://gigablast.com/search?&rxikd=1'
44supported_languages_url = 'https://gigablast.com/search?&rxikd=1'
47
45
46+extra_param = '' # gigablast requires a random extra parameter
47+# which can be extracted from the source code of the search page
48+
49+
50+def parse_extra_param(text):
51+ global extra_param
52+ param_lines = [x for x in text.splitlines() if x.startswith('var url=') or x.startswith('url=url+')]
53+ extra_param = ''
54+ for l in param_lines:
55+ extra_param += l.split("'")[1]
56+ extra_param = extra_param.split('&')[-1]
57+
58+
59+def init(engine_settings=None):
60+ parse_extra_param(get('http://gigablast.com/search?c=main&qlangcountry=en-us&q=south&s=10').text)
61+
48
62
49# do search-request
63# do search-request
50def request(query, params):
64def request(query, params):
65+ print("EXTRAPARAM:", extra_param)
51 offset = (params['pageno'] - 1) * number_of_results
66 offset = (params['pageno'] - 1) * number_of_results
52
67
53 if params['language'] == 'all':
68 if params['language'] == 'all':
@@ -66,13 +81,11 @@ def request(query, params):
66 search_path = search_string.format(query=urlencode({'q': query}),
81 search_path = search_string.format(query=urlencode({'q': query}),
67 offset=offset,
82 offset=offset,
68 number_of_results=number_of_results,
83 number_of_results=number_of_results,
69− rxikd=int(time() * 1000),
70− rxieu=random.randint(1000000000, 9999999999),
71− ulse=random.randint(100000000, 999999999),
72 lang=language,
84 lang=language,
85+ rxikd=int(time() * 1000),
73 safesearch=safesearch)
86 safesearch=safesearch)
74
87
75− params['url'] = base_url + search_path
88+ params['url'] = base_url + search_path + '&' + extra_param
76
89
77 return params
90 return params
78
91
@@ -82,7 +95,11 @@ def response(resp):
82 results = []
95 results = []
83
96
84 # parse results
97 # parse results
98+ try:
85− response_json = loads(resp.text)
99+ response_json = loads(resp.text)
100+ except:
101+ parse_extra_param(resp.text)
102+ raise Exception('extra param expired, please reload')
86
103
87 for result in response_json['results']:
104 for result in response_json['results']:
88 # append result
105 # append result
@@ -98,9 +115,9 @@ def response(resp):
98def _fetch_supported_languages(resp):
115def _fetch_supported_languages(resp):
99 supported_languages = []
116 supported_languages = []
100 dom = fromstring(resp.text)
117 dom = fromstring(resp.text)
101− links = dom.xpath('//span[@id="menu2"]/a')
118+ links = eval_xpath(dom, '//span[@id="menu2"]/a')
102 for link in links:
119 for link in links:
103− href = link.xpath('./@href')[0].split('lang%3A')
120+ href = eval_xpath(link, './@href')[0].split('lang%3A')
104 if len(href) == 2:
121 if len(href) == 2:
105 code = href[1].split('_')
122 code = href[1].split('_')
106 if len(code) == 2:
123 if len(code) == 2:
searx/engines/google.py+23−29
@@ -14,7 +14,7 @@ from lxml import html, etree
14from searx.engines.xpath import extract_text, extract_url
14from searx.engines.xpath import extract_text, extract_url
15from searx import logger
15from searx import logger
16from searx.url_utils import urlencode, urlparse, parse_qsl
16from searx.url_utils import urlencode, urlparse, parse_qsl
17−from searx.utils import match_language
17+from searx.utils import match_language, eval_xpath
18
18
19logger = logger.getChild('google engine')
19logger = logger.getChild('google engine')
20
20
@@ -107,13 +107,12 @@ images_path = '/images'
107supported_languages_url = 'https://www.google.com/preferences?#languages'
107supported_languages_url = 'https://www.google.com/preferences?#languages'
108
108
109# specific xpath variables
109# specific xpath variables
110−results_xpath = '//div[@class="g"]'
110+results_xpath = '//div[contains(@class, "ZINbbc")]'
111−url_xpath = './/h3/a/@href'
111+url_xpath = './/div[@class="kCrYT"][1]/a/@href'
112−title_xpath = './/h3'
112+title_xpath = './/div[@class="kCrYT"][1]/a/div[1]'
113−content_xpath = './/span[@class="st"]'
113+content_xpath = './/div[@class="kCrYT"][2]//div[contains(@class, "BNeawe")]//div[contains(@class, "BNeawe")]'
114−content_misc_xpath = './/div[@class="f slp"]'
115−suggestion_xpath = '//p[@class="_Bmc"]'
114+suggestion_xpath = '//div[contains(@class, "ZINbbc")][last()]//div[@class="rVLSBd"]/a//div[contains(@class, "BNeawe")]'
116−spelling_suggestion_xpath = '//a[@class="spell"]'
115+spelling_suggestion_xpath = '//div[@id="scc"]//a'
117
116
118# map : detail location
117# map : detail location
119map_address_xpath = './/div[@class="s"]//table//td[2]/span/text()'
118map_address_xpath = './/div[@class="s"]//table//td[2]/span/text()'
@@ -156,7 +155,7 @@ def parse_url(url_string, google_hostname):
156
155
157# returns extract_text on the first result selected by the xpath or None
156# returns extract_text on the first result selected by the xpath or None
158def extract_text_from_dom(result, xpath):
157def extract_text_from_dom(result, xpath):
159− r = result.xpath(xpath)
158+ r = eval_xpath(result, xpath)
160 if len(r) > 0:
159 if len(r) > 0:
161 return extract_text(r[0])
160 return extract_text(r[0])
162 return None
161 return None
@@ -199,9 +198,6 @@ def request(query, params):
199 params['headers']['Accept-Language'] = language + ',' + language + '-' + country
198 params['headers']['Accept-Language'] = language + ',' + language + '-' + country
200 params['headers']['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
199 params['headers']['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
201
200
202− # Force Internet Explorer 12 user agent to avoid loading the new UI that Searx can't parse
203− params['headers']['User-Agent'] = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
204−
205 params['google_hostname'] = google_hostname
201 params['google_hostname'] = google_hostname
206
202
207 return params
203 return params
@@ -226,21 +222,21 @@ def response(resp):
226 # convert the text to dom
222 # convert the text to dom
227 dom = html.fromstring(resp.text)
223 dom = html.fromstring(resp.text)
228
224
229− instant_answer = dom.xpath('//div[@id="_vBb"]//text()')
225+ instant_answer = eval_xpath(dom, '//div[@id="_vBb"]//text()')
230 if instant_answer:
226 if instant_answer:
231 results.append({'answer': u' '.join(instant_answer)})
227 results.append({'answer': u' '.join(instant_answer)})
232 try:
228 try:
233− results_num = int(dom.xpath('//div[@id="resultStats"]//text()')[0]
229+ results_num = int(eval_xpath(dom, '//div[@id="resultStats"]//text()')[0]
234 .split()[1].replace(',', ''))
230 .split()[1].replace(',', ''))
235 results.append({'number_of_results': results_num})
231 results.append({'number_of_results': results_num})
236 except:
232 except:
237 pass
233 pass
238
234
239 # parse results
235 # parse results
240− for result in dom.xpath(results_xpath):
236+ for result in eval_xpath(dom, results_xpath):
241 try:
237 try:
242− title = extract_text(result.xpath(title_xpath)[0])
238+ title = extract_text(eval_xpath(result, title_xpath)[0])
243− url = parse_url(extract_url(result.xpath(url_xpath), google_url), google_hostname)
239+ url = parse_url(extract_url(eval_xpath(result, url_xpath), google_url), google_hostname)
244 parsed_url = urlparse(url, google_hostname)
240 parsed_url = urlparse(url, google_hostname)
245
241
246 # map result
242 # map result
@@ -249,7 +245,7 @@ def response(resp):
249 continue
245 continue
250 # if parsed_url.path.startswith(maps_path) or parsed_url.netloc.startswith(map_hostname_start):
246 # if parsed_url.path.startswith(maps_path) or parsed_url.netloc.startswith(map_hostname_start):
251 # print "yooooo"*30
247 # print "yooooo"*30
252− # x = result.xpath(map_near)
248+ # x = eval_xpath(result, map_near)
253 # if len(x) > 0:
249 # if len(x) > 0:
254 # # map : near the location
250 # # map : near the location
255 # results = results + parse_map_near(parsed_url, x, google_hostname)
251 # results = results + parse_map_near(parsed_url, x, google_hostname)
@@ -273,9 +269,7 @@ def response(resp):
273 content = extract_text_from_dom(result, content_xpath)
269 content = extract_text_from_dom(result, content_xpath)
274 if content is None:
270 if content is None:
275 continue
271 continue
276− content_misc = extract_text_from_dom(result, content_misc_xpath)
272+
277− if content_misc is not None:
278− content = content_misc + "<br />" + content
279 # append result
273 # append result
280 results.append({'url': url,
274 results.append({'url': url,
281 'title': title,
275 'title': title,
@@ -286,11 +280,11 @@ def response(resp):
286 continue
280 continue
287
281
288 # parse suggestion
282 # parse suggestion
289− for suggestion in dom.xpath(suggestion_xpath):
283+ for suggestion in eval_xpath(dom, suggestion_xpath):
290 # append suggestion
284 # append suggestion
291 results.append({'suggestion': extract_text(suggestion)})
285 results.append({'suggestion': extract_text(suggestion)})
292
286
293− for correction in dom.xpath(spelling_suggestion_xpath):
287+ for correction in eval_xpath(dom, spelling_suggestion_xpath):
294 results.append({'correction': extract_text(correction)})
288 results.append({'correction': extract_text(correction)})
295
289
296 # return results
290 # return results
@@ -299,9 +293,9 @@ def response(resp):
299
293
300def parse_images(result, google_hostname):
294def parse_images(result, google_hostname):
301 results = []
295 results = []
302− for image in result.xpath(images_xpath):
296+ for image in eval_xpath(result, images_xpath):
303− url = parse_url(extract_text(image.xpath(image_url_xpath)[0]), google_hostname)
297+ url = parse_url(extract_text(eval_xpath(image, image_url_xpath)[0]), google_hostname)
304− img_src = extract_text(image.xpath(image_img_src_xpath)[0])
298+ img_src = extract_text(eval_xpath(image, image_img_src_xpath)[0])
305
299
306 # append result
300 # append result
307 results.append({'url': url,
301 results.append({'url': url,
@@ -388,10 +382,10 @@ def attributes_to_html(attributes):
388def _fetch_supported_languages(resp):
382def _fetch_supported_languages(resp):
389 supported_languages = {}
383 supported_languages = {}
390 dom = html.fromstring(resp.text)
384 dom = html.fromstring(resp.text)
391− options = dom.xpath('//*[@id="langSec"]//input[@name="lr"]')
385+ options = eval_xpath(dom, '//*[@id="langSec"]//input[@name="lr"]')
392 for option in options:
386 for option in options:
393− code = option.xpath('./@value')[0].split('_')[-1]
387+ code = eval_xpath(option, './@value')[0].split('_')[-1]
394− name = option.xpath('./@data-name')[0].title()
388+ name = eval_xpath(option, './@data-name')[0].title()
395 supported_languages[code] = {"name": name}
389 supported_languages[code] = {"name": name}
396
390
397 return supported_languages
391 return supported_languages
searx/engines/google_images.py+13−3
@@ -70,11 +70,21 @@ def response(resp):
70
70
71 try:
71 try:
72 metadata = loads(result)
72 metadata = loads(result)
73− img_format = "{0} {1}x{2}".format(metadata['ity'], str(metadata['ow']), str(metadata['oh']))
73+
74− source = "{0} ({1})".format(metadata['st'], metadata['isu'])
74+ img_format = metadata.get('ity', '')
75+ img_width = metadata.get('ow', '')
76+ img_height = metadata.get('oh', '')
77+ if img_width and img_height:
78+ img_format += " {0}x{1}".format(img_width, img_height)
79+
80+ source = metadata.get('st', '')
81+ source_url = metadata.get('isu', '')
82+ if source_url:
83+ source += " ({0})".format(source_url)
84+
75 results.append({'url': metadata['ru'],
85 results.append({'url': metadata['ru'],
76 'title': metadata['pt'],
86 'title': metadata['pt'],
77− 'content': metadata['s'],
87+ 'content': metadata.get('s', ''),
78 'source': source,
88 'source': source,
79 'img_format': img_format,
89 'img_format': img_format,
80 'thumbnail_src': metadata['tu'],
90 'thumbnail_src': metadata['tu'],
searx/engines/openstreetmap.py+1−1
@@ -24,7 +24,7 @@ result_base_url = 'https://openstreetmap.org/{osm_type}/{osm_id}'
24
24
25# do search-request
25# do search-request
26def request(query, params):
26def request(query, params):
27− params['url'] = base_url + search_string.format(query=query)
27+ params['url'] = base_url + search_string.format(query=query.decode('utf-8'))
28
28
29 return params
29 return params
30
30
searx/engines/qwant.py+1
@@ -50,6 +50,7 @@ def request(query, params):
50 language = match_language(params['language'], supported_languages, language_aliases)
50 language = match_language(params['language'], supported_languages, language_aliases)
51 params['url'] += '&locale=' + language.replace('-', '_').lower()
51 params['url'] += '&locale=' + language.replace('-', '_').lower()
52
52
53+ params['headers']['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0'
53 return params
54 return params
54
55
55
56
searx/engines/seedpeer.pynew file+78
@@ -0,0 +1,78 @@
1+# Seedpeer (Videos, Music, Files)
2+#
3+# @website https://seedpeer.me
4+# @provide-api no (nothing found)
5+#
6+# @using-api no
7+# @results HTML (using search portal)
8+# @stable yes (HTML can change)
9+# @parse url, title, content, seed, leech, magnetlink
10+
11+from lxml import html
12+from json import loads
13+from operator import itemgetter
14+from searx.url_utils import quote, urljoin
15+from searx.engines.xpath import extract_text
16+
17+
18+url = 'https://seedpeer.me/'
19+search_url = url + 'search/{search_term}?page={page_no}'
20+torrent_file_url = url + 'torrent/{torrent_hash}'
21+
22+# specific xpath variables
23+script_xpath = '//script[@type="text/javascript"][not(@src)]'
24+torrent_xpath = '(//table)[2]/tbody/tr'
25+link_xpath = '(./td)[1]/a/@href'
26+age_xpath = '(./td)[2]'
27+size_xpath = '(./td)[3]'
28+
29+
30+# do search-request
31+def request(query, params):
32+ params['url'] = search_url.format(search_term=quote(query),
33+ page_no=params['pageno'])
34+ return params
35+
36+
37+# get response from search-request
38+def response(resp):
39+ results = []
40+ dom = html.fromstring(resp.text)
41+ result_rows = dom.xpath(torrent_xpath)
42+
43+ try:
44+ script_element = dom.xpath(script_xpath)[0]
45+ json_string = script_element.text[script_element.text.find('{'):]
46+ torrents_json = loads(json_string)
47+ except:
48+ return []
49+
50+ # parse results
51+ for torrent_row, torrent_json in zip(result_rows, torrents_json['data']['list']):
52+ title = torrent_json['name']
53+ seed = int(torrent_json['seeds'])
54+ leech = int(torrent_json['peers'])
55+ size = int(torrent_json['size'])
56+ torrent_hash = torrent_json['hash']
57+
58+ torrentfile = torrent_file_url.format(torrent_hash=torrent_hash)
59+ magnetlink = 'magnet:?xt=urn:btih:{}'.format(torrent_hash)
60+
61+ age = extract_text(torrent_row.xpath(age_xpath))
62+ link = torrent_row.xpath(link_xpath)[0]
63+
64+ href = urljoin(url, link)
65+
66+ # append result
67+ results.append({'url': href,
68+ 'title': title,
69+ 'content': age,
70+ 'seed': seed,
71+ 'leech': leech,
72+ 'filesize': size,
73+ 'torrentfile': torrentfile,
74+ 'magnetlink': magnetlink,
75+ 'template': 'torrent.html'})
76+
77+ # return results sorted by seeder
78+ return sorted(results, key=itemgetter('seed'), reverse=True)
searx/engines/soundcloud.py+4−2
@@ -51,7 +51,9 @@ def get_client_id():
51
51
52 if response.ok:
52 if response.ok:
53 tree = html.fromstring(response.content)
53 tree = html.fromstring(response.content)
54+ # script_tags has been moved from /assets/app/ to /assets/ path. I
55+ # found client_id in https://a-v2.sndcdn.com/assets/49-a0c01933-3.js
54− script_tags = tree.xpath("//script[contains(@src, '/assets/app')]")
56+ script_tags = tree.xpath("//script[contains(@src, '/assets/')]")
55 app_js_urls = [script_tag.get('src') for script_tag in script_tags if script_tag is not None]
57 app_js_urls = [script_tag.get('src') for script_tag in script_tags if script_tag is not None]
56
58
57 # extracts valid app_js urls from soundcloud.com content
59 # extracts valid app_js urls from soundcloud.com content
@@ -66,7 +68,7 @@ def get_client_id():
66 return ""
68 return ""
67
69
68
70
69−def init():
71+def init(engine_settings=None):
70 global guest_client_id
72 global guest_client_id
71 # api-key
73 # api-key
72 guest_client_id = get_client_id()
74 guest_client_id = get_client_id()
searx/engines/startpage.py+23−12
@@ -15,6 +15,8 @@ from dateutil import parser
15from datetime import datetime, timedelta
15from datetime import datetime, timedelta
16import re
16import re
17from searx.engines.xpath import extract_text
17from searx.engines.xpath import extract_text
18+from searx.languages import language_codes
19+from searx.utils import eval_xpath
18
20
19# engine dependent config
21# engine dependent config
20categories = ['general']
22categories = ['general']
@@ -22,7 +24,7 @@ categories = ['general']
22# (probably the parameter qid), require
24# (probably the parameter qid), require
23# storing of qid's between mulitble search-calls
25# storing of qid's between mulitble search-calls
24
26
25−# paging = False
27+paging = True
26language_support = True
28language_support = True
27
29
28# search-url
30# search-url
@@ -32,23 +34,32 @@ search_url = base_url + 'do/search'
32# specific xpath variables
34# specific xpath variables
33# ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"]
35# ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"]
34# not ads: div[@class="result"] are the direct childs of div[@id="results"]
36# not ads: div[@class="result"] are the direct childs of div[@id="results"]
35−results_xpath = '//li[contains(@class, "search-result") and contains(@class, "search-item")]'
37+results_xpath = '//div[@class="w-gl__result"]'
36−link_xpath = './/h3/a'
38+link_xpath = './/a[@class="w-gl__result-title"]'
37−content_xpath = './p[@class="search-item__body"]'
39+content_xpath = './/p[@class="w-gl__description"]'
38
40
39
41
40# do search-request
42# do search-request
41def request(query, params):
43def request(query, params):
42− offset = (params['pageno'] - 1) * 10
43
44
44 params['url'] = search_url
45 params['url'] = search_url
45 params['method'] = 'POST'
46 params['method'] = 'POST'
46− params['data'] = {'query': query,
47+ params['data'] = {
47− 'startat': offset}
48+ 'query': query,
49+ 'page': params['pageno'],
50+ 'cat': 'web',
51+ 'cmd': 'process_search',
52+ 'engine0': 'v1all',
53+ }
48
54
49 # set language if specified
55 # set language if specified
50 if params['language'] != 'all':
56 if params['language'] != 'all':
51− params['data']['with_language'] = ('lang_' + params['language'].split('-')[0])
57+ language = 'english'
58+ for lc, _, _, lang in language_codes:
59+ if lc == params['language']:
60+ language = lang
61+ params['data']['language'] = language
62+ params['data']['lui'] = language
52
63
53 return params
64 return params
54
65
@@ -60,8 +71,8 @@ def response(resp):
60 dom = html.fromstring(resp.text)
71 dom = html.fromstring(resp.text)
61
72
62 # parse results
73 # parse results
63− for result in dom.xpath(results_xpath):
74+ for result in eval_xpath(dom, results_xpath):
64− links = result.xpath(link_xpath)
75+ links = eval_xpath(result, link_xpath)
65 if not links:
76 if not links:
66 continue
77 continue
67 link = links[0]
78 link = links[0]
@@ -77,8 +88,8 @@ def response(resp):
77
88
78 title = extract_text(link)
89 title = extract_text(link)
79
90
80− if result.xpath(content_xpath):
91+ if eval_xpath(result, content_xpath):
81− content = extract_text(result.xpath(content_xpath))
92+ content = extract_text(eval_xpath(result, content_xpath))
82 else:
93 else:
83 content = ''
94 content = ''
84
95
searx/engines/wikidata.py+1−17
@@ -16,7 +16,7 @@ from searx.poolrequests import get
16from searx.engines.xpath import extract_text
16from searx.engines.xpath import extract_text
17from searx.engines.wikipedia import _fetch_supported_languages, supported_languages_url
17from searx.engines.wikipedia import _fetch_supported_languages, supported_languages_url
18from searx.url_utils import urlencode
18from searx.url_utils import urlencode
19−from searx.utils import match_language
19+from searx.utils import match_language, eval_xpath
20
20
21from json import loads
21from json import loads
22from lxml.html import fromstring
22from lxml.html import fromstring
@@ -57,22 +57,6 @@ language_fallback_xpath = '//sup[contains(@class,"wb-language-fallback-indicator
57calendar_name_xpath = './/sup[contains(@class,"wb-calendar-name")]'
57calendar_name_xpath = './/sup[contains(@class,"wb-calendar-name")]'
58media_xpath = value_xpath + '//div[contains(@class,"commons-media-caption")]//a'
58media_xpath = value_xpath + '//div[contains(@class,"commons-media-caption")]//a'
59
59
60−# xpath_cache
61−xpath_cache = {}
62−
63−
64−def get_xpath(xpath_str):
65− result = xpath_cache.get(xpath_str, None)
66− if not result:
67− result = etree.XPath(xpath_str)
68− xpath_cache[xpath_str] = result
69− return result
70−
71−
72−def eval_xpath(element, xpath_str):
73− xpath = get_xpath(xpath_str)
74− return xpath(element)
75−
76
60
77def get_id_cache(result):
61def get_id_cache(result):
78 id_cache = {}
62 id_cache = {}
searx/engines/wikipedia.py+7−2
@@ -21,7 +21,8 @@ search_url = base_url + u'w/api.php?'\
21 'action=query'\
21 'action=query'\
22 '&format=json'\
22 '&format=json'\
23 '&{query}'\
23 '&{query}'\
24− '&prop=extracts|pageimages'\
24+ '&prop=extracts|pageimages|pageprops'\
25+ '&ppprop=disambiguation'\
25 '&exintro'\
26 '&exintro'\
26 '&explaintext'\
27 '&explaintext'\
27 '&pithumbsize=300'\
28 '&pithumbsize=300'\
@@ -79,12 +80,15 @@ def response(resp):
79
80
80 # wikipedia article's unique id
81 # wikipedia article's unique id
81 # first valid id is assumed to be the requested article
82 # first valid id is assumed to be the requested article
83+ if 'pages' not in search_result['query']:
84+ return results
85+
82 for article_id in search_result['query']['pages']:
86 for article_id in search_result['query']['pages']:
83 page = search_result['query']['pages'][article_id]
87 page = search_result['query']['pages'][article_id]
84 if int(article_id) > 0:
88 if int(article_id) > 0:
85 break
89 break
86
90
87− if int(article_id) < 0:
91+ if int(article_id) < 0 or 'disambiguation' in page.get('pageprops', {}):
88 return []
92 return []
89
93
90 title = page.get('title')
94 title = page.get('title')
@@ -96,6 +100,7 @@ def response(resp):
96 extract = page.get('extract')
100 extract = page.get('extract')
97
101
98 summary = extract_first_paragraph(extract, title, image)
102 summary = extract_first_paragraph(extract, title, image)
103+ summary = summary.replace('() ', '')
99
104
100 # link to wikipedia article
105 # link to wikipedia article
101 wikipedia_link = base_url.format(language=url_lang(resp.search_params['language'])) \
106 wikipedia_link = base_url.format(language=url_lang(resp.search_params['language'])) \
searx/engines/wolframalpha_noapi.py+1−1
@@ -55,7 +55,7 @@ def obtain_token():
55 return token
55 return token
56
56
57
57
58−def init():
58+def init(engine_settings=None):
59 obtain_token()
59 obtain_token()
60
60
61
61
searx/engines/www1x.py+6−29
@@ -11,8 +11,8 @@
11"""
11"""
12
12
13from lxml import html
13from lxml import html
14−import re
15from searx.url_utils import urlencode, urljoin
14from searx.url_utils import urlencode, urljoin
15+from searx.engines.xpath import extract_text
16
16
17# engine dependent config
17# engine dependent config
18categories = ['images']
18categories = ['images']
@@ -34,41 +34,18 @@ def request(query, params):
34def response(resp):
34def response(resp):
35 results = []
35 results = []
36
36
37− # get links from result-text
37+ dom = html.fromstring(resp.text)
38− regex = re.compile('(</a>|<a)')
38+ for res in dom.xpath('//div[@class="List-item MainListing"]'):
39− results_parts = re.split(regex, resp.text)
40−
41− cur_element = ''
42−
43− # iterate over link parts
44− for result_part in results_parts:
45 # processed start and end of link
39 # processed start and end of link
46− if result_part == '<a':
47− cur_element = result_part
48− continue
49− elif result_part != '</a>':
50− cur_element += result_part
51− continue
52−
53− cur_element += result_part
54−
55− # fix xml-error
56− cur_element = cur_element.replace('"></a>', '"/></a>')
57−
58− dom = html.fromstring(cur_element)
59− link = dom.xpath('//a')[0]
40+ link = res.xpath('//a')[0]
60
41
61 url = urljoin(base_url, link.attrib.get('href'))
42 url = urljoin(base_url, link.attrib.get('href'))
62− title = link.attrib.get('title', '')
43+ title = extract_text(link)
63
44
64− thumbnail_src = urljoin(base_url, link.xpath('.//img')[0].attrib['src'])
45+ thumbnail_src = urljoin(base_url, res.xpath('.//img')[0].attrib['src'])
65 # TODO: get image with higher resolution
46 # TODO: get image with higher resolution
66 img_src = thumbnail_src
47 img_src = thumbnail_src
67
48
68− # check if url is showing to a photo
69− if '/photo/' not in url:
70− continue
71−
72 # append result
49 # append result
73 results.append({'url': url,
50 results.append({'url': url,
74 'title': title,
51 'title': title,
searx/engines/xpath.py+10−10
@@ -1,6 +1,6 @@
1from lxml import html
1from lxml import html
2from lxml.etree import _ElementStringResult, _ElementUnicodeResult
2from lxml.etree import _ElementStringResult, _ElementUnicodeResult
3−from searx.utils import html_to_text
3+from searx.utils import html_to_text, eval_xpath
4from searx.url_utils import unquote, urlencode, urljoin, urlparse
4from searx.url_utils import unquote, urlencode, urljoin, urlparse
5
5
6search_url = None
6search_url = None
@@ -104,15 +104,15 @@ def response(resp):
104 results = []
104 results = []
105 dom = html.fromstring(resp.text)
105 dom = html.fromstring(resp.text)
106 if results_xpath:
106 if results_xpath:
107− for result in dom.xpath(results_xpath):
107+ for result in eval_xpath(dom, results_xpath):
108− url = extract_url(result.xpath(url_xpath), search_url)
108+ url = extract_url(eval_xpath(result, url_xpath), search_url)
109− title = extract_text(result.xpath(title_xpath))
109+ title = extract_text(eval_xpath(result, title_xpath))
110− content = extract_text(result.xpath(content_xpath))
110+ content = extract_text(eval_xpath(result, content_xpath))
111 tmp_result = {'url': url, 'title': title, 'content': content}
111 tmp_result = {'url': url, 'title': title, 'content': content}
112
112
113 # add thumbnail if available
113 # add thumbnail if available
114 if thumbnail_xpath:
114 if thumbnail_xpath:
115− thumbnail_xpath_result = result.xpath(thumbnail_xpath)
115+ thumbnail_xpath_result = eval_xpath(result, thumbnail_xpath)
116 if len(thumbnail_xpath_result) > 0:
116 if len(thumbnail_xpath_result) > 0:
117 tmp_result['img_src'] = extract_url(thumbnail_xpath_result, search_url)
117 tmp_result['img_src'] = extract_url(thumbnail_xpath_result, search_url)
118
118
@@ -120,14 +120,14 @@ def response(resp):
120 else:
120 else:
121 for url, title, content in zip(
121 for url, title, content in zip(
122 (extract_url(x, search_url) for
122 (extract_url(x, search_url) for
123− x in dom.xpath(url_xpath)),
123+ x in eval_xpath(dom, url_xpath)),
124− map(extract_text, dom.xpath(title_xpath)),
124+ map(extract_text, eval_xpath(dom, title_xpath)),
125− map(extract_text, dom.xpath(content_xpath))
125+ map(extract_text, eval_xpath(dom, content_xpath))
126 ):
126 ):
127 results.append({'url': url, 'title': title, 'content': content})
127 results.append({'url': url, 'title': title, 'content': content})
128
128
129 if not suggestion_xpath:
129 if not suggestion_xpath:
130 return results
130 return results
131− for suggestion in dom.xpath(suggestion_xpath):
131+ for suggestion in eval_xpath(dom, suggestion_xpath):
132 results.append({'suggestion': extract_text(suggestion)})
132 results.append({'suggestion': extract_text(suggestion)})
133 return results
133 return results
searx/engines/yahoo.py+9−9
@@ -14,7 +14,7 @@
14from lxml import html
14from lxml import html
15from searx.engines.xpath import extract_text, extract_url
15from searx.engines.xpath import extract_text, extract_url
16from searx.url_utils import unquote, urlencode
16from searx.url_utils import unquote, urlencode
17−from searx.utils import match_language
17+from searx.utils import match_language, eval_xpath
18
18
19# engine dependent config
19# engine dependent config
20categories = ['general']
20categories = ['general']
@@ -109,21 +109,21 @@ def response(resp):
109 dom = html.fromstring(resp.text)
109 dom = html.fromstring(resp.text)
110
110
111 try:
111 try:
112− results_num = int(dom.xpath('//div[@class="compPagination"]/span[last()]/text()')[0]
112+ results_num = int(eval_xpath(dom, '//div[@class="compPagination"]/span[last()]/text()')[0]
113 .split()[0].replace(',', ''))
113 .split()[0].replace(',', ''))
114 results.append({'number_of_results': results_num})
114 results.append({'number_of_results': results_num})
115 except:
115 except:
116 pass
116 pass
117
117
118 # parse results
118 # parse results
119− for result in dom.xpath(results_xpath):
119+ for result in eval_xpath(dom, results_xpath):
120 try:
120 try:
121− url = parse_url(extract_url(result.xpath(url_xpath), search_url))
121+ url = parse_url(extract_url(eval_xpath(result, url_xpath), search_url))
122− title = extract_text(result.xpath(title_xpath)[0])
122+ title = extract_text(eval_xpath(result, title_xpath)[0])
123 except:
123 except:
124 continue
124 continue
125
125
126− content = extract_text(result.xpath(content_xpath)[0])
126+ content = extract_text(eval_xpath(result, content_xpath)[0])
127
127
128 # append result
128 # append result
129 results.append({'url': url,
129 results.append({'url': url,
@@ -131,7 +131,7 @@ def response(resp):
131 'content': content})
131 'content': content})
132
132
133 # if no suggestion found, return results
133 # if no suggestion found, return results
134− suggestions = dom.xpath(suggestion_xpath)
134+ suggestions = eval_xpath(dom, suggestion_xpath)
135 if not suggestions:
135 if not suggestions:
136 return results
136 return results
137
137
@@ -148,9 +148,9 @@ def response(resp):
148def _fetch_supported_languages(resp):
148def _fetch_supported_languages(resp):
149 supported_languages = []
149 supported_languages = []
150 dom = html.fromstring(resp.text)
150 dom = html.fromstring(resp.text)
151− options = dom.xpath('//div[@id="yschlang"]/span/label/input')
151+ options = eval_xpath(dom, '//div[@id="yschlang"]/span/label/input')
152 for option in options:
152 for option in options:
153− code_parts = option.xpath('./@value')[0][5:].split('_')
153+ code_parts = eval_xpath(option, './@value')[0][5:].split('_')
154 if len(code_parts) == 2:
154 if len(code_parts) == 2:
155 code = code_parts[0] + '-' + code_parts[1].upper()
155 code = code_parts[0] + '-' + code_parts[1].upper()
156 else:
156 else:
searx/exceptions.py+1
@@ -28,5 +28,6 @@ class SearxParameterException(SearxException):
28 else:
28 else:
29 message = 'Invalid value "' + value + '" for parameter ' + name
29 message = 'Invalid value "' + value + '" for parameter ' + name
30 super(SearxParameterException, self).__init__(message)
30 super(SearxParameterException, self).__init__(message)
31+ self.message = message
31 self.parameter_name = name
32 self.parameter_name = name
32 self.parameter_value = value
33 self.parameter_value = value
searx/plugins/https_rewrite.py+3
@@ -225,6 +225,9 @@ def https_url_rewrite(result):
225
225
226
226
227def on_result(request, search, result):
227def on_result(request, search, result):
228+ if 'parsed_url' not in result:
229+ return True
230+
228 if result['parsed_url'].scheme == 'http':
231 if result['parsed_url'].scheme == 'http':
229 https_url_rewrite(result)
232 https_url_rewrite(result)
230 return True
233 return True
searx/plugins/oa_doi_rewrite.py+3
@@ -35,6 +35,9 @@ def get_doi_resolver(args, preference_doi_resolver):
35
35
36
36
37def on_result(request, search, result):
37def on_result(request, search, result):
38+ if 'parsed_url' not in result:
39+ return True
40+
38 doi = extract_doi(result['parsed_url'])
41 doi = extract_doi(result['parsed_url'])
39 if doi and len(doi) < 50:
42 if doi and len(doi) < 50:
40 for suffix in ('/', '.pdf', '/full', '/meta', '/abstract'):
43 for suffix in ('/', '.pdf', '/full', '/meta', '/abstract'):
searx/plugins/tracker_url_remover.py+17−10
@@ -17,10 +17,10 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
17
17
18from flask_babel import gettext
18from flask_babel import gettext
19import re
19import re
20−from searx.url_utils import urlunparse
20+from searx.url_utils import urlunparse, parse_qsl, urlencode
21
21
22−regexes = {re.compile(r'utm_[^&]+&?'),
22+regexes = {re.compile(r'utm_[^&]+'),
23− re.compile(r'(wkey|wemail)[^&]+&?'),
23+ re.compile(r'(wkey|wemail)[^&]*'),
24 re.compile(r'&$')}
24 re.compile(r'&$')}
25
25
26name = gettext('Tracker URL remover')
26name = gettext('Tracker URL remover')
@@ -30,16 +30,23 @@ preference_section = 'privacy'
30
30
31
31
32def on_result(request, search, result):
32def on_result(request, search, result):
33+ if 'parsed_url' not in result:
34+ return True
35+
33 query = result['parsed_url'].query
36 query = result['parsed_url'].query
34
37
35 if query == "":
38 if query == "":
36 return True
39 return True
37−
40+ parsed_query = parse_qsl(query)
41+
42+ changes = 0
43+ for i, (param_name, _) in enumerate(list(parsed_query)):
38− for reg in regexes:
44+ for reg in regexes:
39− query = reg.sub('', query)
45+ if reg.match(param_name):
40−
46+ parsed_query.pop(i - changes)
41− if query != result['parsed_url'].query:
47+ changes += 1
42− result['parsed_url'] = result['parsed_url']._replace(query=query)
48+ result['parsed_url'] = result['parsed_url']._replace(query=urlencode(parsed_query))
43− result['url'] = urlunparse(result['parsed_url'])
49+ result['url'] = urlunparse(result['parsed_url'])
50+ break
44
51
45 return True
52 return True
searx/query.py+1−1
@@ -184,7 +184,7 @@ class SearchQuery(object):
184 self.lang = lang
184 self.lang = lang
185 self.safesearch = safesearch
185 self.safesearch = safesearch
186 self.pageno = pageno
186 self.pageno = pageno
187− self.time_range = time_range
187+ self.time_range = None if time_range in ('', 'None', None) else time_range
188 self.timeout_limit = timeout_limit
188 self.timeout_limit = timeout_limit
189
189
190 def __str__(self):
190 def __str__(self):
searx/results.py+50−32
@@ -67,8 +67,9 @@ def merge_two_infoboxes(infobox1, infobox2):
67
67
68 for url2 in infobox2.get('urls', []):
68 for url2 in infobox2.get('urls', []):
69 unique_url = True
69 unique_url = True
70+ parsed_url2 = urlparse(url2.get('url', ''))
70− for url1 in infobox1.get('urls', []):
71+ for url1 in urls1:
71− if compare_urls(urlparse(url1.get('url', '')), urlparse(url2.get('url', ''))):
72+ if compare_urls(urlparse(url1.get('url', '')), parsed_url2):
72 unique_url = False
73 unique_url = False
73 break
74 break
74 if unique_url:
75 if unique_url:
@@ -188,8 +189,9 @@ class ResultContainer(object):
188 add_infobox = True
189 add_infobox = True
189 infobox_id = infobox.get('id', None)
190 infobox_id = infobox.get('id', None)
190 if infobox_id is not None:
191 if infobox_id is not None:
192+ parsed_url_infobox_id = urlparse(infobox_id)
191 for existingIndex in self.infoboxes:
193 for existingIndex in self.infoboxes:
192− if compare_urls(urlparse(existingIndex.get('id', '')), urlparse(infobox_id)):
194+ if compare_urls(urlparse(existingIndex.get('id', '')), parsed_url_infobox_id):
193 merge_two_infoboxes(existingIndex, infobox)
195 merge_two_infoboxes(existingIndex, infobox)
194 add_infobox = False
196 add_infobox = False
195
197
@@ -197,6 +199,13 @@ class ResultContainer(object):
197 self.infoboxes.append(infobox)
199 self.infoboxes.append(infobox)
198
200
199 def _merge_result(self, result, position):
201 def _merge_result(self, result, position):
202+ if 'url' in result:
203+ self.__merge_url_result(result, position)
204+ return
205+
206+ self.__merge_result_no_url(result, position)
207+
208+ def __merge_url_result(self, result, position):
200 result['parsed_url'] = urlparse(result['url'])
209 result['parsed_url'] = urlparse(result['url'])
201
210
202 # if the result has no scheme, use http as default
211 # if the result has no scheme, use http as default
@@ -210,51 +219,60 @@ class ResultContainer(object):
210 if result.get('content'):
219 if result.get('content'):
211 result['content'] = WHITESPACE_REGEX.sub(' ', result['content'])
220 result['content'] = WHITESPACE_REGEX.sub(' ', result['content'])
212
221
213− # check for duplicates
222+ duplicated = self.__find_duplicated_http_result(result)
214− duplicated = False
223+ if duplicated:
224+ self.__merge_duplicated_http_result(duplicated, result, position)
225+ return
226+
227+ # if there is no duplicate found, append result
228+ result['positions'] = [position]
229+ with RLock():
230+ self._merged_results.append(result)
231+
232+ def __find_duplicated_http_result(self, result):
215 result_template = result.get('template')
233 result_template = result.get('template')
216 for merged_result in self._merged_results:
234 for merged_result in self._merged_results:
235+ if 'parsed_url' not in merged_result:
236+ continue
217 if compare_urls(result['parsed_url'], merged_result['parsed_url'])\
237 if compare_urls(result['parsed_url'], merged_result['parsed_url'])\
218 and result_template == merged_result.get('template'):
238 and result_template == merged_result.get('template'):
219 if result_template != 'images.html':
239 if result_template != 'images.html':
220 # not an image, same template, same url : it's a duplicate
240 # not an image, same template, same url : it's a duplicate
221− duplicated = merged_result
241+ return merged_result
222− break
223 else:
242 else:
224 # it's an image
243 # it's an image
225 # it's a duplicate if the parsed_url, template and img_src are differents
244 # it's a duplicate if the parsed_url, template and img_src are differents
226 if result.get('img_src', '') == merged_result.get('img_src', ''):
245 if result.get('img_src', '') == merged_result.get('img_src', ''):
227− duplicated = merged_result
246+ return merged_result
228− break
247+ return None
229
248
230− # merge duplicates together
249+ def __merge_duplicated_http_result(self, duplicated, result, position):
231− if duplicated:
232− # using content with more text
250+ # using content with more text
233− if result_content_len(result.get('content', '')) >\
251+ if result_content_len(result.get('content', '')) >\
234− result_content_len(duplicated.get('content', '')):
252+ result_content_len(duplicated.get('content', '')):
235− duplicated['content'] = result['content']
253+ duplicated['content'] = result['content']
236
254
237− # merge all result's parameters not found in duplicate
255+ # merge all result's parameters not found in duplicate
238− for key in result.keys():
256+ for key in result.keys():
239− if not duplicated.get(key):
257+ if not duplicated.get(key):
240− duplicated[key] = result.get(key)
258+ duplicated[key] = result.get(key)
241
259
242− # add the new position
260+ # add the new position
243− duplicated['positions'].append(position)
261+ duplicated['positions'].append(position)
244
262
245− # add engine to list of result-engines
263+ # add engine to list of result-engines
246− duplicated['engines'].add(result['engine'])
264+ duplicated['engines'].add(result['engine'])
247
265
248− # using https if possible
266+ # using https if possible
249− if duplicated['parsed_url'].scheme != 'https' and result['parsed_url'].scheme == 'https':
267+ if duplicated['parsed_url'].scheme != 'https' and result['parsed_url'].scheme == 'https':
250− duplicated['url'] = result['parsed_url'].geturl()
268+ duplicated['url'] = result['parsed_url'].geturl()
251− duplicated['parsed_url'] = result['parsed_url']
269+ duplicated['parsed_url'] = result['parsed_url']
252
270
253− # if there is no duplicate found, append result
271+ def __merge_result_no_url(self, result, position):
254− else:
272+ result['engines'] = set([result['engine']])
255− result['positions'] = [position]
273+ result['positions'] = [position]
256− with RLock():
274+ with RLock():
257− self._merged_results.append(result)
275+ self._merged_results.append(result)
258
276
259 def order_results(self):
277 def order_results(self):
260 for result in self._merged_results:
278 for result in self._merged_results:
searx/search.py+61−13
@@ -77,7 +77,7 @@ def send_http_request(engine, request_params):
77 return req(request_params['url'], **request_args)
77 return req(request_params['url'], **request_args)
78
78
79
79
80−def search_one_request(engine, query, request_params):
80+def search_one_http_request(engine, query, request_params):
81 # update request parameters dependent on
81 # update request parameters dependent on
82 # search-engine (contained in engines folder)
82 # search-engine (contained in engines folder)
83 engine.request(query, request_params)
83 engine.request(query, request_params)
@@ -97,7 +97,53 @@ def search_one_request(engine, query, request_params):
97 return engine.response(response)
97 return engine.response(response)
98
98
99
99
100+def search_one_offline_request(engine, query, request_params):
101+ return engine.search(query, request_params)
102+
103+
100def search_one_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit):
104def search_one_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit):
105+ if engines[engine_name].offline:
106+ return search_one_offline_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit) # noqa
107+ return search_one_http_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit)
108+
109+
110+def search_one_offline_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit):
111+ engine = engines[engine_name]
112+
113+ try:
114+ search_results = search_one_offline_request(engine, query, request_params)
115+
116+ if search_results:
117+ result_container.extend(engine_name, search_results)
118+
119+ engine_time = time() - start_time
120+ result_container.add_timing(engine_name, engine_time, engine_time)
121+ with threading.RLock():
122+ engine.stats['engine_time'] += engine_time
123+ engine.stats['engine_time_count'] += 1
124+
125+ except ValueError as e:
126+ record_offline_engine_stats_on_error(engine, result_container, start_time)
127+ logger.exception('engine {0} : invalid input : {1}'.format(engine_name, e))
128+ except Exception as e:
129+ record_offline_engine_stats_on_error(engine, result_container, start_time)
130+
131+ result_container.add_unresponsive_engine((
132+ engine_name,
133+ u'{0}: {1}'.format(gettext('unexpected crash'), e),
134+ ))
135+ logger.exception('engine {0} : exception : {1}'.format(engine_name, e))
136+
137+
138+def record_offline_engine_stats_on_error(engine, result_container, start_time):
139+ engine_time = time() - start_time
140+ result_container.add_timing(engine.name, engine_time, engine_time)
141+
142+ with threading.RLock():
143+ engine.stats['errors'] += 1
144+
145+
146+def search_one_http_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit):
101 # set timeout for all HTTP requests
147 # set timeout for all HTTP requests
102 requests_lib.set_timeout_for_thread(timeout_limit, start_time=start_time)
148 requests_lib.set_timeout_for_thread(timeout_limit, start_time=start_time)
103 # reset the HTTP total time
149 # reset the HTTP total time
@@ -111,7 +157,7 @@ def search_one_request_safe(engine_name, query, request_params, result_container
111
157
112 try:
158 try:
113 # send requests and parse the results
159 # send requests and parse the results
114− search_results = search_one_request(engine, query, request_params)
160+ search_results = search_one_http_request(engine, query, request_params)
115
161
116 # check if the engine accepted the request
162 # check if the engine accepted the request
117 if search_results is not None:
163 if search_results is not None:
@@ -427,19 +473,21 @@ class Search(object):
427 continue
473 continue
428
474
429 # set default request parameters
475 # set default request parameters
476+ request_params = {}
477+ if not engine.offline:
430− request_params = default_request_params()
478+ request_params = default_request_params()
431− request_params['headers']['User-Agent'] = user_agent
479+ request_params['headers']['User-Agent'] = user_agent
432− request_params['category'] = selected_engine['category']
433− request_params['pageno'] = search_query.pageno
434
480
435− if hasattr(engine, 'language') and engine.language:
481+ if hasattr(engine, 'language') and engine.language:
436− request_params['language'] = engine.language
482+ request_params['language'] = engine.language
437− else:
483+ else:
438− request_params['language'] = search_query.lang
484+ request_params['language'] = search_query.lang
439
485
440− # 0 = None, 1 = Moderate, 2 = Strict
486+ request_params['safesearch'] = search_query.safesearch
441− request_params['safesearch'] = search_query.safesearch
487+ request_params['time_range'] = search_query.time_range
488+
489+ request_params['category'] = selected_engine['category']
442− request_params['time_range'] = search_query.time_range
490+ request_params['pageno'] = search_query.pageno
443
491
444 # append request to list
492 # append request to list
445 requests.append((selected_engine['name'], search_query.query, request_params))
493 requests.append((selected_engine['name'], search_query.query, request_params))
searx/settings.yml+20−14
@@ -161,11 +161,12 @@ engines:
161 weight : 2
161 weight : 2
162 disabled : True
162 disabled : True
163
163
164+# cloudflare protected
164− - name : digbt
165+# - name : digbt
165− engine : digbt
166+# engine : digbt
166− shortcut : dbt
167+# shortcut : dbt
167− timeout : 6.0
168+# timeout : 6.0
168− disabled : True
169+# disabled : True
169
170
170 - name : digg
171 - name : digg
171 engine : digg
172 engine : digg
@@ -407,7 +408,7 @@ engines:
407
408
408 - name : library genesis
409 - name : library genesis
409 engine : xpath
410 engine : xpath
410− search_url : http://libgen.io/search.php?req={query}
411+ search_url : https://libgen.is/search.php?req={query}
411 url_xpath : //a[contains(@href,"bookfi.net")]/@href
412 url_xpath : //a[contains(@href,"bookfi.net")]/@href
412 title_xpath : //a[contains(@href,"book/")]/text()[1]
413 title_xpath : //a[contains(@href,"book/")]/text()[1]
413 content_xpath : //td/a[1][contains(@href,"=author")]/text()
414 content_xpath : //td/a[1][contains(@href,"=author")]/text()
@@ -463,7 +464,7 @@ engines:
463 - name : openairedatasets
464 - name : openairedatasets
464 engine : json_engine
465 engine : json_engine
465 paging : True
466 paging : True
466− search_url : http://api.openaire.eu/search/datasets?format=json&page={pageno}&size=10&title={query}
467+ search_url : https://api.openaire.eu/search/datasets?format=json&page={pageno}&size=10&title={query}
467 results_query : response/results/result
468 results_query : response/results/result
468 url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
469 url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
469 title_query : metadata/oaf:entity/oaf:result/title/$
470 title_query : metadata/oaf:entity/oaf:result/title/$
@@ -475,7 +476,7 @@ engines:
475 - name : openairepublications
476 - name : openairepublications
476 engine : json_engine
477 engine : json_engine
477 paging : True
478 paging : True
478− search_url : http://api.openaire.eu/search/publications?format=json&page={pageno}&size=10&title={query}
479+ search_url : https://api.openaire.eu/search/publications?format=json&page={pageno}&size=10&title={query}
479 results_query : response/results/result
480 results_query : response/results/result
480 url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
481 url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
481 title_query : metadata/oaf:entity/oaf:result/title/$
482 title_query : metadata/oaf:entity/oaf:result/title/$
@@ -703,9 +704,9 @@ engines:
703 shortcut: vo
704 shortcut: vo
704 categories: social media
705 categories: social media
705 search_url : https://searchvoat.co/?t={query}
706 search_url : https://searchvoat.co/?t={query}
706− url_xpath : //div[@class="entry"]/p/a[contains(@class, "title")]/@href
707+ url_xpath : //div[@class="entry"]//p[@class="title"]/a/@href
707− title_xpath : //div[@class="entry"]/p/a[contains(@class, "title")]
708+ title_xpath : //div[@class="entry"]//p[@class="title"]/a/text()
708− content_xpath : //div[@class="entry"]/p/span[@class="domain"]/a/text()
709+ content_xpath : //div[@class="entry"]//span[@class="domain"]/a/text()
709 timeout : 10.0
710 timeout : 10.0
710 disabled : True
711 disabled : True
711
712
@@ -743,10 +744,15 @@ engines:
743 title_xpath : ./h2
744 title_xpath : ./h2
744 content_xpath : ./p[@class="s"]
745 content_xpath : ./p[@class="s"]
745 suggestion_xpath : /html/body//div[@class="top-info"]/p[@class="top-info spell"]/a
746 suggestion_xpath : /html/body//div[@class="top-info"]/p[@class="top-info spell"]/a
746− first_page_num : 1
747+ first_page_num : 0
747 page_size : 10
748 page_size : 10
748 disabled : True
749 disabled : True
749
750
751+ - name : seedpeer
752+ shortcut : speu
753+ engine : seedpeer
754+ categories: files, music, videos
755+
750# - name : yacy
756# - name : yacy
751# engine : yacy
757# engine : yacy
752# shortcut : ya
758# shortcut : ya
@@ -806,7 +812,7 @@ locales:
806doi_resolvers :
812doi_resolvers :
807 oadoi.org : 'https://oadoi.org/'
813 oadoi.org : 'https://oadoi.org/'
808 doi.org : 'https://doi.org/'
814 doi.org : 'https://doi.org/'
809− doai.io : 'http://doai.io/'
815+ doai.io : 'https://doai.io/'
810− sci-hub.tw : 'http://sci-hub.tw/'
816+ sci-hub.tw : 'https://sci-hub.tw/'
811
817
812default_doi_resolver : 'oadoi.org'
818default_doi_resolver : 'oadoi.org'
searx/settings_robot.yml+2−2
@@ -43,7 +43,7 @@ locales:
43doi_resolvers :
43doi_resolvers :
44 oadoi.org : 'https://oadoi.org/'
44 oadoi.org : 'https://oadoi.org/'
45 doi.org : 'https://doi.org/'
45 doi.org : 'https://doi.org/'
46− doai.io : 'http://doai.io/'
46+ doai.io : 'https://doai.io/'
47− sci-hub.tw : 'http://sci-hub.tw/'
47+ sci-hub.tw : 'https://sci-hub.tw/'
48
48
49default_doi_resolver : 'oadoi.org'
49default_doi_resolver : 'oadoi.org'
searx/static/plugins/js/vim_hotkeys.js+10−2
@@ -125,6 +125,14 @@ $(document).ready(function() {
125 }
125 }
126 });
126 });
127
127
128+ function nextResult(current, direction) {
129+ var next = current[direction]();
130+ while (!next.is('.result') && next.length !== 0) {
131+ next = next[direction]();
132+ }
133+ return next
134+ }
135+
128 function highlightResult(which) {
136 function highlightResult(which) {
129 return function() {
137 return function() {
130 var current = $('.result[data-vim-selected]');
138 var current = $('.result[data-vim-selected]');
@@ -157,13 +165,13 @@ $(document).ready(function() {
157 }
165 }
158 break;
166 break;
159 case 'down':
167 case 'down':
160− next = current.next('.result');
168+ next = nextResult(current, 'next');
161 if (next.length === 0) {
169 if (next.length === 0) {
162 next = $('.result:first');
170 next = $('.result:first');
163 }
171 }
164 break;
172 break;
165 case 'up':
173 case 'up':
166− next = current.prev('.result');
174+ next = nextResult(current, 'prev');
167 if (next.length === 0) {
175 if (next.length === 0) {
168 next = $('.result:last');
176 next = $('.result:last');
169 }
177 }
searx/static/themes/courgette/css/style.css+1−1
@@ -1 +1 @@
1−a,h2{color:#666}.center,html{position:relative}#categories_container>div,.top_margin a{display:inline-block}#categories,.center{text-align:center}#categories .hidden,.cache_link,.highlight .c,.highlight .cm,.highlight .ge,.highlight .sd{font-style:italic}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]{-webkit-appearance:textfield}h2{text-transform:uppercase}body{font-family:sans-serif;line-height:1.5;margin:0;background:#EEE}html{min-height:100%}.title h1{font-size:7em;color:#3498DB;margin:-20px auto 0;line-height:100px;padding-bottom:20px}.center{max-width:70em;background:rgba(255,255,255,.6);padding:2em;margin:7% auto 0}.center.search{position:static;width:auto;background:0 0;margin:auto;padding-top:1.8em}@media screen and (min-width:1001px){.center:after{content:"";z-index:-1;background:url(../img/bg-body-index.jpg) no-repeat;background-size:cover;width:100%;height:100%;top:0;left:0;position:fixed}.center.search:after{content:none}}.autocompleter-choices{position:absolute;margin:0;padding:0;background:#FFF}.autocompleter-choices li{padding:.5em 1em}.autocompleter-choices li:hover{background:#3498DB;color:#FFF;cursor:pointer}.top_margin{position:absolute;bottom:-3.5em;width:100%;left:0}.top_margin a{margin-right:1em;color:#FFF;text-decoration:none}.top_margin a:focus,.top_margin a:hover{text-decoration:underline}@media screen and (max-width:1000px){.center{background:0 0}.top_margin a{color:#333}}.checkbox_container{margin-top:1.5em}.checkbox_container label{padding:.5em 1em;color:#333;cursor:pointer;font-size:.9em}.checkbox_container input[type=checkbox]:checked+label,.checkbox_container label:hover{background:#3498DB;color:#FFF}.checkbox_container input[type=checkbox]{position:absolute;top:-9999px}#categories .hidden{display:none;position:absolute;bottom:1em;left:0;text-align:center;width:100%;font-size:.9em;color:#333}#categories:hover .hidden,.right a{display:block}@media screen and (max-width:900px){#categories_container{letter-spacing:-5px}#categories_container>div{letter-spacing:normal;margin-top:1em}.checkbox_container{margin:0}.checkbox_container label{display:block;background:#CCC;padding:1em;border:1px solid #FFF}.top_margin{position:static}#categories .hidden{position:static;display:block}}@media screen and (max-width:900px) and (min-width:501px){#categories_container>div{width:31%;margin-left:2.333%}#categories_container>div:nth-child(3n+1){margin-left:0}}@media screen and (max-width:500px){#categories_container>div{width:48%;margin-left:2%;font-size:.9em}#categories_container>div:nth-child(2n+1){margin-left:0}.title h1{background:url(../img/searx-mobile.png) no-repeat;width:200px;height:39px}}#search_wrapper{position:relative}.q{padding:.5em 3em .5em 1em;width:100%;font-size:1.5em;border:0;color:#666}.cache_link,.result p{font-size:.9em}#search_submit{position:absolute;top:0;right:0;border:0;background:url(../img/search-icon.png) center center/65% auto no-repeat #3498DB;text-indent:-9999px;width:5em;height:100%;cursor:pointer}#sidebar,.right{position:fixed;width:15em;right:0;text-align:right}#search_submit:focus,#search_submit:hover{background-color:#0665A2}#sidebar{background:#3498DB;top:0;height:100%;padding:1.5em}.right{bottom:1.5em;z-index:1;padding:0 1.5em}.right a{color:#FFF;text-decoration:none}#sidebar form,#suggestions form,.row fieldset{display:inline-block}.right a:focus,.right a:hover{text-decoration:underline}#preferences{background:url(../img/preference-icon.png) right center/12% auto no-repeat;padding-right:1.8em}#search_url input{border:0;padding:.5em}#sidebar>div{margin-bottom:1em;color:#FFF}#sidebar input[type=submit]{background:#CCC;border:0;padding:.5em 1em;cursor:pointer;margin-top:.5em}#sidebar input[type=submit]:focus,#sidebar input[type=submit]:hover{color:#FFF;background-color:#0665A2}#results{padding:0 17em 0 2em}.result .content{margin:0;color:#666}.result .url{margin-top:0;color:#FF6530}.result .favicon{float:left;position:relative;top:.5em;margin-right:.5em}.definition_result{background:#CCC;padding:1em}.definition_result .result_title,.definition_result p{margin:0}.result_title{margin-bottom:0;font-weight:400}.result_title a{color:#3498DB;text-decoration:none}#answers,#suggestions span{color:#666}.result_title a:focus,.result_title a:hover{text-decoration:underline}.cache_link{color:#666}.search.center{padding-right:17em}#answers{border:2px solid #3498DB;padding:20px;text-align:center;max-width:70em;margin:0 auto 20px}#suggestions{margin-bottom:1em}#suggestions form{vertical-align:top;margin-bottom:.5em}#suggestions input[type=submit]{color:#333;padding:.5em 1em;border:0;background:#CCC;cursor:pointer}#suggestions input[type=submit]:focus,#suggestions input[type=submit]:hover{background:#3498DB;color:#FFF}#pagination{margin:1.5em 0 2em}#pagination form+form{float:right;margin-top:-2em}input[type=submit]{display:inline-block;background:#3498DB;color:#FFF;border:0;padding:.6em 1em;cursor:pointer}input[type=submit]:focus,input[type=submit]:hover{background:#0665A2}.row{max-width:60em;margin:auto}.row a{color:#3498DB}.row form{letter-spacing:-5px}.row form>*{letter-spacing:normal}.row p{margin:0}.row fieldset{width:48%;vertical-align:top}.row fieldset:last-of-type{display:block;width:auto;background:0 0;padding:0}fieldset,table tr:nth-child(odd){background:#CCC}.row fieldset:nth-child(odd){margin-right:2%}.row fieldset:nth-child(2){min-height:10.5em}@media screen and (max-width:900px){.row{margin:0 1em}.row fieldset{width:49%}.row fieldset,.row fieldset:nth-child(odd){margin-right:0}.row fieldset:first-child{width:100%;margin-right:0}.row fieldset:nth-child(even){margin-right:2%}}@media screen and (max-width:800px){.row fieldset,select{width:100%}table{font-size:.8em}#sidebar,.right{display:none}#results{padding:0 2em}.search.center{padding-right:2em}}@media screen and (max-width:400px){.row #categories_container>div{width:100%;margin-left:0}}fieldset{border:0;margin:1em 0;padding:1.5em}table{width:100%;text-align:left;border:1px solid #CCC;border-collapse:collapse}table th{background:#999;color:#FFF}table td,table th{padding:.5em 1em;border:1px solid #FFF}.engine_checkbox label{padding:.5em;background:#3498DB;color:#FFF;cursor:pointer}.engine_checkbox .deny{background:#3498DB}.engine_checkbox .allow{display:none;background:#666}.engine_checkbox input{display:none}.engine_checkbox input:checked+.allow{display:inline}.engine_checkbox input:checked+.allow+.deny{display:none}.row input[type=submit]{font-size:1em;margin:1em 0 2em}.row .right{position:static;display:inline-block}.row .right a{color:#333;width:auto;text-align:left;padding:0}.small_font{font-size:.8em}table th{padding:1em}legend{background:#EEE;padding:0 1em;position:relative}select{border:1px solid #DDD;padding:.5em .8em;font-size:1em}.highlight .hll{background-color:#ffc}.highlight{font-weight:700;background:#f8f8f8}.highlight .c{color:#408080}.highlight .err{border:1px solid red}.highlight .k{color:green;font-weight:700}.highlight .o{color:#666}.highlight .cm{color:#408080}.highlight .cp{color:#BC7A00}.highlight .c1,.highlight .cs{color:#408080;font-style:italic}.highlight .gd{color:#A00000}.highlight .gr{color:red}.highlight .gh{color:navy;font-weight:700}.highlight .gi{color:#00A000}.highlight .go{color:#888}.highlight .gp{color:navy;font-weight:700}.highlight .gs{font-weight:700}.highlight .gu{color:purple;font-weight:700}.highlight .gt{color:#04D}.highlight .kc,.highlight .kd,.highlight .kn{color:green;font-weight:700}.highlight .kp{color:green}.highlight .kr{color:green;font-weight:700}.highlight .kt{color:#B00040}.highlight .m{color:#666}.highlight .s{color:#BA2121}.highlight .na{color:#7D9029}.highlight .nb{color:green}.highlight .nc{color:#00F;font-weight:700}.highlight .no{color:#800}.highlight .nd{color:#A2F}.highlight .ni{color:#999;font-weight:700}.highlight .ne{color:#D2413A;font-weight:700}.highlight .nf{color:#00F}.highlight .nl{color:#A0A000}.highlight .nn{color:#00F;font-weight:700}.highlight .nt{color:green;font-weight:700}.highlight .nv{color:#19177C}.highlight .ow{color:#A2F;font-weight:700}.highlight .w{color:#bbb}.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:#666}.highlight .s2,.highlight .sb,.highlight .sc{color:#BA2121}.highlight .sd{color:#BA2121}.highlight .se{color:#B62;font-weight:700}.highlight .sh{color:#BA2121}.highlight .si{color:#B68;font-weight:700}.highlight .sx{color:green}.highlight .sr{color:#B68}.highlight .s1{color:#BA2121}.highlight .ss{color:#19177C}.highlight .bp{color:green}.highlight .vc,.highlight .vg,.highlight .vi{color:#19177C}.highlight .il{color:#666}.highlight pre{overflow:auto}.highlight .lineno{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.highlight .lineno::selection{background:0 0}.highlight .lineno::-moz-selection{background:0 0}
1+a,h2{color:#666}.center,html{position:relative}#categories_container>div,.top_margin a{display:inline-block}#categories,.center{text-align:center}#categories .hidden,.cache_link,.highlight .c,.highlight .cm,.highlight .ge,.highlight .sd{font-style:italic}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]{-webkit-appearance:textfield}h2{text-transform:uppercase}body{font-family:sans-serif;line-height:1.5;margin:0;background:#EEE}html{min-height:100%}.title h1{font-size:7em;color:#3498DB;margin:-20px auto 0;line-height:100px;padding-bottom:20px}.center{max-width:70em;background:rgba(255,255,255,.6);padding:2em;margin:7% auto 0}.center.search{position:static;width:auto;background:0 0;margin:auto;padding-top:1.8em}@media screen and (min-width:1001px){.center:after{content:"";z-index:-1;background:url(../img/bg-body-index.jpg) no-repeat;background-size:cover;width:100%;height:100%;top:0;left:0;position:fixed}.center.search:after{content:none}}.autocompleter-choices{position:absolute;margin:0;padding:0;background:#FFF}.autocompleter-choices li{padding:.5em 1em}.autocompleter-choices li:hover{background:#3498DB;color:#FFF;cursor:pointer}.top_margin{position:absolute;bottom:-3.5em;width:100%;left:0}.top_margin a{margin-right:1em;color:#FFF;text-decoration:none}.top_margin a:focus,.top_margin a:hover{text-decoration:underline}@media screen and (max-width:1000px){.center{background:0 0}.top_margin a{color:#333}}.checkbox_container{margin-top:1.5em}.checkbox_container label{padding:.5em 1em;color:#333;cursor:pointer;font-size:.9em}.checkbox_container input[type=checkbox]:checked+label,.checkbox_container label:hover{background:#3498DB;color:#FFF}.checkbox_container input[type=checkbox]{position:absolute;top:-9999px}#categories .hidden{display:none;position:absolute;bottom:1em;left:0;text-align:center;width:100%;font-size:.9em;color:#333}#categories:hover .hidden,.right a{display:block}@media screen and (max-width:900px){#categories_container{letter-spacing:-5px}#categories_container>div{letter-spacing:normal;margin-top:1em}.checkbox_container{margin:0}.checkbox_container label{display:block;background:#CCC;padding:1em;border:1px solid #FFF}.top_margin{position:static}#categories .hidden{position:static;display:block}}@media screen and (max-width:900px) and (min-width:501px){#categories_container>div{width:31%;margin-left:2.333%}#categories_container>div:nth-child(3n+1){margin-left:0}}@media screen and (max-width:500px){#categories_container>div{width:48%;margin-left:2%;font-size:.9em}#categories_container>div:nth-child(2n+1){margin-left:0}.title h1{background:url(../img/searx-mobile.png) no-repeat;width:200px;height:39px}}#search_wrapper{position:relative}.q{padding:.5em 3em .5em 1em;width:100%;font-size:1.5em;border:0;color:#666}.cache_link,.result p{font-size:.9em}#search_submit{position:absolute;top:0;right:0;border:0;background:url(../img/search-icon.png) center center/65% auto no-repeat #3498DB;text-indent:-9999px;width:5em;height:100%;cursor:pointer}#sidebar,.right{position:fixed;width:15em;right:0;text-align:right}#search_submit:focus,#search_submit:hover{background-color:#0665A2}#sidebar{background:#3498DB;top:0;height:100%;padding:1.5em}.right{bottom:1.5em;z-index:1;padding:0 1.5em}.right a{color:#FFF;text-decoration:none}#sidebar form,#suggestions form,.row fieldset{display:inline-block}.right a:focus,.right a:hover{text-decoration:underline}#preferences{background:url(../img/preference-icon.png) right center/12% auto no-repeat;padding-right:1.8em}#search_url input{border:0;padding:.5em}#sidebar>div{margin-bottom:1em;color:#FFF}#sidebar input[type=submit]{background:#CCC;border:0;padding:.5em 1em;cursor:pointer;margin-top:.5em}#sidebar input[type=submit]:focus,#sidebar input[type=submit]:hover{color:#FFF;background-color:#0665A2}#results{padding:0 17em 0 2em}.result .engines{text-align:right}.result .content{margin:0;color:#666}.result .url{margin-top:0;color:#FF6530}.result .favicon{float:left;position:relative;top:.5em;margin-right:.5em}.definition_result{background:#CCC;padding:1em}.definition_result .result_title,.definition_result p{margin:0}.result_title{margin-bottom:0;font-weight:400}.result_title a{color:#3498DB;text-decoration:none}#answers,#suggestions span{color:#666}.result_title a:focus,.result_title a:hover{text-decoration:underline}.cache_link{color:#666}.search.center{padding-right:17em}#answers{border:2px solid #3498DB;padding:20px;text-align:center;max-width:70em;margin:0 auto 20px}#suggestions{margin-bottom:1em}#suggestions form{vertical-align:top;margin-bottom:.5em}#suggestions input[type=submit]{color:#333;padding:.5em 1em;border:0;background:#CCC;cursor:pointer}#suggestions input[type=submit]:focus,#suggestions input[type=submit]:hover{background:#3498DB;color:#FFF}#pagination{margin:1.5em 0 2em}#pagination form+form{float:right;margin-top:-2em}input[type=submit]{display:inline-block;background:#3498DB;color:#FFF;border:0;padding:.6em 1em;cursor:pointer}input[type=submit]:focus,input[type=submit]:hover{background:#0665A2}.row{max-width:60em;margin:auto}.row a{color:#3498DB}.row form{letter-spacing:-5px}.row form>*{letter-spacing:normal}.row p{margin:0}.row fieldset{width:48%;vertical-align:top}.row fieldset:last-of-type{display:block;width:auto;background:0 0;padding:0}fieldset,table tr:nth-child(odd){background:#CCC}.row fieldset:nth-child(odd){margin-right:2%}.row fieldset:nth-child(2){min-height:10.5em}@media screen and (max-width:900px){.row{margin:0 1em}.row fieldset{width:49%}.row fieldset,.row fieldset:nth-child(odd){margin-right:0}.row fieldset:first-child{width:100%;margin-right:0}.row fieldset:nth-child(even){margin-right:2%}}@media screen and (max-width:800px){.row fieldset,select{width:100%}table{font-size:.8em}#sidebar,.right{display:none}#results{padding:0 2em}.search.center{padding-right:2em}}@media screen and (max-width:400px){.row #categories_container>div{width:100%;margin-left:0}}fieldset{border:0;margin:1em 0;padding:1.5em}table{width:100%;text-align:left;border:1px solid #CCC;border-collapse:collapse}table th{background:#999;color:#FFF}table td,table th{padding:.5em 1em;border:1px solid #FFF}.engine_checkbox label{padding:.5em;background:#3498DB;color:#FFF;cursor:pointer}.engine_checkbox .deny{background:#3498DB}.engine_checkbox .allow{display:none;background:#666}.engine_checkbox input{display:none}.engine_checkbox input:checked+.allow{display:inline}.engine_checkbox input:checked+.allow+.deny{display:none}.row input[type=submit]{font-size:1em;margin:1em 0 2em}.row .right{position:static;display:inline-block}.row .right a{color:#333;width:auto;text-align:left;padding:0}.small_font{font-size:.8em}table th{padding:1em}legend{background:#EEE;padding:0 1em;position:relative}select{border:1px solid #DDD;padding:.5em .8em;font-size:1em}.highlight .hll{background-color:#ffc}.highlight{font-weight:700;background:#f8f8f8}.highlight .c{color:#408080}.highlight .err{border:1px solid red}.highlight .k{color:green;font-weight:700}.highlight .o{color:#666}.highlight .cm{color:#408080}.highlight .cp{color:#BC7A00}.highlight .c1,.highlight .cs{color:#408080;font-style:italic}.highlight .gd{color:#A00000}.highlight .gr{color:red}.highlight .gh{color:navy;font-weight:700}.highlight .gi{color:#00A000}.highlight .go{color:#888}.highlight .gp{color:navy;font-weight:700}.highlight .gs{font-weight:700}.highlight .gu{color:purple;font-weight:700}.highlight .gt{color:#04D}.highlight .kc,.highlight .kd,.highlight .kn{color:green;font-weight:700}.highlight .kp{color:green}.highlight .kr{color:green;font-weight:700}.highlight .kt{color:#B00040}.highlight .m{color:#666}.highlight .s{color:#BA2121}.highlight .na{color:#7D9029}.highlight .nb{color:green}.highlight .nc{color:#00F;font-weight:700}.highlight .no{color:#800}.highlight .nd{color:#A2F}.highlight .ni{color:#999;font-weight:700}.highlight .ne{color:#D2413A;font-weight:700}.highlight .nf{color:#00F}.highlight .nl{color:#A0A000}.highlight .nn{color:#00F;font-weight:700}.highlight .nt{color:green;font-weight:700}.highlight .nv{color:#19177C}.highlight .ow{color:#A2F;font-weight:700}.highlight .w{color:#bbb}.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:#666}.highlight .s2,.highlight .sb,.highlight .sc{color:#BA2121}.highlight .sd{color:#BA2121}.highlight .se{color:#B62;font-weight:700}.highlight .sh{color:#BA2121}.highlight .si{color:#B68;font-weight:700}.highlight .sx{color:green}.highlight .sr{color:#B68}.highlight .s1{color:#BA2121}.highlight .ss{color:#19177C}.highlight .bp{color:green}.highlight .vc,.highlight .vg,.highlight .vi{color:#19177C}.highlight .il{color:#666}.highlight pre{overflow:auto}.highlight .lineno{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.highlight .lineno::selection{background:0 0}.highlight .lineno::-moz-selection{background:0 0}
searx/static/themes/courgette/less/style.less+4
@@ -325,6 +325,10 @@ a {
325 font-size: 0.9em;
325 font-size: 0.9em;
326}
326}
327
327
328+.result .engines {
329+ text-align: right;
330+}
331+
328.result .content {
332.result .content {
329 margin: 0;
333 margin: 0;
330 color: #666;
334 color: #666;
searx/static/themes/legacy/css/style.css+1−1
@@ -1 +1 @@
1−.highlight .c,.highlight .cm,.highlight .ge,.highlight .sd{font-style:italic}#categories,.highlight .lineno{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#container,.search,body,html{padding:0;margin:0}div.title h1,input[type=checkbox]{visibility:hidden}#categories,.checkbox_container label,.engine_checkbox label,.highlight .lineno{-webkit-touch-callout:none;-khtml-user-select:none}#answers input[type=submit],#infoboxes input[type=submit],#sidebar input[type=submit],#suggestions input[type=submit],.result_title a:hover,.torrent_result a:hover{text-decoration:underline}#infoboxes,.result .content,.result .url,.result h3{word-wrap:break-word}#apis,#infoboxes .infobox br,#pagination,#pagination br,.result,.result .content br.last{clear:both}.highlight .hll{background-color:#ffc}.highlight{background:#f8f8f8}.highlight .c{color:#408080}.highlight .err{border:1px solid red}.highlight .k{color:green;font-weight:700}.highlight .o{color:#666}.highlight .cm{color:#408080}.highlight .cp{color:#BC7A00}.highlight .c1,.highlight .cs{color:#408080;font-style:italic}.highlight .gd{color:#A00000}.highlight .gr{color:red}.highlight .gh{color:navy;font-weight:700}.highlight .gi{color:#00A000}.highlight .go{color:#888}.highlight .gp{color:navy;font-weight:700}.highlight .gs{font-weight:700}.highlight .gu{color:purple;font-weight:700}.highlight .gt{color:#04D}.highlight .kc,.highlight .kd,.highlight .kn{color:green;font-weight:700}.highlight .kp{color:green}.highlight .kr{color:green;font-weight:700}.highlight .kt{color:#B00040}.highlight .m{color:#666}.highlight .s{color:#BA2121}.highlight .na{color:#7D9029}.highlight .nb{color:green}.highlight .nc{color:#00F;font-weight:700}.highlight .no{color:#800}.highlight .nd{color:#A2F}.highlight .ni{color:#999;font-weight:700}.highlight .ne{color:#D2413A;font-weight:700}.highlight .nf{color:#00F}.highlight .nl{color:#A0A000}.highlight .nn{color:#00F;font-weight:700}.highlight .nt{color:green;font-weight:700}.highlight .nv{color:#19177C}.highlight .ow{color:#A2F;font-weight:700}.highlight .w{color:#bbb}.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:#666}.highlight .s2,.highlight .sb,.highlight .sc{color:#BA2121}.highlight .sd{color:#BA2121}.highlight .se{color:#B62;font-weight:700}.highlight .sh{color:#BA2121}.highlight .si{color:#B68;font-weight:700}.highlight .sx{color:green}.highlight .sr{color:#B68}.highlight .s1{color:#BA2121}.highlight .ss{color:#19177C}.highlight .bp{color:green}.highlight .vc,.highlight .vg,.highlight .vi{color:#19177C}.highlight .il{color:#666}.highlight pre{overflow:auto}.highlight .lineno{user-select:none;cursor:default}.highlight .lineno::selection{background:0 0}.highlight .lineno::-moz-selection{background:0 0}html{font-family:sans-serif;font-size:.9em;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;color:#444}#container{width:100%;position:absolute;top:0}.search .checkbox_container label{font-size:.9em;border-bottom:2px solid #E8E7E6}.search .checkbox_container label:hover{border-bottom:2px solid #3498DB}.search .checkbox_container input[type=checkbox]:checked+label{border-bottom:2px solid #2980B9}#search_wrapper{position:relative;width:50em;padding:10px}.center #search_wrapper{margin-left:auto;margin-right:auto}.q,ul.autocompleter-choices{margin:0;border:1px solid #3498DB}.q{background:#FFF;color:#222;font-size:16px;height:28px;outline:0;padding:2px 2px 2px 8px;padding-right:0!important;width:100%;z-index:2}#search_submit{position:absolute;top:13px;right:1px;padding:0;border:0;background:url(../img/search-icon.png) no-repeat;background-size:24px 24px;opacity:.8;width:24px;height:30px;font-size:0}@media screen and (max-width:50em){#search_wrapper{width:90%;clear:both;overflow:hidden}}ul.autocompleter-choices{position:absolute;padding:0;list-style:none;border-left-color:#3498DB;border-right-color:#3498DB;border-bottom-color:#3498DB;text-align:left;font-family:Verdana,Geneva,Arial,Helvetica,sans-serif;z-index:50;background-color:#FFF;color:#444}ul.autocompleter-choices li{position:relative;margin:-2px 0 0;padding:.2em 1.5em .2em 1em;display:block;float:none!important;cursor:pointer;font-weight:400;white-space:nowrap;font-size:1em;line-height:1.5em}ul.autocompleter-choices li.autocompleter-selected{background-color:#444;color:#FFF}ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried{color:#9FCFFF}ul.autocompleter-choices span.autocompleter-queried{display:inline;float:none;font-weight:700;margin:0;padding:0}.row{max-width:800px;margin:20px auto;text-align:justify}.row h1{font-size:3em;margin-top:50px}.row p{padding:0 10px;max-width:700px}.row h3,.row ul{margin:4px 8px}.hmarg{margin:0 20px;border:1px solid #3498DB;padding:4px 10px}a:active.hmarg,a:hover.hmarg,a:link.hmarg,a:visited.hmarg{color:#3498DB}.top_margin{margin-top:60px}.center{text-align:center}h1{font-size:5em}div.title{background:url(../img/searx.png) center no-repeat;width:100%;min-height:80px}input[type=submit]{padding:2px 6px;margin:2px 4px;display:inline-block;background:#3498DB;color:#FFF;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:0;cursor:pointer}fieldset{margin:8px;border:1px solid #3498DB}#categories{margin:0 10px;user-select:none}.checkbox_container{display:inline-block;position:relative;margin:0 3px;padding:0}.checkbox_container input{display:none}.checkbox_container label,.engine_checkbox label{cursor:pointer;padding:4px 10px;margin:0;display:block;text-transform:capitalize;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkbox_container input[type=checkbox]:checked+label{background:#3498DB;color:#FFF}.engine_checkbox{padding:4px}label.allow{background:#E74C3C;padding:4px 8px;color:#FFF;display:none}label.deny{background:#2ECC71;padding:4px 8px;color:#444;display:inline}.engine_checkbox input[type=checkbox]:checked+label:nth-child(2)+label{display:none}.engine_checkbox input[type=checkbox]:checked+label.allow{display:inline}a{text-decoration:none;color:#1a11be}a:visited{color:#8E44AD}.result{margin:19px 0 18px;padding:0}.result_title{margin-bottom:0}.result_title a{color:#2980B9;font-weight:400;font-size:1.1em}.result_title a:visited{color:#8E44AD}.cache_link{font-size:10px!important}.result h3{font-size:1em;margin:5px 0 1px;padding:0}.result .content,.result .url,.small_font{font-size:.8em}.result .content{margin:0;padding:0;max-width:54em;line-height:1.24}.result .content img{float:left;margin-right:5px;max-width:200px;max-height:100px}.result .url{margin:0 0 3px;padding:0;max-width:54em;color:#C0392B}.result .published_date{font-size:.8em;color:#888;Margin:5px 20px}.result .thumbnail{width:400px}.engines{color:#888}.small p{margin:2px 0}.right{float:right}.invisible{display:none}.left{float:left}.highlight{color:#094089}.content .highlight{color:#000}.image_result{display:inline-block;margin:10px;position:relative;max-height:160px}.image_result img{border:0;max-height:160px}.image_result p{margin:0;padding:0}.image_result p span a{display:none;color:#FFF}.image_result p:hover span a{display:block;position:absolute;bottom:0;right:0;padding:4px;background-color:rgba(0,0,0,.6);font-size:.7em}#categories_container,.percentage{position:relative}.torrent_result{border-left:10px solid #d3d3d3;padding-left:3px}.torrent_result p{margin:3px;font-size:.8em}.torrent_result a{color:#2980B9}.torrent_result a:visited{color:#8E44AD}.definition_result{border-left:10px solid gray;padding-left:3px}#infoboxes,#sidebar{margin:0 2px 5px 5px;padding:0 2px 2px}.percentage{width:300px}.percentage div{background:#444}table{width:100%}td{padding:0 4px}tr:hover{background:#DDD}#results{margin:auto auto 20px;padding:0;width:50em}#sidebar{position:fixed;bottom:10px;left:10px;width:14em}#answers input,#infoboxes input,#sidebar input,#suggestions input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:0 0;color:#444;cursor:pointer}#suggestions form{display:inline}#answers,#suggestions{margin-top:20px;max-width:45em}#suggestions-title{color:#888}#answers{border:2px solid #2980B9;padding:20px}#answers form,#infoboxes form{min-width:210px}#infoboxes{position:absolute;top:100px;right:20px;max-width:21em}#infoboxes .infobox{margin:10px 0;border:1px solid #ddd;padding:5px;font-size:.8em}#infoboxes .infobox img{max-width:90%;max-heigt:12em;display:block;margin:5px;padding:5px}#infoboxes .infobox h2{margin:0}#apis,#search_url{margin-top:8px}#infoboxes .infobox table{table-layout:fixed}#infoboxes .infobox table td{vertical-align:top}#infoboxes .infobox input{font-size:1em}#search_url input{border:1px solid #888;padding:4px;color:#444;width:14em;display:block;margin:4px;font-size:.8em}#preferences{top:10px;padding:0;border:0;background:url(../img/preference-icon.png) no-repeat;background-size:28px 28px;opacity:.8;width:28px;height:30px;display:block}#preferences *{display:none}@media screen and (max-width:50em){#results{margin:auto;padding:0;width:90%}.github{display:none}.checkbox_container{display:block;width:90%}.checkbox_container label{border-bottom:0}.preferences_container{display:none;postion:fixed!important;top:100px;right:0}}@media screen and (max-width:75em){div.title h1{font-size:1em}html.touch #categories{width:95%;height:30px;text-align:left;overflow-x:scroll;overflow-y:hidden;-webkit-overflow-scrolling:touch}html.touch #categories #categories_container{width:1000px;width:-moz-max-content;width:-webkit-max-content;width:max-content}html.touch #categories #categories_container .checkbox_container{display:inline-block;width:auto}#answers,#suggestions{margin-top:5px}#infoboxes{position:inherit;max-width:inherit}#infoboxes .infobox{clear:both}#infoboxes .infobox img{float:left;max-width:10em}#categories{font-size:90%;clear:both}#categories .checkbox_container{margin:auto}#sidebar{position:static;max-width:50em;margin:0 0 2px;padding:0;float:none;border:none;width:auto}#sidebar input{border:0}#apis,#search_url{display:none}.result{border-top:1px solid #E8E7E6;margin:8px 0}.image_result,.image_result img,.result .thumbnail{max-width:98%}}.favicon{float:left;margin-right:4px;margin-top:2px}.preferences_back{background:#3498DB;border:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px 4px;padding:4px 6px}.preferences_back a{color:#FFF}.hidden{opacity:0;overflow:hidden;font-size:.8em;position:absolute;bottom:-20px;width:100%;text-position:center;background:#fff;transition:opacity 1s ease}#categories_container:hover .hidden{transition:opacity 1s ease;opacity:.8}
1+.highlight .c,.highlight .cm,.highlight .ge,.highlight .sd{font-style:italic}#categories,.highlight .lineno{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#container,.search,body,html{padding:0;margin:0}div.title h1,input[type=checkbox]{visibility:hidden}#categories,.checkbox_container label,.engine_checkbox label,.highlight .lineno{-webkit-touch-callout:none;-khtml-user-select:none}#answers input[type=submit],#infoboxes input[type=submit],#sidebar input[type=submit],#suggestions input[type=submit],.result_title a:hover,.torrent_result a:hover{text-decoration:underline}#infoboxes,.result .content,.result .url,.result h3{word-wrap:break-word}#apis,#infoboxes .infobox br,#pagination,#pagination br,.result,.result .content br.last{clear:both}.highlight .hll{background-color:#ffc}.highlight{background:#f8f8f8}.highlight .c{color:#408080}.highlight .err{border:1px solid red}.highlight .k{color:green;font-weight:700}.highlight .o{color:#666}.highlight .cm{color:#408080}.highlight .cp{color:#BC7A00}.highlight .c1,.highlight .cs{color:#408080;font-style:italic}.highlight .gd{color:#A00000}.highlight .gr{color:red}.highlight .gh{color:navy;font-weight:700}.highlight .gi{color:#00A000}.highlight .go{color:#888}.highlight .gp{color:navy;font-weight:700}.highlight .gs{font-weight:700}.highlight .gu{color:purple;font-weight:700}.highlight .gt{color:#04D}.highlight .kc,.highlight .kd,.highlight .kn{color:green;font-weight:700}.highlight .kp{color:green}.highlight .kr{color:green;font-weight:700}.highlight .kt{color:#B00040}.highlight .m{color:#666}.highlight .s{color:#BA2121}.highlight .na{color:#7D9029}.highlight .nb{color:green}.highlight .nc{color:#00F;font-weight:700}.highlight .no{color:#800}.highlight .nd{color:#A2F}.highlight .ni{color:#999;font-weight:700}.highlight .ne{color:#D2413A;font-weight:700}.highlight .nf{color:#00F}.highlight .nl{color:#A0A000}.highlight .nn{color:#00F;font-weight:700}.highlight .nt{color:green;font-weight:700}.highlight .nv{color:#19177C}.highlight .ow{color:#A2F;font-weight:700}.highlight .w{color:#bbb}.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:#666}.highlight .s2,.highlight .sb,.highlight .sc{color:#BA2121}.highlight .sd{color:#BA2121}.highlight .se{color:#B62;font-weight:700}.highlight .sh{color:#BA2121}.highlight .si{color:#B68;font-weight:700}.highlight .sx{color:green}.highlight .sr{color:#B68}.highlight .s1{color:#BA2121}.highlight .ss{color:#19177C}.highlight .bp{color:green}.highlight .vc,.highlight .vg,.highlight .vi{color:#19177C}.highlight .il{color:#666}.highlight pre{overflow:auto}.highlight .lineno{user-select:none;cursor:default}.highlight .lineno::selection{background:0 0}.highlight .lineno::-moz-selection{background:0 0}html{font-family:sans-serif;font-size:.9em;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;color:#444}#container{width:100%;position:absolute;top:0}.search .checkbox_container label{font-size:.9em;border-bottom:2px solid #E8E7E6}.search .checkbox_container label:hover{border-bottom:2px solid #3498DB}.search .checkbox_container input[type=checkbox]:checked+label{border-bottom:2px solid #2980B9}#search_wrapper{position:relative;width:50em;padding:10px}.center #search_wrapper{margin-left:auto;margin-right:auto}.q,ul.autocompleter-choices{margin:0;border:1px solid #3498DB}.q{background:#FFF;color:#222;font-size:16px;height:28px;outline:0;padding:2px 2px 2px 8px;padding-right:0!important;width:100%;z-index:2}#search_submit{position:absolute;top:13px;right:1px;padding:0;border:0;background:url(../img/search-icon.png) no-repeat;background-size:24px 24px;opacity:.8;width:24px;height:30px;font-size:0}@media screen and (max-width:50em){#search_wrapper{width:90%;clear:both;overflow:hidden}}ul.autocompleter-choices{position:absolute;padding:0;list-style:none;border-left-color:#3498DB;border-right-color:#3498DB;border-bottom-color:#3498DB;text-align:left;font-family:Verdana,Geneva,Arial,Helvetica,sans-serif;z-index:50;background-color:#FFF;color:#444}ul.autocompleter-choices li{position:relative;margin:-2px 0 0;padding:.2em 1.5em .2em 1em;display:block;float:none!important;cursor:pointer;font-weight:400;white-space:nowrap;font-size:1em;line-height:1.5em}ul.autocompleter-choices li.autocompleter-selected{background-color:#444;color:#FFF}ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried{color:#9FCFFF}ul.autocompleter-choices span.autocompleter-queried{display:inline;float:none;font-weight:700;margin:0;padding:0}.row{max-width:800px;margin:20px auto;text-align:justify}.row h1{font-size:3em;margin-top:50px}.row p{padding:0 10px;max-width:700px}.row h3,.row ul{margin:4px 8px}.hmarg{margin:0 20px;border:1px solid #3498DB;padding:4px 10px}a:active.hmarg,a:hover.hmarg,a:link.hmarg,a:visited.hmarg{color:#3498DB}.top_margin{margin-top:60px}.center{text-align:center}h1{font-size:5em}div.title{background:url(../img/searx.png) center no-repeat;width:100%;min-height:80px}input[type=submit]{padding:2px 6px;margin:2px 4px;display:inline-block;background:#3498DB;color:#FFF;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:0;cursor:pointer}fieldset{margin:8px;border:1px solid #3498DB}#categories{margin:0 10px;user-select:none}.checkbox_container{display:inline-block;position:relative;margin:0 3px;padding:0}.checkbox_container input{display:none}.checkbox_container label,.engine_checkbox label{cursor:pointer;padding:4px 10px;margin:0;display:block;text-transform:capitalize;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkbox_container input[type=checkbox]:checked+label{background:#3498DB;color:#FFF}.engine_checkbox{padding:4px}label.allow{background:#E74C3C;padding:4px 8px;color:#FFF;display:none}label.deny{background:#2ECC71;padding:4px 8px;color:#444;display:inline}.engine_checkbox input[type=checkbox]:checked+label:nth-child(2)+label{display:none}.engine_checkbox input[type=checkbox]:checked+label.allow{display:inline}a{text-decoration:none;color:#1a11be}a:visited{color:#8E44AD}.result{margin:19px 0 18px;padding:0}.result_title{margin-bottom:0}.result_title a{color:#2980B9;font-weight:400;font-size:1.1em}.result_title a:visited{color:#8E44AD}.cache_link{font-size:10px!important}.result h3{font-size:1em;margin:5px 0 1px;padding:0}.result .content,.result .url,.small_font{font-size:.8em}.result .content{margin:0;padding:0;max-width:54em;line-height:1.24}.result .content img{float:left;margin-right:5px;max-width:200px;max-height:100px}.result .url{margin:0 0 3px;padding:0;max-width:54em;color:#C0392B}.result .published_date{font-size:.8em;color:#888;Margin:5px 20px}.result .thumbnail{width:400px}.engines{color:#888}.small p{margin:2px 0}.right{float:right}.invisible{display:none}.left{float:left}.highlight{color:#094089}.content .highlight{color:#000}.image_result{display:inline-block;margin:10px;position:relative;max-height:160px}.image_result img{border:0;max-height:160px}.image_result p{margin:0;padding:0}.image_result p span a{display:none;color:#FFF}.image_result p:hover span a{display:block;position:absolute;bottom:0;right:0;padding:4px;background-color:rgba(0,0,0,.6);font-size:.7em}#categories_container,.percentage{position:relative}.torrent_result{border-left:10px solid #d3d3d3;padding-left:3px}.torrent_result p{margin:3px;font-size:.8em}.torrent_result a{color:#2980B9}.torrent_result a:visited{color:#8E44AD}.definition_result{border-left:10px solid gray;padding-left:3px}.percentage{width:300px}.percentage div{background:#444}table{width:100%}.result-table{margin-bottom:10px}#infoboxes,#sidebar{margin:0 2px 5px 5px;padding:0 2px 2px}td{padding:0 4px}tr:hover{background:#DDD}#results{margin:auto auto 20px;padding:0;width:50em}#sidebar{position:fixed;bottom:10px;left:10px;width:14em}#answers input,#infoboxes input,#sidebar input,#suggestions input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:0 0;color:#444;cursor:pointer}#suggestions form{display:inline}#answers,#suggestions{margin-top:20px;max-width:45em}#suggestions-title{color:#888}#answers{border:2px solid #2980B9;padding:20px}#answers form,#infoboxes form{min-width:210px}#infoboxes{position:absolute;top:100px;right:20px;max-width:21em}#infoboxes .infobox{margin:10px 0;border:1px solid #ddd;padding:5px;font-size:.8em}#infoboxes .infobox img{max-width:90%;max-heigt:12em;display:block;margin:5px;padding:5px}#infoboxes .infobox h2{margin:0}#apis,#search_url{margin-top:8px}#infoboxes .infobox table{table-layout:fixed}#infoboxes .infobox table td{vertical-align:top}#infoboxes .infobox input{font-size:1em}#search_url input{border:1px solid #888;padding:4px;color:#444;width:14em;display:block;margin:4px;font-size:.8em}#preferences{top:10px;padding:0;border:0;background:url(../img/preference-icon.png) no-repeat;background-size:28px 28px;opacity:.8;width:28px;height:30px;display:block}#preferences *{display:none}@media screen and (max-width:50em){#results{margin:auto;padding:0;width:90%}.github{display:none}.checkbox_container{display:block;width:90%}.checkbox_container label{border-bottom:0}.preferences_container{display:none;postion:fixed!important;top:100px;right:0}}@media screen and (max-width:75em){div.title h1{font-size:1em}html.touch #categories{width:95%;height:30px;text-align:left;overflow-x:scroll;overflow-y:hidden;-webkit-overflow-scrolling:touch}html.touch #categories #categories_container{width:1000px;width:-moz-max-content;width:-webkit-max-content;width:max-content}html.touch #categories #categories_container .checkbox_container{display:inline-block;width:auto}#answers,#suggestions{margin-top:5px}#infoboxes{position:inherit;max-width:inherit}#infoboxes .infobox{clear:both}#infoboxes .infobox img{float:left;max-width:10em}#categories{font-size:90%;clear:both}#categories .checkbox_container{margin:auto}#sidebar{position:static;max-width:50em;margin:0 0 2px;padding:0;float:none;border:none;width:auto}#sidebar input{border:0}#apis,#search_url{display:none}.result{border-top:1px solid #E8E7E6;margin:8px 0}.image_result,.image_result img,.result .thumbnail{max-width:98%}}.favicon{float:left;margin-right:4px;margin-top:2px}.preferences_back{background:#3498DB;border:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px 4px;padding:4px 6px}.preferences_back a{color:#FFF}.hidden{opacity:0;overflow:hidden;font-size:.8em;position:absolute;bottom:-20px;width:100%;text-position:center;background:#fff;transition:opacity 1s ease}#categories_container:hover .hidden{transition:opacity 1s ease;opacity:.8}
searx/static/themes/legacy/less/autocompleter.less+61−61
@@ -1,61 +1,61 @@
1−/*
1+/*
2− * searx, A privacy-respecting, hackable metasearch engine
2+ * searx, A privacy-respecting, hackable metasearch engine
3− */
3+ */
4−
4+
5−ul {
5+ul {
6− &.autocompleter-choices {
6+ &.autocompleter-choices {
7− position: absolute;
7+ position: absolute;
8− margin: 0;
8+ margin: 0;
9− padding: 0;
9+ padding: 0;
10− list-style: none;
10+ list-style: none;
11− border: 1px solid @color-autocompleter-choices-border;
11+ border: 1px solid @color-autocompleter-choices-border;
12− border-left-color: @color-autocompleter-choices-border-left-right;
12+ border-left-color: @color-autocompleter-choices-border-left-right;
13− border-right-color: @color-autocompleter-choices-border-left-right;
13+ border-right-color: @color-autocompleter-choices-border-left-right;
14− border-bottom-color: @color-autocompleter-choices-border-bottom;
14+ border-bottom-color: @color-autocompleter-choices-border-bottom;
15− text-align: left;
15+ text-align: left;
16− font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
16+ font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
17− z-index: 50;
17+ z-index: 50;
18− background-color: @color-autocompleter-choices-background;
18+ background-color: @color-autocompleter-choices-background;
19− color: @color-autocompleter-choices-font;
19+ color: @color-autocompleter-choices-font;
20−
20+
21− li {
21+ li {
22− position: relative;
22+ position: relative;
23− margin: -2px 0 0 0;
23+ margin: -2px 0 0 0;
24− padding: 0.2em 1.5em 0.2em 1em;
24+ padding: 0.2em 1.5em 0.2em 1em;
25− display: block;
25+ display: block;
26− float: none !important;
26+ float: none !important;
27− cursor: pointer;
27+ cursor: pointer;
28− font-weight: normal;
28+ font-weight: normal;
29− white-space: nowrap;
29+ white-space: nowrap;
30− font-size: 1em;
30+ font-size: 1em;
31− line-height: 1.5em;
31+ line-height: 1.5em;
32−
32+
33− &.autocompleter-selected {
33+ &.autocompleter-selected {
34− background-color: @color-autocompleter-selected-background;
34+ background-color: @color-autocompleter-selected-background;
35− color: @color-autocompleter-selected-font;
35+ color: @color-autocompleter-selected-font;
36−
36+
37− span.autocompleter-queried {
37+ span.autocompleter-queried {
38− color: @color-autocompleter-selected-queried-font;
38+ color: @color-autocompleter-selected-queried-font;
39− }
39+ }
40− }
40+ }
41− }
41+ }
42−
42+
43− span.autocompleter-queried {
43+ span.autocompleter-queried {
44− display: inline;
44+ display: inline;
45− float: none;
45+ float: none;
46− font-weight: bold;
46+ font-weight: bold;
47− margin: 0;
47+ margin: 0;
48− padding: 0;
48+ padding: 0;
49− }
49+ }
50− }
50+ }
51−}
51+}
52−
52+
53−/*.autocompleter-loading {
53+/*.autocompleter-loading {
54− //background-image: url(images/spinner.gif);
54+ //background-image: url(images/spinner.gif);
55− background-repeat: no-repeat;
55+ background-repeat: no-repeat;
56− background-position: right 50%;
56+ background-position: right 50%;
57−}*/
57+}*/
58−
58+
59−/*textarea.autocompleter-loading {
59+/*textarea.autocompleter-loading {
60− background-position: right bottom;
60+ background-position: right bottom;
61−}*/
61+}*/
searx/static/themes/legacy/less/style.less+4
@@ -376,6 +376,10 @@ table {
376 width: 100%;
376 width: 100%;
377}
377}
378
378
379+.result-table {
380+ margin-bottom: 10px;
381+}
382+
379td {
383td {
380 padding: 0 4px;
384 padding: 0 4px;
381}
385}
searx/static/themes/oscar/gruntfile.js+3−3
@@ -24,7 +24,7 @@ module.exports = function(grunt) {
24 jshint: {
24 jshint: {
25 files: ['gruntfile.js', 'js/searx_src/*.js'],
25 files: ['gruntfile.js', 'js/searx_src/*.js'],
26 options: {
26 options: {
27− reporterOutput: "",
27+ reporterOutput: "",
28 // options here to override JSHint defaults
28 // options here to override JSHint defaults
29 globals: {
29 globals: {
30 jQuery: true,
30 jQuery: true,
@@ -55,7 +55,7 @@ module.exports = function(grunt) {
55 "css/logicodev-dark.min.css": "less/logicodev-dark/oscar.less"}
55 "css/logicodev-dark.min.css": "less/logicodev-dark/oscar.less"}
56 },
56 },
57 /*
57 /*
58− // built with ./manage.sh styles
58+ // built with ./manage.sh styles
59 bootstrap: {
59 bootstrap: {
60 options: {
60 options: {
61 paths: ["less/bootstrap"],
61 paths: ["less/bootstrap"],
@@ -90,7 +90,7 @@ module.exports = function(grunt) {
90 grunt.registerTask('test', ['jshint']);
90 grunt.registerTask('test', ['jshint']);
91
91
92 grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'less']);
92 grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'less']);
93−
93+
94 grunt.registerTask('styles', ['less']);
94 grunt.registerTask('styles', ['less']);
95
95
96};
96};
searx/static/themes/oscar/js/searx.js+326−326
@@ -1,26 +1,26 @@
1−/**
1+/**
2− * searx is free software: you can redistribute it and/or modify
2+ * searx is free software: you can redistribute it and/or modify
3− * it under the terms of the GNU Affero General Public License as published by
3+ * it under the terms of the GNU Affero General Public License as published by
4− * the Free Software Foundation, either version 3 of the License, or
4+ * the Free Software Foundation, either version 3 of the License, or
5− * (at your option) any later version.
5+ * (at your option) any later version.
6− *
6+ *
7− * searx is distributed in the hope that it will be useful,
7+ * searx is distributed in the hope that it will be useful,
8− * but WITHOUT ANY WARRANTY; without even the implied warranty of
8+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10− * GNU Affero General Public License for more details.
10+ * GNU Affero General Public License for more details.
11− *
11+ *
12− * You should have received a copy of the GNU Affero General Public License
12+ * You should have received a copy of the GNU Affero General Public License
13− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
13+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
14− *
14+ *
15− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
15+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
16− */
16+ */
17−
17+
18−requirejs.config({
18+requirejs.config({
19− baseUrl: './static/themes/oscar/js',
19+ baseUrl: './static/themes/oscar/js',
20− paths: {
20+ paths: {
21− app: '../app'
21+ app: '../app'
22− }
22+ }
23−});
23+});
24;/**
24;/**
25 * searx is free software: you can redistribute it and/or modify
25 * searx is free software: you can redistribute it and/or modify
26 * it under the terms of the GNU Affero General Public License as published by
26 * it under the terms of the GNU Affero General Public License as published by
@@ -51,306 +51,306 @@ window.searx = (function(d) {
51 method: script.getAttribute('data-method')
51 method: script.getAttribute('data-method')
52 };
52 };
53})(document);
53})(document);
54−;/**
54+;/**
55− * searx is free software: you can redistribute it and/or modify
55+ * searx is free software: you can redistribute it and/or modify
56− * it under the terms of the GNU Affero General Public License as published by
56+ * it under the terms of the GNU Affero General Public License as published by
57− * the Free Software Foundation, either version 3 of the License, or
57+ * the Free Software Foundation, either version 3 of the License, or
58− * (at your option) any later version.
58+ * (at your option) any later version.
59− *
59+ *
60− * searx is distributed in the hope that it will be useful,
60+ * searx is distributed in the hope that it will be useful,
61− * but WITHOUT ANY WARRANTY; without even the implied warranty of
61+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
62− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
62+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
63− * GNU Affero General Public License for more details.
63+ * GNU Affero General Public License for more details.
64− *
64+ *
65− * You should have received a copy of the GNU Affero General Public License
65+ * You should have received a copy of the GNU Affero General Public License
66− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
66+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
67− *
67+ *
68− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
68+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
69− */
69+ */
70−
70+
71−if(searx.autocompleter) {
71+if(searx.autocompleter) {
72− searx.searchResults = new Bloodhound({
72+ searx.searchResults = new Bloodhound({
73− datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
73+ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
74− queryTokenizer: Bloodhound.tokenizers.whitespace,
74+ queryTokenizer: Bloodhound.tokenizers.whitespace,
75− remote: './autocompleter?q=%QUERY'
75+ remote: './autocompleter?q=%QUERY'
76− });
76+ });
77− searx.searchResults.initialize();
77+ searx.searchResults.initialize();
78−}
78+}
79−
79+
80−$(document).ready(function(){
80+$(document).ready(function(){
81− if(searx.autocompleter) {
81+ if(searx.autocompleter) {
82− $('#q').typeahead(null, {
82+ $('#q').typeahead(null, {
83− name: 'search-results',
83+ name: 'search-results',
84− displayKey: function(result) {
84+ displayKey: function(result) {
85− return result;
85+ return result;
86− },
86+ },
87− source: searx.searchResults.ttAdapter()
87+ source: searx.searchResults.ttAdapter()
88− });
88+ });
89− }
89+ }
90−});
90+});
91−;/**
91+;/**
92− * searx is free software: you can redistribute it and/or modify
92+ * searx is free software: you can redistribute it and/or modify
93− * it under the terms of the GNU Affero General Public License as published by
93+ * it under the terms of the GNU Affero General Public License as published by
94− * the Free Software Foundation, either version 3 of the License, or
94+ * the Free Software Foundation, either version 3 of the License, or
95− * (at your option) any later version.
95+ * (at your option) any later version.
96− *
96+ *
97− * searx is distributed in the hope that it will be useful,
97+ * searx is distributed in the hope that it will be useful,
98− * but WITHOUT ANY WARRANTY; without even the implied warranty of
98+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
99− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
99+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
100− * GNU Affero General Public License for more details.
100+ * GNU Affero General Public License for more details.
101− *
101+ *
102− * You should have received a copy of the GNU Affero General Public License
102+ * You should have received a copy of the GNU Affero General Public License
103− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
103+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
104− *
104+ *
105− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
105+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
106− */
106+ */
107−
107+
108−$(document).ready(function(){
108+$(document).ready(function(){
109− /**
109+ /**
110− * focus element if class="autofocus" and id="q"
110+ * focus element if class="autofocus" and id="q"
111− */
111+ */
112− $('#q.autofocus').focus();
112+ $('#q.autofocus').focus();
113−
113+
114− /**
114+ /**
115− * select full content on click if class="select-all-on-click"
115+ * select full content on click if class="select-all-on-click"
116− */
116+ */
117− $(".select-all-on-click").click(function () {
117+ $(".select-all-on-click").click(function () {
118− $(this).select();
118+ $(this).select();
119− });
119+ });
120−
120+
121− /**
121+ /**
122− * change text during btn-collapse click if possible
122+ * change text during btn-collapse click if possible
123− */
123+ */
124− $('.btn-collapse').click(function() {
124+ $('.btn-collapse').click(function() {
125− var btnTextCollapsed = $(this).data('btn-text-collapsed');
125+ var btnTextCollapsed = $(this).data('btn-text-collapsed');
126− var btnTextNotCollapsed = $(this).data('btn-text-not-collapsed');
126+ var btnTextNotCollapsed = $(this).data('btn-text-not-collapsed');
127−
127+
128− if(btnTextCollapsed !== '' && btnTextNotCollapsed !== '') {
128+ if(btnTextCollapsed !== '' && btnTextNotCollapsed !== '') {
129− if($(this).hasClass('collapsed')) {
129+ if($(this).hasClass('collapsed')) {
130− new_html = $(this).html().replace(btnTextCollapsed, btnTextNotCollapsed);
130+ new_html = $(this).html().replace(btnTextCollapsed, btnTextNotCollapsed);
131− } else {
131+ } else {
132− new_html = $(this).html().replace(btnTextNotCollapsed, btnTextCollapsed);
132+ new_html = $(this).html().replace(btnTextNotCollapsed, btnTextCollapsed);
133− }
133+ }
134− $(this).html(new_html);
134+ $(this).html(new_html);
135− }
135+ }
136− });
136+ });
137−
137+
138− /**
138+ /**
139− * change text during btn-toggle click if possible
139+ * change text during btn-toggle click if possible
140− */
140+ */
141− $('.btn-toggle .btn').click(function() {
141+ $('.btn-toggle .btn').click(function() {
142− var btnClass = 'btn-' + $(this).data('btn-class');
142+ var btnClass = 'btn-' + $(this).data('btn-class');
143− var btnLabelDefault = $(this).data('btn-label-default');
143+ var btnLabelDefault = $(this).data('btn-label-default');
144− var btnLabelToggled = $(this).data('btn-label-toggled');
144+ var btnLabelToggled = $(this).data('btn-label-toggled');
145− if(btnLabelToggled !== '') {
145+ if(btnLabelToggled !== '') {
146− if($(this).hasClass('btn-default')) {
146+ if($(this).hasClass('btn-default')) {
147− new_html = $(this).html().replace(btnLabelDefault, btnLabelToggled);
147+ new_html = $(this).html().replace(btnLabelDefault, btnLabelToggled);
148− } else {
148+ } else {
149− new_html = $(this).html().replace(btnLabelToggled, btnLabelDefault);
149+ new_html = $(this).html().replace(btnLabelToggled, btnLabelDefault);
150− }
150+ }
151− $(this).html(new_html);
151+ $(this).html(new_html);
152− }
152+ }
153− $(this).toggleClass(btnClass);
153+ $(this).toggleClass(btnClass);
154− $(this).toggleClass('btn-default');
154+ $(this).toggleClass('btn-default');
155− });
155+ });
156−
156+
157− /**
157+ /**
158− * change text during btn-toggle click if possible
158+ * change text during btn-toggle click if possible
159− */
159+ */
160− $('.media-loader').click(function() {
160+ $('.media-loader').click(function() {
161− var target = $(this).data('target');
161+ var target = $(this).data('target');
162− var iframe_load = $(target + ' > iframe');
162+ var iframe_load = $(target + ' > iframe');
163− var srctest = iframe_load.attr('src');
163+ var srctest = iframe_load.attr('src');
164− if(srctest === undefined || srctest === false){
164+ if(srctest === undefined || srctest === false){
165− iframe_load.attr('src', iframe_load.data('src'));
165+ iframe_load.attr('src', iframe_load.data('src'));
166− }
166+ }
167− });
167+ });
168−
168+
169− /**
169+ /**
170− * Select or deselect every categories on double clic
170+ * Select or deselect every categories on double clic
171− */
171+ */
172− $(".btn-sm").dblclick(function() {
172+ $(".btn-sm").dblclick(function() {
173− var btnClass = 'btn-' + $(this).data('btn-class'); // primary
173+ var btnClass = 'btn-' + $(this).data('btn-class'); // primary
174− if($(this).hasClass('btn-default')) {
174+ if($(this).hasClass('btn-default')) {
175− $(".btn-sm > input").attr('checked', 'checked');
175+ $(".btn-sm > input").attr('checked', 'checked');
176− $(".btn-sm > input").prop("checked", true);
176+ $(".btn-sm > input").prop("checked", true);
177− $(".btn-sm").addClass(btnClass);
177+ $(".btn-sm").addClass(btnClass);
178− $(".btn-sm").addClass('active');
178+ $(".btn-sm").addClass('active');
179− $(".btn-sm").removeClass('btn-default');
179+ $(".btn-sm").removeClass('btn-default');
180− } else {
180+ } else {
181− $(".btn-sm > input").attr('checked', '');
181+ $(".btn-sm > input").attr('checked', '');
182− $(".btn-sm > input").removeAttr('checked');
182+ $(".btn-sm > input").removeAttr('checked');
183− $(".btn-sm > input").checked = false;
183+ $(".btn-sm > input").checked = false;
184− $(".btn-sm").removeClass(btnClass);
184+ $(".btn-sm").removeClass(btnClass);
185− $(".btn-sm").removeClass('active');
185+ $(".btn-sm").removeClass('active');
186− $(".btn-sm").addClass('btn-default');
186+ $(".btn-sm").addClass('btn-default');
187− }
187+ }
188− });
188+ });
189−});
189+});
190−;/**
190+;/**
191− * searx is free software: you can redistribute it and/or modify
191+ * searx is free software: you can redistribute it and/or modify
192− * it under the terms of the GNU Affero General Public License as published by
192+ * it under the terms of the GNU Affero General Public License as published by
193− * the Free Software Foundation, either version 3 of the License, or
193+ * the Free Software Foundation, either version 3 of the License, or
194− * (at your option) any later version.
194+ * (at your option) any later version.
195− *
195+ *
196− * searx is distributed in the hope that it will be useful,
196+ * searx is distributed in the hope that it will be useful,
197− * but WITHOUT ANY WARRANTY; without even the implied warranty of
197+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
198− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
198+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
199− * GNU Affero General Public License for more details.
199+ * GNU Affero General Public License for more details.
200− *
200+ *
201− * You should have received a copy of the GNU Affero General Public License
201+ * You should have received a copy of the GNU Affero General Public License
202− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
202+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
203− *
203+ *
204− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
204+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
205− */
205+ */
206−
206+
207−$(document).ready(function(){
207+$(document).ready(function(){
208− $(".searx_overpass_request").on( "click", function( event ) {
208+ $(".searx_overpass_request").on( "click", function( event ) {
209− var overpass_url = "https://overpass-api.de/api/interpreter?data=";
209+ var overpass_url = "https://overpass-api.de/api/interpreter?data=";
210− var query_start = overpass_url + "[out:json][timeout:25];(";
210+ var query_start = overpass_url + "[out:json][timeout:25];(";
211− var query_end = ");out meta;";
211+ var query_end = ");out meta;";
212−
212+
213− var osm_id = $(this).data('osm-id');
213+ var osm_id = $(this).data('osm-id');
214− var osm_type = $(this).data('osm-type');
214+ var osm_type = $(this).data('osm-type');
215− var result_table = $(this).data('result-table');
215+ var result_table = $(this).data('result-table');
216− var result_table_loadicon = "#" + $(this).data('result-table-loadicon');
216+ var result_table_loadicon = "#" + $(this).data('result-table-loadicon');
217−
217+
218− // tags which can be ignored
218+ // tags which can be ignored
219− var osm_ignore_tags = [ "addr:city", "addr:country", "addr:housenumber", "addr:postcode", "addr:street" ];
219+ var osm_ignore_tags = [ "addr:city", "addr:country", "addr:housenumber", "addr:postcode", "addr:street" ];
220−
220+
221− if(osm_id && osm_type && result_table) {
221+ if(osm_id && osm_type && result_table) {
222− result_table = "#" + result_table;
222+ result_table = "#" + result_table;
223− var query = null;
223+ var query = null;
224− switch(osm_type) {
224+ switch(osm_type) {
225− case 'node':
225+ case 'node':
226− query = query_start + "node(" + osm_id + ");" + query_end;
226+ query = query_start + "node(" + osm_id + ");" + query_end;
227− break;
227+ break;
228− case 'way':
228+ case 'way':
229− query = query_start + "way(" + osm_id + ");" + query_end;
229+ query = query_start + "way(" + osm_id + ");" + query_end;
230− break;
230+ break;
231− case 'relation':
231+ case 'relation':
232− query = query_start + "relation(" + osm_id + ");" + query_end;
232+ query = query_start + "relation(" + osm_id + ");" + query_end;
233− break;
233+ break;
234− default:
234+ default:
235− break;
235+ break;
236− }
236+ }
237− if(query) {
237+ if(query) {
238− //alert(query);
238+ //alert(query);
239− var ajaxRequest = $.ajax( query )
239+ var ajaxRequest = $.ajax( query )
240− .done(function( html) {
240+ .done(function( html) {
241− if(html && html.elements && html.elements[0]) {
241+ if(html && html.elements && html.elements[0]) {
242− var element = html.elements[0];
242+ var element = html.elements[0];
243− var newHtml = $(result_table).html();
243+ var newHtml = $(result_table).html();
244− for (var row in element.tags) {
244+ for (var row in element.tags) {
245− if(element.tags.name === null || osm_ignore_tags.indexOf(row) == -1) {
245+ if(element.tags.name === null || osm_ignore_tags.indexOf(row) == -1) {
246− newHtml += "<tr><td>" + row + "</td><td>";
246+ newHtml += "<tr><td>" + row + "</td><td>";
247− switch(row) {
247+ switch(row) {
248− case "phone":
248+ case "phone":
249− case "fax":
249+ case "fax":
250− newHtml += "<a href=\"tel:" + element.tags[row].replace(/ /g,'') + "\">" + element.tags[row] + "</a>";
250+ newHtml += "<a href=\"tel:" + element.tags[row].replace(/ /g,'') + "\">" + element.tags[row] + "</a>";
251− break;
251+ break;
252− case "email":
252+ case "email":
253− newHtml += "<a href=\"mailto:" + element.tags[row] + "\">" + element.tags[row] + "</a>";
253+ newHtml += "<a href=\"mailto:" + element.tags[row] + "\">" + element.tags[row] + "</a>";
254− break;
254+ break;
255− case "website":
255+ case "website":
256− case "url":
256+ case "url":
257− newHtml += "<a href=\"" + element.tags[row] + "\">" + element.tags[row] + "</a>";
257+ newHtml += "<a href=\"" + element.tags[row] + "\">" + element.tags[row] + "</a>";
258− break;
258+ break;
259− case "wikidata":
259+ case "wikidata":
260− newHtml += "<a href=\"https://www.wikidata.org/wiki/" + element.tags[row] + "\">" + element.tags[row] + "</a>";
260+ newHtml += "<a href=\"https://www.wikidata.org/wiki/" + element.tags[row] + "\">" + element.tags[row] + "</a>";
261− break;
261+ break;
262− case "wikipedia":
262+ case "wikipedia":
263− if(element.tags[row].indexOf(":") != -1) {
263+ if(element.tags[row].indexOf(":") != -1) {
264− newHtml += "<a href=\"https://" + element.tags[row].substring(0,element.tags[row].indexOf(":")) + ".wikipedia.org/wiki/" + element.tags[row].substring(element.tags[row].indexOf(":")+1) + "\">" + element.tags[row] + "</a>";
264+ newHtml += "<a href=\"https://" + element.tags[row].substring(0,element.tags[row].indexOf(":")) + ".wikipedia.org/wiki/" + element.tags[row].substring(element.tags[row].indexOf(":")+1) + "\">" + element.tags[row] + "</a>";
265− break;
265+ break;
266− }
266+ }
267− /* jshint ignore:start */
267+ /* jshint ignore:start */
268− default:
268+ default:
269− /* jshint ignore:end */
269+ /* jshint ignore:end */
270− newHtml += element.tags[row];
270+ newHtml += element.tags[row];
271− break;
271+ break;
272− }
272+ }
273− newHtml += "</td></tr>";
273+ newHtml += "</td></tr>";
274− }
274+ }
275− }
275+ }
276− $(result_table).html(newHtml);
276+ $(result_table).html(newHtml);
277− $(result_table).removeClass('hidden');
277+ $(result_table).removeClass('hidden');
278− $(result_table_loadicon).addClass('hidden');
278+ $(result_table_loadicon).addClass('hidden');
279− }
279+ }
280− })
280+ })
281− .fail(function() {
281+ .fail(function() {
282− $(result_table_loadicon).html($(result_table_loadicon).html() + "<p class=\"text-muted\">could not load data!</p>");
282+ $(result_table_loadicon).html($(result_table_loadicon).html() + "<p class=\"text-muted\">could not load data!</p>");
283− });
283+ });
284− }
284+ }
285− }
285+ }
286−
286+
287− // this event occour only once per element
287+ // this event occour only once per element
288− $( this ).off( event );
288+ $( this ).off( event );
289− });
289+ });
290−
290+
291− $(".searx_init_map").on( "click", function( event ) {
291+ $(".searx_init_map").on( "click", function( event ) {
292− var leaflet_target = $(this).data('leaflet-target');
292+ var leaflet_target = $(this).data('leaflet-target');
293− var map_lon = $(this).data('map-lon');
293+ var map_lon = $(this).data('map-lon');
294− var map_lat = $(this).data('map-lat');
294+ var map_lat = $(this).data('map-lat');
295− var map_zoom = $(this).data('map-zoom');
295+ var map_zoom = $(this).data('map-zoom');
296− var map_boundingbox = $(this).data('map-boundingbox');
296+ var map_boundingbox = $(this).data('map-boundingbox');
297− var map_geojson = $(this).data('map-geojson');
297+ var map_geojson = $(this).data('map-geojson');
298−
298+
299− require(['leaflet-0.7.3.min'], function(leaflet) {
299+ require(['leaflet-0.7.3.min'], function(leaflet) {
300− if(map_boundingbox) {
300+ if(map_boundingbox) {
301− southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);
301+ southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);
302− northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]);
302+ northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]);
303− map_bounds = L.latLngBounds(southWest, northEast);
303+ map_bounds = L.latLngBounds(southWest, northEast);
304− }
304+ }
305−
305+
306− // TODO hack
306+ // TODO hack
307− // change default imagePath
307+ // change default imagePath
308− L.Icon.Default.imagePath = "./static/themes/oscar/img/map";
308+ L.Icon.Default.imagePath = "./static/themes/oscar/img/map";
309−
309+
310− // init map
310+ // init map
311− var map = L.map(leaflet_target);
311+ var map = L.map(leaflet_target);
312−
312+
313− // create the tile layer with correct attribution
313+ // create the tile layer with correct attribution
314− var osmMapnikUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
314+ var osmMapnikUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
315− var osmMapnikAttrib='Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
315+ var osmMapnikAttrib='Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
316− var osmMapnik = new L.TileLayer(osmMapnikUrl, {minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib});
316+ var osmMapnik = new L.TileLayer(osmMapnikUrl, {minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib});
317−
317+
318− var osmWikimediaUrl='https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png';
318+ var osmWikimediaUrl='https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png';
319− var osmWikimediaAttrib = 'Wikimedia maps beta | Maps data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
319+ var osmWikimediaAttrib = 'Wikimedia maps beta | Maps data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
320− var osmWikimedia = new L.TileLayer(osmWikimediaUrl, {minZoom: 1, maxZoom: 19, attribution: osmWikimediaAttrib});
320+ var osmWikimedia = new L.TileLayer(osmWikimediaUrl, {minZoom: 1, maxZoom: 19, attribution: osmWikimediaAttrib});
321−
321+
322− // init map view
322+ // init map view
323− if(map_bounds) {
323+ if(map_bounds) {
324− // TODO hack: https://github.com/Leaflet/Leaflet/issues/2021
324+ // TODO hack: https://github.com/Leaflet/Leaflet/issues/2021
325− setTimeout(function () {
325+ setTimeout(function () {
326− map.fitBounds(map_bounds, {
326+ map.fitBounds(map_bounds, {
327− maxZoom:17
327+ maxZoom:17
328− });
328+ });
329− }, 0);
329+ }, 0);
330− } else if (map_lon && map_lat) {
330+ } else if (map_lon && map_lat) {
331− if(map_zoom)
331+ if(map_zoom)
332− map.setView(new L.LatLng(map_lat, map_lon),map_zoom);
332+ map.setView(new L.LatLng(map_lat, map_lon),map_zoom);
333− else
333+ else
334− map.setView(new L.LatLng(map_lat, map_lon),8);
334+ map.setView(new L.LatLng(map_lat, map_lon),8);
335− }
335+ }
336−
336+
337− map.addLayer(osmMapnik);
337+ map.addLayer(osmMapnik);
338−
338+
339− var baseLayers = {
339+ var baseLayers = {
340− "OSM Mapnik": osmMapnik/*,
340+ "OSM Mapnik": osmMapnik/*,
341− "OSM Wikimedia": osmWikimedia*/
341+ "OSM Wikimedia": osmWikimedia*/
342− };
342+ };
343−
343+
344− L.control.layers(baseLayers).addTo(map);
344+ L.control.layers(baseLayers).addTo(map);
345−
345+
346−
346+
347− if(map_geojson)
347+ if(map_geojson)
348− L.geoJson(map_geojson).addTo(map);
348+ L.geoJson(map_geojson).addTo(map);
349− /*else if(map_bounds)
349+ /*else if(map_bounds)
350− L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);*/
350+ L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);*/
351− });
351+ });
352−
352+
353− // this event occour only once per element
353+ // this event occour only once per element
354− $( this ).off( event );
354+ $( this ).off( event );
355− });
355+ });
356−});
356+});
searx/static/themes/oscar/js/searx_src/00_requirejs_config.js+23−23
@@ -1,23 +1,23 @@
1−/**
1+/**
2− * searx is free software: you can redistribute it and/or modify
2+ * searx is free software: you can redistribute it and/or modify
3− * it under the terms of the GNU Affero General Public License as published by
3+ * it under the terms of the GNU Affero General Public License as published by
4− * the Free Software Foundation, either version 3 of the License, or
4+ * the Free Software Foundation, either version 3 of the License, or
5− * (at your option) any later version.
5+ * (at your option) any later version.
6− *
6+ *
7− * searx is distributed in the hope that it will be useful,
7+ * searx is distributed in the hope that it will be useful,
8− * but WITHOUT ANY WARRANTY; without even the implied warranty of
8+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10− * GNU Affero General Public License for more details.
10+ * GNU Affero General Public License for more details.
11− *
11+ *
12− * You should have received a copy of the GNU Affero General Public License
12+ * You should have received a copy of the GNU Affero General Public License
13− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
13+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
14− *
14+ *
15− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
15+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
16− */
16+ */
17−
17+
18−requirejs.config({
18+requirejs.config({
19− baseUrl: './static/themes/oscar/js',
19+ baseUrl: './static/themes/oscar/js',
20− paths: {
20+ paths: {
21− app: '../app'
21+ app: '../app'
22− }
22+ }
23−});
23+});
searx/static/themes/oscar/js/searx_src/autocompleter.js+37−37
@@ -1,37 +1,37 @@
1−/**
1+/**
2− * searx is free software: you can redistribute it and/or modify
2+ * searx is free software: you can redistribute it and/or modify
3− * it under the terms of the GNU Affero General Public License as published by
3+ * it under the terms of the GNU Affero General Public License as published by
4− * the Free Software Foundation, either version 3 of the License, or
4+ * the Free Software Foundation, either version 3 of the License, or
5− * (at your option) any later version.
5+ * (at your option) any later version.
6− *
6+ *
7− * searx is distributed in the hope that it will be useful,
7+ * searx is distributed in the hope that it will be useful,
8− * but WITHOUT ANY WARRANTY; without even the implied warranty of
8+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10− * GNU Affero General Public License for more details.
10+ * GNU Affero General Public License for more details.
11− *
11+ *
12− * You should have received a copy of the GNU Affero General Public License
12+ * You should have received a copy of the GNU Affero General Public License
13− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
13+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
14− *
14+ *
15− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
15+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
16− */
16+ */
17−
17+
18−if(searx.autocompleter) {
18+if(searx.autocompleter) {
19− searx.searchResults = new Bloodhound({
19+ searx.searchResults = new Bloodhound({
20− datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
20+ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
21− queryTokenizer: Bloodhound.tokenizers.whitespace,
21+ queryTokenizer: Bloodhound.tokenizers.whitespace,
22− remote: './autocompleter?q=%QUERY'
22+ remote: './autocompleter?q=%QUERY'
23− });
23+ });
24− searx.searchResults.initialize();
24+ searx.searchResults.initialize();
25−}
25+}
26−
26+
27−$(document).ready(function(){
27+$(document).ready(function(){
28− if(searx.autocompleter) {
28+ if(searx.autocompleter) {
29− $('#q').typeahead(null, {
29+ $('#q').typeahead(null, {
30− name: 'search-results',
30+ name: 'search-results',
31− displayKey: function(result) {
31+ displayKey: function(result) {
32− return result;
32+ return result;
33− },
33+ },
34− source: searx.searchResults.ttAdapter()
34+ source: searx.searchResults.ttAdapter()
35− });
35+ });
36− }
36+ }
37−});
37+});
searx/static/themes/oscar/js/searx_src/element_modifiers.js+99−99
@@ -1,99 +1,99 @@
1−/**
1+/**
2− * searx is free software: you can redistribute it and/or modify
2+ * searx is free software: you can redistribute it and/or modify
3− * it under the terms of the GNU Affero General Public License as published by
3+ * it under the terms of the GNU Affero General Public License as published by
4− * the Free Software Foundation, either version 3 of the License, or
4+ * the Free Software Foundation, either version 3 of the License, or
5− * (at your option) any later version.
5+ * (at your option) any later version.
6− *
6+ *
7− * searx is distributed in the hope that it will be useful,
7+ * searx is distributed in the hope that it will be useful,
8− * but WITHOUT ANY WARRANTY; without even the implied warranty of
8+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10− * GNU Affero General Public License for more details.
10+ * GNU Affero General Public License for more details.
11− *
11+ *
12− * You should have received a copy of the GNU Affero General Public License
12+ * You should have received a copy of the GNU Affero General Public License
13− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
13+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
14− *
14+ *
15− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
15+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
16− */
16+ */
17−
17+
18−$(document).ready(function(){
18+$(document).ready(function(){
19− /**
19+ /**
20− * focus element if class="autofocus" and id="q"
20+ * focus element if class="autofocus" and id="q"
21− */
21+ */
22− $('#q.autofocus').focus();
22+ $('#q.autofocus').focus();
23−
23+
24− /**
24+ /**
25− * select full content on click if class="select-all-on-click"
25+ * select full content on click if class="select-all-on-click"
26− */
26+ */
27− $(".select-all-on-click").click(function () {
27+ $(".select-all-on-click").click(function () {
28− $(this).select();
28+ $(this).select();
29− });
29+ });
30−
30+
31− /**
31+ /**
32− * change text during btn-collapse click if possible
32+ * change text during btn-collapse click if possible
33− */
33+ */
34− $('.btn-collapse').click(function() {
34+ $('.btn-collapse').click(function() {
35− var btnTextCollapsed = $(this).data('btn-text-collapsed');
35+ var btnTextCollapsed = $(this).data('btn-text-collapsed');
36− var btnTextNotCollapsed = $(this).data('btn-text-not-collapsed');
36+ var btnTextNotCollapsed = $(this).data('btn-text-not-collapsed');
37−
37+
38− if(btnTextCollapsed !== '' && btnTextNotCollapsed !== '') {
38+ if(btnTextCollapsed !== '' && btnTextNotCollapsed !== '') {
39− if($(this).hasClass('collapsed')) {
39+ if($(this).hasClass('collapsed')) {
40− new_html = $(this).html().replace(btnTextCollapsed, btnTextNotCollapsed);
40+ new_html = $(this).html().replace(btnTextCollapsed, btnTextNotCollapsed);
41− } else {
41+ } else {
42− new_html = $(this).html().replace(btnTextNotCollapsed, btnTextCollapsed);
42+ new_html = $(this).html().replace(btnTextNotCollapsed, btnTextCollapsed);
43− }
43+ }
44− $(this).html(new_html);
44+ $(this).html(new_html);
45− }
45+ }
46− });
46+ });
47−
47+
48− /**
48+ /**
49− * change text during btn-toggle click if possible
49+ * change text during btn-toggle click if possible
50− */
50+ */
51− $('.btn-toggle .btn').click(function() {
51+ $('.btn-toggle .btn').click(function() {
52− var btnClass = 'btn-' + $(this).data('btn-class');
52+ var btnClass = 'btn-' + $(this).data('btn-class');
53− var btnLabelDefault = $(this).data('btn-label-default');
53+ var btnLabelDefault = $(this).data('btn-label-default');
54− var btnLabelToggled = $(this).data('btn-label-toggled');
54+ var btnLabelToggled = $(this).data('btn-label-toggled');
55− if(btnLabelToggled !== '') {
55+ if(btnLabelToggled !== '') {
56− if($(this).hasClass('btn-default')) {
56+ if($(this).hasClass('btn-default')) {
57− new_html = $(this).html().replace(btnLabelDefault, btnLabelToggled);
57+ new_html = $(this).html().replace(btnLabelDefault, btnLabelToggled);
58− } else {
58+ } else {
59− new_html = $(this).html().replace(btnLabelToggled, btnLabelDefault);
59+ new_html = $(this).html().replace(btnLabelToggled, btnLabelDefault);
60− }
60+ }
61− $(this).html(new_html);
61+ $(this).html(new_html);
62− }
62+ }
63− $(this).toggleClass(btnClass);
63+ $(this).toggleClass(btnClass);
64− $(this).toggleClass('btn-default');
64+ $(this).toggleClass('btn-default');
65− });
65+ });
66−
66+
67− /**
67+ /**
68− * change text during btn-toggle click if possible
68+ * change text during btn-toggle click if possible
69− */
69+ */
70− $('.media-loader').click(function() {
70+ $('.media-loader').click(function() {
71− var target = $(this).data('target');
71+ var target = $(this).data('target');
72− var iframe_load = $(target + ' > iframe');
72+ var iframe_load = $(target + ' > iframe');
73− var srctest = iframe_load.attr('src');
73+ var srctest = iframe_load.attr('src');
74− if(srctest === undefined || srctest === false){
74+ if(srctest === undefined || srctest === false){
75− iframe_load.attr('src', iframe_load.data('src'));
75+ iframe_load.attr('src', iframe_load.data('src'));
76− }
76+ }
77− });
77+ });
78−
78+
79− /**
79+ /**
80− * Select or deselect every categories on double clic
80+ * Select or deselect every categories on double clic
81− */
81+ */
82− $(".btn-sm").dblclick(function() {
82+ $(".btn-sm").dblclick(function() {
83− var btnClass = 'btn-' + $(this).data('btn-class'); // primary
83+ var btnClass = 'btn-' + $(this).data('btn-class'); // primary
84− if($(this).hasClass('btn-default')) {
84+ if($(this).hasClass('btn-default')) {
85− $(".btn-sm > input").attr('checked', 'checked');
85+ $(".btn-sm > input").attr('checked', 'checked');
86− $(".btn-sm > input").prop("checked", true);
86+ $(".btn-sm > input").prop("checked", true);
87− $(".btn-sm").addClass(btnClass);
87+ $(".btn-sm").addClass(btnClass);
88− $(".btn-sm").addClass('active');
88+ $(".btn-sm").addClass('active');
89− $(".btn-sm").removeClass('btn-default');
89+ $(".btn-sm").removeClass('btn-default');
90− } else {
90+ } else {
91− $(".btn-sm > input").attr('checked', '');
91+ $(".btn-sm > input").attr('checked', '');
92− $(".btn-sm > input").removeAttr('checked');
92+ $(".btn-sm > input").removeAttr('checked');
93− $(".btn-sm > input").checked = false;
93+ $(".btn-sm > input").checked = false;
94− $(".btn-sm").removeClass(btnClass);
94+ $(".btn-sm").removeClass(btnClass);
95− $(".btn-sm").removeClass('active');
95+ $(".btn-sm").removeClass('active');
96− $(".btn-sm").addClass('btn-default');
96+ $(".btn-sm").addClass('btn-default');
97− }
97+ }
98− });
98+ });
99−});
99+});
searx/static/themes/oscar/js/searx_src/leaflet_map.js+167−167
@@ -1,167 +1,167 @@
1−/**
1+/**
2− * searx is free software: you can redistribute it and/or modify
2+ * searx is free software: you can redistribute it and/or modify
3− * it under the terms of the GNU Affero General Public License as published by
3+ * it under the terms of the GNU Affero General Public License as published by
4− * the Free Software Foundation, either version 3 of the License, or
4+ * the Free Software Foundation, either version 3 of the License, or
5− * (at your option) any later version.
5+ * (at your option) any later version.
6− *
6+ *
7− * searx is distributed in the hope that it will be useful,
7+ * searx is distributed in the hope that it will be useful,
8− * but WITHOUT ANY WARRANTY; without even the implied warranty of
8+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10− * GNU Affero General Public License for more details.
10+ * GNU Affero General Public License for more details.
11− *
11+ *
12− * You should have received a copy of the GNU Affero General Public License
12+ * You should have received a copy of the GNU Affero General Public License
13− * along with searx. If not, see < http://www.gnu.org/licenses/ >.
13+ * along with searx. If not, see < http://www.gnu.org/licenses/ >.
14− *
14+ *
15− * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
15+ * (C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
16− */
16+ */
17−
17+
18−$(document).ready(function(){
18+$(document).ready(function(){
19− $(".searx_overpass_request").on( "click", function( event ) {
19+ $(".searx_overpass_request").on( "click", function( event ) {
20− var overpass_url = "https://overpass-api.de/api/interpreter?data=";
20+ var overpass_url = "https://overpass-api.de/api/interpreter?data=";
21− var query_start = overpass_url + "[out:json][timeout:25];(";
21+ var query_start = overpass_url + "[out:json][timeout:25];(";
22− var query_end = ");out meta;";
22+ var query_end = ");out meta;";
23−
23+
24− var osm_id = $(this).data('osm-id');
24+ var osm_id = $(this).data('osm-id');
25− var osm_type = $(this).data('osm-type');
25+ var osm_type = $(this).data('osm-type');
26− var result_table = $(this).data('result-table');
26+ var result_table = $(this).data('result-table');
27− var result_table_loadicon = "#" + $(this).data('result-table-loadicon');
27+ var result_table_loadicon = "#" + $(this).data('result-table-loadicon');
28−
28+
29− // tags which can be ignored
29+ // tags which can be ignored
30− var osm_ignore_tags = [ "addr:city", "addr:country", "addr:housenumber", "addr:postcode", "addr:street" ];
30+ var osm_ignore_tags = [ "addr:city", "addr:country", "addr:housenumber", "addr:postcode", "addr:street" ];
31−
31+
32− if(osm_id && osm_type && result_table) {
32+ if(osm_id && osm_type && result_table) {
33− result_table = "#" + result_table;
33+ result_table = "#" + result_table;
34− var query = null;
34+ var query = null;
35− switch(osm_type) {
35+ switch(osm_type) {
36− case 'node':
36+ case 'node':
37− query = query_start + "node(" + osm_id + ");" + query_end;
37+ query = query_start + "node(" + osm_id + ");" + query_end;
38− break;
38+ break;
39− case 'way':
39+ case 'way':
40− query = query_start + "way(" + osm_id + ");" + query_end;
40+ query = query_start + "way(" + osm_id + ");" + query_end;
41− break;
41+ break;
42− case 'relation':
42+ case 'relation':
43− query = query_start + "relation(" + osm_id + ");" + query_end;
43+ query = query_start + "relation(" + osm_id + ");" + query_end;
44− break;
44+ break;
45− default:
45+ default:
46− break;
46+ break;
47− }
47+ }
48− if(query) {
48+ if(query) {
49− //alert(query);
49+ //alert(query);
50− var ajaxRequest = $.ajax( query )
50+ var ajaxRequest = $.ajax( query )
51− .done(function( html) {
51+ .done(function( html) {
52− if(html && html.elements && html.elements[0]) {
52+ if(html && html.elements && html.elements[0]) {
53− var element = html.elements[0];
53+ var element = html.elements[0];
54− var newHtml = $(result_table).html();
54+ var newHtml = $(result_table).html();
55− for (var row in element.tags) {
55+ for (var row in element.tags) {
56− if(element.tags.name === null || osm_ignore_tags.indexOf(row) == -1) {
56+ if(element.tags.name === null || osm_ignore_tags.indexOf(row) == -1) {
57− newHtml += "<tr><td>" + row + "</td><td>";
57+ newHtml += "<tr><td>" + row + "</td><td>";
58− switch(row) {
58+ switch(row) {
59− case "phone":
59+ case "phone":
60− case "fax":
60+ case "fax":
61− newHtml += "<a href=\"tel:" + element.tags[row].replace(/ /g,'') + "\">" + element.tags[row] + "</a>";
61+ newHtml += "<a href=\"tel:" + element.tags[row].replace(/ /g,'') + "\">" + element.tags[row] + "</a>";
62− break;
62+ break;
63− case "email":
63+ case "email":
64− newHtml += "<a href=\"mailto:" + element.tags[row] + "\">" + element.tags[row] + "</a>";
64+ newHtml += "<a href=\"mailto:" + element.tags[row] + "\">" + element.tags[row] + "</a>";
65− break;
65+ break;
66− case "website":
66+ case "website":
67− case "url":
67+ case "url":
68− newHtml += "<a href=\"" + element.tags[row] + "\">" + element.tags[row] + "</a>";
68+ newHtml += "<a href=\"" + element.tags[row] + "\">" + element.tags[row] + "</a>";
69− break;
69+ break;
70− case "wikidata":
70+ case "wikidata":
71− newHtml += "<a href=\"https://www.wikidata.org/wiki/" + element.tags[row] + "\">" + element.tags[row] + "</a>";
71+ newHtml += "<a href=\"https://www.wikidata.org/wiki/" + element.tags[row] + "\">" + element.tags[row] + "</a>";
72− break;
72+ break;
73− case "wikipedia":
73+ case "wikipedia":
74− if(element.tags[row].indexOf(":") != -1) {
74+ if(element.tags[row].indexOf(":") != -1) {
75− newHtml += "<a href=\"https://" + element.tags[row].substring(0,element.tags[row].indexOf(":")) + ".wikipedia.org/wiki/" + element.tags[row].substring(element.tags[row].indexOf(":")+1) + "\">" + element.tags[row] + "</a>";
75+ newHtml += "<a href=\"https://" + element.tags[row].substring(0,element.tags[row].indexOf(":")) + ".wikipedia.org/wiki/" + element.tags[row].substring(element.tags[row].indexOf(":")+1) + "\">" + element.tags[row] + "</a>";
76− break;
76+ break;
77− }
77+ }
78− /* jshint ignore:start */
78+ /* jshint ignore:start */
79− default:
79+ default:
80− /* jshint ignore:end */
80+ /* jshint ignore:end */
81− newHtml += element.tags[row];
81+ newHtml += element.tags[row];
82− break;
82+ break;
83− }
83+ }
84− newHtml += "</td></tr>";
84+ newHtml += "</td></tr>";
85− }
85+ }
86− }
86+ }
87− $(result_table).html(newHtml);
87+ $(result_table).html(newHtml);
88− $(result_table).removeClass('hidden');
88+ $(result_table).removeClass('hidden');
89− $(result_table_loadicon).addClass('hidden');
89+ $(result_table_loadicon).addClass('hidden');
90− }
90+ }
91− })
91+ })
92− .fail(function() {
92+ .fail(function() {
93− $(result_table_loadicon).html($(result_table_loadicon).html() + "<p class=\"text-muted\">could not load data!</p>");
93+ $(result_table_loadicon).html($(result_table_loadicon).html() + "<p class=\"text-muted\">could not load data!</p>");
94− });
94+ });
95− }
95+ }
96− }
96+ }
97−
97+
98− // this event occour only once per element
98+ // this event occour only once per element
99− $( this ).off( event );
99+ $( this ).off( event );
100− });
100+ });
101−
101+
102− $(".searx_init_map").on( "click", function( event ) {
102+ $(".searx_init_map").on( "click", function( event ) {
103− var leaflet_target = $(this).data('leaflet-target');
103+ var leaflet_target = $(this).data('leaflet-target');
104− var map_lon = $(this).data('map-lon');
104+ var map_lon = $(this).data('map-lon');
105− var map_lat = $(this).data('map-lat');
105+ var map_lat = $(this).data('map-lat');
106− var map_zoom = $(this).data('map-zoom');
106+ var map_zoom = $(this).data('map-zoom');
107− var map_boundingbox = $(this).data('map-boundingbox');
107+ var map_boundingbox = $(this).data('map-boundingbox');
108− var map_geojson = $(this).data('map-geojson');
108+ var map_geojson = $(this).data('map-geojson');
109−
109+
110− require(['leaflet-0.7.3.min'], function(leaflet) {
110+ require(['leaflet-0.7.3.min'], function(leaflet) {
111− if(map_boundingbox) {
111+ if(map_boundingbox) {
112− southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);
112+ southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);
113− northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]);
113+ northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]);
114− map_bounds = L.latLngBounds(southWest, northEast);
114+ map_bounds = L.latLngBounds(southWest, northEast);
115− }
115+ }
116−
116+
117− // TODO hack
117+ // TODO hack
118− // change default imagePath
118+ // change default imagePath
119− L.Icon.Default.imagePath = "./static/themes/oscar/img/map";
119+ L.Icon.Default.imagePath = "./static/themes/oscar/img/map";
120−
120+
121− // init map
121+ // init map
122− var map = L.map(leaflet_target);
122+ var map = L.map(leaflet_target);
123−
123+
124− // create the tile layer with correct attribution
124+ // create the tile layer with correct attribution
125− var osmMapnikUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
125+ var osmMapnikUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
126− var osmMapnikAttrib='Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
126+ var osmMapnikAttrib='Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
127− var osmMapnik = new L.TileLayer(osmMapnikUrl, {minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib});
127+ var osmMapnik = new L.TileLayer(osmMapnikUrl, {minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib});
128−
128+
129− var osmWikimediaUrl='https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png';
129+ var osmWikimediaUrl='https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png';
130− var osmWikimediaAttrib = 'Wikimedia maps beta | Maps data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
130+ var osmWikimediaAttrib = 'Wikimedia maps beta | Maps data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
131− var osmWikimedia = new L.TileLayer(osmWikimediaUrl, {minZoom: 1, maxZoom: 19, attribution: osmWikimediaAttrib});
131+ var osmWikimedia = new L.TileLayer(osmWikimediaUrl, {minZoom: 1, maxZoom: 19, attribution: osmWikimediaAttrib});
132−
132+
133− // init map view
133+ // init map view
134− if(map_bounds) {
134+ if(map_bounds) {
135− // TODO hack: https://github.com/Leaflet/Leaflet/issues/2021
135+ // TODO hack: https://github.com/Leaflet/Leaflet/issues/2021
136− setTimeout(function () {
136+ setTimeout(function () {
137− map.fitBounds(map_bounds, {
137+ map.fitBounds(map_bounds, {
138− maxZoom:17
138+ maxZoom:17
139− });
139+ });
140− }, 0);
140+ }, 0);
141− } else if (map_lon && map_lat) {
141+ } else if (map_lon && map_lat) {
142− if(map_zoom)
142+ if(map_zoom)
143− map.setView(new L.LatLng(map_lat, map_lon),map_zoom);
143+ map.setView(new L.LatLng(map_lat, map_lon),map_zoom);
144− else
144+ else
145− map.setView(new L.LatLng(map_lat, map_lon),8);
145+ map.setView(new L.LatLng(map_lat, map_lon),8);
146− }
146+ }
147−
147+
148− map.addLayer(osmMapnik);
148+ map.addLayer(osmMapnik);
149−
149+
150− var baseLayers = {
150+ var baseLayers = {
151− "OSM Mapnik": osmMapnik/*,
151+ "OSM Mapnik": osmMapnik/*,
152− "OSM Wikimedia": osmWikimedia*/
152+ "OSM Wikimedia": osmWikimedia*/
153− };
153+ };
154−
154+
155− L.control.layers(baseLayers).addTo(map);
155+ L.control.layers(baseLayers).addTo(map);
156−
156+
157−
157+
158− if(map_geojson)
158+ if(map_geojson)
159− L.geoJson(map_geojson).addTo(map);
159+ L.geoJson(map_geojson).addTo(map);
160− /*else if(map_bounds)
160+ /*else if(map_bounds)
161− L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);*/
161+ L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);*/
162− });
162+ });
163−
163+
164− // this event occour only once per element
164+ // this event occour only once per element
165− $( this ).off( event );
165+ $( this ).off( event );
166− });
166+ });
167−});
167+});
searx/static/themes/oscar/less/logicodev-dark/oscar.less+2−2
@@ -109,7 +109,7 @@ ul.nav li a {
109
109
110.btn:hover {
110.btn:hover {
111 color:#444 !important;
111 color:#444 !important;
112− background-color: #BBB !important;
112+ background-color: #BBB !important;
113}
113}
114
114
115.btn-primary.active {
115.btn-primary.active {
@@ -221,7 +221,7 @@ p.btn.btn-default{
221}
221}
222
222
223.table-hover > tbody > tr:hover > td, .table-hover > tbody > tr:hover > th {
223.table-hover > tbody > tr:hover > td, .table-hover > tbody > tr:hover > th {
224− background: rgb(102, 105, 110) !important;
224+ background: rgb(102, 105, 110) !important;
225}
225}
226
226
227.btn-success {
227.btn-success {
searx/static/themes/oscar/less/logicodev/code.less+1−3
@@ -78,7 +78,7 @@ pre, code{
78 user-select: none;
78 user-select: none;
79 cursor: default;
79 cursor: default;
80 color: #556366;
80 color: #556366;
81−
81+
82 &::selection {
82 &::selection {
83 background: transparent; /* WebKit/Blink Browsers */
83 background: transparent; /* WebKit/Blink Browsers */
84 }
84 }
@@ -99,5 +99,3 @@ pre, code{
99.highlight {
99.highlight {
100 font-weight: 700;
100 font-weight: 700;
101}
101}
102−
103−
searx/static/themes/oscar/less/logicodev/infobox.less+1−1
@@ -30,7 +30,7 @@
30 table-layout: fixed;
30 table-layout: fixed;
31
31
32 }
32 }
33−
33+
34 .infobox_part:last-child {
34 .infobox_part:last-child {
35 margin-bottom: 0;
35 margin-bottom: 0;
36 }
36 }
searx/static/themes/oscar/less/logicodev/navbar.less−1
@@ -28,4 +28,3 @@
28 width: 80%;
28 width: 80%;
29 }
29 }
30}
30}
31−
searx/static/themes/oscar/less/pointhi/code.less+1−1
@@ -69,7 +69,7 @@
69 -ms-user-select: none;
69 -ms-user-select: none;
70 user-select: none;
70 user-select: none;
71 cursor: default;
71 cursor: default;
72−
72+
73 &::selection {
73 &::selection {
74 background: transparent; /* WebKit/Blink Browsers */
74 background: transparent; /* WebKit/Blink Browsers */
75 }
75 }
searx/static/themes/oscar/less/pointhi/infobox.less+1−1
@@ -4,7 +4,7 @@
4 word-wrap: break-word;
4 word-wrap: break-word;
5 table-layout: fixed;
5 table-layout: fixed;
6 }
6 }
7−
7+
8 .infobox_part:last-child {
8 .infobox_part:last-child {
9 margin-bottom: 0;
9 margin-bottom: 0;
10 }
10 }
searx/static/themes/simple/leaflet/leaflet.css+636−636
@@ -1,636 +1,636 @@
1−/* required styles */
1+/* required styles */
2−
2+
3−.leaflet-pane,
3+.leaflet-pane,
4−.leaflet-tile,
4+.leaflet-tile,
5−.leaflet-marker-icon,
5+.leaflet-marker-icon,
6−.leaflet-marker-shadow,
6+.leaflet-marker-shadow,
7−.leaflet-tile-container,
7+.leaflet-tile-container,
8−.leaflet-pane > svg,
8+.leaflet-pane > svg,
9−.leaflet-pane > canvas,
9+.leaflet-pane > canvas,
10−.leaflet-zoom-box,
10+.leaflet-zoom-box,
11−.leaflet-image-layer,
11+.leaflet-image-layer,
12−.leaflet-layer {
12+.leaflet-layer {
13− position: absolute;
13+ position: absolute;
14− left: 0;
14+ left: 0;
15− top: 0;
15+ top: 0;
16− }
16+ }
17−.leaflet-container {
17+.leaflet-container {
18− overflow: hidden;
18+ overflow: hidden;
19− }
19+ }
20−.leaflet-tile,
20+.leaflet-tile,
21−.leaflet-marker-icon,
21+.leaflet-marker-icon,
22−.leaflet-marker-shadow {
22+.leaflet-marker-shadow {
23− -webkit-user-select: none;
23+ -webkit-user-select: none;
24− -moz-user-select: none;
24+ -moz-user-select: none;
25− user-select: none;
25+ user-select: none;
26− -webkit-user-drag: none;
26+ -webkit-user-drag: none;
27− }
27+ }
28−/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
28+/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
29−.leaflet-safari .leaflet-tile {
29+.leaflet-safari .leaflet-tile {
30− image-rendering: -webkit-optimize-contrast;
30+ image-rendering: -webkit-optimize-contrast;
31− }
31+ }
32−/* hack that prevents hw layers "stretching" when loading new tiles */
32+/* hack that prevents hw layers "stretching" when loading new tiles */
33−.leaflet-safari .leaflet-tile-container {
33+.leaflet-safari .leaflet-tile-container {
34− width: 1600px;
34+ width: 1600px;
35− height: 1600px;
35+ height: 1600px;
36− -webkit-transform-origin: 0 0;
36+ -webkit-transform-origin: 0 0;
37− }
37+ }
38−.leaflet-marker-icon,
38+.leaflet-marker-icon,
39−.leaflet-marker-shadow {
39+.leaflet-marker-shadow {
40− display: block;
40+ display: block;
41− }
41+ }
42−/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
42+/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
43−/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
43+/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
44−.leaflet-container .leaflet-overlay-pane svg,
44+.leaflet-container .leaflet-overlay-pane svg,
45−.leaflet-container .leaflet-marker-pane img,
45+.leaflet-container .leaflet-marker-pane img,
46−.leaflet-container .leaflet-shadow-pane img,
46+.leaflet-container .leaflet-shadow-pane img,
47−.leaflet-container .leaflet-tile-pane img,
47+.leaflet-container .leaflet-tile-pane img,
48−.leaflet-container img.leaflet-image-layer {
48+.leaflet-container img.leaflet-image-layer {
49− max-width: none !important;
49+ max-width: none !important;
50− max-height: none !important;
50+ max-height: none !important;
51− }
51+ }
52−
52+
53−.leaflet-container.leaflet-touch-zoom {
53+.leaflet-container.leaflet-touch-zoom {
54− -ms-touch-action: pan-x pan-y;
54+ -ms-touch-action: pan-x pan-y;
55− touch-action: pan-x pan-y;
55+ touch-action: pan-x pan-y;
56− }
56+ }
57−.leaflet-container.leaflet-touch-drag {
57+.leaflet-container.leaflet-touch-drag {
58− -ms-touch-action: pinch-zoom;
58+ -ms-touch-action: pinch-zoom;
59− /* Fallback for FF which doesn't support pinch-zoom */
59+ /* Fallback for FF which doesn't support pinch-zoom */
60− touch-action: none;
60+ touch-action: none;
61− touch-action: pinch-zoom;
61+ touch-action: pinch-zoom;
62−}
62+}
63−.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
63+.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
64− -ms-touch-action: none;
64+ -ms-touch-action: none;
65− touch-action: none;
65+ touch-action: none;
66−}
66+}
67−.leaflet-container {
67+.leaflet-container {
68− -webkit-tap-highlight-color: transparent;
68+ -webkit-tap-highlight-color: transparent;
69−}
69+}
70−.leaflet-container a {
70+.leaflet-container a {
71− -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
71+ -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
72−}
72+}
73−.leaflet-tile {
73+.leaflet-tile {
74− filter: inherit;
74+ filter: inherit;
75− visibility: hidden;
75+ visibility: hidden;
76− }
76+ }
77−.leaflet-tile-loaded {
77+.leaflet-tile-loaded {
78− visibility: inherit;
78+ visibility: inherit;
79− }
79+ }
80−.leaflet-zoom-box {
80+.leaflet-zoom-box {
81− width: 0;
81+ width: 0;
82− height: 0;
82+ height: 0;
83− -moz-box-sizing: border-box;
83+ -moz-box-sizing: border-box;
84− box-sizing: border-box;
84+ box-sizing: border-box;
85− z-index: 800;
85+ z-index: 800;
86− }
86+ }
87−/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
87+/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
88−.leaflet-overlay-pane svg {
88+.leaflet-overlay-pane svg {
89− -moz-user-select: none;
89+ -moz-user-select: none;
90− }
90+ }
91−
91+
92−.leaflet-pane { z-index: 400; }
92+.leaflet-pane { z-index: 400; }
93−
93+
94−.leaflet-tile-pane { z-index: 200; }
94+.leaflet-tile-pane { z-index: 200; }
95−.leaflet-overlay-pane { z-index: 400; }
95+.leaflet-overlay-pane { z-index: 400; }
96−.leaflet-shadow-pane { z-index: 500; }
96+.leaflet-shadow-pane { z-index: 500; }
97−.leaflet-marker-pane { z-index: 600; }
97+.leaflet-marker-pane { z-index: 600; }
98−.leaflet-tooltip-pane { z-index: 650; }
98+.leaflet-tooltip-pane { z-index: 650; }
99−.leaflet-popup-pane { z-index: 700; }
99+.leaflet-popup-pane { z-index: 700; }
100−
100+
101−.leaflet-map-pane canvas { z-index: 100; }
101+.leaflet-map-pane canvas { z-index: 100; }
102−.leaflet-map-pane svg { z-index: 200; }
102+.leaflet-map-pane svg { z-index: 200; }
103−
103+
104−.leaflet-vml-shape {
104+.leaflet-vml-shape {
105− width: 1px;
105+ width: 1px;
106− height: 1px;
106+ height: 1px;
107− }
107+ }
108−.lvml {
108+.lvml {
109− behavior: url(#default#VML);
109+ behavior: url(#default#VML);
110− display: inline-block;
110+ display: inline-block;
111− position: absolute;
111+ position: absolute;
112− }
112+ }
113−
113+
114−
114+
115−/* control positioning */
115+/* control positioning */
116−
116+
117−.leaflet-control {
117+.leaflet-control {
118− position: relative;
118+ position: relative;
119− z-index: 800;
119+ z-index: 800;
120− pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
120+ pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
121− pointer-events: auto;
121+ pointer-events: auto;
122− }
122+ }
123−.leaflet-top,
123+.leaflet-top,
124−.leaflet-bottom {
124+.leaflet-bottom {
125− position: absolute;
125+ position: absolute;
126− z-index: 1000;
126+ z-index: 1000;
127− pointer-events: none;
127+ pointer-events: none;
128− }
128+ }
129−.leaflet-top {
129+.leaflet-top {
130− top: 0;
130+ top: 0;
131− }
131+ }
132−.leaflet-right {
132+.leaflet-right {
133− right: 0;
133+ right: 0;
134− }
134+ }
135−.leaflet-bottom {
135+.leaflet-bottom {
136− bottom: 0;
136+ bottom: 0;
137− }
137+ }
138−.leaflet-left {
138+.leaflet-left {
139− left: 0;
139+ left: 0;
140− }
140+ }
141−.leaflet-control {
141+.leaflet-control {
142− float: left;
142+ float: left;
143− clear: both;
143+ clear: both;
144− }
144+ }
145−.leaflet-right .leaflet-control {
145+.leaflet-right .leaflet-control {
146− float: right;
146+ float: right;
147− }
147+ }
148−.leaflet-top .leaflet-control {
148+.leaflet-top .leaflet-control {
149− margin-top: 10px;
149+ margin-top: 10px;
150− }
150+ }
151−.leaflet-bottom .leaflet-control {
151+.leaflet-bottom .leaflet-control {
152− margin-bottom: 10px;
152+ margin-bottom: 10px;
153− }
153+ }
154−.leaflet-left .leaflet-control {
154+.leaflet-left .leaflet-control {
155− margin-left: 10px;
155+ margin-left: 10px;
156− }
156+ }
157−.leaflet-right .leaflet-control {
157+.leaflet-right .leaflet-control {
158− margin-right: 10px;
158+ margin-right: 10px;
159− }
159+ }
160−
160+
161−
161+
162−/* zoom and fade animations */
162+/* zoom and fade animations */
163−
163+
164−.leaflet-fade-anim .leaflet-tile {
164+.leaflet-fade-anim .leaflet-tile {
165− will-change: opacity;
165+ will-change: opacity;
166− }
166+ }
167−.leaflet-fade-anim .leaflet-popup {
167+.leaflet-fade-anim .leaflet-popup {
168− opacity: 0;
168+ opacity: 0;
169− -webkit-transition: opacity 0.2s linear;
169+ -webkit-transition: opacity 0.2s linear;
170− -moz-transition: opacity 0.2s linear;
170+ -moz-transition: opacity 0.2s linear;
171− -o-transition: opacity 0.2s linear;
171+ -o-transition: opacity 0.2s linear;
172− transition: opacity 0.2s linear;
172+ transition: opacity 0.2s linear;
173− }
173+ }
174−.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
174+.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
175− opacity: 1;
175+ opacity: 1;
176− }
176+ }
177−.leaflet-zoom-animated {
177+.leaflet-zoom-animated {
178− -webkit-transform-origin: 0 0;
178+ -webkit-transform-origin: 0 0;
179− -ms-transform-origin: 0 0;
179+ -ms-transform-origin: 0 0;
180− transform-origin: 0 0;
180+ transform-origin: 0 0;
181− }
181+ }
182−.leaflet-zoom-anim .leaflet-zoom-animated {
182+.leaflet-zoom-anim .leaflet-zoom-animated {
183− will-change: transform;
183+ will-change: transform;
184− }
184+ }
185−.leaflet-zoom-anim .leaflet-zoom-animated {
185+.leaflet-zoom-anim .leaflet-zoom-animated {
186− -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
186+ -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
187− -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
187+ -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
188− -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
188+ -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
189− transition: transform 0.25s cubic-bezier(0,0,0.25,1);
189+ transition: transform 0.25s cubic-bezier(0,0,0.25,1);
190− }
190+ }
191−.leaflet-zoom-anim .leaflet-tile,
191+.leaflet-zoom-anim .leaflet-tile,
192−.leaflet-pan-anim .leaflet-tile {
192+.leaflet-pan-anim .leaflet-tile {
193− -webkit-transition: none;
193+ -webkit-transition: none;
194− -moz-transition: none;
194+ -moz-transition: none;
195− -o-transition: none;
195+ -o-transition: none;
196− transition: none;
196+ transition: none;
197− }
197+ }
198−
198+
199−.leaflet-zoom-anim .leaflet-zoom-hide {
199+.leaflet-zoom-anim .leaflet-zoom-hide {
200− visibility: hidden;
200+ visibility: hidden;
201− }
201+ }
202−
202+
203−
203+
204−/* cursors */
204+/* cursors */
205−
205+
206−.leaflet-interactive {
206+.leaflet-interactive {
207− cursor: pointer;
207+ cursor: pointer;
208− }
208+ }
209−.leaflet-grab {
209+.leaflet-grab {
210− cursor: -webkit-grab;
210+ cursor: -webkit-grab;
211− cursor: -moz-grab;
211+ cursor: -moz-grab;
212− }
212+ }
213−.leaflet-crosshair,
213+.leaflet-crosshair,
214−.leaflet-crosshair .leaflet-interactive {
214+.leaflet-crosshair .leaflet-interactive {
215− cursor: crosshair;
215+ cursor: crosshair;
216− }
216+ }
217−.leaflet-popup-pane,
217+.leaflet-popup-pane,
218−.leaflet-control {
218+.leaflet-control {
219− cursor: auto;
219+ cursor: auto;
220− }
220+ }
221−.leaflet-dragging .leaflet-grab,
221+.leaflet-dragging .leaflet-grab,
222−.leaflet-dragging .leaflet-grab .leaflet-interactive,
222+.leaflet-dragging .leaflet-grab .leaflet-interactive,
223−.leaflet-dragging .leaflet-marker-draggable {
223+.leaflet-dragging .leaflet-marker-draggable {
224− cursor: move;
224+ cursor: move;
225− cursor: -webkit-grabbing;
225+ cursor: -webkit-grabbing;
226− cursor: -moz-grabbing;
226+ cursor: -moz-grabbing;
227− }
227+ }
228−
228+
229−/* marker & overlays interactivity */
229+/* marker & overlays interactivity */
230−.leaflet-marker-icon,
230+.leaflet-marker-icon,
231−.leaflet-marker-shadow,
231+.leaflet-marker-shadow,
232−.leaflet-image-layer,
232+.leaflet-image-layer,
233−.leaflet-pane > svg path,
233+.leaflet-pane > svg path,
234−.leaflet-tile-container {
234+.leaflet-tile-container {
235− pointer-events: none;
235+ pointer-events: none;
236− }
236+ }
237−
237+
238−.leaflet-marker-icon.leaflet-interactive,
238+.leaflet-marker-icon.leaflet-interactive,
239−.leaflet-image-layer.leaflet-interactive,
239+.leaflet-image-layer.leaflet-interactive,
240−.leaflet-pane > svg path.leaflet-interactive {
240+.leaflet-pane > svg path.leaflet-interactive {
241− pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
241+ pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
242− pointer-events: auto;
242+ pointer-events: auto;
243− }
243+ }
244−
244+
245−/* visual tweaks */
245+/* visual tweaks */
246−
246+
247−.leaflet-container {
247+.leaflet-container {
248− background: #ddd;
248+ background: #ddd;
249− outline: 0;
249+ outline: 0;
250− }
250+ }
251−.leaflet-container a {
251+.leaflet-container a {
252− color: #0078A8;
252+ color: #0078A8;
253− }
253+ }
254−.leaflet-container a.leaflet-active {
254+.leaflet-container a.leaflet-active {
255− outline: 2px solid orange;
255+ outline: 2px solid orange;
256− }
256+ }
257−.leaflet-zoom-box {
257+.leaflet-zoom-box {
258− border: 2px dotted #38f;
258+ border: 2px dotted #38f;
259− background: rgba(255,255,255,0.5);
259+ background: rgba(255,255,255,0.5);
260− }
260+ }
261−
261+
262−
262+
263−/* general typography */
263+/* general typography */
264−.leaflet-container {
264+.leaflet-container {
265− font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
265+ font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
266− }
266+ }
267−
267+
268−
268+
269−/* general toolbar styles */
269+/* general toolbar styles */
270−
270+
271−.leaflet-bar {
271+.leaflet-bar {
272− box-shadow: 0 1px 5px rgba(0,0,0,0.65);
272+ box-shadow: 0 1px 5px rgba(0,0,0,0.65);
273− border-radius: 4px;
273+ border-radius: 4px;
274− }
274+ }
275−.leaflet-bar a,
275+.leaflet-bar a,
276−.leaflet-bar a:hover {
276+.leaflet-bar a:hover {
277− background-color: #fff;
277+ background-color: #fff;
278− border-bottom: 1px solid #ccc;
278+ border-bottom: 1px solid #ccc;
279− width: 26px;
279+ width: 26px;
280− height: 26px;
280+ height: 26px;
281− line-height: 26px;
281+ line-height: 26px;
282− display: block;
282+ display: block;
283− text-align: center;
283+ text-align: center;
284− text-decoration: none;
284+ text-decoration: none;
285− color: black;
285+ color: black;
286− }
286+ }
287−.leaflet-bar a,
287+.leaflet-bar a,
288−.leaflet-control-layers-toggle {
288+.leaflet-control-layers-toggle {
289− background-position: 50% 50%;
289+ background-position: 50% 50%;
290− background-repeat: no-repeat;
290+ background-repeat: no-repeat;
291− display: block;
291+ display: block;
292− }
292+ }
293−.leaflet-bar a:hover {
293+.leaflet-bar a:hover {
294− background-color: #f4f4f4;
294+ background-color: #f4f4f4;
295− }
295+ }
296−.leaflet-bar a:first-child {
296+.leaflet-bar a:first-child {
297− border-top-left-radius: 4px;
297+ border-top-left-radius: 4px;
298− border-top-right-radius: 4px;
298+ border-top-right-radius: 4px;
299− }
299+ }
300−.leaflet-bar a:last-child {
300+.leaflet-bar a:last-child {
301− border-bottom-left-radius: 4px;
301+ border-bottom-left-radius: 4px;
302− border-bottom-right-radius: 4px;
302+ border-bottom-right-radius: 4px;
303− border-bottom: none;
303+ border-bottom: none;
304− }
304+ }
305−.leaflet-bar a.leaflet-disabled {
305+.leaflet-bar a.leaflet-disabled {
306− cursor: default;
306+ cursor: default;
307− background-color: #f4f4f4;
307+ background-color: #f4f4f4;
308− color: #bbb;
308+ color: #bbb;
309− }
309+ }
310−
310+
311−.leaflet-touch .leaflet-bar a {
311+.leaflet-touch .leaflet-bar a {
312− width: 30px;
312+ width: 30px;
313− height: 30px;
313+ height: 30px;
314− line-height: 30px;
314+ line-height: 30px;
315− }
315+ }
316−.leaflet-touch .leaflet-bar a:first-child {
316+.leaflet-touch .leaflet-bar a:first-child {
317− border-top-left-radius: 2px;
317+ border-top-left-radius: 2px;
318− border-top-right-radius: 2px;
318+ border-top-right-radius: 2px;
319− }
319+ }
320−.leaflet-touch .leaflet-bar a:last-child {
320+.leaflet-touch .leaflet-bar a:last-child {
321− border-bottom-left-radius: 2px;
321+ border-bottom-left-radius: 2px;
322− border-bottom-right-radius: 2px;
322+ border-bottom-right-radius: 2px;
323− }
323+ }
324−
324+
325−/* zoom control */
325+/* zoom control */
326−
326+
327−.leaflet-control-zoom-in,
327+.leaflet-control-zoom-in,
328−.leaflet-control-zoom-out {
328+.leaflet-control-zoom-out {
329− font: bold 18px 'Lucida Console', Monaco, monospace;
329+ font: bold 18px 'Lucida Console', Monaco, monospace;
330− text-indent: 1px;
330+ text-indent: 1px;
331− }
331+ }
332−
332+
333−.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
333+.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
334− font-size: 22px;
334+ font-size: 22px;
335− }
335+ }
336−
336+
337−
337+
338−/* layers control */
338+/* layers control */
339−
339+
340−.leaflet-control-layers {
340+.leaflet-control-layers {
341− box-shadow: 0 1px 5px rgba(0,0,0,0.4);
341+ box-shadow: 0 1px 5px rgba(0,0,0,0.4);
342− background: #fff;
342+ background: #fff;
343− border-radius: 5px;
343+ border-radius: 5px;
344− }
344+ }
345−.leaflet-control-layers-toggle {
345+.leaflet-control-layers-toggle {
346− background-image: url(images/layers.png);
346+ background-image: url(images/layers.png);
347− width: 36px;
347+ width: 36px;
348− height: 36px;
348+ height: 36px;
349− }
349+ }
350−.leaflet-retina .leaflet-control-layers-toggle {
350+.leaflet-retina .leaflet-control-layers-toggle {
351− background-image: url(images/layers-2x.png);
351+ background-image: url(images/layers-2x.png);
352− background-size: 26px 26px;
352+ background-size: 26px 26px;
353− }
353+ }
354−.leaflet-touch .leaflet-control-layers-toggle {
354+.leaflet-touch .leaflet-control-layers-toggle {
355− width: 44px;
355+ width: 44px;
356− height: 44px;
356+ height: 44px;
357− }
357+ }
358−.leaflet-control-layers .leaflet-control-layers-list,
358+.leaflet-control-layers .leaflet-control-layers-list,
359−.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
359+.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
360− display: none;
360+ display: none;
361− }
361+ }
362−.leaflet-control-layers-expanded .leaflet-control-layers-list {
362+.leaflet-control-layers-expanded .leaflet-control-layers-list {
363− display: block;
363+ display: block;
364− position: relative;
364+ position: relative;
365− }
365+ }
366−.leaflet-control-layers-expanded {
366+.leaflet-control-layers-expanded {
367− padding: 6px 10px 6px 6px;
367+ padding: 6px 10px 6px 6px;
368− color: #333;
368+ color: #333;
369− background: #fff;
369+ background: #fff;
370− }
370+ }
371−.leaflet-control-layers-scrollbar {
371+.leaflet-control-layers-scrollbar {
372− overflow-y: scroll;
372+ overflow-y: scroll;
373− overflow-x: hidden;
373+ overflow-x: hidden;
374− padding-right: 5px;
374+ padding-right: 5px;
375− }
375+ }
376−.leaflet-control-layers-selector {
376+.leaflet-control-layers-selector {
377− margin-top: 2px;
377+ margin-top: 2px;
378− position: relative;
378+ position: relative;
379− top: 1px;
379+ top: 1px;
380− }
380+ }
381−.leaflet-control-layers label {
381+.leaflet-control-layers label {
382− display: block;
382+ display: block;
383− }
383+ }
384−.leaflet-control-layers-separator {
384+.leaflet-control-layers-separator {
385− height: 0;
385+ height: 0;
386− border-top: 1px solid #ddd;
386+ border-top: 1px solid #ddd;
387− margin: 5px -10px 5px -6px;
387+ margin: 5px -10px 5px -6px;
388− }
388+ }
389−
389+
390−/* Default icon URLs */
390+/* Default icon URLs */
391−.leaflet-default-icon-path {
391+.leaflet-default-icon-path {
392− background-image: url(images/marker-icon.png);
392+ background-image: url(images/marker-icon.png);
393− }
393+ }
394−
394+
395−
395+
396−/* attribution and scale controls */
396+/* attribution and scale controls */
397−
397+
398−.leaflet-container .leaflet-control-attribution {
398+.leaflet-container .leaflet-control-attribution {
399− background: #fff;
399+ background: #fff;
400− background: rgba(255, 255, 255, 0.7);
400+ background: rgba(255, 255, 255, 0.7);
401− margin: 0;
401+ margin: 0;
402− }
402+ }
403−.leaflet-control-attribution,
403+.leaflet-control-attribution,
404−.leaflet-control-scale-line {
404+.leaflet-control-scale-line {
405− padding: 0 5px;
405+ padding: 0 5px;
406− color: #333;
406+ color: #333;
407− }
407+ }
408−.leaflet-control-attribution a {
408+.leaflet-control-attribution a {
409− text-decoration: none;
409+ text-decoration: none;
410− }
410+ }
411−.leaflet-control-attribution a:hover {
411+.leaflet-control-attribution a:hover {
412− text-decoration: underline;
412+ text-decoration: underline;
413− }
413+ }
414−.leaflet-container .leaflet-control-attribution,
414+.leaflet-container .leaflet-control-attribution,
415−.leaflet-container .leaflet-control-scale {
415+.leaflet-container .leaflet-control-scale {
416− font-size: 11px;
416+ font-size: 11px;
417− }
417+ }
418−.leaflet-left .leaflet-control-scale {
418+.leaflet-left .leaflet-control-scale {
419− margin-left: 5px;
419+ margin-left: 5px;
420− }
420+ }
421−.leaflet-bottom .leaflet-control-scale {
421+.leaflet-bottom .leaflet-control-scale {
422− margin-bottom: 5px;
422+ margin-bottom: 5px;
423− }
423+ }
424−.leaflet-control-scale-line {
424+.leaflet-control-scale-line {
425− border: 2px solid #777;
425+ border: 2px solid #777;
426− border-top: none;
426+ border-top: none;
427− line-height: 1.1;
427+ line-height: 1.1;
428− padding: 2px 5px 1px;
428+ padding: 2px 5px 1px;
429− font-size: 11px;
429+ font-size: 11px;
430− white-space: nowrap;
430+ white-space: nowrap;
431− overflow: hidden;
431+ overflow: hidden;
432− -moz-box-sizing: border-box;
432+ -moz-box-sizing: border-box;
433− box-sizing: border-box;
433+ box-sizing: border-box;
434−
434+
435− background: #fff;
435+ background: #fff;
436− background: rgba(255, 255, 255, 0.5);
436+ background: rgba(255, 255, 255, 0.5);
437− }
437+ }
438−.leaflet-control-scale-line:not(:first-child) {
438+.leaflet-control-scale-line:not(:first-child) {
439− border-top: 2px solid #777;
439+ border-top: 2px solid #777;
440− border-bottom: none;
440+ border-bottom: none;
441− margin-top: -2px;
441+ margin-top: -2px;
442− }
442+ }
443−.leaflet-control-scale-line:not(:first-child):not(:last-child) {
443+.leaflet-control-scale-line:not(:first-child):not(:last-child) {
444− border-bottom: 2px solid #777;
444+ border-bottom: 2px solid #777;
445− }
445+ }
446−
446+
447−.leaflet-touch .leaflet-control-attribution,
447+.leaflet-touch .leaflet-control-attribution,
448−.leaflet-touch .leaflet-control-layers,
448+.leaflet-touch .leaflet-control-layers,
449−.leaflet-touch .leaflet-bar {
449+.leaflet-touch .leaflet-bar {
450− box-shadow: none;
450+ box-shadow: none;
451− }
451+ }
452−.leaflet-touch .leaflet-control-layers,
452+.leaflet-touch .leaflet-control-layers,
453−.leaflet-touch .leaflet-bar {
453+.leaflet-touch .leaflet-bar {
454− border: 2px solid rgba(0,0,0,0.2);
454+ border: 2px solid rgba(0,0,0,0.2);
455− background-clip: padding-box;
455+ background-clip: padding-box;
456− }
456+ }
457−
457+
458−
458+
459−/* popup */
459+/* popup */
460−
460+
461−.leaflet-popup {
461+.leaflet-popup {
462− position: absolute;
462+ position: absolute;
463− text-align: center;
463+ text-align: center;
464− margin-bottom: 20px;
464+ margin-bottom: 20px;
465− }
465+ }
466−.leaflet-popup-content-wrapper {
466+.leaflet-popup-content-wrapper {
467− padding: 1px;
467+ padding: 1px;
468− text-align: left;
468+ text-align: left;
469− border-radius: 12px;
469+ border-radius: 12px;
470− }
470+ }
471−.leaflet-popup-content {
471+.leaflet-popup-content {
472− margin: 13px 19px;
472+ margin: 13px 19px;
473− line-height: 1.4;
473+ line-height: 1.4;
474− }
474+ }
475−.leaflet-popup-content p {
475+.leaflet-popup-content p {
476− margin: 18px 0;
476+ margin: 18px 0;
477− }
477+ }
478−.leaflet-popup-tip-container {
478+.leaflet-popup-tip-container {
479− width: 40px;
479+ width: 40px;
480− height: 20px;
480+ height: 20px;
481− position: absolute;
481+ position: absolute;
482− left: 50%;
482+ left: 50%;
483− margin-left: -20px;
483+ margin-left: -20px;
484− overflow: hidden;
484+ overflow: hidden;
485− pointer-events: none;
485+ pointer-events: none;
486− }
486+ }
487−.leaflet-popup-tip {
487+.leaflet-popup-tip {
488− width: 17px;
488+ width: 17px;
489− height: 17px;
489+ height: 17px;
490− padding: 1px;
490+ padding: 1px;
491−
491+
492− margin: -10px auto 0;
492+ margin: -10px auto 0;
493−
493+
494− -webkit-transform: rotate(45deg);
494+ -webkit-transform: rotate(45deg);
495− -moz-transform: rotate(45deg);
495+ -moz-transform: rotate(45deg);
496− -ms-transform: rotate(45deg);
496+ -ms-transform: rotate(45deg);
497− -o-transform: rotate(45deg);
497+ -o-transform: rotate(45deg);
498− transform: rotate(45deg);
498+ transform: rotate(45deg);
499− }
499+ }
500−.leaflet-popup-content-wrapper,
500+.leaflet-popup-content-wrapper,
501−.leaflet-popup-tip {
501+.leaflet-popup-tip {
502− background: white;
502+ background: white;
503− color: #333;
503+ color: #333;
504− box-shadow: 0 3px 14px rgba(0,0,0,0.4);
504+ box-shadow: 0 3px 14px rgba(0,0,0,0.4);
505− }
505+ }
506−.leaflet-container a.leaflet-popup-close-button {
506+.leaflet-container a.leaflet-popup-close-button {
507− position: absolute;
507+ position: absolute;
508− top: 0;
508+ top: 0;
509− right: 0;
509+ right: 0;
510− padding: 4px 4px 0 0;
510+ padding: 4px 4px 0 0;
511− border: none;
511+ border: none;
512− text-align: center;
512+ text-align: center;
513− width: 18px;
513+ width: 18px;
514− height: 14px;
514+ height: 14px;
515− font: 16px/14px Tahoma, Verdana, sans-serif;
515+ font: 16px/14px Tahoma, Verdana, sans-serif;
516− color: #c3c3c3;
516+ color: #c3c3c3;
517− text-decoration: none;
517+ text-decoration: none;
518− font-weight: bold;
518+ font-weight: bold;
519− background: transparent;
519+ background: transparent;
520− }
520+ }
521−.leaflet-container a.leaflet-popup-close-button:hover {
521+.leaflet-container a.leaflet-popup-close-button:hover {
522− color: #999;
522+ color: #999;
523− }
523+ }
524−.leaflet-popup-scrolled {
524+.leaflet-popup-scrolled {
525− overflow: auto;
525+ overflow: auto;
526− border-bottom: 1px solid #ddd;
526+ border-bottom: 1px solid #ddd;
527− border-top: 1px solid #ddd;
527+ border-top: 1px solid #ddd;
528− }
528+ }
529−
529+
530−.leaflet-oldie .leaflet-popup-content-wrapper {
530+.leaflet-oldie .leaflet-popup-content-wrapper {
531− zoom: 1;
531+ zoom: 1;
532− }
532+ }
533−.leaflet-oldie .leaflet-popup-tip {
533+.leaflet-oldie .leaflet-popup-tip {
534− width: 24px;
534+ width: 24px;
535− margin: 0 auto;
535+ margin: 0 auto;
536−
536+
537− -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
537+ -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
538− filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
538+ filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
539− }
539+ }
540−.leaflet-oldie .leaflet-popup-tip-container {
540+.leaflet-oldie .leaflet-popup-tip-container {
541− margin-top: -1px;
541+ margin-top: -1px;
542− }
542+ }
543−
543+
544−.leaflet-oldie .leaflet-control-zoom,
544+.leaflet-oldie .leaflet-control-zoom,
545−.leaflet-oldie .leaflet-control-layers,
545+.leaflet-oldie .leaflet-control-layers,
546−.leaflet-oldie .leaflet-popup-content-wrapper,
546+.leaflet-oldie .leaflet-popup-content-wrapper,
547−.leaflet-oldie .leaflet-popup-tip {
547+.leaflet-oldie .leaflet-popup-tip {
548− border: 1px solid #999;
548+ border: 1px solid #999;
549− }
549+ }
550−
550+
551−
551+
552−/* div icon */
552+/* div icon */
553−
553+
554−.leaflet-div-icon {
554+.leaflet-div-icon {
555− background: #fff;
555+ background: #fff;
556− border: 1px solid #666;
556+ border: 1px solid #666;
557− }
557+ }
558−
558+
559−
559+
560−/* Tooltip */
560+/* Tooltip */
561−/* Base styles for the element that has a tooltip */
561+/* Base styles for the element that has a tooltip */
562−.leaflet-tooltip {
562+.leaflet-tooltip {
563− position: absolute;
563+ position: absolute;
564− padding: 6px;
564+ padding: 6px;
565− background-color: #fff;
565+ background-color: #fff;
566− border: 1px solid #fff;
566+ border: 1px solid #fff;
567− border-radius: 3px;
567+ border-radius: 3px;
568− color: #222;
568+ color: #222;
569− white-space: nowrap;
569+ white-space: nowrap;
570− -webkit-user-select: none;
570+ -webkit-user-select: none;
571− -moz-user-select: none;
571+ -moz-user-select: none;
572− -ms-user-select: none;
572+ -ms-user-select: none;
573− user-select: none;
573+ user-select: none;
574− pointer-events: none;
574+ pointer-events: none;
575− box-shadow: 0 1px 3px rgba(0,0,0,0.4);
575+ box-shadow: 0 1px 3px rgba(0,0,0,0.4);
576− }
576+ }
577−.leaflet-tooltip.leaflet-clickable {
577+.leaflet-tooltip.leaflet-clickable {
578− cursor: pointer;
578+ cursor: pointer;
579− pointer-events: auto;
579+ pointer-events: auto;
580− }
580+ }
581−.leaflet-tooltip-top:before,
581+.leaflet-tooltip-top:before,
582−.leaflet-tooltip-bottom:before,
582+.leaflet-tooltip-bottom:before,
583−.leaflet-tooltip-left:before,
583+.leaflet-tooltip-left:before,
584−.leaflet-tooltip-right:before {
584+.leaflet-tooltip-right:before {
585− position: absolute;
585+ position: absolute;
586− pointer-events: none;
586+ pointer-events: none;
587− border: 6px solid transparent;
587+ border: 6px solid transparent;
588− background: transparent;
588+ background: transparent;
589− content: "";
589+ content: "";
590− }
590+ }
591−
591+
592−/* Directions */
592+/* Directions */
593−
593+
594−.leaflet-tooltip-bottom {
594+.leaflet-tooltip-bottom {
595− margin-top: 6px;
595+ margin-top: 6px;
596−}
596+}
597−.leaflet-tooltip-top {
597+.leaflet-tooltip-top {
598− margin-top: -6px;
598+ margin-top: -6px;
599−}
599+}
600−.leaflet-tooltip-bottom:before,
600+.leaflet-tooltip-bottom:before,
601−.leaflet-tooltip-top:before {
601+.leaflet-tooltip-top:before {
602− left: 50%;
602+ left: 50%;
603− margin-left: -6px;
603+ margin-left: -6px;
604− }
604+ }
605−.leaflet-tooltip-top:before {
605+.leaflet-tooltip-top:before {
606− bottom: 0;
606+ bottom: 0;
607− margin-bottom: -12px;
607+ margin-bottom: -12px;
608− border-top-color: #fff;
608+ border-top-color: #fff;
609− }
609+ }
610−.leaflet-tooltip-bottom:before {
610+.leaflet-tooltip-bottom:before {
611− top: 0;
611+ top: 0;
612− margin-top: -12px;
612+ margin-top: -12px;
613− margin-left: -6px;
613+ margin-left: -6px;
614− border-bottom-color: #fff;
614+ border-bottom-color: #fff;
615− }
615+ }
616−.leaflet-tooltip-left {
616+.leaflet-tooltip-left {
617− margin-left: -6px;
617+ margin-left: -6px;
618−}
618+}
619−.leaflet-tooltip-right {
619+.leaflet-tooltip-right {
620− margin-left: 6px;
620+ margin-left: 6px;
621−}
621+}
622−.leaflet-tooltip-left:before,
622+.leaflet-tooltip-left:before,
623−.leaflet-tooltip-right:before {
623+.leaflet-tooltip-right:before {
624− top: 50%;
624+ top: 50%;
625− margin-top: -6px;
625+ margin-top: -6px;
626− }
626+ }
627−.leaflet-tooltip-left:before {
627+.leaflet-tooltip-left:before {
628− right: 0;
628+ right: 0;
629− margin-right: -12px;
629+ margin-right: -12px;
630− border-left-color: #fff;
630+ border-left-color: #fff;
631− }
631+ }
632−.leaflet-tooltip-right:before {
632+.leaflet-tooltip-right:before {
633− left: 0;
633+ left: 0;
634− margin-left: -12px;
634+ margin-left: -12px;
635− border-right-color: #fff;
635+ border-right-color: #fff;
636− }
636+ }
searx/templates/courgette/result_templates/key-value.htmlnew file+13
@@ -0,0 +1,13 @@
1+<div class="result">
2+<table>
3+ {% for key, value in result.items() %}
4+ {% if key in ['engine', 'engines', 'template', 'score', 'category', 'positions'] %}
5+ {% continue %}
6+ {% endif %}
7+ <tr>
8+ <td><b>{{ key|upper }}</b>: {{ value|safe }}</td>
9+ </tr>
10+ {% endfor %}
11+</table>
12+<p class="engines">{{ result.engines|join(', ') }}</p>
13+</div>
searx/templates/courgette/result_templates/torrent.html+1−1
@@ -4,7 +4,7 @@
4 {% endif %}
4 {% endif %}
5 <h3 class="result_title"><a href="{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ result.title|safe }}</a></h3>
5 <h3 class="result_title"><a href="{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ result.title|safe }}</a></h3>
6 {% if result.content %}<span class="content">{{ result.content|safe }}</span><br />{% endif %}
6 {% if result.content %}<span class="content">{{ result.content|safe }}</span><br />{% endif %}
7− {% if result.seed %}<span class="stats">{{ _('Seeder') }} : {{ result.seed }}, {{ _('Leecher') }} : {{ result.leech }}</span><br />{% endif %}
7+ {% if result.seed is defined %}<span class="stats">{{ _('Seeder') }} : {{ result.seed }}, {{ _('Leecher') }} : {{ result.leech }}</span><br />{% endif %}
8 <span>
8 <span>
9 {% if result.magnetlink %}<a href="{{ result.magnetlink }}" class="magnetlink">{{ _('magnet link') }}</a>{% endif %}
9 {% if result.magnetlink %}<a href="{{ result.magnetlink }}" class="magnetlink">{{ _('magnet link') }}</a>{% endif %}
10 {% if result.torrentfile %}<a href="{{ result.torrentfile }}" class="torrentfile" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ _('torrent file') }}</a>{% endif %}
10 {% if result.torrentfile %}<a href="{{ result.torrentfile }}" class="torrentfile" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ _('torrent file') }}</a>{% endif %}
searx/templates/legacy/result_templates/key-value.htmlnew file+13
@@ -0,0 +1,13 @@
1+<table class="result-table">
2+ {% for key, value in result.items() %}
3+ {% if key in ['engine', 'engines', 'template', 'score', 'category', 'positions'] %}
4+ {% continue %}
5+ {% endif %}
6+ <tr>
7+ <td><b>{{ key|upper }}</b>: {{ value|safe }}</td>
8+ </tr>
9+ {% endfor %}
10+ <tr>
11+ <td><b>ENGINES</b>: {{ result.engines|join(', ') }}</td>
12+ </tr>
13+</table>
searx/templates/legacy/result_templates/torrent.html+1−1
@@ -8,6 +8,6 @@
8 <p>
8 <p>
9 {% if result.magnetlink %}<a href="{{ result.magnetlink }}" class="magnetlink">{{ _('magnet link') }}</a>{% endif %}
9 {% if result.magnetlink %}<a href="{{ result.magnetlink }}" class="magnetlink">{{ _('magnet link') }}</a>{% endif %}
10 {% if result.torrentfile %}<a href="{{ result.torrentfile }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} class="torrentfile">{{ _('torrent file') }}</a>{% endif %} -
10 {% if result.torrentfile %}<a href="{{ result.torrentfile }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} class="torrentfile">{{ _('torrent file') }}</a>{% endif %} -
11− {% if result.seed %}<span class="stats">{{ _('Seeder') }} : {{ result.seed }}, {{ _('Leecher') }} : {{ result.leech }}</span>{% endif %}
11+ {% if result.seed is defined %}<span class="stats">{{ _('Seeder') }} : {{ result.seed }}, {{ _('Leecher') }} : {{ result.leech }}</span>{% endif %}
12 </p>
12 </p>
13</div>
13</div>
searx/templates/oscar/advanced.html+5−4
@@ -1,16 +1,17 @@
1<input type="checkbox" name="advanced_search" id="check-advanced" {% if advanced_search %} checked="checked"{% endif %}>
1<input type="checkbox" name="advanced_search" id="check-advanced" {% if advanced_search %} checked="checked"{% endif %}>
2−<label for="check-advanced">
2+<label for="check-advanced">{{- "" -}}
3 <span class="glyphicon glyphicon-cog"></span>
3 <span class="glyphicon glyphicon-cog"></span>
4− {{ _('Advanced settings') }}
4+ {{- _('Advanced settings') -}}
5</label>
5</label>
6<div id="advanced-search-container">
6<div id="advanced-search-container">
7 {% include 'oscar/categories.html' %}
7 {% include 'oscar/categories.html' %}
8+
8 <div class="row">
9 <div class="row">
9 <div class="col-xs-6">
10 <div class="col-xs-6">
10− {% include 'oscar/time-range.html' %}
11+ {%- include 'oscar/time-range.html' -%}
11 </div>
12 </div>
12 <div class="col-xs-6">
13 <div class="col-xs-6">
13− {% include 'oscar/languages.html' %}
14+ {%- include 'oscar/languages.html' -%}
14 </div>
15 </div>
15 </div>
16 </div>
16</div>
17</div>
searx/templates/oscar/base.html+12−9
@@ -10,16 +10,17 @@
10 <meta name="referrer" content="no-referrer">
10 <meta name="referrer" content="no-referrer">
11 <meta name="viewport" content="width=device-width, initial-scale=1 , maximum-scale=1.0, user-scalable=1" />
11 <meta name="viewport" content="width=device-width, initial-scale=1 , maximum-scale=1.0, user-scalable=1" />
12 {% block meta %}{% endblock %}
12 {% block meta %}{% endblock %}
13− <title>{% block title %}{% endblock %}{{ instance_name }}</title>
14
13
14+ <title>{% block title %}{% endblock %}{{ instance_name }}</title>
15 <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" type="text/css" />
15 <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" type="text/css" />
16− {% if preferences.get_value('oscar-style') %}
16+ {% if preferences.get_value('oscar-style') -%}
17− <link rel="stylesheet" href="{{ url_for('static', filename='css/'+preferences.get_value('oscar-style')+'.min.css') }}" type="text/css" />
17+ {{' '}}<link rel="stylesheet" href="{{ url_for('static', filename='css/'+preferences.get_value('oscar-style')+'.min.css') }}" type="text/css" />
18− {% else %}
18+ {%- else -%}
19− <link rel="stylesheet" href="{{ url_for('static', filename='css/logicodev.min.css') }}" type="text/css" />
19+ {{' '}}<link rel="stylesheet" href="{{ url_for('static', filename='css/logicodev.min.css') }}" type="text/css" />
20− {% endif %}
20+ {%- endif %}
21+
21 <link rel="stylesheet" href="{{ url_for('static', filename='css/leaflet.min.css') }}" type="text/css" />
22 <link rel="stylesheet" href="{{ url_for('static', filename='css/leaflet.min.css') }}" type="text/css" />
22− {% for css in styles %}
23+ {%- for css in styles %}
23 <link rel="stylesheet" href="{{ url_for('static', filename=css) }}" type="text/css" />
24 <link rel="stylesheet" href="{{ url_for('static', filename=css) }}" type="text/css" />
24 {% endfor %}
25 {% endfor %}
25
26
@@ -48,6 +49,7 @@
48</head>
49</head>
49<body>
50<body>
50 {% include 'oscar/navbar.html' %}
51 {% include 'oscar/navbar.html' %}
52+
51 <div class="container">
53 <div class="container">
52 {% if errors %}
54 {% if errors %}
53 <div class="alert alert-danger fade in" role="alert">
55 <div class="alert alert-danger fade in" role="alert">
@@ -93,13 +95,14 @@
93 </div>
95 </div>
94 <script src="{{ url_for('static', filename='js/jquery-1.11.1.min.js') }}"></script>
96 <script src="{{ url_for('static', filename='js/jquery-1.11.1.min.js') }}"></script>
95 <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
97 <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
96− {% if autocomplete %}<script src="{{ url_for('static', filename='js/typeahead.bundle.min.js') }}"></script>{% endif %}
98+ {% if autocomplete %} <script src="{{ url_for('static', filename='js/typeahead.bundle.min.js') }}"></script>{% endif %}
99+
97 <script src="{{ url_for('static', filename='js/require-2.1.15.min.js') }}"></script>
100 <script src="{{ url_for('static', filename='js/require-2.1.15.min.js') }}"></script>
98 <script src="{{ url_for('static', filename='js/searx.min.js') }}"
101 <script src="{{ url_for('static', filename='js/searx.min.js') }}"
99 data-method="{{ method or 'POST' }}"
102 data-method="{{ method or 'POST' }}"
100 data-autocompleter="{% if autocomplete %}true{% else %}false{% endif %}"></script>
103 data-autocompleter="{% if autocomplete %}true{% else %}false{% endif %}"></script>
101 {% for script in scripts %}
104 {% for script in scripts %}
102− <script src="{{ url_for('static', filename=script) }}"></script>
105+ {{""}}<script src="{{ url_for('static', filename=script) }}"></script>
103 {% endfor %}
106 {% endfor %}
104 <noscript>
107 <noscript>
105 <style>
108 <style>
searx/templates/oscar/categories.html+9−9
@@ -1,13 +1,13 @@
1<div id="categories">
1<div id="categories">
2−{% if rtl %}
2+{%- if rtl -%}
3− {% for category in categories | reverse %}
3+ {% for category in categories | reverse -%}
4− <input class="hidden" type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}" {% if category in selected_categories %}checked="checked"{% endif %} />
4+ <input class="hidden" type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}" {% if category in selected_categories %}checked="checked"{% endif %} />{{- '' -}}
5 <label for="checkbox_{{ category|replace(' ', '_') }}">{{ _(category) }}</label>
5 <label for="checkbox_{{ category|replace(' ', '_') }}">{{ _(category) }}</label>
6− {% endfor %}
6+ {%- endfor %}
7−{% else %}
7+{%- else -%}
8− {% for category in categories %}
8+ {% for category in categories -%}
9− <input class="hidden" type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}" {% if category in selected_categories %}checked="checked"{% endif %} />
9+ <input class="hidden" type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}" {% if category in selected_categories %}checked="checked"{% endif %} />{{- '' -}}
10 <label for="checkbox_{{ category|replace(' ', '_') }}">{{ _(category) }}</label>
10 <label for="checkbox_{{ category|replace(' ', '_') }}">{{ _(category) }}</label>
11− {% endfor %}
11+ {%- endfor %}
12−{% endif %}
12+{%- endif -%}
13</div>
13</div>
searx/templates/oscar/infobox.html+17−16
@@ -1,34 +1,35 @@
1{% from 'oscar/macros.html' import result_link with context %}
1{% from 'oscar/macros.html' import result_link with context %}
2<div class="panel panel-default infobox">
2<div class="panel panel-default infobox">
3− <div class="panel-heading">
3+ <div class="panel-heading">{{- "" -}}
4− <h4 class="panel-title infobox_part"><bdi>{{ infobox.infobox }}</bdi></h4>
4+ <h4 class="panel-title infobox_part"><bdi>{{ infobox.infobox }}</bdi></h4>{{- "" -}}
5 </div>
5 </div>
6 <div class="panel-body">
6 <div class="panel-body">
7 {% if infobox.img_src %}<img class="img-responsive center-block infobox_part" src="{{ image_proxify(infobox.img_src) }}" alt="{{ infobox.infobox }}" />{% endif %}
7 {% if infobox.img_src %}<img class="img-responsive center-block infobox_part" src="{{ image_proxify(infobox.img_src) }}" alt="{{ infobox.infobox }}" />{% endif %}
8− {% if infobox.content %}<bdi><p class="infobox_part">{{ infobox.content }}</bdi></p>{% endif %}
9
8
9+ {% if infobox.content %}<bdi><p class="infobox_part">{{ infobox.content | safe }}</p></bdi>{% endif %}
10+
10− {% if infobox.attributes %}
11+ {% if infobox.attributes -%}
11 <table class="table table-striped infobox_part">
12 <table class="table table-striped infobox_part">
12− {% for attribute in infobox.attributes %}
13+ {% for attribute in infobox.attributes -%}
13− <tr>
14+ <tr>{{- "" -}}
14 <td><bdi>{{ attribute.label }}</bdi></td>
15 <td><bdi>{{ attribute.label }}</bdi></td>
15− {% if attribute.image %}
16+ {%- if attribute.image -%}
16 <td><img class="img-responsive" src="{{ image_proxify(attribute.image.src) }}" alt="{{ attribute.image.alt }}" /></td>
17 <td><img class="img-responsive" src="{{ image_proxify(attribute.image.src) }}" alt="{{ attribute.image.alt }}" /></td>
17− {% else %}
18+ {%- else -%}
18 <td><bdi>{{ attribute.value }}</bdi></td>
19 <td><bdi>{{ attribute.value }}</bdi></td>
19− {% endif %}
20+ {%- endif -%}
20 </tr>
21 </tr>
21− {% endfor %}
22+ {% endfor -%}
22 </table>
23 </table>
23 {% endif %}
24 {% endif %}
24
25
25− {% if infobox.urls %}
26+ {% if infobox.urls -%}
26− <div class="infobox_part">
27+ <div class="infobox_part">{{- "\n" -}}
27 <bdi>
28 <bdi>
28− {% for url in infobox.urls %}
29+ {%- for url in infobox.urls -%}
29− <p class="btn btn-default btn-xs">{{ result_link(url.url, url.title) }}</a></p>
30+ <p class="btn btn-default btn-xs">{{ result_link(url.url, url.title) }}</p>
30− {% endfor %}
31+ {% endfor -%}
31− </bdi>
32+ </bdi>{{- "" -}}
32 </div>
33 </div>
33 {% endif %}
34 {% endif %}
34 </div>
35 </div>
searx/templates/oscar/languages.html+7−11
@@ -1,12 +1,8 @@
1−{% if preferences %}
2−<select class="custom-select form-control" name='language'>
3−{% else %}
4−<select class="time_range custom-select form-control" id='language' name='language'>
1+<select class="language custom-select form-control" id="language" name="language" accesskey="l">
5−{% endif %}
6− <option value="all" {% if current_language == 'all' %}selected="selected"{% endif %}>{{ _('Default language') }}</option>
2+ <option value="all" {% if current_language == 'all' %}selected="selected"{% endif %}>{{ _('Default language') }}</option>
7− {% for lang_id,lang_name,country_name,english_name in language_codes | sort(attribute=1) %}
3+{%- for lang_id,lang_name,country_name,english_name in language_codes | sort(attribute=1) -%}
8− <option value="{{ lang_id }}" {% if lang_id == current_language %}selected="selected"{% endif %}>
4+ <option value="{{ lang_id }}" {% if lang_id == current_language %}selected="selected"{% endif %}>
9− {{ lang_name }} {% if country_name %}({{ country_name }}) {% endif %}- {{ lang_id }}
5+ {{- lang_name }} {% if country_name %}({{ country_name }}) {% endif %}- {{ lang_id -}}
10− </option>
6+ </option>
11− {% endfor %}
7+{%- endfor -%}
12</select>
8</select>
searx/templates/oscar/macros.html+24−16
@@ -14,7 +14,7 @@
14
14
15<!-- Draw result header -->
15<!-- Draw result header -->
16{% macro result_header(result, favicons) -%}
16{% macro result_header(result, favicons) -%}
17−<h4 class="result_header">{% if result.engine~".png" in favicons %}{{ draw_favicon(result.engine) }} {% endif %}{{ result_link(result.url, result.title|safe) }}</h4>
17+<h4 class="result_header">{% if result.engine~".png" in favicons %}{{ draw_favicon(result.engine) }} {% endif %}{% if result.url %}{{ result_link(result.url, result.title|safe) }}{% else %}{{ result.title|safe}}{% endif %}</h4>
18{%- endmacro %}
18{%- endmacro %}
19
19
20<!-- Draw result sub header -->
20<!-- Draw result sub header -->
@@ -26,30 +26,38 @@
26
26
27<!-- Draw result footer -->
27<!-- Draw result footer -->
28{% macro result_footer(result) -%}
28{% macro result_footer(result) -%}
29− <div class="clearfix"></div>
29+ <div class="clearfix"></div>{{- "" -}}
30 <div class="pull-right">
30 <div class="pull-right">
31− {% for engine in result.engines %}
31+ {%- for engine in result.engines -%}
32− <span class="label label-default">{{ engine }}</span>
32+ <span class="label label-default">{{ engine }}</span>
33− {% endfor %}
33+ {%- endfor -%}
34+ {%- if result.url -%}
34− <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info") }}</small>
35+ <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info") }}</small>
36+ {%- endif -%}
35− {% if proxify %}
37+ {%- if proxify -%}
36− <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
38+ <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
37− {% endif %}
39+ {%- endif -%}
38−</div>
40+ </div>
41+ {%- if result.pretty_url -%}
39−<div class="external-link">{{ result.pretty_url }}</div>
42+ <div class="external-link">{{ result.pretty_url }}</div>
43+ {%- endif -%}
40{%- endmacro %}
44{%- endmacro %}
41
45
42<!-- Draw result footer -->
46<!-- Draw result footer -->
43{% macro result_footer_rtl(result) -%}
47{% macro result_footer_rtl(result) -%}
44− <div class="clearfix"></div>
48+ <div class="clearfix"></div>{{- "" -}}
45− {% for engine in result.engines %}
49+ {% for engine in result.engines -%}
46 <span class="label label-default">{{ engine }}</span>
50 <span class="label label-default">{{ engine }}</span>
47− {% endfor %}
51+ {%- endfor %}
52+ {%- if result.url -%}
48 <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info") }}</small>
53 <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info") }}</small>
54+ {%- endif -%}
49− {% if proxify %}
55+ {% if proxify -%}
50 <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
56 <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
51− {% endif %}
57+ {%- endif %}
58+ {%- if result.pretty_url -%}
52 <div class="external-link">{{ result.pretty_url }}</div>
59 <div class="external-link">{{ result.pretty_url }}</div>
60+ {%- endif %}
53{%- endmacro %}
61{%- endmacro %}
54
62
55{% macro preferences_item_header(info, label, rtl) -%}
63{% macro preferences_item_header(info, label, rtl) -%}
searx/templates/oscar/navbar.html+8−8
@@ -1,9 +1,9 @@
1−<div class="searx-navbar">
1+<div class="searx-navbar">{{- "" -}}
2− <span class="instance {% if rtl %}pull-right{% else %}pull-left{% endif%}">
2+ <span class="instance {% if rtl %}pull-right{% else %}pull-left{% endif%}">{{- "" -}}
3− <a href="{{ url_for('index') }}">{{ instance_name }}</a>
3+ <a href="{{ url_for('index') }}">{{ instance_name }}</a>{{- "" -}}
4− </span>
4+ </span>{{- "" -}}
5− <span class="{% if rtl %}pull-left{% else %}pull-right{% endif %}">
5+ <span class="{% if rtl %}pull-left{% else %}pull-right{% endif %}">{{- "" -}}
6− <a href="{{ url_for('about') }}">{{ _('about') }}</a>
6+ <a href="{{ url_for('about') }}">{{ _('about') }}</a>{{- "" -}}
7− <a href="{{ url_for('preferences') }}">{{ _('preferences') }}</a>
7+ <a href="{{ url_for('preferences') }}">{{ _('preferences') }}</a>{{- "" -}}
8− </span>
8+ </span>{{- "" -}}
9</div>
9</div>
searx/templates/oscar/preferences.html+33−33
@@ -41,7 +41,7 @@
41 {% set language_label = _('Search language') %}
41 {% set language_label = _('Search language') %}
42 {% set language_info = _('What language do you prefer for search?') %}
42 {% set language_info = _('What language do you prefer for search?') %}
43 {{ preferences_item_header(language_info, language_label, rtl) }}
43 {{ preferences_item_header(language_info, language_label, rtl) }}
44− {% include 'oscar/languages.html' %}
44+ {% include 'oscar/languages.html' %}
45 {{ preferences_item_footer(language_info, language_label, rtl) }}
45 {{ preferences_item_footer(language_info, language_label, rtl) }}
46
46
47 {% set locale_label = _('Interface language') %}
47 {% set locale_label = _('Interface language') %}
@@ -156,26 +156,26 @@
156 <div class="container-fluid">
156 <div class="container-fluid">
157 <fieldset>
157 <fieldset>
158 <div class="table-responsive">
158 <div class="table-responsive">
159− <table class="table table-hover table-condensed table-striped">
159+ <table class="table table-hover table-condensed table-striped">
160− <tr>
160+ <tr>
161 {% if not rtl %}
161 {% if not rtl %}
162− <th>{{ _("Allow") }}</th>
162+ <th>{{ _("Allow") }}</th>
163− <th>{{ _("Engine name") }}</th>
163+ <th>{{ _("Engine name") }}</th>
164− <th>{{ _("Shortcut") }}</th>
164+ <th>{{ _("Shortcut") }}</th>
165− <th>{{ _("Selected language") }}</th>
165+ <th>{{ _("Selected language") }}</th>
166− <th>{{ _("SafeSearch") }}</th>
166+ <th>{{ _("SafeSearch") }}</th>
167− <th>{{ _("Time range") }}</th>
167+ <th>{{ _("Time range") }}</th>
168− <th>{{ _("Avg. time") }}</th>
168+ <th>{{ _("Avg. time") }}</th>
169− <th>{{ _("Max time") }}</th>
169+ <th>{{ _("Max time") }}</th>
170 {% else %}
170 {% else %}
171− <th>{{ _("Max time") }}</th>
171+ <th>{{ _("Max time") }}</th>
172− <th>{{ _("Avg. time") }}</th>
172+ <th>{{ _("Avg. time") }}</th>
173− <th>{{ _("Time range") }}</th>
173+ <th>{{ _("Time range") }}</th>
174− <th>{{ _("SafeSearch") }}</th>
174+ <th>{{ _("SafeSearch") }}</th>
175− <th>{{ _("Selected language") }}</th>
175+ <th>{{ _("Selected language") }}</th>
176− <th>{{ _("Shortcut") }}</th>
176+ <th>{{ _("Shortcut") }}</th>
177− <th>{{ _("Engine name") }}</th>
177+ <th>{{ _("Engine name") }}</th>
178− <th>{{ _("Allow") }}</th>
178+ <th>{{ _("Allow") }}</th>
179 {% endif %}
179 {% endif %}
180 </tr>
180 </tr>
181 {% for search_engine in engines_by_category[categ] %}
181 {% for search_engine in engines_by_category[categ] %}
@@ -186,19 +186,19 @@
186 {{ checkbox_toggle('engine_' + search_engine.name|replace(' ', '_') + '__' + categ|replace(' ', '_'), (search_engine.name, categ) in disabled_engines) }}
186 {{ checkbox_toggle('engine_' + search_engine.name|replace(' ', '_') + '__' + categ|replace(' ', '_'), (search_engine.name, categ) in disabled_engines) }}
187 </td>
187 </td>
188 <th>{{ search_engine.name }}</th>
188 <th>{{ search_engine.name }}</th>
189− <td class="name">{{ shortcuts[search_engine.name] }}</td>
189+ <td class="name">{{ shortcuts[search_engine.name] }}</td>
190− <td>{{ support_toggle(stats[search_engine.name].supports_selected_language) }}</td>
190+ <td>{{ support_toggle(stats[search_engine.name].supports_selected_language) }}</td>
191− <td>{{ support_toggle(search_engine.safesearch==True) }}</td>
191+ <td>{{ support_toggle(search_engine.safesearch==True) }}</td>
192− <td>{{ support_toggle(search_engine.time_range_support==True) }}</td>
192+ <td>{{ support_toggle(search_engine.time_range_support==True) }}</td>
193− <td class="{{ 'danger' if stats[search_engine.name]['warn_time'] else '' }}">{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}</td>
193+ <td class="{{ 'danger' if stats[search_engine.name]['warn_time'] else '' }}">{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}</td>
194− <td class="{{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{{ search_engine.timeout }}</td>
194+ <td class="{{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{{ search_engine.timeout }}</td>
195− {% else %}
195+ {% else %}
196− <td class="{{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{{ search_engine.timeout }}</td>
196+ <td class="{{ 'danger' if stats[search_engine.name]['warn_timeout'] else '' }}">{{ search_engine.timeout }}</td>
197− <td class="{{ 'danger' if stats[search_engine.name]['warn_time'] else '' }}">{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}</td>
197+ <td class="{{ 'danger' if stats[search_engine.name]['warn_time'] else '' }}">{{ 'N/A' if stats[search_engine.name].time==None else stats[search_engine.name].time }}</td>
198− <td>{{ support_toggle(search_engine.time_range_support==True) }}</td>
198+ <td>{{ support_toggle(search_engine.time_range_support==True) }}</td>
199− <td>{{ support_toggle(search_engine.safesearch==True) }}</td>
199+ <td>{{ support_toggle(search_engine.safesearch==True) }}</td>
200− <td>{{ support_toggle(stats[search_engine.name].supports_selected_language) }}</td>
200+ <td>{{ support_toggle(stats[search_engine.name].supports_selected_language) }}</td>
201− <td>{{ shortcuts[search_engine.name] }}</td>
201+ <td>{{ shortcuts[search_engine.name] }}</td>
202 <th>{{ search_engine.name }}</th>
202 <th>{{ search_engine.name }}</th>
203 <td class="onoff-checkbox">
203 <td class="onoff-checkbox">
204 {{ checkbox_toggle('engine_' + search_engine.name|replace(' ', '_') + '__' + categ|replace(' ', '_'), (search_engine.name, categ) in disabled_engines) }}
204 {{ checkbox_toggle('engine_' + search_engine.name|replace(' ', '_') + '__' + categ|replace(' ', '_'), (search_engine.name, categ) in disabled_engines) }}
@@ -207,7 +207,7 @@
207 </tr>
207 </tr>
208 {% endif %}
208 {% endif %}
209 {% endfor %}
209 {% endfor %}
210− </table>
210+ </table>
211 </div>
211 </div>
212 </fieldset>
212 </fieldset>
213 </div>
213 </div>
searx/templates/oscar/result_templates/code.html+18−18
@@ -1,18 +1,18 @@
1−{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %}
1+{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %}
2−
2+
3−{{ result_header(result, favicons) }}
3+{{ result_header(result, favicons) }}
4−{{ result_sub_header(result) }}
4+{{ result_sub_header(result) }}
5−
5+
6−{% if result.content %}<p class="result-content">{{ result.content|safe }}</p>{% endif %}
6+{% if result.content %}<p class="result-content">{{ result.content|safe }}</p>{% endif %}
7−
7+
8−{% if result.repository %}<p class="result-content">{{ icon('file') }} <a href="{{ result.repository }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ result.repository }}</a></p>{% endif %}
8+{% if result.repository %}<p class="result-content">{{ icon('file') }} <a href="{{ result.repository }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ result.repository }}</a></p>{% endif %}
9−
9+
10−<div dir="ltr">
10+<div dir="ltr">
11−{{ result.codelines|code_highlighter(result.code_language)|safe }}
11+{{ result.codelines|code_highlighter(result.code_language)|safe }}
12−</div>
12+</div>
13−
13+
14−{% if rtl %}
14+{% if rtl %}
15−{{ result_footer_rtl(result) }}
15+{{ result_footer_rtl(result) }}
16−{% else %}
16+{% else %}
17−{{ result_footer(result) }}
17+{{ result_footer(result) }}
18−{% endif %}
18+{% endif %}
searx/templates/oscar/result_templates/default.html+31−31
@@ -1,31 +1,31 @@
1−{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon with context %}
1+{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon with context %}
2−
2+
3−{{ result_header(result, favicons) }}
3+{{- result_header(result, favicons) -}}
4−{{ result_sub_header(result) }}
4+{{- result_sub_header(result) -}}
5−
5+
6−{% if result.embedded %}
6+{%- if result.embedded -%}
7− <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer media-loader disabled_if_nojs" data-toggle="collapse" data-target="#result-media-{{ index }}" data-btn-text-collapsed="{{ _('show media') }}" data-btn-text-not-collapsed="{{ _('hide media') }}">{{ icon('music') }} {{ _('show media') }}</a></small>
7+ <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer media-loader disabled_if_nojs" data-toggle="collapse" data-target="#result-media-{{ index }}" data-btn-text-collapsed="{{ _('show media') }}" data-btn-text-not-collapsed="{{ _('hide media') }}">{{ icon('music') }} {{ _('show media') }}</a></small>
8−{% endif %}
8+{%- endif -%}
9−
9+
10−{% if result.embedded %}
10+{%- if result.embedded -%}
11−<div id="result-media-{{ index }}" class="collapse">
11+<div id="result-media-{{ index }}" class="collapse">
12− {{ result.embedded|safe }}
12+ {{ result.embedded|safe }}
13−</div>
13+</div>
14−{% endif %}
14+{%- endif -%}
15−
15+
16−{% if result.img_src %}
16+{%- if result.img_src -%}
17−<div class="container-fluid">
17+<div class="container-fluid">
18− <div class="row">
18+ <div class="row">
19−<img src="{{ image_proxify(result.img_src) }}" alt="{{ result.title|striptags }}" title="{{ result.title|striptags }}" style="width: auto; max-height: 60px; min-height: 60px;" class="col-xs-2 col-sm-4 col-md-4 result-content">
19+<img src="{{ image_proxify(result.img_src) }}" alt="{{ result.title|striptags }}" title="{{ result.title|striptags }}" style="width: auto; max-height: 60px; min-height: 60px;" class="col-xs-2 col-sm-4 col-md-4 result-content">
20−{% if result.content %}<p class="result-content col-xs-8 col-sm-8 col-md-8">{{ result.content|safe }}</p>{% endif %}
20+{% if result.content %}<p class="result-content col-xs-8 col-sm-8 col-md-8">{{ result.content|safe }}</p>{% endif -%}
21− </div>
21+ </div>
22−</div>
22+</div>
23−{% else %}
23+{%- else -%}
24−{% if result.content %}<p class="result-content">{{ result.content|safe }}</p>{% endif %}
24+{%- if result.content %}<p class="result-content">{{ result.content|safe }}</p>{% endif -%}
25−{% endif %}
25+{%- endif -%}
26−
26+
27−{% if rtl %}
27+{%- if rtl -%}
28−{{ result_footer_rtl(result) }}
28+{{ result_footer_rtl(result) }}
29−{% else %}
29+{%- else -%}
30−{{ result_footer(result) }}
30+{{ result_footer(result) }}
31−{% endif %}
31+{%- endif -%}
searx/templates/oscar/result_templates/images.html+36−49
@@ -1,49 +1,36 @@
1−{% from 'oscar/macros.html' import draw_favicon %}
1+{%- from 'oscar/macros.html' import draw_favicon -%}
2−
2+
3−<a href="{{ result.img_src }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} data-toggle="modal" data-target="#modal-{{ index }}-{{pageno}}">
3+<a href="{{ result.img_src }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} data-toggle="modal" data-target="#modal-{{ index }}-{{pageno}}">{{- "" -}}
4− <img src="{% if result.thumbnail_src %}{{ image_proxify(result.thumbnail_src) }}{% else %}{{ image_proxify(result.img_src) }}{% endif %}" alt="{{ result.title|striptags }}" title="{{ result.title|striptags }}" class="img-thumbnail">
4+ <img src="{% if result.thumbnail_src %}{{ image_proxify(result.thumbnail_src) }}{% else %}{{ image_proxify(result.img_src) }}{% endif %}" alt="{{ result.title|striptags }}" title="{{ result.title|striptags }}" class="img-thumbnail">{{- "" -}}
5−</a>
5+</a>
6−
7−<div class="modal fade" id="modal-{{ index }}-{{ pageno }}" tabindex="-1" role="dialog" aria-hidden="true">
6+<div class="modal fade" id="modal-{{ index }}-{{ pageno }}" tabindex="-1" role="dialog" aria-hidden="true">{{- "" -}}
8− <div class="modal-dialog">
7+ <div class="modal-dialog">{{- "" -}}
9− <div class="modal-wrapper">
8+ <div class="modal-wrapper">{{- "" -}}
10− <div class="modal-header">
9+ <div class="modal-header">{{- "" -}}
11− <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
10+ <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>{{- "" -}}
12− <h4 class="modal-title">{% if result.engine~".png" in favicons %}{{ draw_favicon(result.engine) }} {% endif %}{{ result.title|striptags }}</h4>
11+ <h4 class="modal-title">{% if result.engine~".png" in favicons %}{{ draw_favicon(result.engine) }} {% endif %}{{ result.title|striptags }}</h4>{{- "" -}}
13− </div>
12+ </div>{{- "" -}}
14− <div class="modal-body">
13+ <div class="modal-body">{{- "" -}}
15− <img class="img-responsive center-block" src="{% if result.thumbnail_src %}{{ image_proxify(result.thumbnail_src) }}{% else %}{{ image_proxify(result.img_src) }}{% endif %}" alt="{{ result.title|striptags }}">
14+ <img class="img-responsive center-block" src="{% if result.thumbnail_src %}{{ image_proxify(result.thumbnail_src) }}{% else %}{{ image_proxify(result.img_src) }}{% endif %}" alt="{{ result.title|striptags }}">
16− {% if result.author %}<span class="photo-author">{{ result.author }}</span><br />{% endif %}
15+ {%- if result.author %}<span class="photo-author">{{ result.author }}</span><br />{% endif -%}
17− {% if result.content %}
18− <p class="result-content">
19− {{ result.content|striptags }}
16+ {%- if result.content %}<p class="result-content">{{ result.content|striptags }}</p>{% endif -%}
20− </p>
21− {% endif %}
22− {% if result.img_format %}
17+ {%- if result.img_format %}<p class="result-format">{{ result.img_format }}</p>{% endif -%}
23− <p class="result-format">
24− {{ result.img_format }}
25− </p>
26− {% endif %}
27− {% if result.source %}
28− <p class="result-source">
18+ {%- if result.source %}<p class="result-source">{{ result.source }}</p>{% endif -%}
29− {{ result.source }}
30− </p>
31− {% endif %}
32− </div>
19+ </div>{{- "" -}}
33− <div class="modal-footer">
20+ <div class="modal-footer">{{- "" -}}
34− <div class="clearfix"></div>
21+ <div class="clearfix"></div>{{- "" -}}
35− <span class="label label-default pull-right">{{ result.engine }}</span>
22+ <span class="label label-default pull-right">{{ result.engine }}</span>{{- "" -}}
36− <p class="text-muted pull-left">{{ result.pretty_url }}</p>
23+ <p class="text-muted pull-left">{{ result.pretty_url }}</p>{{- "" -}}
37− <div class="clearfix"></div>
24+ <div class="clearfix"></div>{{- "" -}}
38− <div class="row">
25+ <div class="row">{{- "" -}}
39− <div class="col-md-6">
26+ <div class="col-md-6">{{- "" -}}
40− <a href="{{ result.img_src }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} class="btn btn-default">{{ _('Get image') }}</a>
27+ <a href="{{ result.img_src }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} class="btn btn-default">{{ _('Get image') }}</a>{{- "" -}}
41− </div>
28+ </div>{{- "" -}}
42− <div class="col-md-6">
29+ <div class="col-md-6">{{- "" -}}
43− <a href="{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} class="btn btn-default">{{ _('View source') }}</a>
30+ <a href="{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} class="btn btn-default">{{ _('View source') }}</a>{{- "" -}}
44− </div>
31+ </div>{{- "" -}}
45− </div>
32+ </div>{{- "" -}}
46− </div>
33+ </div>{{- "" -}}
47− </div>
34+ </div>{{- "" -}}
48− </div>
35+ </div>{{- "" -}}
49−</div>
36+</div>{{- "" -}}
searx/templates/oscar/result_templates/key-value.htmlnew file+19
@@ -0,0 +1,19 @@
1+{% from 'oscar/macros.html' import result_footer, result_footer_rtl with context %}
2+<div class="panel panel-default">
3+<table class="table table-responsive table-bordered table-condensed">
4+ {% for key, value in result.items() %}
5+ {% if key in ['engine', 'engines', 'template', 'score', 'category', 'positions'] %}
6+ {% continue %}
7+ {% endif %}
8+ <tr>
9+ <td><b>{{ key|upper }}</b>: {{ value }}</td>
10+ </tr>
11+ {% endfor %}
12+</table>
13+
14+{% if rtl %}
15+{{ result_footer_rtl(result) }}
16+{% else %}
17+{{ result_footer(result) }}
18+{% endif %}
19+</div>
searx/templates/oscar/result_templates/map.html+72−72
@@ -1,72 +1,72 @@
1−{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %}
1+{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %}
2−
2+
3−{{ result_header(result, favicons) }}
3+{{ result_header(result, favicons) }}
4−{{ result_sub_header(result) }}
4+{{ result_sub_header(result) }}
5−
5+
6−{% if (result.latitude and result.longitude) or result.boundingbox %}
6+{% if (result.latitude and result.longitude) or result.boundingbox %}
7− <small> &bull; <a class="text-info btn-collapse collapsed searx_init_map cursor-pointer disabled_if_nojs" data-toggle="collapse" data-target="#result-map-{{ index }}" data-leaflet-target="osm-map-{{ index }}" data-map-lon="{{ result.longitude }}" data-map-lat="{{ result.latitude }}" {% if result.boundingbox %}data-map-boundingbox='{{ result.boundingbox|tojson|safe }}'{% endif %} {% if result.geojson %}data-map-geojson='{{ result.geojson|tojson|safe }}'{% endif %} data-btn-text-collapsed="{{ _('show map') }}" data-btn-text-not-collapsed="{{ _('hide map') }}">{{ icon('globe') }} {{ _('show map') }}</a></small>
7+ <small> &bull; <a class="text-info btn-collapse collapsed searx_init_map cursor-pointer disabled_if_nojs" data-toggle="collapse" data-target="#result-map-{{ index }}" data-leaflet-target="osm-map-{{ index }}" data-map-lon="{{ result.longitude }}" data-map-lat="{{ result.latitude }}" {% if result.boundingbox %}data-map-boundingbox='{{ result.boundingbox|tojson|safe }}'{% endif %} {% if result.geojson %}data-map-geojson='{{ result.geojson|tojson|safe }}'{% endif %} data-btn-text-collapsed="{{ _('show map') }}" data-btn-text-not-collapsed="{{ _('hide map') }}">{{ icon('globe') }} {{ _('show map') }}</a></small>
8−{% endif %}
8+{% endif %}
9−
9+
10−{% if result.osm and (result.osm.type and result.osm.id) %}
10+{% if result.osm and (result.osm.type and result.osm.id) %}
11− <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer searx_overpass_request disabled_if_nojs" data-toggle="collapse" data-target="#result-overpass-{{ index }}" data-osm-type="{{ result.osm.type }}" data-osm-id="{{ result.osm.id }}" data-result-table="result-overpass-table-{{ index }}" data-result-table-loadicon="result-overpass-table-loading-{{ index }}" data-btn-text-collapsed="{{ _('show details') }}" data-btn-text-not-collapsed="{{ _('hide details') }}">{{ icon('map-marker') }} {{ _('show details') }}</a></small>
11+ <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer searx_overpass_request disabled_if_nojs" data-toggle="collapse" data-target="#result-overpass-{{ index }}" data-osm-type="{{ result.osm.type }}" data-osm-id="{{ result.osm.id }}" data-result-table="result-overpass-table-{{ index }}" data-result-table-loadicon="result-overpass-table-loading-{{ index }}" data-btn-text-collapsed="{{ _('show details') }}" data-btn-text-not-collapsed="{{ _('hide details') }}">{{ icon('map-marker') }} {{ _('show details') }}</a></small>
12−{% endif %}
12+{% endif %}
13−
13+
14−{# {% if (result.latitude and result.longitude) %}
14+{# {% if (result.latitude and result.longitude) %}
15− <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer disabled_if_nojs" data-toggle="collapse" data-target="#result-geodata-{{ index }}" data-btn-text-collapsed="{{ _('show geodata') }}" data-btn-text-not-collapsed="{{ _('hide geodata') }}">{{ icon('map-marker') }} {{ _('show geodata') }}</a></small>
15+ <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer disabled_if_nojs" data-toggle="collapse" data-target="#result-geodata-{{ index }}" data-btn-text-collapsed="{{ _('show geodata') }}" data-btn-text-not-collapsed="{{ _('hide geodata') }}">{{ icon('map-marker') }} {{ _('show geodata') }}</a></small>
16−{% endif %} #}
16+{% endif %} #}
17−
17+
18−<div class="container-fluid">
18+<div class="container-fluid">
19−
19+
20−{% if result.address %}
20+{% if result.address %}
21−<p class="row result-content result-adress col-xs-12 col-sm-5 col-md-4" itemscope itemtype="http://schema.org/PostalAddress">
21+<p class="row result-content result-adress col-xs-12 col-sm-5 col-md-4" itemscope itemtype="http://schema.org/PostalAddress">
22− {% if result.address.name %}
22+ {% if result.address.name %}
23− <strong itemprop="name">{{ result.address.name }}</strong><br/>
23+ <strong itemprop="name">{{ result.address.name }}</strong><br/>
24− {% endif %}
24+ {% endif %}
25− {% if result.address.road %}
25+ {% if result.address.road %}
26− <span itemprop="streetAddress">
26+ <span itemprop="streetAddress">
27− {% if result.address.house_number %}{{ result.address.house_number }}, {% endif %}
27+ {% if result.address.house_number %}{{ result.address.house_number }}, {% endif %}
28− {{ result.address.road }}
28+ {{ result.address.road }}
29− </span><br/>
29+ </span><br/>
30− {% endif %}
30+ {% endif %}
31− {% if result.address.locality %}
31+ {% if result.address.locality %}
32− <span itemprop="addressLocality">{{ result.address.locality }}</span>
32+ <span itemprop="addressLocality">{{ result.address.locality }}</span>
33− {% if result.address.postcode %}, <span itemprop="postalCode">{{ result.address.postcode }}</span>{% endif %}
33+ {% if result.address.postcode %}, <span itemprop="postalCode">{{ result.address.postcode }}</span>{% endif %}
34− <br/>
34+ <br/>
35− {% endif %}
35+ {% endif %}
36− {% if result.address.country %}
36+ {% if result.address.country %}
37− <span itemprop="addressCountry">{{ result.address.country }}</span>
37+ <span itemprop="addressCountry">{{ result.address.country }}</span>
38− {% endif %}
38+ {% endif %}
39−</p>
39+</p>
40−{% endif %}
40+{% endif %}
41−
41+
42−{% if result.osm and (result.osm.type and result.osm.id) %}
42+{% if result.osm and (result.osm.type and result.osm.id) %}
43− <div class="row result-content collapse col-xs-12 col-sm-7 col-md-8" id="result-overpass-{{ index }}"{% if rtl %} dir="ltr"{% endif %}>
43+ <div class="row result-content collapse col-xs-12 col-sm-7 col-md-8" id="result-overpass-{{ index }}"{% if rtl %} dir="ltr"{% endif %}>
44− <div class="text-center" id="result-overpass-table-loading-{{ index }}"><img src="{{ url_for('static', filename='img/loader.gif') }}" alt="Loading ..."/></div>
44+ <div class="text-center" id="result-overpass-table-loading-{{ index }}"><img src="{{ url_for('static', filename='img/loader.gif') }}" alt="Loading ..."/></div>
45− <table class="table table-striped table-condensed hidden" id="result-overpass-table-{{ index }}">
45+ <table class="table table-striped table-condensed hidden" id="result-overpass-table-{{ index }}">
46− <tr><th>key</th><th>value</th></tr>
46+ <tr><th>key</th><th>value</th></tr>
47− </table>
47+ </table>
48− </div>
48+ </div>
49−{% endif %}
49+{% endif %}
50−
50+
51−{# {% if (result.latitude and result.longitude) %}
51+{# {% if (result.latitude and result.longitude) %}
52− <div class="row collapse col-xs-12 col-sm-5 col-md-4" id="result-geodata-{{ index }}">
52+ <div class="row collapse col-xs-12 col-sm-5 col-md-4" id="result-geodata-{{ index }}">
53− <strong>Longitude:</strong> {{ result.longitude }} <br/>
53+ <strong>Longitude:</strong> {{ result.longitude }} <br/>
54− <strong>Latitude:</strong> {{ result.latitude }}
54+ <strong>Latitude:</strong> {{ result.latitude }}
55− </div>
55+ </div>
56−{% endif %} #}
56+{% endif %} #}
57−
57+
58−{% if result.content %}<p class="row result-content col-xs-12 col-sm-12 col-md-12">{{ result.content|safe }}</p>{% endif %}
58+{% if result.content %}<p class="row result-content col-xs-12 col-sm-12 col-md-12">{{ result.content|safe }}</p>{% endif %}
59−
59+
60−</div>
60+</div>
61−
61+
62−{% if (result.latitude and result.longitude) or result.boundingbox %}
62+{% if (result.latitude and result.longitude) or result.boundingbox %}
63− <div class="collapse" id="result-map-{{ index }}">
63+ <div class="collapse" id="result-map-{{ index }}">
64− <div style="height:300px; width:100%; margin: 10px 0;" id="osm-map-{{ index }}"></div>
64+ <div style="height:300px; width:100%; margin: 10px 0;" id="osm-map-{{ index }}"></div>
65− </div>
65+ </div>
66−{% endif %}
66+{% endif %}
67−
67+
68−{% if rtl %}
68+{% if rtl %}
69−{{ result_footer_rtl(result) }}
69+{{ result_footer_rtl(result) }}
70−{% else %}
70+{% else %}
71−{{ result_footer(result) }}
71+{{ result_footer(result) }}
72−{% endif %}
72+{% endif %}
searx/templates/oscar/result_templates/torrent.html+1−1
@@ -3,7 +3,7 @@
3{{ result_header(result, favicons) }}
3{{ result_header(result, favicons) }}
4{{ result_sub_header(result) }}
4{{ result_sub_header(result) }}
5
5
6−{% if result.seed %}<p class="result-content">{{ icon('transfer') }} {{ _('Seeder') }} <span class="badge">{{ result.seed }}</span> &bull; {{ _('Leecher') }} <span class="badge">{{ result.leech }}</span>{% endif %}
6+{% if result.seed is defined %}<p class="result-content">{{ icon('transfer') }} {{ _('Seeder') }} <span class="badge">{{ result.seed }}</span> &bull; {{ _('Leecher') }} <span class="badge">{{ result.leech }}</span>{% endif %}
7{% if result.filesize %}<br />{{ icon('floppy-disk') }} {{ _('Filesize') }}
7{% if result.filesize %}<br />{{ icon('floppy-disk') }} {{ _('Filesize') }}
8 <span class="badge">
8 <span class="badge">
9 {% if result.filesize < 1024 %}{{ result.filesize }} {{ _('Bytes') }}
9 {% if result.filesize < 1024 %}{{ result.filesize }} {{ _('Bytes') }}
searx/templates/oscar/result_templates/videos.html+27−27
@@ -1,27 +1,27 @@
1−{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %}
1+{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer, result_footer_rtl, icon %}
2−
2+
3−{{ result_header(result, favicons) }}
3+{{ result_header(result, favicons) }}
4−{{ result_sub_header(result) }}
4+{{ result_sub_header(result) }}
5−
5+
6−{% if result.embedded %}
6+{% if result.embedded %}
7− <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer media-loader disabled_if_nojs" data-toggle="collapse" data-target="#result-video-{{ index }}" data-btn-text-collapsed="{{ _('show video') }}" data-btn-text-not-collapsed="{{ _('hide video') }}">{{ icon('film') }} {{ _('show video') }}</a></small>
7+ <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer media-loader disabled_if_nojs" data-toggle="collapse" data-target="#result-video-{{ index }}" data-btn-text-collapsed="{{ _('show video') }}" data-btn-text-not-collapsed="{{ _('hide video') }}">{{ icon('film') }} {{ _('show video') }}</a></small>
8−{% endif %}
8+{% endif %}
9−
9+
10−{% if result.embedded %}
10+{% if result.embedded %}
11−<div id="result-video-{{ index }}" class="collapse">
11+<div id="result-video-{{ index }}" class="collapse">
12− {{ result.embedded|safe }}
12+ {{ result.embedded|safe }}
13−</div>
13+</div>
14−{% endif %}
14+{% endif %}
15−
15+
16−<div class="container-fluid">
16+<div class="container-fluid">
17− <div class="row">
17+ <div class="row">
18− <a href="{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}><img class="thumbnail col-xs-6 col-sm-4 col-md-4 result-content" src="{{ image_proxify(result.thumbnail) }}" alt="{{ result.title|striptags }} {{ result.engine }}" /></a>
18+ <a href="{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}><img class="thumbnail col-xs-6 col-sm-4 col-md-4 result-content" src="{{ image_proxify(result.thumbnail) }}" alt="{{ result.title|striptags }} {{ result.engine }}" /></a>
19− {% if result.content %}<p class="col-xs-12 col-sm-8 col-md-8 result-content">{{ result.content|safe }}</p>{% endif %}
19+ {% if result.content %}<p class="col-xs-12 col-sm-8 col-md-8 result-content">{{ result.content|safe }}</p>{% endif %}
20− </div>
20+ </div>
21−</div>
21+</div>
22−
22+
23−{% if rtl %}
23+{% if rtl %}
24−{{ result_footer_rtl(result) }}
24+{{ result_footer_rtl(result) }}
25−{% else %}
25+{% else %}
26−{{ result_footer(result) }}
26+{{ result_footer(result) }}
27−{% endif %}
27+{% endif %}
searx/templates/oscar/results.html+156−156
@@ -1,156 +1,156 @@
1−{% extends "oscar/base.html" %}
1+{% extends "oscar/base.html" %}
2−{% macro search_form_attrs(pageno) -%}
2+{% macro search_form_attrs(pageno) -%}
3− {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1"/>{% endfor %}
3+ {%- for category in selected_categories -%}<input type="hidden" name="category_{{ category }}" value="1"/>{%- endfor -%}
4− <input type="hidden" name="q" value="{{ q|e }}" />
4+ <input type="hidden" name="q" value="{{ q|e }}" />{{- "" -}}
5− <input type="hidden" name="pageno" value="{{ pageno }}" />
5+ <input type="hidden" name="pageno" value="{{ pageno }}" />{{- "" -}}
6− <input type="hidden" name="time_range" value="{{ time_range }}" />
6+ <input type="hidden" name="time_range" value="{{ time_range }}" />{{- "" -}}
7− <input type="hidden" name="language" value="{{ current_language }}" />
7+ <input type="hidden" name="language" value="{{ current_language }}" />{{- "" -}}
8− {% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit|e }}" />{% endif %}
8+ {% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit|e }}" />{% endif -%}
9−{%- endmacro %}
9+{%- endmacro %}
10−{%- macro search_url() %}{{ base_url }}?q={{ q|urlencode }}{% if selected_categories %}&amp;categories={{ selected_categories|join(",") | replace(' ','+') }}{% endif %}{% if pageno > 1 %}&amp;pageno={{ pageno }}{% endif %}{% if time_range %}&amp;time_range={{ time_range }}{% endif %}{% if current_language != 'all' %}&amp;language={{ current_language }}{% endif %}{% endmacro -%}
10+{%- macro search_url() %}{{ base_url }}?q={{ q|urlencode }}{% if selected_categories %}&amp;categories={{ selected_categories|join(",") | replace(' ','+') }}{% endif %}{% if pageno > 1 %}&amp;pageno={{ pageno }}{% endif %}{% if time_range %}&amp;time_range={{ time_range }}{% endif %}{% if current_language != 'all' %}&amp;language={{ current_language }}{% endif %}{% endmacro -%}
11−
11+
12−{% block title %}{{ q|e }} - {% endblock %}
12+{% block title %}{{ q|e }} - {% endblock %}
13−{% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q|e }}" href="{{ search_url() }}&amp;format=rss">{% endblock %}
13+{% block meta %}{{" "}}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q|e }}" href="{{ search_url() }}&amp;format=rss">{% endblock %}
14−{% block content %}
14+{% block content %}
15− {% include 'oscar/search.html' %}
15+ {% include 'oscar/search.html' %}
16+
16− <div class="row">
17+ <div class="row">
17− <div class="col-sm-8" id="main_results">
18+ <div class="col-sm-8" id="main_results">
18− <h1 class="sr-only">{{ _('Search results') }}</h1>
19+ <h1 class="sr-only">{{ _('Search results') }}</h1>
19−
20+
20− {% if corrections %}
21+ {% if corrections -%}
21− <div class="result">
22+ <div class="result">
22− <span class="result_header text-muted form-inline pull-left suggestion_item">{{ _('Try searching for:') }}</span>
23+ <span class="result_header text-muted form-inline pull-left suggestion_item">{{ _('Try searching for:') }}</span>
23− {% for correction in corrections %}
24+ {% for correction in corrections -%}
24− <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" role="navigation" class="form-inline pull-left suggestion_item">
25+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" role="navigation" class="form-inline pull-left suggestion_item">{{- "" -}}
25− <input type="hidden" name="q" value="{{ query_prefix + correction }}">
26+ <input type="hidden" name="q" value="{{ correction.url }}">{{- "" -}}
26− <button type="submit" class="btn btn-default btn-xs">{{ correction }}</button>
27+ <button type="submit" class="btn btn-default btn-xs">{{ correction.title }}</button>{{- "" -}}
27− </form>
28+ </form>
28− {% endfor %}
29+ {% endfor %}
29− </div>
30+ </div>
30− {% endif %}
31+ {%- endif %}
31−
32+
32− {% if answers %}
33+ {% if answers -%}
33− {% for answer in answers %}
34+ {%- for answer in answers %}
34− <div class="result well">
35+ <div class="result well">
35− <span>{{ answer }}</span>
36+ <span>{{ answer }}</span>
36− </div>
37+ </div>
37− {% endfor %}
38+ {%- endfor %}
38− {% endif %}
39+ {%- endif %}
39−
40+
40− {% for result in results %}
41+ {% for result in results -%}
41− <div class="result {% if result['template'] %}result-{{ result.template|replace('.html', '') }}{% else %}result-default{% endif %}">
42+ <div class="result {% if result['template'] %}result-{{ result.template|replace('.html', '') }}{% else %}result-default{% endif %}">
42− {% set index = loop.index %}
43+ {%- set index = loop.index -%}
43− {% if result.template %}
44+ {%- if result.template -%}
44− {% include get_result_template('oscar', result['template']) %}
45+ {% include get_result_template('oscar', result['template']) %}
45− {% else %}
46+ {%- else -%}
46− {% include 'oscar/result_templates/default.html' %}
47+ {% include 'oscar/result_templates/default.html' %}
47− {% endif %}
48+ {%- endif -%}
48− </div>
49+ </div>
49− {% endfor %}
50+ {% endfor %}
50−
51+
51− {% if not results and not answers %}
52+ {% if not results and not answers -%}
52− {% include 'oscar/messages/no_results.html' %}
53+ {% include 'oscar/messages/no_results.html' %}
53− {% endif %}
54+ {% endif %}
54−
55+
55− <div class="clearfix"></div>
56+ <div class="clearfix"></div>
56−
57+
57− {% if paging %}
58+ {% if paging -%}
58− {% if rtl %}
59+ {% if rtl %}
59− <div id="pagination">
60+ <div id="pagination">
60− <div class="pull-left">
61+ <div class="pull-left">{{- "" -}}
61− <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
62+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
62− {{ search_form_attrs(pageno+1) }}
63+ {{- search_form_attrs(pageno+1) -}}
63− <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-backward"></span> {{ _('next page') }}</button>
64+ <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-backward"></span> {{ _('next page') }}</button>{{- "" -}}
64− </form>
65+ </form>{{- "" -}}
65− </div>
66+ </div>
66− <div class="pull-right">
67+ <div class="pull-right">{{- "" -}}
67− <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
68+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
68− {{ search_form_attrs(pageno-1) }}
69+ {{- search_form_attrs(pageno-1) -}}
69− <button type="submit" class="btn btn-default" {% if pageno == 1 %}disabled{% endif %}><span class="glyphicon glyphicon-forward"></span> {{ _('previous page') }}</button>
70+ <button type="submit" class="btn btn-default" {% if pageno == 1 %}disabled{% endif %}><span class="glyphicon glyphicon-forward"></span> {{ _('previous page') }}</button>{{- "" -}}
70− </form>
71+ </form>{{- "" -}}
71− </div>
72+ </div>
72− </div><!-- /#pagination -->
73+ </div><!-- /#pagination -->
73− <div class="clearfix"></div>
74+ <div class="clearfix"></div>
74− {% else %}
75+ {% else %}
75− <div id="pagination">
76+ <div id="pagination">
76− <div class="pull-left">
77+ <div class="pull-left">{{- "" -}}
77− <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
78+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
78− {{ search_form_attrs(pageno-1) }}
79+ {{- search_form_attrs(pageno-1) -}}
79− <button type="submit" class="btn btn-default" {% if pageno == 1 %}disabled{% endif %}><span class="glyphicon glyphicon-backward"></span> {{ _('previous page') }}</button>
80+ <button type="submit" class="btn btn-default" {% if pageno == 1 %}disabled{% endif %}><span class="glyphicon glyphicon-backward"></span> {{ _('previous page') }}</button>{{- "" -}}
80− </form>
81+ </form>{{- "" -}}
81− </div>
82+ </div>
82− <div class="pull-right">
83+ <div class="pull-right">{{- "" -}}
83− <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
84+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
84− {{ search_form_attrs(pageno+1) }}
85+ {{- search_form_attrs(pageno+1) -}}
85− <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-forward"></span> {{ _('next page') }}</button>
86+ <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-forward"></span> {{ _('next page') }}</button>{{- "" -}}
86− </form>
87+ </form>{{- "" -}}
87− </div>
88+ </div>
88− </div><!-- /#pagination -->
89+ </div><!-- /#pagination -->
89− <div class="clearfix"></div>
90+ <div class="clearfix"></div>
90− {% endif %}
91+ {% endif %}
91− {% endif %}
92+ {% endif %}
92− </div><!-- /#main_results -->
93+ </div><!-- /#main_results -->
93−
94+
94− <div class="col-sm-4" id="sidebar_results">
95+ <div class="col-sm-4" id="sidebar_results">
95− {% if number_of_results != '0' %}
96+ {% if number_of_results != '0' -%}
96− <p><small>{{ _('Number of results') }}: {{ number_of_results }}</small></p>
97+ <p><small>{{ _('Number of results') }}: {{ number_of_results }}</small></p>
97− {% endif %}
98+ {%- endif %}
98−
99+
99− {% if unresponsive_engines and results|length >= 1 %}
100+ {% if unresponsive_engines and results|length >= 1 -%}
100− <div class="alert alert-danger fade in" role="alert">
101+ <div class="alert alert-danger fade in" role="alert">
101− <p>{{ _('Engines cannot retrieve results') }}:</p>
102+ <p>{{ _('Engines cannot retrieve results') }}:</p>
102− {% for engine_name, error_type in unresponsive_engines %}
103+ {%- for engine_name, error_type in unresponsive_engines -%}
103− {{ engine_name }} ({{ error_type }}){% if not loop.last %}, {% endif %}
104+ {{- engine_name }} ({{ error_type }}){% if not loop.last %}, {% endif %}{{- "" -}}
104− {% endfor %}
105+ {%- endfor -%}
105− </div>
106+ </div>
106− {% endif %}
107+ {%- endif %}
107−
108+
108− {% if infoboxes %}
109+ {% if infoboxes -%}
109− {% for infobox in infoboxes %}
110+ {% for infobox in infoboxes %}
110− {% include 'oscar/infobox.html' %}
111+ {% include 'oscar/infobox.html' %}{{- "\n\n" -}}
111− {% endfor %}
112+ {% endfor %}
112− {% endif %}
113+ {%- endif %}
113−
114+
114− {% if suggestions %}
115+ {% if suggestions %}
115− <div class="panel panel-default">
116+ <div class="panel panel-default">
116− <div class="panel-heading">
117+ <div class="panel-heading">
117− <h4 class="panel-title">{{ _('Suggestions') }}</h4>
118+ <h4 class="panel-title">{{ _('Suggestions') }}</h4>
118− </div>
119+ </div>
119− <div class="panel-body">
120+ <div class="panel-body">
120− {% for suggestion in suggestions %}
121+ {% for suggestion in suggestions %}
121− <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" role="navigation" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} suggestion_item">
122+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" role="navigation" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} suggestion_item">
122− <input type="hidden" name="q" value="{{ suggestion.url }}">
123+ <input type="hidden" name="q" value="{{ suggestion.url }}">
123− <button type="submit" class="btn btn-default btn-xs">{{ suggestion.title }}</button>
124+ <button type="submit" class="btn btn-default btn-xs">{{ suggestion.title }}</button>
124− </form>
125+ </form>
125− {% endfor %}
126+ {% endfor %}
126− </div>
127+ </div>
127− </div>
128+ </div>
128− {% endif %}
129+ {%- endif %}
129−
130+
130− <div class="panel panel-default">
131+ <div class="panel panel-default">
131− <div class="panel-heading">
132+ <div class="panel-heading">{{- "" -}}
132− <h4 class="panel-title">{{ _('Links') }}</h4>
133+ <h4 class="panel-title">{{ _('Links') }}</h4>{{- "" -}}
133− </div>
134+ </div>
134− <div class="panel-body">
135+ <div class="panel-body">
135− <form role="form">
136+ <form role="form">{{- "" -}}
136− <div class="form-group">
137+ <div class="form-group">{{- "" -}}
137− <label for="search_url">{{ _('Search URL') }}</label>
138+ <label for="search_url">{{ _('Search URL') }}</label>{{- "" -}}
138− <input id="search_url" type="url" class="form-control select-all-on-click cursor-text" name="search_url" value="{{ search_url() }}" readonly>
139+ <input id="search_url" type="url" class="form-control select-all-on-click cursor-text" name="search_url" value="{{ search_url() }}" readonly>{{- "" -}}
139− </div>
140+ </div>{{- "" -}}
140− </form>
141+ </form>
141−
142− <label>{{ _('Download results') }}</label>
142+ <label>{{ _('Download results') }}</label>
143− <div class="clearfix"></div>
143+ <div class="clearfix"></div>
144− {% for output_type in ('csv', 'json', 'rss') %}
144+ {% for output_type in ('csv', 'json', 'rss') %}
145− <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} result_download">
145+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} result_download">
146− {{ search_form_attrs(pageno) }}
146+ {{- search_form_attrs(pageno) -}}
147− <input type="hidden" name="format" value="{{ output_type }}">
147+ <input type="hidden" name="format" value="{{ output_type }}">{{- "" -}}
148− <button type="submit" class="btn btn-default">{{ output_type }}</button>
148+ <button type="submit" class="btn btn-default">{{ output_type }}</button>{{- "" -}}
149− </form>
149+ </form>
150− {% endfor %}
150+ {% endfor %}
151− <div class="clearfix"></div>
151+ <div class="clearfix"></div>
152− </div>
152+ </div>
153− </div>
153+ </div>
154− </div><!-- /#sidebar_results -->
154+ </div><!-- /#sidebar_results -->
155− </div>
155+ </div>
156−{% endblock %}
156+{% endblock %}
searx/templates/oscar/search.html+24−24
@@ -1,24 +1,24 @@
1−{% from 'oscar/macros.html' import icon %}
1+{% from 'oscar/macros.html' import icon %}
2−<form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" id="search_form" role="search">
2+<form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" id="search_form" role="search">
3− <div class="row">
3+ <div class="row">
4− <div class="col-xs-12 col-md-8">
4+ <div class="col-xs-12 col-md-8">
5− <div class="input-group search-margin">
5+ <div class="input-group search-margin">
6− <input type="search" name="q" class="form-control" id="q" placeholder="{{ _('Search for...') }}" autocomplete="off" value="{{ q }}">
6+ <input type="search" name="q" class="form-control" id="q" placeholder="{{ _('Search for...') }}" aria-label="{{ _('Search for...') }}" autocomplete="off" value="{{ q }}" accesskey="s">
7− <span class="input-group-btn">
7+ <span class="input-group-btn">
8− <button type="submit" class="btn btn-default"><span class="hide_if_nojs">{{ icon('search') }}</span><span class="hidden active_if_nojs">{{ _('Start search') }}</span></button>
8+ <button type="submit" class="btn btn-default" aria-label="{{ _('Start search') }}"><span class="hide_if_nojs">{{ icon('search') }}</span><span class="hidden active_if_nojs">{{ _('Start search') }}</span></button>
9− </span>
9+ </span>
10− </div>
10+ </div>
11− </div>
11+ </div>
12− <div class="col-xs-6 col-md-2 search-margin">
12+ <div class="col-xs-6 col-md-2 search-margin">
13− {% include 'oscar/time-range.html' %}
13+ {%- include 'oscar/time-range.html' -%}
14− </div>
14+ </div>
15− <div class="col-xs-6 col-md-2 search-margin">
15+ <div class="col-xs-6 col-md-2 search-margin">
16− {% include 'oscar/languages.html' %}
16+ {%- include 'oscar/languages.html' -%}
17− </div>
17+ </div>
18− </div>
18+ </div>
19− <div class="row">
19+ <div class="row">
20− <div class="col-sm-12">
20+ <div class="col-sm-12">
21− {% include 'oscar/categories.html' %}
21+ {%- include 'oscar/categories.html' -%}
22− </div>
22+ </div>
23− </div>
23+ </div>
24−</form><!-- / #search_form_full -->
24+</form><!-- / #search_form_full -->
searx/templates/oscar/search_full.html+18−18
@@ -1,18 +1,18 @@
1−{% from 'oscar/macros.html' import icon %}
1+{% from 'oscar/macros.html' import icon %}
2−
2+
3−<form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" id="search_form" role="search">
3+<form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" id="search_form" role="search">
4− {% if rtl %}
4+ {% if rtl %}
5− <div class="input-group">
5+ <div class="input-group">
6− {% else %}
6+ {% else %}
7− <div class="input-group col-md-8 col-md-offset-2">
7+ <div class="input-group col-md-8 col-md-offset-2">
8− {% endif %}
8+ {% endif %}
9− <input type="search" name="q" class="form-control input-lg autofocus" id="q" placeholder="{{ _('Search for...') }}" autocomplete="off" value="{{ q }}">
9+ <input type="search" name="q" class="form-control input-lg autofocus" id="q" placeholder="{{ _('Search for...') }}" aria-label="{{ _('Search for...') }}" autocomplete="off" value="{{ q }}" accesskey="s">
10− <span class="input-group-btn">
10+ <span class="input-group-btn">
11− <button type="submit" class="btn btn-default input-lg"><span class="hide_if_nojs">{{ icon('search') }}</span><span class="hidden active_if_nojs">{{ _('Start search') }}</span></button>
11+ <button type="submit" class="btn btn-default input-lg" aria-label="{{ _('Start search') }}"><span class="hide_if_nojs">{{ icon('search') }}</span><span class="hidden active_if_nojs">{{ _('Start search') }}</span></button>
12− </span>
12+ </span>
13− </div>
13+ </div>
14− <div class="col-md-8 col-md-offset-2 advanced">
14+ <div class="col-md-8 col-md-offset-2 advanced">
15− {% include 'oscar/advanced.html' %}
15+ {% include 'oscar/advanced.html' %}
16− </div>
16+ </div>
17−
17+
18−</form><!-- / #search_form_full -->
18+</form><!-- / #search_form_full -->
searx/templates/oscar/time-range.html+11−11
@@ -1,17 +1,17 @@
1−<select name="time_range" id="time-range" class="custom-select form-control">
1+<select name="time_range" id="time-range" class="custom-select form-control" accesskey="t">{{- "" -}}
2 <option id="time-range-anytime" value="" {{ "selected" if time_range=="" or not time_range else ""}}>
2 <option id="time-range-anytime" value="" {{ "selected" if time_range=="" or not time_range else ""}}>
3− {{ _('Anytime') }}
3+ {{- _('Anytime') -}}
4− </option>
4+ </option>{{- "" -}}
5 <option id="time-range-day" value="day" {{ "selected" if time_range=="day" else ""}}>
5 <option id="time-range-day" value="day" {{ "selected" if time_range=="day" else ""}}>
6− {{ _('Last day') }}
6+ {{- _('Last day') -}}
7− </option>
7+ </option>{{- "" -}}
8 <option id="time-range-week" value="week" {{ "selected" if time_range=="week" else ""}}>
8 <option id="time-range-week" value="week" {{ "selected" if time_range=="week" else ""}}>
9− {{ _('Last week') }}
9+ {{- _('Last week') -}}
10− </option>
10+ </option>{{- "" -}}
11 <option id="time-range-month" value="month" {{ "selected" if time_range=="month" else ""}}>
11 <option id="time-range-month" value="month" {{ "selected" if time_range=="month" else ""}}>
12− {{ _('Last month') }}
12+ {{- _('Last month') -}}
13− </option>
13+ </option>{{- "" -}}
14 <option id="time-range-year" value="year" {{ "selected" if time_range=="year" else ""}}>
14 <option id="time-range-year" value="year" {{ "selected" if time_range=="year" else ""}}>
15− {{ _('Last year') }}
15+ {{- _('Last year') -}}
16− </option>
16+ </option>{{- "" -}}
17</select>
17</select>
searx/templates/simple/result_templates/key-value.htmlnew file+11
@@ -0,0 +1,11 @@
1+<table>
2+ {% for key, value in result.items() %}
3+ {% if key in ['engine', 'engines', 'template', 'score', 'category', 'positions'] %}
4+ {% continue %}
5+ {% endif %}
6+ <tr>
7+ <td><b>{{ key|upper }}</b>: {{ value }}</td>
8+ </tr>
9+ {% endfor %}
10+</table>
11+<div class="engines">{% for engine in result.engines %}<span>{{ engine }}</span>{% endfor %}</div>{{- '' -}}
searx/templates/simple/result_templates/torrent.html+1−1
@@ -6,7 +6,7 @@
6{% if result.magnetlink %}<p class="altlink"> &bull; {{ result_link(result.magnetlink, icon('magnet') + _('magnet link'), "magnetlink") }}</p>{% endif %}
6{% if result.magnetlink %}<p class="altlink"> &bull; {{ result_link(result.magnetlink, icon('magnet') + _('magnet link'), "magnetlink") }}</p>{% endif %}
7{% if result.torrentfile %}<p class="altlink"> &bull; {{ result_link(result.torrentfile, icon('download-alt') + _('torrent file'), "torrentfile") }}</p>{% endif %}
7{% if result.torrentfile %}<p class="altlink"> &bull; {{ result_link(result.torrentfile, icon('download-alt') + _('torrent file'), "torrentfile") }}</p>{% endif %}
8
8
9−{% if result.seed %}<p class="stat"> &bull; {{ icon('arrow-swap') }} {{ _('Seeder') }} <span class="badge">{{ result.seed }}</span> &bull; {{ _('Leecher') }} <span class="badge">{{ result.leech }}</span></p>{% endif %}
9+{% if result.seed is defined %}<p class="stat"> &bull; {{ icon('arrow-swap') }} {{ _('Seeder') }} <span class="badge">{{ result.seed }}</span> &bull; {{ _('Leecher') }} <span class="badge">{{ result.leech }}</span></p>{% endif %}
10
10
11{%- if result.filesize %}<p class="stat">{{ icon('floppy-disk') }} {{ _('Filesize') }}<span class="badge">
11{%- if result.filesize %}<p class="stat">{{ icon('floppy-disk') }} {{ _('Filesize') }}<span class="badge">
12 {%- if result.filesize < 1024 %}{{ result.filesize }} {{ _('Bytes') }}
12 {%- if result.filesize < 1024 %}{{ result.filesize }} {{ _('Bytes') }}
searx/templates/simple/results.html+2−2
@@ -95,13 +95,13 @@
95 {% for correction in corrections %}
95 {% for correction in corrections %}
96 <div class="left">
96 <div class="left">
97 <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" role="navigation">
97 <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" role="navigation">
98− <input type="hidden" name="q" value="{{ correction }}">
98+ <input type="hidden" name="q" value="{{ correction.url }}">
99 <input type="hidden" name="time_range" value="{{ time_range }}">
99 <input type="hidden" name="time_range" value="{{ time_range }}">
100 <input type="hidden" name="language" value="{{ current_language }}">
100 <input type="hidden" name="language" value="{{ current_language }}">
101 <input type="hidden" name="safesearch" value="{{ safesearch }}">
101 <input type="hidden" name="safesearch" value="{{ safesearch }}">
102 <input type="hidden" name="theme" value="{{ theme }}">
102 <input type="hidden" name="theme" value="{{ theme }}">
103 {% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit }}" >{% endif %}
103 {% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit }}" >{% endif %}
104− <input type="submit" value="{{ correction }}">
104+ <input type="submit" value="{{ correction.title }}">
105 </form>
105 </form>
106 </div>
106 </div>
107 {% endfor %}
107 {% endfor %}
searx/utils.py+33−2
@@ -13,6 +13,7 @@ from numbers import Number
13from os.path import splitext, join
13from os.path import splitext, join
14from io import open
14from io import open
15from random import choice
15from random import choice
16+from lxml.etree import XPath
16import sys
17import sys
17import json
18import json
18
19
@@ -51,6 +52,7 @@ ecma_unescape2_re = re.compile(r'%([0-9a-fA-F]{2})', re.UNICODE)
51useragents = json.loads(open(os.path.dirname(os.path.realpath(__file__))
52useragents = json.loads(open(os.path.dirname(os.path.realpath(__file__))
52 + "/data/useragents.json", 'r', encoding='utf-8').read())
53 + "/data/useragents.json", 'r', encoding='utf-8').read())
53
54
55+xpath_cache = dict()
54lang_to_lc_cache = dict()
56lang_to_lc_cache = dict()
55
57
56
58
@@ -308,14 +310,15 @@ def int_or_zero(num):
308
310
309def is_valid_lang(lang):
311def is_valid_lang(lang):
310 is_abbr = (len(lang) == 2)
312 is_abbr = (len(lang) == 2)
313+ lang = lang.lower().decode('utf-8')
311 if is_abbr:
314 if is_abbr:
312 for l in language_codes:
315 for l in language_codes:
313− if l[0][:2] == lang.lower():
316+ if l[0][:2] == lang:
314 return (True, l[0][:2], l[3].lower())
317 return (True, l[0][:2], l[3].lower())
315 return False
318 return False
316 else:
319 else:
317 for l in language_codes:
320 for l in language_codes:
318− if l[1].lower() == lang.lower():
321+ if l[1].lower() == lang or l[3].lower() == lang:
319 return (True, l[0][:2], l[3].lower())
322 return (True, l[0][:2], l[3].lower())
320 return False
323 return False
321
324
@@ -434,3 +437,31 @@ def ecma_unescape(s):
434 # "%20" becomes " ", "%F3" becomes "ó"
437 # "%20" becomes " ", "%F3" becomes "ó"
435 s = ecma_unescape2_re.sub(lambda e: unichr(int(e.group(1), 16)), s)
438 s = ecma_unescape2_re.sub(lambda e: unichr(int(e.group(1), 16)), s)
436 return s
439 return s
440+
441+
442+def get_engine_from_settings(name):
443+ """Return engine configuration from settings.yml of a given engine name"""
444+
445+ if 'engines' not in settings:
446+ return {}
447+
448+ for engine in settings['engines']:
449+ if 'name' not in engine:
450+ continue
451+ if name == engine['name']:
452+ return engine
453+
454+ return {}
455+
456+
457+def get_xpath(xpath_str):
458+ result = xpath_cache.get(xpath_str, None)
459+ if result is None:
460+ result = XPath(xpath_str)
461+ xpath_cache[xpath_str] = result
462+ return result
463+
464+
465+def eval_xpath(element, xpath_str):
466+ xpath = get_xpath(xpath_str)
467+ return xpath(element)
searx/webapp.py+28−18
@@ -41,7 +41,10 @@ except:
41 logger.critical("cannot import dependency: pygments")
41 logger.critical("cannot import dependency: pygments")
42 from sys import exit
42 from sys import exit
43 exit(1)
43 exit(1)
44+try:
44−from cgi import escape
45+ from cgi import escape
46+except:
47+ from html import escape
45from datetime import datetime, timedelta
48from datetime import datetime, timedelta
46from time import time
49from time import time
47from werkzeug.contrib.fixers import ProxyFix
50from werkzeug.contrib.fixers import ProxyFix
@@ -124,6 +127,7 @@ app = Flask(
124
127
125app.jinja_env.trim_blocks = True
128app.jinja_env.trim_blocks = True
126app.jinja_env.lstrip_blocks = True
129app.jinja_env.lstrip_blocks = True
130+app.jinja_env.add_extension('jinja2.ext.loopcontrols')
127app.secret_key = settings['server']['secret_key']
131app.secret_key = settings['server']['secret_key']
128
132
129if not searx_debug \
133if not searx_debug \
@@ -153,20 +157,18 @@ outgoing_proxies = settings['outgoing'].get('proxies') or None
153
157
154@babel.localeselector
158@babel.localeselector
155def get_locale():
159def get_locale():
156− locale = request.accept_languages.best_match(settings['locales'].keys())
160+ if 'locale' in request.form\
157−
161+ and request.form['locale'] in settings['locales']:
158− if request.preferences.get_value('locale') != '':
159− locale = request.preferences.get_value('locale')
162+ return request.form['locale']
160
163
161 if 'locale' in request.args\
164 if 'locale' in request.args\
162 and request.args['locale'] in settings['locales']:
165 and request.args['locale'] in settings['locales']:
163− locale = request.args['locale']
166+ return request.args['locale']
164
167
165− if 'locale' in request.form\
168+ if request.preferences.get_value('locale') != '':
166− and request.form['locale'] in settings['locales']:
167− locale = request.form['locale']
169+ return request.preferences.get_value('locale')
168
170
169− return locale
171+ return request.accept_languages.best_match(settings['locales'].keys())
170
172
171
173
172# code-highlighter
174# code-highlighter
@@ -538,14 +540,16 @@ def index():
538 if output_format == 'html':
540 if output_format == 'html':
539 if 'content' in result and result['content']:
541 if 'content' in result and result['content']:
540 result['content'] = highlight_content(escape(result['content'][:1024]), search_query.query)
542 result['content'] = highlight_content(escape(result['content'][:1024]), search_query.query)
543+ if 'title' in result and result['title']:
541− result['title'] = highlight_content(escape(result['title'] or u''), search_query.query)
544+ result['title'] = highlight_content(escape(result['title'] or u''), search_query.query)
542 else:
545 else:
543 if result.get('content'):
546 if result.get('content'):
544 result['content'] = html_to_text(result['content']).strip()
547 result['content'] = html_to_text(result['content']).strip()
545 # removing html content and whitespace duplications
548 # removing html content and whitespace duplications
546 result['title'] = ' '.join(html_to_text(result['title']).strip().split())
549 result['title'] = ' '.join(html_to_text(result['title']).strip().split())
547
550
551+ if 'url' in result:
548− result['pretty_url'] = prettify_url(result['url'])
552+ result['pretty_url'] = prettify_url(result['url'])
549
553
550 # TODO, check if timezone is calculated right
554 # TODO, check if timezone is calculated right
551 if 'publishedDate' in result:
555 if 'publishedDate' in result:
@@ -602,11 +606,17 @@ def index():
602 # HTML output format
606 # HTML output format
603
607
604 # suggestions: use RawTextQuery to get the suggestion URLs with the same bang
608 # suggestions: use RawTextQuery to get the suggestion URLs with the same bang
605− suggestion_urls = map(lambda suggestion: {
609+ suggestion_urls = list(map(lambda suggestion: {
606− 'url': raw_text_query.changeSearchQuery(suggestion).getFullQuery(),
610+ 'url': raw_text_query.changeSearchQuery(suggestion).getFullQuery(),
607− 'title': suggestion
611+ 'title': suggestion
608− },
612+ },
609− result_container.suggestions)
613+ result_container.suggestions))
614+
615+ correction_urls = list(map(lambda correction: {
616+ 'url': raw_text_query.changeSearchQuery(correction).getFullQuery(),
617+ 'title': correction
618+ },
619+ result_container.corrections))
610 #
620 #
611 return render(
621 return render(
612 'results.html',
622 'results.html',
@@ -619,7 +629,7 @@ def index():
619 advanced_search=advanced_search,
629 advanced_search=advanced_search,
620 suggestions=suggestion_urls,
630 suggestions=suggestion_urls,
621 answers=result_container.answers,
631 answers=result_container.answers,
622− corrections=result_container.corrections,
632+ corrections=correction_urls,
623 infoboxes=result_container.infoboxes,
633 infoboxes=result_container.infoboxes,
624 paging=result_container.paging,
634 paging=result_container.paging,
625 unresponsive_engines=result_container.unresponsive_engines,
635 unresponsive_engines=result_container.unresponsive_engines,
setup.py+6−6
@@ -11,14 +11,14 @@ import sys
11sys.path.insert(0, './searx')
11sys.path.insert(0, './searx')
12from version import VERSION_STRING
12from version import VERSION_STRING
13
13
14+with open('README.rst') as f:
15+ long_description = f.read()
14
16
15−def read(*rnames):
17+with open('requirements.txt') as f:
16− return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
18+ requirements = [ l.strip() for l in f.readlines()]
17
19
18−
19−long_description = read('README.rst')
20−requirements = map(str.strip, open('requirements.txt').readlines())
21−dev_requirements = map(str.strip, open('requirements-dev.txt').readlines())
20+with open('requirements-dev.txt') as f:
21+ dev_requirements = [ l.strip() for l in f.readlines()]
22
22
23setup(
23setup(
24 name='searx',
24 name='searx',
tests/unit/engines/__init__.pydeleted

No changes to the content.

tests/unit/engines/pubmed.pydeleted−37
@@ -1,37 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import pubmed
5−from searx.testing import SearxTestCase
6−
7−
8−class TestPubmedEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = pubmed.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn('eutils.ncbi.nlm.nih.gov/', params['url'])
17− self.assertIn('term', params['url'])
18−
19− def test_response(self):
20− self.assertRaises(AttributeError, pubmed.response, None)
21− self.assertRaises(AttributeError, pubmed.response, [])
22− self.assertRaises(AttributeError, pubmed.response, '')
23− self.assertRaises(AttributeError, pubmed.response, '[]')
24−
25− response = mock.Mock(text='<PubmedArticleSet></PubmedArticleSet>')
26− self.assertEqual(pubmed.response(response), [])
27−
28− xml_mock = """<eSearchResult><Count>1</Count><RetMax>1</RetMax><RetStart>0</RetStart><IdList>
29−<Id>1</Id>
30−</IdList></eSearchResult>
31−"""
32−
33− response = mock.Mock(text=xml_mock.encode('utf-8'))
34− results = pubmed.response(response)
35− self.assertEqual(type(results), list)
36− self.assertEqual(len(results), 1)
37− self.assertEqual(results[0]['content'], 'No abstract is available for this publication.')
tests/unit/engines/seedpeer_fixture.htmldeleted−110
@@ -1,110 +0,0 @@
1−<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2− <html xmlns="http://www.w3.org/1999/xhtml">
3− <head>
4− </head>
5− <body>
6− <div id="header">
7− <div id="whoIsYou">
8− <a href="/lang.php"><small>SeedPeer in your own language?</small></a>&nbsp;&nbsp;&nbsp;<a href="http://www.seedpeer.eu"><img src="/images/flags/uk.gif" width="16px" alt="Torrents EN" /></a> <a href="http://spanish.seedpeer.eu"><img src="/images/flags/es.gif" width="16px" alt="Torrents ES" /></a> <a href="http://german.seedpeer.eu"><img src="/images/flags/de.gif" width="16px" alt="Torrents DE" /></a> <a href="http://french.seedpeer.eu"><img src="/images/flags/fr.gif" width="16px" alt="Torrents FR" /></a> <a href="http://portuguese.seedpeer.eu"><img src="/images/flags/pt.gif" width="16px" alt="Torrents Portuguese" /></a> <a href="http://swedish.seedpeer.eu"><img src="/images/flags/se.gif" width="16px" alt="Torrents Sweden" /></a>
9− </div>
10−
11− <script type="text/javascript">
12− whoIsYou();
13− </script>
14− <div id="search">
15− <form action="/search.php" method="get">
16− <input id="topsearchbar" name="search" value="narcos season 2" />
17− <input type="submit" class="searchbutton" value="Torrents" />
18− <input style="color:#000" type="submit" class="searchbutton" name="usenet" value="Usenet Binaries" />
19− </form>
20− <div id="suggestion"></div>
21− </div>
22− <div id="logo"><a href="/"><img src="/images/logo2.gif" alt="Seedpeer homepage" width="415" height="143" /></a></div>
23− <div id="subtext"><a href="/">Home</a> &gt; <a href="/search.html">Torrent search</a> &gt; Narcos season 2 | page 1</div>
24− </div>
25− <div id="nav">
26− <ul>
27− <!--
28− <li><font style="color:red;font-size:9px;font-weight:bold;">NEW</font><a title="Download TOP Games for FREE" rel="nofollow" href="http://www.bigrebelads.com/affiliate/index?ref=9301" target="_blank">FREE Games</a></li>
29−
30− -->
31− <li style="border-left:none" id="categories"><a title="Browse Torrent Categories" href="/browse.html">Categories</a>
32− <ul>
33− <li><a title="Browse Anime Torrents" href="/browse.html#6">Anime</a></li>
34− <li><a title="Browse Game Torrents" href="/browse.html#4">Games</a></li>
35− <li><a title="Browse Movie Torrents" href="/browse.html#1">Movies</a></li>
36− <li><a title="Browse Music Torrents" href="/browse.html#3">Music</a></li>
37− <li><a title="Browse Software Torrents" href="/browse.html#5">Software</a></li>
38− <li><a title="Browse TV Torrents" href="/browse.html#2">TV Shows</a></li>
39− <li><a title="Browse Other Torrents" href="/browse.html#7">Others</a></li>
40− </ul>
41− </li>
42− <li><a title="Upload A Torrents" href="/upload.html">Upload torrent</a></li>
43− <li id="verified"><a title="Verified Torrents" href="/verified.html">Verified</a></li>
44− <li id="searchoptions"><a title="Search Torrents" href="/search.html">Torrent search</a></li>
45− <li id="newsgroups"><a style="color:#212b3e" title="News Groups" href="/usenet.html">Usenet Binaries</a></li>
46− <li id="about" style="border-right:none"><a rel="nofollow" href="/faq.html">About Us</a>
47− <ul>
48− <li><a title="SeedPeer Statistics" href="/stats.html">Statistics</a></li>
49− <li><a title="Contact Us" href="/contact.html">Contact</a></li>
50− <li><a title="Frequently Asked Questions" href="/faq.html">FAQ</a></li>
51− <li><a title="SeedPeer API" href="http://api.seedpeer.eu">Our API</a></li>
52− <li><a title="SeedPeer Blog" href="/blog">Blog</a></li>
53− </ul>
54− </li>
55− <!--<li><a href="/toolbar.php">Our Toolbar</a></li>-->
56− </ul>
57− <div class="clear"></div>
58− </div>
59− <div id="body"><div id="pageTop"></div>
60− <div id="headerbox"><h1>Verified <font class="colored">Narcos season 2</font> torrents</h1></div><table width="100%"><tr><th>
61− <span style="float:right">
62− <a href="/search/narcos-season-2/8/1.html"><img style="vertical-align:middle" src="/images/comments.gif" alt="comments" /></a> |
63− <a href="/search/narcos-season-2/7/1.html"><img style="vertical-align:middle" src="/images/ver.gif" alt="verified" /></a>
64− </span>
65− <a href="/search/narcos-season-2/1/1.html">Torrent name</a></th><th class="right"><a href="/search/narcos-season-2/2/1.html">Age</a></th><th class="right"><a href="/search/narcos-season-2/3/1.html">Size</a></th><th class="right"><a href="/search/narcos-season-2/4/1.html">Seeds</a></th><th class="right"><a href="/search/narcos-season-2/5/1.html">Peers</a></th><th class="center"><a href="/search/narcos-season-2/6/1.html">Health</a></th><td class="tableAd" rowspan="6"><iframe src="http://creative.wwwpromoter.com/13689?d=300x250" width="300" height="250" style="border: none;" frameborder="0" scrolling="no"></iframe></td></tr><tr class=""><td><a class="pblink" id="pblink_table_item_1" href="" data-tad="431726" data-last-search="narcos+season+2" target="_blank" rel="nofollow"><strong class='colored'>Narcos season 2</strong> Full Version</a></td><td class="right">20 hours</td><td class="right">681.3 MB</td><td class="right"><font color="green">28</font> </td><td class="right"><font color="navy">654</font> </td><td class="center"><img src="/images/h5.gif" alt="Health" /></td></tr><tr class="tdark"><td><a class="pblink" id="pblink_table_item_2" href="" data-tad="431727" data-url="narcos+season+2" target="_blank" rel="nofollow"><strong class='colored'>Narcos season 2</strong> Trusted Source</a></td><td class="right">12 hours</td><td class="right">787.1 MB</td><td class="right"><font color="green">64</font> </td><td class="right"><font color="navy">220</font> </td><td class="center"><img src="/images/h5.gif" alt="Health" /></td></tr><tr class=""><td><a class="pblink" id="pblink_table_item_3" href="" data-tad="431729" data-last-search="narcos+season+2" target="_blank" rel="nofollow"><strong class='colored'>Full Narcos season 2 Download</strong></a> <small><a class="pblink" id="pblink_table_item_4" href="" data-tad="431729" data-last-search="narcos+season+2" target="_blank" rel="nofollow">Usenet</a></small></td><td class="right">24 hours</td><td class="right">775.5 MB</td><td class="right"><font color="green">60</font> </td><td class="right"><font color="navy">236</font> </td><td class="center"><img src="/images/h5.gif" alt="Health" /></td></tr><tr class="tdark"><td><a class="pblink" id="pblink_table_item_5" href="" data-tad="431730" data-last-search="narcos+season+2" target="_blank" rel="nofollow"><strong class='colored'>Narcos season 2</strong> 2014 - DIRECT STREAMING</a> <small><a class="pblink" id="pblink_table_item_6" href="" data-tad="431729" data-last-search="narcos+season+2" target="_blank" rel="nofollow">Movies</a></small></td><td class="right">17 hours</td><td class="right">654.1 MB</td><td class="right"><font color="green">2</font> </td><td class="right"><font color="navy">391</font> </td><td class="center"><img src="/images/h5.gif" alt="Health" /></td></tr><tr class=""><td><a class="pblink" id="pblink_table_item_7" href="" data-tad="431731" data-last-search="narcos+season+2" target="_blank" rel="nofollow"><strong class='colored'>Narcos season 2</strong> 2014</a> <small><a class="pblink" id="pblink_table_item_8" href="" data-tad="431729" data-last-search="narcos+season+2" target="_blank" rel="nofollow">Movies</a></small></td><td class="right">20 hours</td><td class="right">754.5 MB</td><td class="right"><font color="green">21</font> </td><td class="right"><font color="navy">919</font> </td><td class="center"><img src="/images/h5.gif" alt="Health" /></td></tr></table><br /><br /><center><iframe src='http://creative.wwwpromoter.com/13689?d=728x90' width='728' height='90' style='border: none;' frameborder='0' scrolling='no'></iframe><center><span style="float:right;margin:1em .2em 0 0"><a title="Download at the speed of your connection" href="/usenet.php?search=narcos+season+2"><img src="/images/dlf.gif" alt="Search Binaries" /></a></span><div style="margin-bottom:1em;margin-right:290px" id="headerbox"><h1><a href="/searchfeed/narcos+season+2.xml" target="_blank" title="SeedPeer RSS Torrent Search Feed fornarcos season 2"><img src="/images/feedIcon.png" border="0" /></a>&nbsp;2 <font class="colored">Narcos season 2</font> Torrents were found</h1></div><table width="100%"><tr><th>
66− <span style="float:right">
67− <a href="/search/narcos-season-2/8/1.html"><img style="vertical-align:middle" src="/images/comments.gif" alt="comments" /></a> |
68− <a href="/search/narcos-season-2/7/1.html"><img style="vertical-align:middle" src="/images/ver.gif" alt="verified" /></a>
69− </span>
70− <a href="/search/narcos-season-2/1/1.html">Torrent name</a></th><th class="right"><a href="/search/narcos-season-2/2/1.html">Age</a></th><th class="right"><a href="/search/narcos-season-2/3/1.html">Size</a></th><th class="right"><a href="/search/narcos-season-2/4/1.html">Seeds</a></th><th class="right"><a href="/search/narcos-season-2/5/1.html">Peers</a></th><th class="center"><a href="/search/narcos-season-2/6/1.html">Health</a></th></tr><tr class=""><td><small class="comments"><a href="http://www.facebook.com/sharer.php?t=Download%20<strong class='colored'>Narcos</strong> <strong class='colored'>Season</strong> <strong class='colored'>2</strong> Complete 7<strong class='colored'>2</strong>0p WebRip EN-SUB x<strong class='colored'>2</strong>64-[MULVAcoded] S0<strong class='colored'>2</strong>%20 torrent&u=http://seedpeer.seedpeer.eu/details/11686840/Narcos-Season-2-Complete-720p-WebRip-EN-SUB-x264-[MULVAcoded]-S02.html"><img src="/images/facebook.png" alt="Add to Facebook" width="14" height="14" /></a></small><a href="/details/11686840/Narcos-Season-2-Complete-720p-WebRip-EN-SUB-x264-[MULVAcoded]-S02.html"><strong class='colored'>Narcos</strong> <strong class='colored'>Season</strong> <strong class='colored'>2</strong> Complete 7<strong class='colored'>2</strong>0p WebRip EN-SUB x<strong class='colored'>2</strong>64-[MULVAcoded] S0<strong class='colored'>2</strong> <small><a href="/browse.html#11686840"></a></small></a></td><td class="right">19 hours</td><td class="right">4.39 GB</td><td class="right"><font color="green">715</font> </td><td class="right"><font color="navy">183</font> </td><td class="center"><img src="/images/h5.gif" alt="Health" width="40" height="11" /></td></tr><tr class="tdark"><td><small class="comments"><a href="http://www.facebook.com/sharer.php?t=Download%20<strong class='colored'>Narcos</strong> - <strong class='colored'>Season</strong> <strong class='colored'>2</strong> - 7<strong class='colored'>2</strong>0p WEBRiP - x<strong class='colored'>2</strong>65 HEVC - ShAaNiG%20 torrent&u=http://seedpeer.seedpeer.eu/details/11685972/Narcos---Season-2---720p-WEBRiP---x265-HEVC---ShAaNiG.html"><img src="/images/facebook.png" alt="Add to Facebook" width="14" height="14" /></a></small><a href="/details/11685972/Narcos---Season-2---720p-WEBRiP---x265-HEVC---ShAaNiG.html"><strong class='colored'>Narcos</strong> - <strong class='colored'>Season</strong> <strong class='colored'>2</strong> - 7<strong class='colored'>2</strong>0p WEBRiP - x<strong class='colored'>2</strong>65 HEVC - ShAaNiG <small><a href="/browse.html#11685972"></a></small></a></td><td class="right">1 day</td><td class="right">2.48 GB</td><td class="right"><font color="green">861</font> </td><td class="right"><font color="navy">332</font> </td><td class="center"><img src="/images/h5.gif" alt="Health" width="40" height="11" /></td></tr></table><div id="headerbox"><h1>Related searches for: <font class="colored">Narcos season 2</font></h1></div><div id="search_suggestions"><br />Other suggested searches: </div><br /><a href="http://torrentz2.eu/search?f=narcos-season-2">Search for "narcos-season-2" on Torrentz2.eu</a><br /><a href="http://torrent-finder.info/show.php?q=narcos-season-2">Search for "narcos-season-2" on Torrent-Finder</a><br /><center><iframe src='http://creative.wwwpromoter.com/13689?d=300x250' width='300' height='250' style='border: none;' frameborder='0' scrolling='no'></iframe>&nbsp;<iframe src='http://creative.wwwpromoter.com/13689?d=300x250' width='300' height='250' style='border: none;' frameborder='0' scrolling='no'></iframe>&nbsp;<iframe src='http://creative.wwwpromoter.com/13689?d=300x250' width='300' height='250' style='border: none;' frameborder='0' scrolling='no'></iframe></center><div id="footer">
71− <table width="100%">
72− <tr>
73− <td width="30%">
74− <h2>Torrents Download</h2>
75− <a href="/">Torrent search</a><br />
76− <a href="/browse.html">Browse categories</a><br />
77− <a href="/verified.html">Verified Torrents</a><br />
78− <a href="/order-date.html">Today's torrents</a><br />
79− <a href="/yesterday.html">Yesterday's torrents</a><br />
80− <a href="/stats.html">Statistics</a><br />
81− <br />
82− <a href="/faq.html#copyright"><strong>Copyright & Removal</strong></a>
83− </td>
84− <td width="30%"><h2>Cool Stuff</h2>
85− <a href="/promotional.php">Promotional</a><br />
86− <a href="/contact.html">Advertising Information</a><br />
87− <strong><a href="/plugins.php" title="Add a search plugin to Firefox or Internet Explorer">Search Plugin <span style="color:red">*</span></a></strong><br />
88− <a href="http://www.utorrent.com">&micro;Torrent Client</a><br />
89− <a href="/blog">Seedpeer Blog</a><br />
90− </td>
91− <td width="30%"><h2>Links</h2>
92− <a href="http://www.sumotorrent.com" target="_blank"><strong>SumoTorrent</strong></a><br />
93− <a href="http://www.torrent-finder.info" target="_blank"><strong>Torrent Finder</strong></a><br />
94− <a href="http://www.torrentpond.com" target="_blank"><strong>TorrentPond</strong></a><br />
95− <a href="https://www.limetorrents.cc" target="_blank">LimeTorrents.cc</a><br />
96− <a href="http://www.torrents.to/" target="_blank">Torrents.to</a><br />
97− <a href="http://www.torrentfunk.com" target="_blank">TorrentFunk</a><br />
98− <a href="https://monova.org" target="_blank">Monova</a><br />
99− <a href="http://www.torrentroom.com" target="_blank">TorrentRoom</a><br />
100− <a href="http://www.katcr.co/" target="_blank">Kickass Torrents Community</a><br />
101− </td>
102− <td width="10%"><div id="bottomlogo"></div></td>
103− </tr>
104− </table>
105− <br />
106− <br />
107− </div>
108− </div>
109− </body>
110− </html>
tests/unit/engines/test_acgsou.pydeleted−78
@@ -1,78 +0,0 @@
1−# coding=utf-8
2−from collections import defaultdict
3−import mock
4−from searx.engines import acgsou
5−from searx.testing import SearxTestCase
6−
7−
8−class TestAcgsouEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dic = defaultdict(dict)
13− dic['pageno'] = 1
14− params = acgsou.request(query, dic)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertTrue('acgsou.com' in params['url'])
18−
19− def test_response(self):
20− resp = mock.Mock(text='<html></html>')
21− self.assertEqual(acgsou.response(resp), [])
22−
23− html = u"""
24− <html>
25−<table id="listTable" class="list_style table_fixed">
26− <thead class="tcat">
27− <tr>
28− <th axis="string" class="l1 tableHeaderOver">test</th>
29− <th axis="string" class="l2 tableHeaderOver">test</th>
30− <th axis="string" class="l3 tableHeaderOver">test</th>
31− <th axis="size" class="l4 tableHeaderOver">test</th>
32− <th axis="number" class="l5 tableHeaderOver">test</th>
33− <th axis="number" class="l6 tableHeaderOver">test</th>
34− <th axis="number" class="l7 tableHeaderOver">test</th>
35− <th axis="string" class="l8 tableHeaderOver">test</th>
36− </tr>
37− </thead>
38− <tbody class="tbody" id="data_list">
39− <tr class="alt1 ">
40− <td nowrap="nowrap">date</td>
41− <td><a href="category.html">testcategory テスト</a></td>
42− <td style="text-align:left;">
43− <a href="show-torrentid.html" target="_blank">torrentname テスト</a>
44− </td>
45− <td>1MB</td>
46− <td nowrap="nowrap">
47− <span class="bts_1">
48− 29
49− </span>
50− </td>
51− <td nowrap="nowrap">
52− <span class="btl_1">
53− 211
54− </span>
55− </td>
56− <td nowrap="nowrap">
57− <span class="btc_">
58− 168
59− </span>
60− </td>
61− <td><a href="random.html">user</a></td>
62− </tr>
63− </tbody>
64−</table>
65−</html>
66− """
67−
68− resp = mock.Mock(text=html)
69− results = acgsou.response(resp)
70−
71− self.assertEqual(type(results), list)
72− self.assertEqual(len(results), 1)
73−
74− r = results[0]
75− self.assertEqual(r['url'], 'http://www.acgsou.com/show-torrentid.html')
76− self.assertEqual(r['content'], u'Category: "testcategory テスト".')
77− self.assertEqual(r['title'], u'torrentname テスト')
78− self.assertEqual(r['filesize'], 1048576)
tests/unit/engines/test_archlinux.pydeleted−111
@@ -1,111 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import archlinux
4−from searx.testing import SearxTestCase
5−
6−domains = {
7− 'all': 'https://wiki.archlinux.org',
8− 'de': 'https://wiki.archlinux.de',
9− 'fr': 'https://wiki.archlinux.fr',
10− 'ja': 'https://wiki.archlinuxjp.org',
11− 'ro': 'http://wiki.archlinux.ro',
12− 'tr': 'http://archtr.org/wiki'
13−}
14−
15−
16−class TestArchLinuxEngine(SearxTestCase):
17−
18− def test_request(self):
19− query = 'test_query'
20− dic = defaultdict(dict)
21− dic['pageno'] = 1
22− dic['language'] = 'en-US'
23− params = archlinux.request(query, dic)
24− self.assertTrue('url' in params)
25− self.assertTrue(query in params['url'])
26− self.assertTrue('wiki.archlinux.org' in params['url'])
27−
28− for lang, name in archlinux.main_langs:
29− dic['language'] = lang
30− params = archlinux.request(query, dic)
31− self.assertTrue(name in params['url'])
32−
33− for lang, domain in domains.items():
34− dic['language'] = lang
35− params = archlinux.request(query, dic)
36− self.assertTrue(domain in params['url'])
37−
38− def test_response(self):
39− response = mock.Mock(text='<html></html>',
40− search_params={'language': 'en_US'})
41− self.assertEqual(archlinux.response(response), [])
42−
43− html = """
44− <ul class="mw-search-results">
45− <li>
46− <div class="mw-search-result-heading">
47− <a href="/index.php/ATI" title="ATI">ATI</a>
48− </div>
49− <div class="searchresult">
50− Lorem ipsum dolor sit amet
51− </div>
52− <div class="mw-search-result-data">
53− 30 KB (4,630 words) - 19:04, 17 March 2016</div>
54− </li>
55− <li>
56− <div class="mw-search-result-heading">
57− <a href="/index.php/Frequently_asked_questions" title="Frequently asked questions">
58− Frequently asked questions
59− </a>
60− </div>
61− <div class="searchresult">
62− CPUs with AMDs instruction set "AMD64"
63− </div>
64− <div class="mw-search-result-data">
65− 17 KB (2,722 words) - 20:13, 21 March 2016
66− </div>
67− </li>
68− <li>
69− <div class="mw-search-result-heading">
70− <a href="/index.php/CPU_frequency_scaling" title="CPU frequency scaling">CPU frequency scaling</a>
71− </div>
72− <div class="searchresult">
73− ondemand for AMD and older Intel CPU
74− </div>
75− <div class="mw-search-result-data">
76− 15 KB (2,319 words) - 23:46, 16 March 2016
77− </div>
78− </li>
79− </ul>
80− """
81−
82− expected = [
83− {
84− 'title': 'ATI',
85− 'url': 'https://wiki.archlinux.org/index.php/ATI'
86− },
87− {
88− 'title': 'Frequently asked questions',
89− 'url': 'https://wiki.archlinux.org/index.php/Frequently_asked_questions'
90− },
91− {
92− 'title': 'CPU frequency scaling',
93− 'url': 'https://wiki.archlinux.org/index.php/CPU_frequency_scaling'
94− }
95− ]
96−
97− response = mock.Mock(text=html)
98− response.search_params = {
99− 'language': 'en_US'
100− }
101− results = archlinux.response(response)
102−
103− self.assertEqual(type(results), list)
104− self.assertEqual(len(results), len(expected))
105−
106− i = 0
107− for exp in expected:
108− res = results[i]
109− i += 1
110− for key, value in exp.items():
111− self.assertEqual(res[key], value)
tests/unit/engines/test_arxiv.pydeleted−58
@@ -1,58 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import arxiv
5−from searx.testing import SearxTestCase
6−
7−
8−class TestBaseEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'.encode('utf-8')
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = arxiv.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn('export.arxiv.org/api/', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, arxiv.response, None)
20− self.assertRaises(AttributeError, arxiv.response, [])
21− self.assertRaises(AttributeError, arxiv.response, '')
22− self.assertRaises(AttributeError, arxiv.response, '[]')
23−
24− response = mock.Mock(content=b'''<?xml version="1.0" encoding="UTF-8"?>
25−<feed xmlns="http://www.w3.org/2005/Atom"></feed>''')
26− self.assertEqual(arxiv.response(response), [])
27−
28− xml_mock = b'''<?xml version="1.0" encoding="UTF-8"?>
29−<feed xmlns="http://www.w3.org/2005/Atom">
30− <title type="html">ArXiv Query: search_query=all:test_query&amp;id_list=&amp;start=0&amp;max_results=1</title>
31− <id>http://arxiv.org/api/1</id>
32− <updated>2000-01-21T00:00:00-01:00</updated>
33− <opensearch:totalResults xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">1</opensearch:totalResults>
34− <opensearch:startIndex xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">0</opensearch:startIndex>
35− <opensearch:itemsPerPage xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">1</opensearch:itemsPerPage>
36− <entry>
37− <id>http://arxiv.org/1</id>
38− <updated>2000-01-01T00:00:01Z</updated>
39− <published>2000-01-01T00:00:01Z</published>
40− <title>Mathematical proof.</title>
41− <summary>Mathematical formula.</summary>
42− <author>
43− <name>A. B.</name>
44− </author>
45− <link href="http://arxiv.org/1" rel="alternate" type="text/html"/>
46− <link title="pdf" href="http://arxiv.org/1" rel="related" type="application/pdf"/>
47− <category term="math.QA" scheme="http://arxiv.org/schemas/atom"/>
48− <category term="1" scheme="http://arxiv.org/schemas/atom"/>
49− </entry>
50−</feed>
51−'''
52−
53− response = mock.Mock(content=xml_mock)
54− results = arxiv.response(response)
55− self.assertEqual(type(results), list)
56− self.assertEqual(len(results), 1)
57− self.assertEqual(results[0]['title'], 'Mathematical proof.')
58− self.assertEqual(results[0]['content'], 'Mathematical formula.')
tests/unit/engines/test_base.pydeleted−91
@@ -1,91 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import base
5−from searx.testing import SearxTestCase
6−
7−
8−class TestBaseEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = base.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn('base-search.net', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, base.response, None)
20− self.assertRaises(AttributeError, base.response, [])
21− self.assertRaises(AttributeError, base.response, '')
22− self.assertRaises(AttributeError, base.response, '[]')
23−
24− response = mock.Mock(content=b'<response></response>')
25− self.assertEqual(base.response(response), [])
26−
27− xml_mock = b"""<?xml version="1.0"?>
28−<response>
29− <lst name="responseHeader">
30− <int name="status">0</int>
31− <int name="QTime">1</int>
32− </lst>
33− <result name="response" numFound="1" start="0">
34− <doc>
35− <date name="dchdate">2000-01-01T01:01:01Z</date>
36− <str name="dcdocid">1</str>
37− <str name="dccontinent">cna</str>
38− <str name="dccountry">us</str>
39− <str name="dccollection">ftciteseerx</str>
40− <str name="dcprovider">CiteSeerX</str>
41− <str name="dctitle">Science and more</str>
42− <arr name="dccreator">
43− <str>Someone</str>
44− </arr>
45− <arr name="dcperson">
46− <str>Someone</str>
47− </arr>
48− <arr name="dcsubject">
49− <str>Science and more</str>
50− </arr>
51− <str name="dcdescription">Science, and even more.</str>
52− <arr name="dccontributor">
53− <str>The neighbour</str>
54− </arr>
55− <str name="dcdate">2001</str>
56− <int name="dcyear">2001</int>
57− <arr name="dctype">
58− <str>text</str>
59− </arr>
60− <arr name="dctypenorm">
61− <str>1</str>
62− </arr>
63− <arr name="dcformat">
64− <str>application/pdf</str>
65− </arr>
66− <arr name="dccontenttype">
67− <str>application/pdf</str>
68− </arr>
69− <arr name="dcidentifier">
70− <str>http://example.org/</str>
71− </arr>
72− <str name="dclink">http://example.org</str>
73− <str name="dcsource">http://example.org</str>
74− <arr name="dclanguage">
75− <str>en</str>
76− </arr>
77− <str name="dcrights">Under the example.org licence</str>
78− <int name="dcoa">1</int>
79− <arr name="dclang">
80− <str>eng</str>
81− </arr>
82− </doc>
83− </result>
84−</response>"""
85−
86− response = mock.Mock(content=xml_mock)
87− results = base.response(response)
88− self.assertEqual(type(results), list)
89− self.assertEqual(len(results), 1)
90− self.assertEqual(results[0]['title'], 'Science and more')
91− self.assertEqual(results[0]['content'], 'Science, and even more.')
tests/unit/engines/test_bing.pydeleted−178
@@ -1,178 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import bing
5−from searx.testing import SearxTestCase
6−
7−
8−class TestBingEngine(SearxTestCase):
9−
10− def test_request(self):
11− bing.supported_languages = ['en', 'fr', 'zh-CHS', 'zh-CHT', 'pt-PT', 'pt-BR']
12− query = u'test_query'
13− dicto = defaultdict(dict)
14− dicto['pageno'] = 1
15− dicto['language'] = 'fr-FR'
16− params = bing.request(query.encode('utf-8'), dicto)
17− self.assertTrue('url' in params)
18− self.assertTrue(query in params['url'])
19− self.assertTrue('language%3AFR' in params['url'])
20− self.assertTrue('bing.com' in params['url'])
21−
22− dicto['language'] = 'all'
23− params = bing.request(query.encode('utf-8'), dicto)
24− self.assertTrue('language' in params['url'])
25−
26− def test_response(self):
27− dicto = defaultdict(dict)
28− dicto['pageno'] = 1
29− dicto['language'] = 'fr-FR'
30− self.assertRaises(AttributeError, bing.response, None)
31− self.assertRaises(AttributeError, bing.response, [])
32− self.assertRaises(AttributeError, bing.response, '')
33− self.assertRaises(AttributeError, bing.response, '[]')
34−
35− response = mock.Mock(text='<html></html>')
36− response.search_params = dicto
37− self.assertEqual(bing.response(response), [])
38−
39− response = mock.Mock(text='<html></html>')
40− response.search_params = dicto
41− self.assertEqual(bing.response(response), [])
42−
43− html = """
44− <div>
45− <div id="b_tween">
46− <span class="sb_count" data-bm="4">23 900 000 résultats</span>
47− </div>
48− <ol id="b_results" role="main">
49− <div class="sa_cc" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
50− <div Class="sa_mc">
51− <div class="sb_tlst">
52− <h3>
53− <a href="http://this.should.be.the.link/" h="ID=SERP,5124.1">
54− <strong>This</strong> should be the title</a>
55− </h3>
56− </div>
57− <div class="sb_meta"><cite><strong>this</strong>.meta.com</cite>
58− <span class="c_tlbxTrg">
59− <span class="c_tlbxH" H="BASE:CACHEDPAGEDEFAULT" K="SERP,5125.1">
60− </span>
61− </span>
62− </div>
63− <p><strong>This</strong> should be the content.</p>
64− </div>
65− </div>
66− </ol>
67− </div>
68− """
69− response = mock.Mock(text=html)
70− response.search_params = dicto
71− results = bing.response(response)
72− self.assertEqual(type(results), list)
73− self.assertEqual(len(results), 2)
74− self.assertEqual(results[0]['title'], 'This should be the title')
75− self.assertEqual(results[0]['url'], 'http://this.should.be.the.link/')
76− self.assertEqual(results[0]['content'], 'This should be the content.')
77− self.assertEqual(results[-1]['number_of_results'], 23900000)
78−
79− html = """
80− <div>
81− <div id="b_tween">
82− <span class="sb_count" data-bm="4">9-18 résultats sur 23 900 000</span>
83− </div>
84− <ol id="b_results" role="main">
85− <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
86− <div Class="sa_mc">
87− <div class="sb_tlst">
88− <h2>
89− <a href="http://this.should.be.the.link/" h="ID=SERP,5124.1">
90− <strong>This</strong> should be the title</a>
91− </h2>
92− </div>
93− <div class="sb_meta"><cite><strong>this</strong>.meta.com</cite>
94− <span class="c_tlbxTrg">
95− <span class="c_tlbxH" H="BASE:CACHEDPAGEDEFAULT" K="SERP,5125.1">
96− </span>
97− </span>
98− </div>
99− <p><strong>This</strong> should be the content.</p>
100− </div>
101− </li>
102− </ol>
103− </div>
104− """
105− dicto['pageno'] = 2
106− response = mock.Mock(text=html)
107− response.search_params = dicto
108− results = bing.response(response)
109− self.assertEqual(type(results), list)
110− self.assertEqual(len(results), 2)
111− self.assertEqual(results[0]['title'], 'This should be the title')
112− self.assertEqual(results[0]['url'], 'http://this.should.be.the.link/')
113− self.assertEqual(results[0]['content'], 'This should be the content.')
114− self.assertEqual(results[-1]['number_of_results'], 23900000)
115−
116− html = """
117− <div>
118− <div id="b_tween">
119− <span class="sb_count" data-bm="4">23 900 000 résultats</span>
120− </div>
121− <ol id="b_results" role="main">
122− <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
123− <div Class="sa_mc">
124− <div class="sb_tlst">
125− <h2>
126− <a href="http://this.should.be.the.link/" h="ID=SERP,5124.1">
127− <strong>This</strong> should be the title</a>
128− </h2>
129− </div>
130− <div class="sb_meta"><cite><strong>this</strong>.meta.com</cite>
131− <span class="c_tlbxTrg">
132− <span class="c_tlbxH" H="BASE:CACHEDPAGEDEFAULT" K="SERP,5125.1">
133− </span>
134− </span>
135− </div>
136− <p><strong>This</strong> should be the content.</p>
137− </div>
138− </li>
139− </ol>
140− </div>
141− """
142− dicto['pageno'] = 33900000
143− response = mock.Mock(text=html)
144− response.search_params = dicto
145− results = bing.response(response)
146− self.assertEqual(bing.response(response), [])
147−
148− def test_fetch_supported_languages(self):
149− html = """<html></html>"""
150− response = mock.Mock(text=html)
151− results = bing._fetch_supported_languages(response)
152− self.assertEqual(type(results), list)
153− self.assertEqual(len(results), 0)
154−
155− html = """
156− <html>
157− <body>
158− <form>
159− <div id="limit-languages">
160− <div>
161− <div><input id="es" value="es"></input></div>
162− </div>
163− <div>
164− <div><input id="pt_BR" value="pt_BR"></input></div>
165− <div><input id="pt_PT" value="pt_PT"></input></div>
166− </div>
167− </div>
168− </form>
169− </body>
170− </html>
171− """
172− response = mock.Mock(text=html)
173− languages = bing._fetch_supported_languages(response)
174− self.assertEqual(type(languages), list)
175− self.assertEqual(len(languages), 3)
176− self.assertIn('es', languages)
177− self.assertIn('pt-BR', languages)
178− self.assertIn('pt-PT', languages)
tests/unit/engines/test_bing_images.pydeleted−132
@@ -1,132 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import bing_images
5−from searx.testing import SearxTestCase
6−
7−
8−class TestBingImagesEngine(SearxTestCase):
9−
10− def test_request(self):
11− bing_images.supported_languages = ['fr-FR', 'en-US']
12− bing_images.language_aliases = {}
13− query = 'test_query'
14− dicto = defaultdict(dict)
15− dicto['pageno'] = 1
16− dicto['language'] = 'fr-FR'
17− dicto['safesearch'] = 1
18− dicto['time_range'] = ''
19− params = bing_images.request(query, dicto)
20− self.assertTrue('url' in params)
21− self.assertTrue(query in params['url'])
22− self.assertTrue('bing.com' in params['url'])
23− self.assertTrue('SRCHHPGUSR' in params['cookies'])
24− self.assertTrue('DEMOTE' in params['cookies']['SRCHHPGUSR'])
25− self.assertTrue('_EDGE_S' in params['cookies'])
26− self.assertTrue('fr-fr' in params['cookies']['_EDGE_S'])
27−
28− dicto['language'] = 'fr'
29− params = bing_images.request(query, dicto)
30− self.assertTrue('_EDGE_S' in params['cookies'])
31− self.assertTrue('fr-fr' in params['cookies']['_EDGE_S'])
32−
33− dicto['language'] = 'all'
34− params = bing_images.request(query, dicto)
35− self.assertTrue('_EDGE_S' in params['cookies'])
36− self.assertTrue('en-us' in params['cookies']['_EDGE_S'])
37−
38− def test_response(self):
39− self.assertRaises(AttributeError, bing_images.response, None)
40− self.assertRaises(AttributeError, bing_images.response, [])
41− self.assertRaises(AttributeError, bing_images.response, '')
42− self.assertRaises(AttributeError, bing_images.response, '[]')
43−
44− response = mock.Mock(text='<html></html>')
45− self.assertEqual(bing_images.response(response), [])
46−
47− response = mock.Mock(text='<html></html>')
48− self.assertEqual(bing_images.response(response), [])
49−
50− html = """
51− <div id="mmComponent_images_1">
52− <ul>
53− <li>
54− <div>
55− <div class="imgpt">
56− <a m='{"purl":"page_url","murl":"img_url","turl":"thumb_url","t":"Page 1 title"}'>
57− <img src="" alt="alt text" />
58− </a>
59− <div class="img_info">
60− <span>1 x 1 - jpeg</span>
61− <a>1.example.org</a>
62− </div>
63− </div>
64− <div></div>
65− </div>
66− <div>
67− <div class="imgpt">
68− <a m='{"purl":"page_url2","murl":"img_url2","turl":"thumb_url2","t":"Page 2 title"}'>
69− <img src="" alt="alt text 2" />
70− </a>
71− <div class="img_info">
72− <span>2 x 2 - jpeg</span>
73− <a>2.example.org</a>
74− </div>
75− </div>
76− </div>
77− </li>
78− </ul>
79− <ul>
80− <li>
81− <div>
82− <div class="imgpt">
83− <a m='{"purl":"page_url3","murl":"img_url3","turl":"thumb_url3","t":"Page 3 title"}'>
84− <img src="" alt="alt text 3" />
85− </a>
86− <div class="img_info">
87− <span>3 x 3 - jpeg</span>
88− <a>3.example.org</a>
89− </div>
90− </div>
91− </div>
92− </li>
93− </ul>
94− </div>
95− """
96− html = html.replace('\r\n', '').replace('\n', '').replace('\r', '')
97− response = mock.Mock(text=html)
98− results = bing_images.response(response)
99− self.assertEqual(type(results), list)
100− self.assertEqual(len(results), 3)
101− self.assertEqual(results[0]['title'], 'Page 1 title')
102− self.assertEqual(results[0]['url'], 'page_url')
103− self.assertEqual(results[0]['content'], '')
104− self.assertEqual(results[0]['thumbnail_src'], 'thumb_url')
105− self.assertEqual(results[0]['img_src'], 'img_url')
106− self.assertEqual(results[0]['img_format'], '1 x 1 - jpeg')
107− self.assertEqual(results[0]['source'], '1.example.org')
108−
109− def test_fetch_supported_languages(self):
110− html = """
111− <div>
112− <div id="region-section-content">
113− <ul class="b_vList">
114− <li>
115− <a href="https://bing...&setmkt=de-DE&s...">Germany</a>
116− <a href="https://bing...&setmkt=nb-NO&s...">Norway</a>
117− </li>
118− </ul>
119− <ul class="b_vList">
120− <li>
121− <a href="https://bing...&setmkt=es-AR&s...">Argentina</a>
122− </li>
123− </ul>
124− </div>
125− </div>
126− """
127− response = mock.Mock(text=html)
128− languages = list(bing_images._fetch_supported_languages(response))
129− self.assertEqual(len(languages), 3)
130− self.assertIn('de-DE', languages)
131− self.assertIn('no-NO', languages)
132− self.assertIn('es-AR', languages)
tests/unit/engines/test_bing_news.pydeleted−147
@@ -1,147 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import bing_news
4−from searx.testing import SearxTestCase
5−import lxml
6−
7−
8−class TestBingNewsEngine(SearxTestCase):
9−
10− def test_request(self):
11− bing_news.supported_languages = ['en', 'fr']
12− query = 'test_query'
13− dicto = defaultdict(dict)
14− dicto['pageno'] = 1
15− dicto['language'] = 'fr-FR'
16− dicto['time_range'] = ''
17− params = bing_news.request(query, dicto)
18− self.assertIn('url', params)
19− self.assertIn(query, params['url'])
20− self.assertIn('bing.com', params['url'])
21− self.assertIn('fr', params['url'])
22−
23− dicto['language'] = 'all'
24− params = bing_news.request(query, dicto)
25− self.assertIn('en', params['url'])
26−
27− def test_no_url_in_request_year_time_range(self):
28− dicto = defaultdict(dict)
29− query = 'test_query'
30− dicto['time_range'] = 'year'
31− params = bing_news.request(query, dicto)
32− self.assertEqual({}, params['url'])
33−
34− def test_response(self):
35− self.assertRaises(AttributeError, bing_news.response, None)
36− self.assertRaises(AttributeError, bing_news.response, [])
37− self.assertRaises(AttributeError, bing_news.response, '')
38− self.assertRaises(AttributeError, bing_news.response, '[]')
39−
40− response = mock.Mock(content='<html></html>')
41− self.assertEqual(bing_news.response(response), [])
42−
43− response = mock.Mock(content='<html></html>')
44− self.assertEqual(bing_news.response(response), [])
45−
46− html = """<?xml version="1.0" encoding="utf-8" ?>
47−<rss version="2.0" xmlns:News="https://www.bing.com:443/news/search?q=python&amp;setmkt=en-US&amp;first=1&amp;format=RSS">
48− <channel>
49− <title>python - Bing News</title>
50− <link>https://www.bing.com:443/news/search?q=python&amp;setmkt=en-US&amp;first=1&amp;format=RSS</link>
51− <description>Search results</description>
52− <image>
53− <url>http://10.53.64.9/rsslogo.gif</url>
54− <title>test</title>
55− <link>https://www.bing.com:443/news/search?q=test&amp;setmkt=en-US&amp;first=1&amp;format=RSS</link>
56− </image>
57− <copyright>Copyright</copyright>
58− <item>
59− <title>Title</title>
60− <link>https://www.bing.com/news/apiclick.aspx?ref=FexRss&amp;aid=&amp;tid=c237eccc50bd4758b106a5e3c94fce09&amp;url=http%3a%2f%2furl.of.article%2f&amp;c=xxxxxxxxx&amp;mkt=en-us</link>
61− <description>Article Content</description>
62− <pubDate>Tue, 02 Jun 2015 13:37:00 GMT</pubDate>
63− <News:Source>Infoworld</News:Source>
64− <News:Image>http://a1.bing4.com/th?id=ON.13371337133713371337133713371337&amp;pid=News</News:Image>
65− <News:ImageSize>w={0}&amp;h={1}&amp;c=7</News:ImageSize>
66− <News:ImageKeepOriginalRatio></News:ImageKeepOriginalRatio>
67− <News:ImageMaxWidth>620</News:ImageMaxWidth>
68− <News:ImageMaxHeight>413</News:ImageMaxHeight>
69− </item>
70− <item>
71− <title>Another Title</title>
72− <link>https://www.bing.com/news/apiclick.aspx?ref=FexRss&amp;aid=&amp;tid=c237eccc50bd4758b106a5e3c94fce09&amp;url=http%3a%2f%2fanother.url.of.article%2f&amp;c=xxxxxxxxx&amp;mkt=en-us</link>
73− <description>Another Article Content</description>
74− <pubDate>Tue, 02 Jun 2015 13:37:00 GMT</pubDate>
75− </item>
76− </channel>
77−</rss>""" # noqa
78− response = mock.Mock(content=html.encode('utf-8'))
79− results = bing_news.response(response)
80− self.assertEqual(type(results), list)
81− self.assertEqual(len(results), 2)
82− self.assertEqual(results[0]['title'], 'Title')
83− self.assertEqual(results[0]['url'], 'http://url.of.article/')
84− self.assertEqual(results[0]['content'], 'Article Content')
85− self.assertEqual(results[0]['img_src'], 'https://www.bing.com/th?id=ON.13371337133713371337133713371337')
86− self.assertEqual(results[1]['title'], 'Another Title')
87− self.assertEqual(results[1]['url'], 'http://another.url.of.article/')
88− self.assertEqual(results[1]['content'], 'Another Article Content')
89− self.assertNotIn('img_src', results[1])
90−
91− html = """<?xml version="1.0" encoding="utf-8" ?>
92−<rss version="2.0" xmlns:News="https://www.bing.com:443/news/search?q=python&amp;setmkt=en-US&amp;first=1&amp;format=RSS">
93− <channel>
94− <title>python - Bing News</title>
95− <link>https://www.bing.com:443/news/search?q=python&amp;setmkt=en-US&amp;first=1&amp;format=RSS</link>
96− <description>Search results</description>
97− <image>
98− <url>http://10.53.64.9/rsslogo.gif</url>
99− <title>test</title>
100− <link>https://www.bing.com:443/news/search?q=test&amp;setmkt=en-US&amp;first=1&amp;format=RSS</link>
101− </image>
102− <copyright>Copyright</copyright>
103− <item>
104− <title>Title</title>
105− <link>http://another.url.of.article/</link>
106− <description>Article Content</description>
107− <pubDate>garbage</pubDate>
108− <News:Source>Infoworld</News:Source>
109− <News:Image>http://another.bing.com/image</News:Image>
110− <News:ImageSize>w={0}&amp;h={1}&amp;c=7</News:ImageSize>
111− <News:ImageKeepOriginalRatio></News:ImageKeepOriginalRatio>
112− <News:ImageMaxWidth>620</News:ImageMaxWidth>
113− <News:ImageMaxHeight>413</News:ImageMaxHeight>
114− </item>
115− </channel>
116−</rss>""" # noqa
117− response = mock.Mock(content=html.encode('utf-8'))
118− results = bing_news.response(response)
119− self.assertEqual(type(results), list)
120− self.assertEqual(len(results), 1)
121− self.assertEqual(results[0]['title'], 'Title')
122− self.assertEqual(results[0]['url'], 'http://another.url.of.article/')
123− self.assertEqual(results[0]['content'], 'Article Content')
124− self.assertEqual(results[0]['img_src'], 'http://another.bing.com/image')
125−
126− html = """<?xml version="1.0" encoding="utf-8" ?>
127−<rss version="2.0" xmlns:News="https://www.bing.com:443/news/search?q=python&amp;setmkt=en-US&amp;first=1&amp;format=RSS">
128− <channel>
129− <title>python - Bing News</title>
130− <link>https://www.bing.com:443/news/search?q=python&amp;setmkt=en-US&amp;first=1&amp;format=RSS</link>
131− <description>Search results</description>
132− <image>
133− <url>http://10.53.64.9/rsslogo.gif</url>
134− <title>test</title>
135− <link>https://www.bing.com:443/news/search?q=test&amp;setmkt=en-US&amp;first=1&amp;format=RSS</link>
136− </image>
137− </channel>
138−</rss>""" # noqa
139−
140− response = mock.Mock(content=html.encode('utf-8'))
141− results = bing_news.response(response)
142− self.assertEqual(type(results), list)
143− self.assertEqual(len(results), 0)
144−
145− html = """<?xml version="1.0" encoding="utf-8" ?>gabarge"""
146− response = mock.Mock(content=html.encode('utf-8'))
147− self.assertRaises(lxml.etree.XMLSyntaxError, bing_news.response, response)
tests/unit/engines/test_bing_videos.pydeleted−72
@@ -1,72 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import bing_videos
5−from searx.testing import SearxTestCase
6−
7−
8−class TestBingVideosEngine(SearxTestCase):
9−
10− def test_request(self):
11− bing_videos.supported_languages = ['fr-FR', 'en-US']
12− bing_videos.language_aliases = {}
13− query = 'test_query'
14− dicto = defaultdict(dict)
15− dicto['pageno'] = 1
16− dicto['language'] = 'fr-FR'
17− dicto['safesearch'] = 0
18− dicto['time_range'] = ''
19− params = bing_videos.request(query, dicto)
20− self.assertTrue('url' in params)
21− self.assertTrue(query in params['url'])
22− self.assertTrue('bing.com' in params['url'])
23− self.assertTrue('SRCHHPGUSR' in params['cookies'])
24− self.assertTrue('OFF' in params['cookies']['SRCHHPGUSR'])
25− self.assertTrue('_EDGE_S' in params['cookies'])
26− self.assertTrue('fr-fr' in params['cookies']['_EDGE_S'])
27−
28− dicto['pageno'] = 2
29− dicto['time_range'] = 'day'
30− dicto['safesearch'] = 2
31− params = bing_videos.request(query, dicto)
32− self.assertTrue('first=29' in params['url'])
33− self.assertTrue('1440' in params['url'])
34− self.assertIn('SRCHHPGUSR', params['cookies'])
35− self.assertTrue('STRICT' in params['cookies']['SRCHHPGUSR'])
36−
37− def test_response(self):
38− self.assertRaises(AttributeError, bing_videos.response, None)
39− self.assertRaises(AttributeError, bing_videos.response, [])
40− self.assertRaises(AttributeError, bing_videos.response, '')
41− self.assertRaises(AttributeError, bing_videos.response, '[]')
42−
43− response = mock.Mock(text='<html></html>')
44− self.assertEqual(bing_videos.response(response), [])
45−
46− response = mock.Mock(text='<html></html>')
47− self.assertEqual(bing_videos.response(response), [])
48−
49− html = """
50− <div class="dg_u">
51− <div>
52− <a>
53− <div>
54− <div>
55− <div class="mc_vtvc_meta_block">
56− <div><span>100 views</span><span>1 year ago</span></div><div><span>ExampleTube</span><span>Channel 1<span></div> #noqa
57− </div>
58− </div>
59− <div class="vrhdata" vrhm='{"du":"01:11","murl":"https://www.example.com/watch?v=DEADBEEF","thid":"OVP.BINGTHUMB1","vt":"Title 1"}'></div> # noqa
60− </div>
61− </a>
62− </div>
63− </div>
64− """
65− response = mock.Mock(text=html)
66− results = bing_videos.response(response)
67− self.assertEqual(type(results), list)
68− self.assertEqual(len(results), 1)
69− self.assertEqual(results[0]['title'], 'Title 1')
70− self.assertEqual(results[0]['url'], 'https://www.example.com/watch?v=DEADBEEF')
71− self.assertEqual(results[0]['content'], '01:11 - 100 views - 1 year ago - ExampleTube - Channel 1')
72− self.assertEqual(results[0]['thumbnail'], 'https://www.bing.com/th?id=OVP.BINGTHUMB1')
tests/unit/engines/test_btdigg.pydeleted−112
@@ -1,112 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import btdigg
5−from searx.testing import SearxTestCase
6−
7−
8−class TestBtdiggEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 0
14− params = btdigg.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn(query, params['url'])
17− self.assertIn('btdig.com', params['url'])
18−
19− def test_response(self):
20− self.assertRaises(AttributeError, btdigg.response, None)
21− self.assertRaises(AttributeError, btdigg.response, [])
22− self.assertRaises(AttributeError, btdigg.response, '')
23− self.assertRaises(AttributeError, btdigg.response, '[]')
24−
25− response = mock.Mock(text='<html></html>')
26− self.assertEqual(btdigg.response(response), [])
27−
28− html = u"""
29− <div class="one_result" style="display:table-row;background-color:#e8e8e8">
30− <div style="display:table-cell;color:rgb(0, 0, 0)">
31− <div style="display:table">
32− <div style="display:table-row">
33− <div class="torrent_name" style="display:table-cell">
34− <a style="color:rgb(0, 0, 204);text-decoration:underline;font-size:150%"
35− href="http://btdig.com/a72f35b7ee3a10928f02bb799e40ae5db701ed1c/pdf?q=pdf&amp;p=1&amp;order=0"
36− >3.9GBdeLibrosByHuasoFromHell(3de4)</a>
37− </div>
38− </div>
39− </div>
40− <div style="display:table">
41− <div style="display:table-row">
42− <div style="display:table-cell">
43− <span class="torrent_files" style="color:#666;padding-left:10px">4217</span> files <span
44− class="torrent_size" style="color:#666;padding-left:10px">1 GB</span><span
45− class="torrent_age" style="color:rgb(0, 102, 0);padding-left:10px;margin: 0px 4px"
46− >found 3 years ago</span>
47− </div>
48− </div>
49− </div>
50− <div style="display:table;width:100%;padding:10px">
51− <div style="display:table-row">
52− <div class="torrent_magnet" style="display:table-cell">
53− <div class="fa fa-magnet" style="color:#cc0000">
54− <a href="magnet:?xt=urn:btih:a72f35b7ee3a10928f02bb799e40ae5db701ed1c&amp;dn=3.9GBdeLibrosBy..."
55− title="Download via magnet-link"> magnet:?xt=urn:btih:a72f35b7ee...</a>
56− </div>
57− </div>
58− <div style="display:table-cell;color:rgb(0, 0, 0);text-align:right">
59− <span style="color:rgb(136, 136, 136);margin: 0px 0px 0px 4px"></span><span
60− style="color:rgb(0, 102, 0);margin: 0px 4px">found 3 years ago</span>
61− </div>
62− </div>
63− </div>
64− <div class="torrent_excerpt" style="display:table;padding:10px;white-space:nowrap">
65− <div class="fa fa-folder-open" style="padding-left:0em"> 3.9GBdeLibrosByHuasoFromHell(3de4)</div><br/>
66− <div class="fa fa-folder-open" style="padding-left:1em"> Libros H-Z</div><br/>
67− <div class="fa fa-folder-open" style="padding-left:2em"> H</div><br/><div class="fa fa-file-archive-o"
68− style="padding-left:3em"> H.H. Hollis - El truco de la espada-<b
69− style="color:red; background-color:yellow">pdf</b>.zip</div><span
70− style="color:#666;padding-left:10px">17 KB</span><br/>
71− <div class="fa fa-file-archive-o" style="padding-left:3em"> Hagakure - El Libro del Samurai-<b
72− style="color:red; background-color:yellow">pdf</b>.zip</div><span
73− style="color:#666;padding-left:10px">95 KB</span><br/>
74− <div class="fa fa-folder-open" style="padding-left:3em"> Hamsun, Knut (1859-1952)</div><br/>
75− <div class="fa fa-file-archive-o" style="padding-left:4em"> Hamsun, Knut - Hambre-<b
76− style="color:red; background-color:yellow">pdf</b>.zip</div><span
77− style="color:#666;padding-left:10px">786 KB</span><br/>
78− <div class="fa fa-plus-circle"><a
79− href="http://btdig.com/a72f35b7ee3a10928f02bb799e40ae5db701ed1c/pdf?q=pdf&amp;p=1&amp;order=0"
80− > 4214 hidden files<span style="color:#666;padding-left:10px">1 GB</span></a></div>
81− </div>
82− </div>
83− </div>
84− """
85− response = mock.Mock(text=html.encode('utf-8'))
86− results = btdigg.response(response)
87− self.assertEqual(type(results), list)
88− self.assertEqual(len(results), 1)
89− self.assertEqual(results[0]['title'], '3.9GBdeLibrosByHuasoFromHell(3de4)')
90− self.assertEqual(results[0]['url'],
91− 'http://btdig.com/a72f35b7ee3a10928f02bb799e40ae5db701ed1c/pdf?q=pdf&p=1&order=0')
92− self.assertEqual(results[0]['content'],
93− '3.9GBdeLibrosByHuasoFromHell(3de4) | ' +
94− 'Libros H-Z | ' +
95− 'H H.H. Hollis - El truco de la espada-pdf.zip17 KB | ' +
96− 'Hagakure - El Libro del Samurai-pdf.zip95 KB | ' +
97− 'Hamsun, Knut (1859-1952) | Hamsun, Knut - Hambre-pdf.zip786 KB | ' +
98− '4214 hidden files1 GB')
99− self.assertEqual(results[0]['filesize'], 1 * 1024 * 1024 * 1024)
100− self.assertEqual(results[0]['files'], 4217)
101− self.assertEqual(results[0]['magnetlink'],
102− 'magnet:?xt=urn:btih:a72f35b7ee3a10928f02bb799e40ae5db701ed1c&dn=3.9GBdeLibrosBy...')
103−
104− html = """
105− <div style="display:table-row;background-color:#e8e8e8">
106−
107− </div>
108− """
109− response = mock.Mock(text=html.encode('utf-8'))
110− results = btdigg.response(response)
111− self.assertEqual(type(results), list)
112− self.assertEqual(len(results), 0)
tests/unit/engines/test_currency_convert.pydeleted−56
@@ -1,56 +0,0 @@
1−from collections import defaultdict
2−from datetime import datetime
3−import mock
4−from searx.engines import currency_convert
5−from searx.testing import SearxTestCase
6−
7−
8−class TestCurrencyConvertEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = b'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = currency_convert.request(query, dicto)
15− self.assertNotIn('url', params)
16−
17− query = b'convert 10 Pound Sterlings to United States Dollars'
18− params = currency_convert.request(query, dicto)
19− self.assertIn('url', params)
20− self.assertIn('duckduckgo.com', params['url'])
21− self.assertIn('GBP', params['url'])
22− self.assertIn('USD', params['url'])
23−
24− def test_response(self):
25− dicto = defaultdict(dict)
26− dicto['amount'] = float(10)
27− dicto['from'] = "GBP"
28− dicto['to'] = "USD"
29− dicto['from_name'] = "pound sterling"
30− dicto['to_name'] = "United States dollar"
31− response = mock.Mock(text='a,b,c,d', search_params=dicto)
32− self.assertEqual(currency_convert.response(response), [])
33− body = """ddg_spice_currency(
34− {
35− "conversion":{
36− "converted-amount": "0.5"
37− },
38− "topConversions":[
39− {
40− },
41− {
42− }
43− ]
44− }
45− );
46− """
47− response = mock.Mock(text=body, search_params=dicto)
48− results = currency_convert.response(response)
49− self.assertEqual(type(results), list)
50− self.assertEqual(len(results), 1)
51− self.assertEqual(results[0]['answer'], '10.0 GBP = 5.0 USD, 1 GBP (pound sterling)' +
52− ' = 0.5 USD (United States dollar)')
53−
54− target_url = 'https://duckduckgo.com/js/spice/currency/1/{}/{}'.format(
55− dicto['from'], dicto['to'])
56− self.assertEqual(results[0]['url'], target_url)
tests/unit/engines/test_dailymotion.pydeleted−112
@@ -1,112 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import dailymotion
5−from searx.testing import SearxTestCase
6−
7−
8−class TestDailymotionEngine(SearxTestCase):
9−
10− def test_request(self):
11− dailymotion.supported_languages = ['en', 'fr']
12− query = 'test_query'
13− dicto = defaultdict(dict)
14− dicto['pageno'] = 0
15− dicto['language'] = 'fr-FR'
16− params = dailymotion.request(query, dicto)
17− self.assertTrue('url' in params)
18− self.assertTrue(query in params['url'])
19− self.assertTrue('dailymotion.com' in params['url'])
20− self.assertTrue('fr' in params['url'])
21−
22− dicto['language'] = 'all'
23− params = dailymotion.request(query, dicto)
24− self.assertTrue('en' in params['url'])
25−
26− def test_response(self):
27− self.assertRaises(AttributeError, dailymotion.response, None)
28− self.assertRaises(AttributeError, dailymotion.response, [])
29− self.assertRaises(AttributeError, dailymotion.response, '')
30− self.assertRaises(AttributeError, dailymotion.response, '[]')
31−
32− response = mock.Mock(text='{}')
33− self.assertEqual(dailymotion.response(response), [])
34−
35− response = mock.Mock(text='{"data": []}')
36− self.assertEqual(dailymotion.response(response), [])
37−
38− json = """
39− {
40− "page": 1,
41− "limit": 5,
42− "explicit": false,
43− "total": 289487,
44− "has_more": true,
45− "list": [
46− {
47− "created_time": 1422173451,
48− "title": "Title",
49− "description": "Description",
50− "duration": 81,
51− "url": "http://www.url",
52− "thumbnail_360_url": "http://thumbnail",
53− "id": "x2fit7q"
54− }
55− ]
56− }
57− """
58− response = mock.Mock(text=json)
59− results = dailymotion.response(response)
60− self.assertEqual(type(results), list)
61− self.assertEqual(len(results), 1)
62− self.assertEqual(results[0]['title'], 'Title')
63− self.assertEqual(results[0]['url'], 'http://www.url')
64− self.assertEqual(results[0]['content'], 'Description')
65− self.assertIn('x2fit7q', results[0]['embedded'])
66−
67− json = r"""
68− {"toto":[
69− {"id":200,"name":"Artist Name",
70− "link":"http:\/\/www.dailymotion.com\/artist\/1217","type":"artist"}
71− ]}
72− """
73− response = mock.Mock(text=json)
74− results = dailymotion.response(response)
75− self.assertEqual(type(results), list)
76− self.assertEqual(len(results), 0)
77−
78− def test_fetch_supported_languages(self):
79− json = r"""
80− {"list":[{"code":"af","name":"Afrikaans","native_name":"Afrikaans",
81− "localized_name":"Afrikaans","display_name":"Afrikaans"},
82− {"code":"ar","name":"Arabic","native_name":"\u0627\u0644\u0639\u0631\u0628\u064a\u0629",
83− "localized_name":"Arabic","display_name":"Arabic"},
84− {"code":"la","name":"Latin","native_name":null,
85− "localized_name":"Latin","display_name":"Latin"}
86− ]}
87− """
88− response = mock.Mock(text=json)
89− languages = dailymotion._fetch_supported_languages(response)
90− self.assertEqual(type(languages), dict)
91− self.assertEqual(len(languages), 3)
92− self.assertIn('af', languages)
93− self.assertIn('ar', languages)
94− self.assertIn('la', languages)
95−
96− self.assertEqual(type(languages['af']), dict)
97− self.assertEqual(type(languages['ar']), dict)
98− self.assertEqual(type(languages['la']), dict)
99−
100− self.assertIn('name', languages['af'])
101− self.assertIn('name', languages['ar'])
102− self.assertNotIn('name', languages['la'])
103−
104− self.assertIn('english_name', languages['af'])
105− self.assertIn('english_name', languages['ar'])
106− self.assertIn('english_name', languages['la'])
107−
108− self.assertEqual(languages['af']['name'], 'Afrikaans')
109− self.assertEqual(languages['af']['english_name'], 'Afrikaans')
110− self.assertEqual(languages['ar']['name'], u'العربية')
111− self.assertEqual(languages['ar']['english_name'], 'Arabic')
112− self.assertEqual(languages['la']['english_name'], 'Latin')
tests/unit/engines/test_deezer.pydeleted−57
@@ -1,57 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import deezer
4−from searx.testing import SearxTestCase
5−
6−
7−class TestDeezerEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = deezer.request(query, dicto)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('deezer.com' in params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, deezer.response, None)
20− self.assertRaises(AttributeError, deezer.response, [])
21− self.assertRaises(AttributeError, deezer.response, '')
22− self.assertRaises(AttributeError, deezer.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(deezer.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(deezer.response(response), [])
29−
30− json = r"""
31− {"data":[
32− {"id":100, "title":"Title of track",
33− "link":"https:\/\/www.deezer.com\/track\/1094042","duration":232,
34− "artist":{"id":200,"name":"Artist Name",
35− "link":"https:\/\/www.deezer.com\/artist\/1217","type":"artist"},
36− "album":{"id":118106,"title":"Album Title","type":"album"},"type":"track"}
37− ]}
38− """
39− response = mock.Mock(text=json)
40− results = deezer.response(response)
41− self.assertEqual(type(results), list)
42− self.assertEqual(len(results), 1)
43− self.assertEqual(results[0]['title'], 'Title of track')
44− self.assertEqual(results[0]['url'], 'https://www.deezer.com/track/1094042')
45− self.assertEqual(results[0]['content'], 'Artist Name - Album Title - Title of track')
46− self.assertTrue('100' in results[0]['embedded'])
47−
48− json = r"""
49− {"data":[
50− {"id":200,"name":"Artist Name",
51− "link":"https:\/\/www.deezer.com\/artist\/1217","type":"artist"}
52− ]}
53− """
54− response = mock.Mock(text=json)
55− results = deezer.response(response)
56− self.assertEqual(type(results), list)
57− self.assertEqual(len(results), 0)
tests/unit/engines/test_deviantart.pydeleted−95
@@ -1,95 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import deviantart
4−from searx.testing import SearxTestCase
5−
6−
7−class TestDeviantartEngine(SearxTestCase):
8−
9− def test_request(self):
10− dicto = defaultdict(dict)
11− query = 'test_query'
12− dicto['pageno'] = 0
13− dicto['time_range'] = ''
14− params = deviantart.request(query, dicto)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertTrue('deviantart.com' in params['url'])
18−
19− def test_no_url_in_request_year_time_range(self):
20− dicto = defaultdict(dict)
21− query = 'test_query'
22− dicto['time_range'] = 'year'
23− params = deviantart.request(query, dicto)
24− self.assertEqual({}, params['url'])
25−
26− def test_response(self):
27− self.assertRaises(AttributeError, deviantart.response, None)
28− self.assertRaises(AttributeError, deviantart.response, [])
29− self.assertRaises(AttributeError, deviantart.response, '')
30− self.assertRaises(AttributeError, deviantart.response, '[]')
31−
32− response = mock.Mock(text='<html></html>')
33− self.assertEqual(deviantart.response(response), [])
34−
35− response = mock.Mock(status_code=302)
36− self.assertEqual(deviantart.response(response), [])
37−
38− html = """
39− <div id="page-1-results" class="page-results results-page-thumb torpedo-container">
40− <span class="thumb wide" href="http://amai911.deviantart.com/art/Horse-195212845"
41− data-super-full-width="900" data-super-full-height="600">
42− <a class="torpedo-thumb-link" href="https://url.of.image">
43− <img data-sigil="torpedo-img" src="https://url.of.thumbnail" />
44− </a>
45− <span class="info"><span class="title-wrap"><span class="title">Title of image</span></span>
46− </div>
47− """
48− response = mock.Mock(text=html)
49− results = deviantart.response(response)
50− self.assertEqual(type(results), list)
51− self.assertEqual(len(results), 1)
52− self.assertEqual(results[0]['title'], 'Title of image')
53− self.assertEqual(results[0]['url'], 'https://url.of.image')
54− self.assertNotIn('content', results[0])
55− self.assertEqual(results[0]['thumbnail_src'], 'https://url.of.thumbnail')
56−
57− html = """
58− <span class="tt-fh-tc" style="width: 202px;">
59− <span class="tt-bb" style="width: 202px;">
60− </span>
61− <span class="shadow">
62− <a class="thumb" href="http://url.of.result/2nd.part.of.url"
63− title="Behoimi BE Animation Test by test-0, Jan 4,
64− 2010 in Digital Art &gt; Animation"> <i></i>
65− <img width="200" height="200" alt="Test"
66− src="http://url.of.thumbnail" data-src="http://th08.deviantart.net/test.jpg">
67− </a>
68− </span>
69− <!-- ^TTT -->
70− </span>
71− <span class="details">
72− <a href="http://test-0.deviantart.com/art/Test" class="t"
73− title="Behoimi BE Animation Test by test-0, Jan 4, 2010">
74− <span class="tt-fh-oe">Title of image</span> </a>
75− <small>
76− <span class="category">
77− <span class="age">
78− 5 years ago
79− </span>
80− in <a title="Behoimi BE Animation Test by test-0, Jan 4, 2010"
81− href="http://www.deviantart.com/browse/all/digitalart/animation/">Animation</a>
82− </span>
83− <div class="commentcount">
84− <a href="http://test-0.deviantart.com/art/Test#comments">
85− <span class="iconcommentsstats"></span>9 Comments</a>
86− </div>
87− <a class="mlt-link" href="http://www.deviantart.com/morelikethis/149167425">
88− <span class="mlt-icon"></span> <span class="mlt-text">More Like This</span> </a>
89− </span>
90− </small> <!-- TTT$ -->
91− """
92− response = mock.Mock(text=html)
93− results = deviantart.response(response)
94− self.assertEqual(type(results), list)
95− self.assertEqual(len(results), 0)
tests/unit/engines/test_digbt.pydeleted−61
@@ -1,61 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import digbt
4−from searx.testing import SearxTestCase
5−
6−
7−class TestDigBTEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = digbt.request(query, dicto)
14− self.assertIn('url', params)
15− self.assertIn(query, params['url'])
16− self.assertIn('digbt.org', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, digbt.response, None)
20− self.assertRaises(AttributeError, digbt.response, [])
21− self.assertRaises(AttributeError, digbt.response, '')
22− self.assertRaises(AttributeError, digbt.response, '[]')
23−
24− response = mock.Mock(text='<html></html>')
25− self.assertEqual(digbt.response(response), [])
26−
27− html = """
28− <table class="table">
29− <tr><td class="x-item">
30− <div>
31− <a title="The Big Bang Theory" class="title" href="/The-Big-Bang-Theory-d2.html">
32− The Big <span class="highlight">Bang</span> Theory
33− </a>
34− <span class="ctime"><span style="color:red;">4 hours ago</span></span>
35− </div>
36− <div class="files">
37− <ul>
38− <li>The Big Bang Theory 2.9 GB</li>
39− <li>....</li>
40− </ul>
41− </div>
42− <div class="tail">
43− Files: 1 Size: 2.9 GB Downloads: 1 Updated: <span style="color:red;">4 hours ago</span>
44− &nbsp; &nbsp;
45− <a class="title" href="magnet:?xt=urn:btih:a&amp;dn=The+Big+Bang+Theory">
46− <span class="glyphicon glyphicon-magnet"></span> magnet-link
47− </a>
48− &nbsp; &nbsp;
49− </div>
50− </td></tr>
51− </table>
52− """
53− response = mock.Mock(text=html.encode('utf-8'))
54− results = digbt.response(response)
55− self.assertEqual(type(results), list)
56− self.assertEqual(len(results), 1)
57− self.assertEqual(results[0]['title'], 'The Big Bang Theory')
58− self.assertEqual(results[0]['url'], 'https://digbt.org/The-Big-Bang-Theory-d2.html')
59− self.assertEqual(results[0]['content'], 'The Big Bang Theory 2.9 GB ....')
60− self.assertEqual(results[0]['filesize'], 3113851289)
61− self.assertEqual(results[0]['magnetlink'], 'magnet:?xt=urn:btih:a&dn=The+Big+Bang+Theory')
tests/unit/engines/test_digg.pydeleted−101
@@ -1,101 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import digg
4−from searx.testing import SearxTestCase
5−
6−
7−class TestDiggEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 1
13− params = digg.request(query, dicto)
14− self.assertIn('url', params)
15− self.assertIn(query, params['url'])
16− self.assertIn('digg.com', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, digg.response, None)
20− self.assertRaises(AttributeError, digg.response, [])
21− self.assertRaises(AttributeError, digg.response, '')
22− self.assertRaises(AttributeError, digg.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(digg.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(digg.response(response), [])
29−
30− json = """
31− {
32− "status": "ok",
33− "num": 10,
34− "next_position": 20,
35− "html": "<article itemscope itemtype=\\"http://schema.org/Article\\"
36− class=\\"story-container digg-story-el hentry entry story-1sRANah col-1\\"
37− data-content-id=\\"1sRANah\\" data-contenturl=\\"http://url.of.link\\"
38− data-position=\\"0\\" data-diggs=\\"24\\" data-tweets=\\"69\\"
39− data-digg-score=\\"1190\\"> <div class=\\"story-image story-image-thumb\\">
40− <a data-position=\\"0\\" data-content-id=\\"1sRANah\\"
41− class=\\"story-link\\" href=\\"http://www.thedailybeast.com/\\"
42− target=\\"_blank\\"><img class=\\"story-image-img\\"
43− src=\\"http://url.of.image.jpeg\\" width=\\"312\\" height=\\"170\\"
44− alt=\\"\\" /> </a> </div> <div class=\\"story-content\\"><header
45− class=\\"story-header\\"> <div itemprop=\\"alternativeHeadline\\"
46− class=\\"story-kicker\\" >Kicker</div> <h2 itemprop=\\"headline\\"
47− class=\\"story-title entry-title\\"><a class=\\"story-title-link story-link\\"
48− rel=\\"bookmark\\" itemprop=\\"url\\" href=\\"http://www.thedailybeast.com/\\"
49− target=\\"_blank\\">Title of article</h2> <div class=\\"story-meta\\">
50− <div class=\\"story-score \\">
51− <div class=\\"story-score-diggscore diggscore-1sRANah\\">1190</div>
52− <div class=\\"story-score-details\\"> <div class=\\"arrow\\"></div>
53− <ul class=\\"story-score-details-list\\"> <li
54− class=\\"story-score-detail story-score-diggs\\"><span
55− class=\\"label\\">Diggs:</span> <span class=\\"count diggs-1sRANah\\">24</span>
56− </li> <li class=\\"story-score-detail story-score-twitter\\"><span
57− class=\\"label\\">Tweets:</span> <span class=\\"count tweets-1sRANah\\">69</span>
58− </li> <li class=\\"story-score-detail story-score-facebook\\"><span
59− class=\\"label\\">Facebook Shares:</span> <span
60− class=\\"count fb_shares-1sRANah\\">1097</span></li> </ul> </div> </div>
61− <span class=\\"story-meta-item story-source\\"> <a
62− itemprop=\\"publisher copyrightHolder sourceOrganization provider\\"
63− class=\\"story-meta-item-link story-source-link\\"
64− href=\\"/source/thedailybeast.com\\">The Daily Beast </a> </span>
65− <span class=\\"story-meta-item story-tag first-tag\\"> <a
66− itemprop=\\"keywords\\" rel=\\"tag\\"
67− class=\\"story-meta-item-link story-tag-link\\" href=\\"/tag/news\\">News</a>
68− </span> <abbr class=\\"published story-meta-item story-timestamp\\"
69− title=\\"2014-10-18 14:53:45\\"> <time datetime=\\"2014-10-18 14:53:45\\">18 Oct 2014</time>
70− </abbr> </div> </header> </div> <ul class=\\"story-actions\\"> <li
71− class=\\"story-action story-action-digg btn-story-action-container\\">
72− <a class=\\"target digg-1sRANah\\" href=\\"#\\">Digg</a></li> <li
73− class=\\"story-action story-action-save btn-story-action-container\\">
74− <a class=\\"target save-1sRANah\\" href=\\"#\\">Save</a></li> <li
75− class=\\"story-action story-action-share\\"><a
76− class=\\"target share-facebook\\" href=\\"https://www.facebook.com/\\">Facebook</a></li>
77− <li class=\\"story-action story-action-share\\"><a class=\\"target share-twitter\\"
78− href=\\"https://twitter.com/\\">Twitter</a></li> </ul> </article>"
79− }
80− """
81− json = json.replace('\r\n', '').replace('\n', '').replace('\r', '')
82− response = mock.Mock(text=json)
83− results = digg.response(response)
84− self.assertEqual(type(results), list)
85− self.assertEqual(len(results), 1)
86− self.assertEqual(results[0]['title'], 'Title of article')
87− self.assertEqual(results[0]['url'], 'http://url.of.link')
88− self.assertEqual(results[0]['thumbnail'], 'http://url.of.image.jpeg')
89− self.assertEqual(results[0]['content'], '')
90−
91− json = """
92− {
93− "status": "error",
94− "num": 10,
95− "next_position": 20
96− }
97− """
98− response = mock.Mock(text=json)
99− results = digg.response(response)
100− self.assertEqual(type(results), list)
101− self.assertEqual(len(results), 0)
tests/unit/engines/test_doku.pydeleted−79
@@ -1,79 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import doku
5−from searx.testing import SearxTestCase
6−
7−
8−class TestDokuEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− params = doku.request(query, dicto)
14− self.assertIn('url', params)
15− self.assertIn(query, params['url'])
16−
17− def test_response(self):
18− self.assertRaises(AttributeError, doku.response, None)
19− self.assertRaises(AttributeError, doku.response, [])
20− self.assertRaises(AttributeError, doku.response, '')
21− self.assertRaises(AttributeError, doku.response, '[]')
22−
23− response = mock.Mock(text='<html></html>')
24− self.assertEqual(doku.response(response), [])
25−
26− html = u"""
27− <div class="search_quickresult">
28− <h3>Pages trouvées :</h3>
29− <ul class="search_quickhits">
30− <li> <a href="/xfconf-query" class="wikilink1" title="xfconf-query">xfconf-query</a></li>
31− </ul>
32− <div class="clearer"></div>
33− </div>
34− """
35− response = mock.Mock(text=html)
36− results = doku.response(response)
37− expected = [{'content': '', 'title': 'xfconf-query', 'url': 'http://localhost:8090/xfconf-query'}]
38− self.assertEqual(doku.response(response), expected)
39−
40− html = u"""
41− <dl class="search_results">
42− <dt><a href="/xvnc?s[]=query" class="wikilink1" title="xvnc">xvnc</a>: 40 Occurrences trouvées</dt>
43− <dd>er = /usr/bin/Xvnc
44− server_args = -inetd -<strong class="search_hit">query</strong> localhost -geometry 640x480 ... er = /usr/bin/Xvnc
45− server_args = -inetd -<strong class="search_hit">query</strong> localhost -geometry 800x600 ... er = /usr/bin/Xvnc
46− server_args = -inetd -<strong class="search_hit">query</strong> localhost -geometry 1024x768 ... er = /usr/bin/Xvnc
47− server_args = -inetd -<strong class="search_hit">query</strong> localhost -geometry 1280x1024 -depth 8 -Sec</dd>
48− <dt><a href="/postfix_mysql_tls_sasl_1404?s[]=query"
49− class="wikilink1"
50− title="postfix_mysql_tls_sasl_1404">postfix_mysql_tls_sasl_1404</a>: 14 Occurrences trouvées</dt>
51− <dd>tdepasse
52− hosts = 127.0.0.1
53− dbname = postfix
54− <strong class="search_hit">query</strong> = SELECT goto FROM alias WHERE address='%s' AND a... tdepasse
55− hosts = 127.0.0.1
56− dbname = postfix
57− <strong class="search_hit">query</strong> = SELECT domain FROM domain WHERE domain='%s'
58− #optional <strong class="search_hit">query</strong> to use when relaying for backup MX
59− #<strong class="search_hit">query</strong> = SELECT domain FROM domain WHERE domain='%s' and backupmx =</dd>
60− <dt><a href="/bind9?s[]=query" class="wikilink1" title="bind9">bind9</a>: 12 Occurrences trouvées</dt>
61− <dd> printcmd
62−;; Got answer:
63−;; -&gt;&gt;HEADER&lt;&lt;- opcode: <strong class="search_hit">QUERY</strong>, status: NOERROR, id: 13427
64−;; flags: qr aa rd ra; <strong class="search_hit">QUERY</strong>: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1
65−
66−[...]
67−
68−;; <strong class="search_hit">Query</strong> time: 1 msec
69−;; SERVER: 127.0.0.1#53(127.0.0.1)
70−;... par la requête (<strong class="search_hit">Query</strong> time) , entre la première et la deuxième requête.</dd>
71− </dl>
72− """
73− response = mock.Mock(text=html)
74− results = doku.response(response)
75− self.assertEqual(type(results), list)
76− self.assertEqual(len(results), 3)
77− self.assertEqual(results[0]['title'], 'xvnc')
78−# FIXME self.assertEqual(results[0]['url'], u'http://this.should.be.the.link/ű')
79−# FIXME self.assertEqual(results[0]['content'], 'This should be the content.')
tests/unit/engines/test_duckduckgo.pydeleted−106
@@ -1,106 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import load_engine, duckduckgo
5−from searx.testing import SearxTestCase
6−
7−
8−class TestDuckduckgoEngine(SearxTestCase):
9−
10− def test_request(self):
11− duckduckgo = load_engine({'engine': 'duckduckgo', 'name': 'duckduckgo'})
12−
13− query = 'test_query'
14− dicto = defaultdict(dict)
15− dicto['pageno'] = 1
16− dicto['time_range'] = ''
17−
18− dicto['language'] = 'de-CH'
19− params = duckduckgo.request(query, dicto)
20− self.assertIn('url', params)
21− self.assertIn(query, params['url'])
22− self.assertIn('duckduckgo.com', params['url'])
23− self.assertIn('ch-de', params['url'])
24− self.assertIn('s=0', params['url'])
25−
26− # when ddg uses non standard codes
27− dicto['language'] = 'zh-HK'
28− params = duckduckgo.request(query, dicto)
29− self.assertIn('hk-tzh', params['url'])
30−
31− dicto['language'] = 'en-GB'
32− params = duckduckgo.request(query, dicto)
33− self.assertIn('uk-en', params['url'])
34−
35− # no country given
36− dicto['language'] = 'en'
37− params = duckduckgo.request(query, dicto)
38− self.assertIn('us-en', params['url'])
39−
40− def test_no_url_in_request_year_time_range(self):
41− dicto = defaultdict(dict)
42− query = 'test_query'
43− dicto['time_range'] = 'year'
44− params = duckduckgo.request(query, dicto)
45− self.assertEqual({}, params['url'])
46−
47− def test_response(self):
48− self.assertRaises(AttributeError, duckduckgo.response, None)
49− self.assertRaises(AttributeError, duckduckgo.response, [])
50− self.assertRaises(AttributeError, duckduckgo.response, '')
51− self.assertRaises(AttributeError, duckduckgo.response, '[]')
52−
53− response = mock.Mock(text='<html></html>')
54− self.assertEqual(duckduckgo.response(response), [])
55−
56− html = u"""
57− <div class="result results_links results_links_deep web-result result--no-result">
58− <div class="links_main links_deep result__body">
59− <h2 class="result__title">
60− </h2>
61− <div class="no-results">No results</div>
62− <div class="result__extras">
63− </div>
64− </div>
65− </div>
66− """
67− response = mock.Mock(text=html)
68− results = duckduckgo.response(response)
69− self.assertEqual(duckduckgo.response(response), [])
70−
71− html = u"""
72− <div class="result results_links results_links_deep web-result ">
73− <div class="links_main links_deep result__body">
74− <h2 class="result__title">
75− <a rel="nofollow" class="result__a" href="http://this.should.be.the.link/ű">
76− This <b>is</b> <b>the</b> title
77− </a>
78− </h2>
79− <a class="result__snippet" href="http://this.should.be.the.link/ű">
80− <b>This</b> should be the content.
81− </a>
82− <div class="result__extras">
83− </div>
84− </div>
85− </div>
86− """
87− response = mock.Mock(text=html)
88− results = duckduckgo.response(response)
89− self.assertEqual(type(results), list)
90− self.assertEqual(len(results), 1)
91− self.assertEqual(results[0]['title'], 'This is the title')
92− self.assertEqual(results[0]['url'], u'http://this.should.be.the.link/ű')
93− self.assertEqual(results[0]['content'], 'This should be the content.')
94−
95− def test_fetch_supported_languages(self):
96− js = """some code...regions:{
97− "wt-wt":"All Results","ar-es":"Argentina","au-en":"Australia","at-de":"Austria","be-fr":"Belgium (fr)"
98− }some more code..."""
99− response = mock.Mock(text=js)
100− languages = list(duckduckgo._fetch_supported_languages(response))
101− self.assertEqual(len(languages), 5)
102− self.assertIn('wt-WT', languages)
103− self.assertIn('es-AR', languages)
104− self.assertIn('en-AU', languages)
105− self.assertIn('de-AT', languages)
106− self.assertIn('fr-BE', languages)
tests/unit/engines/test_duckduckgo_definitions.pydeleted−255
@@ -1,255 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import duckduckgo_definitions
4−from searx.testing import SearxTestCase
5−
6−
7−class TestDDGDefinitionsEngine(SearxTestCase):
8−
9− def test_result_to_text(self):
10− url = ''
11− text = 'Text'
12− html_result = 'Html'
13− result = duckduckgo_definitions.result_to_text(url, text, html_result)
14− self.assertEqual(result, text)
15−
16− html_result = '<a href="url">Text in link</a>'
17− result = duckduckgo_definitions.result_to_text(url, text, html_result)
18− self.assertEqual(result, 'Text in link')
19−
20− def test_request(self):
21− duckduckgo_definitions.supported_languages = ['en-US', 'es-ES']
22− query = 'test_query'
23− dicto = defaultdict(dict)
24− dicto['pageno'] = 1
25− dicto['language'] = 'es'
26− params = duckduckgo_definitions.request(query, dicto)
27− self.assertIn('url', params)
28− self.assertIn(query, params['url'])
29− self.assertIn('duckduckgo.com', params['url'])
30− self.assertIn('headers', params)
31− self.assertIn('Accept-Language', params['headers'])
32− self.assertIn('es', params['headers']['Accept-Language'])
33−
34− def test_response(self):
35− self.assertRaises(AttributeError, duckduckgo_definitions.response, None)
36− self.assertRaises(AttributeError, duckduckgo_definitions.response, [])
37− self.assertRaises(AttributeError, duckduckgo_definitions.response, '')
38− self.assertRaises(AttributeError, duckduckgo_definitions.response, '[]')
39−
40− response = mock.Mock(text='{}')
41− self.assertEqual(duckduckgo_definitions.response(response), [])
42−
43− response = mock.Mock(text='{"data": []}')
44− self.assertEqual(duckduckgo_definitions.response(response), [])
45−
46− json = """
47− {
48− "DefinitionSource": "definition source",
49− "Heading": "heading",
50− "ImageWidth": 0,
51− "RelatedTopics": [
52− {
53− "Result": "Top-level domains",
54− "Icon": {
55− "URL": "",
56− "Height": "",
57− "Width": ""
58− },
59− "FirstURL": "https://first.url",
60− "Text": "text"
61− },
62− {
63− "Topics": [
64− {
65− "Result": "result topic",
66− "Icon": {
67− "URL": "",
68− "Height": "",
69− "Width": ""
70− },
71− "FirstURL": "https://duckduckgo.com/?q=2%2F2",
72− "Text": "result topic text"
73− }
74− ],
75− "Name": "name"
76− }
77− ],
78− "Entity": "Entity",
79− "Type": "A",
80− "Redirect": "",
81− "DefinitionURL": "http://definition.url",
82− "AbstractURL": "https://abstract.url",
83− "Definition": "this is the definition",
84− "AbstractSource": "abstract source",
85− "Infobox": {
86− "content": [
87− {
88− "data_type": "string",
89− "value": "1999",
90− "label": "Introduced",
91− "wiki_order": 0
92− }
93− ],
94− "meta": [
95− {
96− "data_type": "string",
97− "value": ".test",
98− "label": "article_title"
99− }
100− ]
101− },
102− "Image": "image.png",
103− "ImageIsLogo": 0,
104− "Abstract": "abstract",
105− "AbstractText": "abstract text",
106− "AnswerType": "",
107− "ImageHeight": 0,
108− "Results": [{
109− "Result" : "result title",
110− "Icon" : {
111− "URL" : "result url",
112− "Height" : 16,
113− "Width" : 16
114− },
115− "FirstURL" : "result first url",
116− "Text" : "result text"
117− }
118− ],
119− "Answer": "answer"
120− }
121− """
122− response = mock.Mock(text=json)
123− results = duckduckgo_definitions.response(response)
124− self.assertEqual(type(results), list)
125− self.assertEqual(len(results), 4)
126− self.assertEqual(results[0]['answer'], 'answer')
127− self.assertEqual(results[1]['title'], 'heading')
128− self.assertEqual(results[1]['url'], 'result first url')
129− self.assertEqual(results[2]['suggestion'], 'text')
130− self.assertEqual(results[3]['infobox'], 'heading')
131− self.assertEqual(results[3]['id'], 'https://definition.url')
132− self.assertEqual(results[3]['entity'], 'Entity')
133− self.assertIn('abstract', results[3]['content'])
134− self.assertIn('this is the definition', results[3]['content'])
135− self.assertEqual(results[3]['img_src'], 'image.png')
136− self.assertIn('Introduced', results[3]['attributes'][0]['label'])
137− self.assertIn('1999', results[3]['attributes'][0]['value'])
138− self.assertIn({'url': 'https://abstract.url', 'title': 'abstract source'}, results[3]['urls'])
139− self.assertIn({'url': 'http://definition.url', 'title': 'definition source'}, results[3]['urls'])
140− self.assertIn({'name': 'name', 'suggestions': ['result topic text']}, results[3]['relatedTopics'])
141−
142− json = """
143− {
144− "DefinitionSource": "definition source",
145− "Heading": "heading",
146− "ImageWidth": 0,
147− "RelatedTopics": [],
148− "Entity": "Entity",
149− "Type": "A",
150− "Redirect": "",
151− "DefinitionURL": "",
152− "AbstractURL": "https://abstract.url",
153− "Definition": "",
154− "AbstractSource": "abstract source",
155− "Image": "",
156− "ImageIsLogo": 0,
157− "Abstract": "",
158− "AbstractText": "abstract text",
159− "AnswerType": "",
160− "ImageHeight": 0,
161− "Results": [],
162− "Answer": ""
163− }
164− """
165− response = mock.Mock(text=json)
166− results = duckduckgo_definitions.response(response)
167− self.assertEqual(type(results), list)
168− self.assertEqual(len(results), 1)
169− self.assertEqual(results[0]['url'], 'https://abstract.url')
170− self.assertEqual(results[0]['title'], 'heading')
171− self.assertEqual(results[0]['content'], '')
172−
173− json = """
174− {
175− "DefinitionSource": "definition source",
176− "Heading": "heading",
177− "ImageWidth": 0,
178− "RelatedTopics": [
179− {
180− "Result": "Top-level domains",
181− "Icon": {
182− "URL": "",
183− "Height": "",
184− "Width": ""
185− },
186− "FirstURL": "https://first.url",
187− "Text": "heading"
188− },
189− {
190− "Name": "name"
191− },
192− {
193− "Topics": [
194− {
195− "Result": "result topic",
196− "Icon": {
197− "URL": "",
198− "Height": "",
199− "Width": ""
200− },
201− "FirstURL": "https://duckduckgo.com/?q=2%2F2",
202− "Text": "heading"
203− }
204− ],
205− "Name": "name"
206− }
207− ],
208− "Entity": "Entity",
209− "Type": "A",
210− "Redirect": "",
211− "DefinitionURL": "http://definition.url",
212− "AbstractURL": "https://abstract.url",
213− "Definition": "this is the definition",
214− "AbstractSource": "abstract source",
215− "Infobox": {
216− "meta": [
217− {
218− "data_type": "string",
219− "value": ".test",
220− "label": "article_title"
221− }
222− ]
223− },
224− "Image": "image.png",
225− "ImageIsLogo": 0,
226− "Abstract": "abstract",
227− "AbstractText": "abstract text",
228− "AnswerType": "",
229− "ImageHeight": 0,
230− "Results": [{
231− "Result" : "result title",
232− "Icon" : {
233− "URL" : "result url",
234− "Height" : 16,
235− "Width" : 16
236− },
237− "Text" : "result text"
238− }
239− ],
240− "Answer": ""
241− }
242− """
243− response = mock.Mock(text=json)
244− results = duckduckgo_definitions.response(response)
245− self.assertEqual(type(results), list)
246− self.assertEqual(len(results), 1)
247− self.assertEqual(results[0]['infobox'], 'heading')
248− self.assertEqual(results[0]['id'], 'https://definition.url')
249− self.assertEqual(results[0]['entity'], 'Entity')
250− self.assertIn('abstract', results[0]['content'])
251− self.assertIn('this is the definition', results[0]['content'])
252− self.assertEqual(results[0]['img_src'], 'image.png')
253− self.assertIn({'url': 'https://abstract.url', 'title': 'abstract source'}, results[0]['urls'])
254− self.assertIn({'url': 'http://definition.url', 'title': 'definition source'}, results[0]['urls'])
255− self.assertIn({'name': 'name', 'suggestions': []}, results[0]['relatedTopics'])
tests/unit/engines/test_duckduckgo_images.pydeleted−75
@@ -1,75 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import duckduckgo_images
5−from searx.testing import SearxTestCase
6−
7−
8−class TestDuckduckgoImagesEngine(SearxTestCase):
9−
10− def test_request(self):
11− duckduckgo_images.supported_languages = ['de-CH', 'en-US']
12− query = 'test_query'
13− dicto = defaultdict(dict)
14− dicto['is_test'] = True
15− dicto['pageno'] = 1
16− dicto['safesearch'] = 0
17− dicto['language'] = 'all'
18− params = duckduckgo_images.request(query, dicto)
19− self.assertIn('url', params)
20− self.assertIn(query, params['url'])
21− self.assertIn('duckduckgo.com', params['url'])
22− self.assertIn('s=0', params['url'])
23− self.assertIn('p=-1', params['url'])
24− self.assertIn('vqd=12345', params['url'])
25−
26− # test paging, safe search and language
27− dicto['pageno'] = 2
28− dicto['safesearch'] = 2
29− dicto['language'] = 'de'
30− params = duckduckgo_images.request(query, dicto)
31− self.assertIn('url', params)
32− self.assertIn(query, params['url'])
33− self.assertIn('s=50', params['url'])
34− self.assertIn('p=1', params['url'])
35− self.assertIn('ch-de', params['url'])
36−
37− def test_response(self):
38− self.assertRaises(AttributeError, duckduckgo_images.response, None)
39− self.assertRaises(AttributeError, duckduckgo_images.response, [])
40− self.assertRaises(AttributeError, duckduckgo_images.response, '')
41− self.assertRaises(AttributeError, duckduckgo_images.response, '[]')
42−
43− response = mock.Mock(text='If this error persists, please let us know: ops@duckduckgo.com')
44− self.assertRaises(Exception, duckduckgo_images.response, response)
45−
46− json = u"""
47− {
48− "query": "test_query",
49− "results": [
50− {
51− "title": "Result 1",
52− "url": "https://site1.url",
53− "thumbnail": "https://thumb1.nail",
54− "image": "https://image1"
55− },
56− {
57− "title": "Result 2",
58− "url": "https://site2.url",
59− "thumbnail": "https://thumb2.nail",
60− "image": "https://image2"
61− }
62− ]
63− }
64− """
65− response = mock.Mock(text=json)
66− results = duckduckgo_images.response(response)
67− self.assertEqual(len(results), 2)
68− self.assertEqual(results[0]['title'], 'Result 1')
69− self.assertEqual(results[0]['url'], 'https://site1.url')
70− self.assertEqual(results[0]['thumbnail_src'], 'https://thumb1.nail')
71− self.assertEqual(results[0]['img_src'], 'https://image1')
72− self.assertEqual(results[1]['title'], 'Result 2')
73− self.assertEqual(results[1]['url'], 'https://site2.url')
74− self.assertEqual(results[1]['thumbnail_src'], 'https://thumb2.nail')
75− self.assertEqual(results[1]['img_src'], 'https://image2')
tests/unit/engines/test_duden.pydeleted−47
@@ -1,47 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import duden
4−from searx.testing import SearxTestCase
5−from datetime import datetime
6−
7−
8−class TestDudenEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'Haus'
12− dic = defaultdict(dict)
13− data = [
14− [1, 'https://www.duden.de/suchen/dudenonline/Haus'],
15− [2, 'https://www.duden.de/suchen/dudenonline/Haus?search_api_fulltext=&page=1']
16− ]
17− for page_no, exp_res in data:
18− dic['pageno'] = page_no
19− params = duden.request(query, dic)
20− self.assertTrue('url' in params)
21− self.assertTrue(query in params['url'])
22− self.assertTrue('duden.de' in params['url'])
23− self.assertEqual(params['url'], exp_res)
24−
25− def test_response(self):
26− resp = mock.Mock(text='<html></html>')
27− self.assertEqual(duden.response(resp), [])
28−
29− html = """
30− <section class="vignette">
31− <h2"> <a href="/rechtschreibung/Haus">
32− <strong>This is the title also here</strong>
33− </a> </h2>
34− <p>This is the content</p>
35− </section>
36− """
37− resp = mock.Mock(text=html)
38− results = duden.response(resp)
39−
40− self.assertEqual(len(results), 1)
41− self.assertEqual(type(results), list)
42−
43− # testing result (dictionary entry)
44− r = results[0]
45− self.assertEqual(r['url'], 'https://www.duden.de/rechtschreibung/Haus')
46− self.assertEqual(r['title'], 'This is the title also here')
47− self.assertEqual(r['content'], 'This is the content')
tests/unit/engines/test_dummy.pydeleted−26
@@ -1,26 +0,0 @@
1−from searx.engines import dummy
2−from searx.testing import SearxTestCase
3−
4−
5−class TestDummyEngine(SearxTestCase):
6−
7− def test_request(self):
8− test_params = [
9− [1, 2, 3],
10− ['a'],
11− [],
12− 1
13− ]
14− for params in test_params:
15− self.assertEqual(dummy.request(None, params), params)
16−
17− def test_response(self):
18− responses = [
19− None,
20− [],
21− True,
22− dict(),
23− tuple()
24− ]
25− for response in responses:
26− self.assertEqual(dummy.response(response), [])
tests/unit/engines/test_faroo.pydeleted−113
@@ -1,113 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import faroo
5−from searx.testing import SearxTestCase
6−
7−
8−class TestFarooEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− dicto['language'] = 'fr-FR'
15− dicto['category'] = 'general'
16− params = faroo.request(query, dicto)
17− self.assertIn('url', params)
18− self.assertIn(query, params['url'])
19− self.assertIn('faroo.com', params['url'])
20− self.assertIn('en', params['url'])
21− self.assertIn('web', params['url'])
22−
23− dicto['language'] = 'all'
24− params = faroo.request(query, dicto)
25− self.assertIn('en', params['url'])
26−
27− dicto['language'] = 'de-DE'
28− params = faroo.request(query, dicto)
29− self.assertIn('de', params['url'])
30−
31− def test_response(self):
32− self.assertRaises(AttributeError, faroo.response, None)
33− self.assertRaises(AttributeError, faroo.response, [])
34− self.assertRaises(AttributeError, faroo.response, '')
35− self.assertRaises(AttributeError, faroo.response, '[]')
36−
37− response = mock.Mock(text='{}')
38− self.assertEqual(faroo.response(response), [])
39−
40− response = mock.Mock(text='{"data": []}')
41− self.assertEqual(faroo.response(response), [])
42−
43− response = mock.Mock(text='{"data": []}', status_code=429)
44− self.assertRaises(Exception, faroo.response, response)
45−
46− json = """
47− {
48− "results": [
49− {
50− "title": "This is the title",
51− "kwic": "This is the content",
52− "content": "",
53− "url": "http://this.is.the.url/",
54− "iurl": "",
55− "domain": "css3test.com",
56− "author": "Jim Dalrymple",
57− "news": true,
58− "votes": "10",
59− "date": 1360622563000,
60− "related": []
61− },
62− {
63− "title": "This is the title2",
64− "kwic": "This is the content2",
65− "content": "",
66− "url": "http://this.is.the.url2/",
67− "iurl": "",
68− "domain": "css3test.com",
69− "author": "Jim Dalrymple",
70− "news": false,
71− "votes": "10",
72− "related": []
73− },
74− {
75− "title": "This is the title3",
76− "kwic": "This is the content3",
77− "content": "",
78− "url": "http://this.is.the.url3/",
79− "iurl": "http://upload.wikimedia.org/optimized.jpg",
80− "domain": "css3test.com",
81− "author": "Jim Dalrymple",
82− "news": false,
83− "votes": "10",
84− "related": []
85− }
86− ],
87− "query": "test",
88− "suggestions": [],
89− "count": 100,
90− "start": 1,
91− "length": 10,
92− "time": "15"
93− }
94− """
95− response = mock.Mock(text=json)
96− results = faroo.response(response)
97− self.assertEqual(type(results), list)
98− self.assertEqual(len(results), 3)
99− self.assertEqual(results[0]['title'], 'This is the title')
100− self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
101− self.assertEqual(results[0]['content'], 'This is the content')
102− self.assertEqual(results[1]['title'], 'This is the title2')
103− self.assertEqual(results[1]['url'], 'http://this.is.the.url2/')
104− self.assertEqual(results[1]['content'], 'This is the content2')
105− self.assertEqual(results[2]['thumbnail'], 'http://upload.wikimedia.org/optimized.jpg')
106−
107− json = """
108− {}
109− """
110− response = mock.Mock(text=json)
111− results = faroo.response(response)
112− self.assertEqual(type(results), list)
113− self.assertEqual(len(results), 0)
tests/unit/engines/test_fdroid.pydeleted−60
@@ -1,60 +0,0 @@
1−import mock
2−from collections import defaultdict
3−from searx.engines import fdroid
4−from searx.testing import SearxTestCase
5−
6−
7−class TestFdroidEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dic = defaultdict(dict)
12− dic['pageno'] = 1
13− params = fdroid.request(query, dic)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('search.f-droid.org' in params['url'])
17−
18− def test_response_empty(self):
19− resp = mock.Mock(text='<html></html>')
20− self.assertEqual(fdroid.response(resp), [])
21−
22− def test_response_oneresult(self):
23− html = """
24−<!DOCTYPE html>
25−<html>
26−<head>
27− <title>test</title>
28−</head>
29−<body>
30− <div class="site-wrapper">
31− <div class="main-content">
32− <a class="package-header" href="https://example.com/app.url">
33− <img class="package-icon" src="https://example.com/appexample.logo.png" />
34−
35− <div class="package-info">
36− <h4 class="package-name">
37− App Example 1
38− </h4>
39−
40− <div class="package-desc">
41− <span class="package-summary">Description App Example 1</span>
42− <span class="package-license">GPL-3.0-only</span>
43− </div>
44− </div>
45− </a>
46− </div>
47− </div>
48−</body>
49−</html>
50− """
51−
52− resp = mock.Mock(text=html)
53− results = fdroid.response(resp)
54−
55− self.assertEqual(type(results), list)
56− self.assertEqual(len(results), 1)
57− self.assertEqual(results[0]['url'], 'https://example.com/app.url')
58− self.assertEqual(results[0]['title'], 'App Example 1')
59− self.assertEqual(results[0]['content'], 'Description App Example 1 - GPL-3.0-only')
60− self.assertEqual(results[0]['img_src'], 'https://example.com/appexample.logo.png')
tests/unit/engines/test_flickr.pydeleted−142
@@ -1,142 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import flickr
4−from searx.testing import SearxTestCase
5−
6−
7−class TestFlickrEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = flickr.request(query, dicto)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('flickr.com' in params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, flickr.response, None)
20− self.assertRaises(AttributeError, flickr.response, [])
21− self.assertRaises(AttributeError, flickr.response, '')
22− self.assertRaises(AttributeError, flickr.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(flickr.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(flickr.response(response), [])
29−
30− json = r"""
31− { "photos": { "page": 1, "pages": "41001", "perpage": 100, "total": "4100032",
32− "photo": [
33− { "id": "15751017054", "owner": "66847915@N08",
34− "secret": "69c22afc40", "server": "7285", "farm": 8,
35− "title": "Photo title", "ispublic": 1,
36− "isfriend": 0, "isfamily": 0,
37− "description": { "_content": "Description" },
38− "ownername": "Owner",
39− "url_o": "https:\/\/farm8.staticflickr.com\/7285\/15751017054_9178e0f963_o.jpg",
40− "height_o": "2100", "width_o": "2653",
41− "url_n": "https:\/\/farm8.staticflickr.com\/7285\/15751017054_69c22afc40_n.jpg",
42− "height_n": "253", "width_n": "320",
43− "url_z": "https:\/\/farm8.staticflickr.com\/7285\/15751017054_69c22afc40_z.jpg",
44− "height_z": "507", "width_z": "640" }
45− ] }, "stat": "ok" }
46− """
47− response = mock.Mock(text=json)
48− results = flickr.response(response)
49− self.assertEqual(type(results), list)
50− self.assertEqual(len(results), 1)
51− self.assertEqual(results[0]['title'], 'Photo title')
52− self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
53− self.assertTrue('o.jpg' in results[0]['img_src'])
54− self.assertTrue('n.jpg' in results[0]['thumbnail_src'])
55− self.assertTrue('Owner' in results[0]['author'])
56− self.assertTrue('Description' in results[0]['content'])
57−
58− json = r"""
59− { "photos": { "page": 1, "pages": "41001", "perpage": 100, "total": "4100032",
60− "photo": [
61− { "id": "15751017054", "owner": "66847915@N08",
62− "secret": "69c22afc40", "server": "7285", "farm": 8,
63− "title": "Photo title", "ispublic": 1,
64− "isfriend": 0, "isfamily": 0,
65− "description": { "_content": "Description" },
66− "ownername": "Owner",
67− "url_z": "https:\/\/farm8.staticflickr.com\/7285\/15751017054_69c22afc40_z.jpg",
68− "height_z": "507", "width_z": "640" }
69− ] }, "stat": "ok" }
70− """
71− response = mock.Mock(text=json)
72− results = flickr.response(response)
73− self.assertEqual(type(results), list)
74− self.assertEqual(len(results), 1)
75− self.assertEqual(results[0]['title'], 'Photo title')
76− self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
77− self.assertTrue('z.jpg' in results[0]['img_src'])
78− self.assertTrue('z.jpg' in results[0]['thumbnail_src'])
79− self.assertTrue('Owner' in results[0]['author'])
80− self.assertTrue('Description' in results[0]['content'])
81−
82− json = r"""
83− { "photos": { "page": 1, "pages": "41001", "perpage": 100, "total": "4100032",
84− "photo": [
85− { "id": "15751017054", "owner": "66847915@N08",
86− "secret": "69c22afc40", "server": "7285", "farm": 8,
87− "title": "Photo title", "ispublic": 1,
88− "isfriend": 0, "isfamily": 0,
89− "description": { "_content": "Description" },
90− "ownername": "Owner",
91− "url_o": "https:\/\/farm8.staticflickr.com\/7285\/15751017054_9178e0f963_o.jpg",
92− "height_o": "2100", "width_o": "2653" }
93− ] }, "stat": "ok" }
94− """
95− response = mock.Mock(text=json)
96− results = flickr.response(response)
97− self.assertEqual(type(results), list)
98− self.assertEqual(len(results), 1)
99− self.assertEqual(results[0]['title'], 'Photo title')
100− self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
101− self.assertTrue('o.jpg' in results[0]['img_src'])
102− self.assertTrue('o.jpg' in results[0]['thumbnail_src'])
103− self.assertTrue('Owner' in results[0]['author'])
104− self.assertTrue('Description' in results[0]['content'])
105−
106− json = r"""
107− { "photos": { "page": 1, "pages": "41001", "perpage": 100, "total": "4100032",
108− "photo": [
109− { "id": "15751017054", "owner": "66847915@N08",
110− "secret": "69c22afc40", "server": "7285", "farm": 8,
111− "title": "Photo title", "ispublic": 1,
112− "isfriend": 0, "isfamily": 0,
113− "description": { "_content": "Description" },
114− "ownername": "Owner",
115− "url_n": "https:\/\/farm8.staticflickr.com\/7285\/15751017054_69c22afc40_n.jpg",
116− "height_n": "253", "width_n": "320" }
117− ] }, "stat": "ok" }
118− """
119− response = mock.Mock(text=json)
120− results = flickr.response(response)
121− self.assertEqual(type(results), list)
122− self.assertEqual(len(results), 0)
123−
124− json = """
125− { "photos": { "page": 1, "pages": "41001", "perpage": 100, "total": "4100032",
126− "toto": [] }, "stat": "ok" }
127− """
128− response = mock.Mock(text=json)
129− results = flickr.response(response)
130− self.assertEqual(type(results), list)
131− self.assertEqual(len(results), 0)
132−
133− json = r"""
134− {"toto":[
135− {"id":200,"name":"Artist Name",
136− "link":"http:\/\/www.flickr.com\/artist\/1217","type":"artist"}
137− ]}
138− """
139− response = mock.Mock(text=json)
140− results = flickr.response(response)
141− self.assertEqual(type(results), list)
142− self.assertEqual(len(results), 0)
tests/unit/engines/test_flickr_noapi.pydeleted−357
@@ -1,357 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import flickr_noapi
4−from searx.testing import SearxTestCase
5−
6−
7−class TestFlickrNoapiEngine(SearxTestCase):
8−
9− def test_build_flickr_url(self):
10− url = flickr_noapi.build_flickr_url("uid", "pid")
11− self.assertIn("uid", url)
12− self.assertIn("pid", url)
13−
14− def test_request(self):
15− query = 'test_query'
16− dicto = defaultdict(dict)
17− dicto['pageno'] = 1
18− dicto['time_range'] = ''
19− params = flickr_noapi.request(query, dicto)
20− self.assertIn('url', params)
21− self.assertIn(query, params['url'])
22− self.assertIn('flickr.com', params['url'])
23−
24− def test_response(self):
25− self.assertRaises(AttributeError, flickr_noapi.response, None)
26− self.assertRaises(AttributeError, flickr_noapi.response, [])
27− self.assertRaises(AttributeError, flickr_noapi.response, '')
28− self.assertRaises(AttributeError, flickr_noapi.response, '[]')
29−
30− response = mock.Mock(text='"modelExport:{"legend":[],"main":{"search-photos-lite-models":[{"photos":{}}]}}')
31− self.assertEqual(flickr_noapi.response(response), [])
32−
33− response = \
34− mock.Mock(text='"modelExport:{"legend":[],"main":{"search-photos-lite-models":[{"photos":{"_data":[]}}]}}')
35− self.assertEqual(flickr_noapi.response(response), [])
36−
37− # everthing is ok test
38− json = """
39− modelExport: {
40− "legend": [
41− [
42− "search-photos-lite-models",
43− "0",
44− "photos",
45− "_data",
46− "0"
47− ]
48− ],
49− "main": {
50− "search-photos-lite-models": [
51− {
52− "photos": {
53− "_data": [
54− {
55− "_flickrModelRegistry": "photo-lite-models",
56− "title": "This%20is%20the%20title",
57− "username": "Owner",
58− "pathAlias": "klink692",
59− "realname": "Owner",
60− "license": 0,
61− "ownerNsid": "59729010@N00",
62− "canComment": false,
63− "commentCount": 14,
64− "faveCount": 21,
65− "id": "14001294434",
66− "sizes": {
67− "c": {
68− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_c.jpg",
69− "width": 541,
70− "height": 800,
71− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_c.jpg",
72− "key": "c"
73− },
74− "h": {
75− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_761d32237a_h.jpg",
76− "width": 1081,
77− "height": 1600,
78− "url": "//c4.staticflickr.com/8/7246/14001294434_761d32237a_h.jpg",
79− "key": "h"
80− },
81− "k": {
82− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_f145a2c11a_k.jpg",
83− "width": 1383,
84− "height": 2048,
85− "url": "//c4.staticflickr.com/8/7246/14001294434_f145a2c11a_k.jpg",
86− "key": "k"
87− },
88− "l": {
89− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_b.jpg",
90− "width": 692,
91− "height": 1024,
92− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_b.jpg",
93− "key": "l"
94− },
95− "m": {
96− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777.jpg",
97− "width": 338,
98− "height": 500,
99− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777.jpg",
100− "key": "m"
101− },
102− "n": {
103− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_n.jpg",
104− "width": 216,
105− "height": 320,
106− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_n.jpg",
107− "key": "n"
108− },
109− "q": {
110− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_q.jpg",
111− "width": 150,
112− "height": 150,
113− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_q.jpg",
114− "key": "q"
115− },
116− "s": {
117− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_m.jpg",
118− "width": 162,
119− "height": 240,
120− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_m.jpg",
121− "key": "s"
122− },
123− "sq": {
124− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_s.jpg",
125− "width": 75,
126− "height": 75,
127− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_s.jpg",
128− "key": "sq"
129− },
130− "t": {
131− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_t.jpg",
132− "width": 68,
133− "height": 100,
134− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_t.jpg",
135− "key": "t"
136− },
137− "z": {
138− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_z.jpg",
139− "width": 433,
140− "height": 640,
141− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_z.jpg",
142− "key": "z"
143− }
144− }
145− }
146− ]
147− }
148− }
149− ]
150− }
151− }
152− """
153− # Flickr serves search results in a json block named 'modelExport' buried inside a script tag,
154− # this json is served as a single line terminating with a comma.
155− json = ''.join(json.split()) + ',\n'
156− response = mock.Mock(text=json)
157− results = flickr_noapi.response(response)
158− self.assertEqual(type(results), list)
159− self.assertEqual(len(results), 1)
160− self.assertEqual(results[0]['title'], 'This is the title')
161− self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
162− self.assertIn('k.jpg', results[0]['img_src'])
163− self.assertIn('n.jpg', results[0]['thumbnail_src'])
164− self.assertIn('Owner', results[0]['author'])
165−
166− # no n size, only the z size
167− json = """
168− modelExport: {
169− "legend": [
170− [
171− "search-photos-lite-models",
172− "0",
173− "photos",
174− "_data",
175− "0"
176− ]
177− ],
178− "main": {
179− "search-photos-lite-models": [
180− {
181− "photos": {
182− "_data": [
183− {
184− "_flickrModelRegistry": "photo-lite-models",
185− "title": "This%20is%20the%20title",
186− "username": "Owner",
187− "pathAlias": "klink692",
188− "realname": "Owner",
189− "license": 0,
190− "ownerNsid": "59729010@N00",
191− "canComment": false,
192− "commentCount": 14,
193− "faveCount": 21,
194− "id": "14001294434",
195− "sizes": {
196− "z": {
197− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_z.jpg",
198− "width": 433,
199− "height": 640,
200− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_z.jpg",
201− "key": "z"
202− }
203− }
204− }
205− ]
206− }
207− }
208− ]
209− }
210− }
211− """
212− json = ''.join(json.split()) + ',\n'
213− response = mock.Mock(text=json)
214− results = flickr_noapi.response(response)
215− self.assertEqual(type(results), list)
216− self.assertEqual(len(results), 1)
217− self.assertEqual(results[0]['title'], 'This is the title')
218− self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
219− self.assertIn('z.jpg', results[0]['img_src'])
220− self.assertIn('z.jpg', results[0]['thumbnail_src'])
221− self.assertIn('Owner', results[0]['author'])
222−
223− # no z or n size
224− json = """
225− modelExport: {
226− "legend": [
227− [
228− "search-photos-lite-models",
229− "0",
230− "photos",
231− "_data",
232− "0"
233− ]
234− ],
235− "main": {
236− "search-photos-lite-models": [
237− {
238− "photos": {
239− "_data": [
240− {
241− "_flickrModelRegistry": "photo-lite-models",
242− "title": "This%20is%20the%20title",
243− "username": "Owner",
244− "pathAlias": "klink692",
245− "realname": "Owner",
246− "license": 0,
247− "ownerNsid": "59729010@N00",
248− "canComment": false,
249− "commentCount": 14,
250− "faveCount": 21,
251− "id": "14001294434",
252− "sizes": {
253− "o": {
254− "displayUrl": "//farm8.staticflickr.com/7246/14001294434_410f653777_o.jpg",
255− "width": 433,
256− "height": 640,
257− "url": "//c4.staticflickr.com/8/7246/14001294434_410f653777_o.jpg",
258− "key": "o"
259− }
260− }
261− }
262− ]
263− }
264− }
265− ]
266− }
267− }
268− """
269− json = ''.join(json.split()) + ',\n'
270− response = mock.Mock(text=json)
271− results = flickr_noapi.response(response)
272− self.assertEqual(type(results), list)
273− self.assertEqual(len(results), 1)
274− self.assertEqual(results[0]['title'], 'This is the title')
275− self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
276− self.assertIn('o.jpg', results[0]['img_src'])
277− self.assertIn('o.jpg', results[0]['thumbnail_src'])
278− self.assertIn('Owner', results[0]['author'])
279−
280− # no image test
281− json = """
282− modelExport: {
283− "legend": [
284− [
285− "search-photos-lite-models",
286− "0",
287− "photos",
288− "_data",
289− "0"
290− ]
291− ],
292− "main": {
293− "search-photos-lite-models": [
294− {
295− "photos": {
296− "_data": [
297− {
298− "_flickrModelRegistry": "photo-lite-models",
299− "title": "This is the title",
300− "username": "Owner",
301− "pathAlias": "klink692",
302− "realname": "Owner",
303− "license": 0,
304− "ownerNsid": "59729010@N00",
305− "canComment": false,
306− "commentCount": 14,
307− "faveCount": 21,
308− "id": "14001294434",
309− "sizes": {
310− }
311− }
312− ]
313− }
314− }
315− ]
316− }
317− }
318− """
319− json = ''.join(json.split()) + ',\n'
320− response = mock.Mock(text=json)
321− results = flickr_noapi.response(response)
322− self.assertEqual(type(results), list)
323− self.assertEqual(len(results), 0)
324−
325− # null test
326− json = """
327− modelExport: {
328− "legend": [null],
329− "main": {
330− "search-photos-lite-models": [
331− {
332− "photos": {
333− "_data": [null]
334− }
335− }
336− ]
337− }
338− }
339− """
340− json = ''.join(json.split()) + ',\n'
341− response = mock.Mock(text=json)
342− results = flickr_noapi.response(response)
343− self.assertEqual(type(results), list)
344− self.assertEqual(len(results), 0)
345−
346− # garbage test
347− json = r"""
348− {"toto":[
349− {"id":200,"name":"Artist Name",
350− "link":"http:\/\/www.flickr.com\/artist\/1217","type":"artist"}
351− ]}
352− """
353− json = ''.join(json.split()) + ',\n'
354− response = mock.Mock(text=json)
355− results = flickr_noapi.response(response)
356− self.assertEqual(type(results), list)
357− self.assertEqual(len(results), 0)
tests/unit/engines/test_framalibre.pydeleted−103
@@ -1,103 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import framalibre
5−from searx.testing import SearxTestCase
6−
7−
8−class TestFramalibreEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 0
14− params = framalibre.request(query, dicto)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertTrue('framalibre.org' in params['url'])
18−
19− def test_response(self):
20− self.assertRaises(AttributeError, framalibre.response, None)
21− self.assertRaises(AttributeError, framalibre.response, [])
22− self.assertRaises(AttributeError, framalibre.response, '')
23− self.assertRaises(AttributeError, framalibre.response, '[]')
24−
25− response = mock.Mock(text='{}')
26− self.assertEqual(framalibre.response(response), [])
27−
28− response = mock.Mock(text='{"data": []}')
29− self.assertEqual(framalibre.response(response), [])
30−
31− html = u"""
32− <div class="nodes-list-row">
33− <div id="node-431"
34− class="node node-logiciel-annuaires node-promoted node-teaser node-teaser node-sheet clearfix nodes-list"
35− about="/content/gogs" typeof="sioc:Item foaf:Document">
36− <header class="media">
37− <div class="media-left">
38− <div class="field field-name-field-logo field-type-image field-label-hidden">
39− <div class="field-items">
40− <div class="field-item even">
41− <a href="/content/gogs">
42− <img class="media-object img-responsive" typeof="foaf:Image"
43− src="https://framalibre.org/sites/default/files/styles/teaser_logo/public/leslogos/gogs-lg.png?itok=rrCxKKBy"
44− width="70" height="70" alt="" />
45− </a>
46− </div>
47− </div>
48− </div>
49− </div>
50− <div class="media-body">
51− <h3 class="node-title"><a href="/content/gogs">Gogs</a></h3>
52− <span property="dc:title" content="Gogs" class="rdf-meta element-hidden"></span>
53− <div class="field field-name-field-annuaires field-type-taxonomy-term-reference field-label-hidden">
54− <div class="field-items">
55− <div class="field-item even">
56− <a href="/annuaires/cloudwebapps"
57− typeof="skos:Concept" property="rdfs:label skos:prefLabel"
58− datatype="" class="label label-primary">Cloud/webApps</a>
59− </div>
60− </div>
61− </div>
62− </div>
63− </header>
64− <div class="content">
65− <div class="field field-name-field-votre-appr-ciation field-type-fivestar field-label-hidden">
66− <div class="field-items">
67− <div class="field-item even">
68− </div>
69− </div>
70− </div>
71− <div class="field field-name-body field-type-text-with-summary field-label-hidden">
72− <div class="field-items">
73− <div class="field-item even" property="content:encoded">
74− <p>Gogs est une interface web basée sur git et une bonne alternative à GitHub.</p>
75− </div>
76− </div>
77− </div>
78− </div>
79− <footer>
80− <a href="/content/gogs" class="read-more btn btn-default btn-sm">Voir la notice</a>
81− <div class="field field-name-field-lien-officiel field-type-link-field field-label-hidden">
82− <div class="field-items">
83− <div class="field-item even">
84− <a href="https://gogs.io/" target="_blank" title="Voir le site officiel">
85− <span class="glyphicon glyphicon-globe"></span>
86− <span class="sr-only">Lien officiel</span>
87− </a>
88− </div>
89− </div>
90− </div>
91− </footer>
92− </div>
93− </div>
94− """
95− response = mock.Mock(text=html)
96− results = framalibre.response(response)
97− self.assertEqual(type(results), list)
98− self.assertEqual(len(results), 1)
99− self.assertEqual(results[0]['title'], 'Gogs')
100− self.assertEqual(results[0]['url'],
101− 'https://framalibre.org/content/gogs')
102− self.assertEqual(results[0]['content'],
103− u"Gogs est une interface web basée sur git et une bonne alternative à GitHub.")
tests/unit/engines/test_frinkiac.pydeleted−50
@@ -1,50 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import frinkiac
5−from searx.testing import SearxTestCase
6−
7−
8−class TestFrinkiacEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− request_dict = defaultdict(dict)
13− params = frinkiac.request(query, request_dict)
14− self.assertTrue('url' in params)
15−
16− def test_response(self):
17− self.assertRaises(AttributeError, frinkiac.response, None)
18− self.assertRaises(AttributeError, frinkiac.response, [])
19− self.assertRaises(AttributeError, frinkiac.response, '')
20− self.assertRaises(AttributeError, frinkiac.response, '[]')
21−
22− text = """
23−[{"Id":770931,
24− "Episode":"S06E18",
25− "Timestamp":534616,
26− "Filename":""},
27− {"Id":1657080,
28− "Episode":"S12E14",
29− "Timestamp":910868,
30− "Filename":""},
31− {"Id":1943753,
32− "Episode":"S14E21",
33− "Timestamp":773439,
34− "Filename":""},
35− {"Id":107835,
36− "Episode":"S02E03",
37− "Timestamp":531709,
38− "Filename":""}]
39− """
40−
41− response = mock.Mock(text=text)
42− results = frinkiac.response(response)
43− self.assertEqual(type(results), list)
44− self.assertEqual(len(results), 4)
45− self.assertEqual(results[0]['title'], u'S06E18')
46− self.assertIn('p=caption', results[0]['url'])
47− self.assertIn('e=S06E18', results[0]['url'])
48− self.assertIn('t=534616', results[0]['url'])
49− self.assertEqual(results[0]['thumbnail_src'], 'https://frinkiac.com/img/S06E18/534616/medium.jpg')
50− self.assertEqual(results[0]['img_src'], 'https://frinkiac.com/img/S06E18/534616.jpg')
tests/unit/engines/test_genius.pydeleted−231
@@ -1,231 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from datetime import datetime
4−from searx.engines import genius
5−from searx.testing import SearxTestCase
6−
7−
8−class TestGeniusEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = genius.request(query, dicto)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertTrue('genius.com' in params['url'])
18−
19− def test_response(self):
20−
21− json_empty = """
22− {
23− "meta": {
24− "status": 200
25− },
26− "response": {
27− "sections": [
28− {
29− "type": "top_hit",
30− "hits": []
31− },
32− {
33− "type": "song",
34− "hits": []
35− },
36− {
37− "type": "lyric",
38− "hits": []
39− },
40− {
41− "type": "artist",
42− "hits": []
43− },
44− {
45− "type": "album",
46− "hits": []
47− },
48− {
49− "type": "tag",
50− "hits": []
51− },
52− {
53− "type": "video",
54− "hits": []
55− },
56− {
57− "type": "article",
58− "hits": []
59− },
60− {
61− "type": "user",
62− "hits": []
63− }
64− ]
65− }
66− }
67− """
68−
69− resp = mock.Mock(text=json_empty)
70− self.assertEqual(genius.response(resp), [])
71−
72− json = """
73− {
74− "meta": {
75− "status": 200
76− },
77− "response": {
78− "sections": [
79− {
80− "type": "lyric",
81− "hits": [
82− {
83− "highlights": [
84− {
85− "property": "lyrics",
86− "value": "Sample lyrics",
87− "snippet": true,
88− "ranges": []
89− }
90− ],
91− "index": "lyric",
92− "type": "song",
93− "result": {
94− "_type": "song",
95− "annotation_count": 45,
96− "api_path": "/songs/52916",
97− "full_title": "J't'emmerde by MC Jean Gab'1",
98− "header_image_thumbnail_url": "https://images.genius.com/xxx.300x300x1.jpg",
99− "header_image_url": "https://images.genius.com/ef9f736a86df3c3b1772f3fb7fbdb21c.1000x1000x1.jpg",
100− "id": 52916,
101− "instrumental": false,
102− "lyrics_owner_id": 15586,
103− "lyrics_state": "complete",
104− "lyrics_updated_at": 1498744545,
105− "path": "/Mc-jean-gab1-jtemmerde-lyrics",
106− "pyongs_count": 4,
107− "song_art_image_thumbnail_url": "https://images.genius.com/xxx.300x300x1.jpg",
108− "stats": {
109− "hot": false,
110− "unreviewed_annotations": 0,
111− "pageviews": 62490
112− },
113− "title": "J't'emmerde",
114− "title_with_featured": "J't'emmerde",
115− "updated_by_human_at": 1498744546,
116− "url": "https://genius.com/Mc-jean-gab1-jtemmerde-lyrics",
117− "primary_artist": {
118− "_type": "artist",
119− "api_path": "/artists/12691",
120− "header_image_url": "https://images.genius.com/c7847662a58f8c2b0f02a6e217d60907.960x657x1.jpg",
121− "id": 12691,
122− "image_url": "https://s3.amazonaws.com/rapgenius/Mc-jean-gab1.jpg",
123− "index_character": "m",
124− "is_meme_verified": false,
125− "is_verified": false,
126− "name": "MC Jean Gab'1",
127− "slug": "Mc-jean-gab1",
128− "url": "https://genius.com/artists/Mc-jean-gab1"
129− }
130− }
131− }
132− ]
133− },
134− {
135− "type": "artist",
136− "hits": [
137− {
138− "highlights": [],
139− "index": "artist",
140− "type": "artist",
141− "result": {
142− "_type": "artist",
143− "api_path": "/artists/191580",
144− "header_image_url": "https://assets.genius.com/images/default_avatar_300.png?1503090542",
145− "id": 191580,
146− "image_url": "https://assets.genius.com/images/default_avatar_300.png?1503090542",
147− "index_character": "a",
148− "is_meme_verified": false,
149− "is_verified": false,
150− "name": "ASDF Guy",
151− "slug": "Asdf-guy",
152− "url": "https://genius.com/artists/Asdf-guy"
153− }
154− }
155− ]
156− },
157− {
158− "type": "album",
159− "hits": [
160− {
161− "highlights": [],
162− "index": "album",
163− "type": "album",
164− "result": {
165− "_type": "album",
166− "api_path": "/albums/132332",
167− "cover_art_thumbnail_url": "https://images.genius.com/xxx.300x300x1.jpg",
168− "cover_art_url": "https://images.genius.com/xxx.600x600x1.jpg",
169− "full_title": "ASD by A Skylit Drive",
170− "id": 132332,
171− "name": "ASD",
172− "name_with_artist": "ASD (artist: A Skylit Drive)",
173− "release_date_components": {
174− "year": 2015,
175− "month": null,
176− "day": null
177− },
178− "url": "https://genius.com/albums/A-skylit-drive/Asd",
179− "artist": {
180− "_type": "artist",
181− "api_path": "/artists/48712",
182− "header_image_url": "https://images.genius.com/814c1551293172c56306d0e310c6aa89.620x400x1.jpg",
183− "id": 48712,
184− "image_url": "https://images.genius.com/814c1551293172c56306d0e310c6aa89.620x400x1.jpg",
185− "index_character": "s",
186− "is_meme_verified": false,
187− "is_verified": false,
188− "name": "A Skylit Drive",
189− "slug": "A-skylit-drive",
190− "url": "https://genius.com/artists/A-skylit-drive"
191− }
192− }
193− }
194− ]
195− }
196− ]
197− }
198− }
199− """
200−
201− resp = mock.Mock(text=json)
202− results = genius.response(resp)
203−
204− self.assertEqual(len(results), 3)
205− self.assertEqual(type(results), list)
206−
207− # check lyric parsing
208− r = results[0]
209− self.assertEqual(r['url'], 'https://genius.com/Mc-jean-gab1-jtemmerde-lyrics')
210− self.assertEqual(r['title'], "J't'emmerde by MC Jean Gab'1")
211− self.assertEqual(r['content'], "Sample lyrics")
212− self.assertEqual(r['template'], 'videos.html')
213− self.assertEqual(r['thumbnail'], 'https://images.genius.com/xxx.300x300x1.jpg')
214− created = datetime.fromtimestamp(1498744545)
215− self.assertEqual(r['publishedDate'], created)
216−
217− # check artist parsing
218− r = results[1]
219− self.assertEqual(r['url'], 'https://genius.com/artists/Asdf-guy')
220− self.assertEqual(r['title'], "ASDF Guy")
221− self.assertEqual(r['content'], None)
222− self.assertEqual(r['template'], 'videos.html')
223− self.assertEqual(r['thumbnail'], 'https://assets.genius.com/images/default_avatar_300.png?1503090542')
224−
225− # check album parsing
226− r = results[2]
227− self.assertEqual(r['url'], 'https://genius.com/albums/A-skylit-drive/Asd')
228− self.assertEqual(r['title'], "ASD by A Skylit Drive")
229− self.assertEqual(r['content'], "Released: 2015")
230− self.assertEqual(r['template'], 'videos.html')
231− self.assertEqual(r['thumbnail'], 'https://images.genius.com/xxx.600x600x1.jpg')
tests/unit/engines/test_gigablast.pydeleted−119
@@ -1,119 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import gigablast
4−from searx.testing import SearxTestCase
5−
6−
7−class TestGigablastEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− dicto['safesearch'] = 0
14− dicto['language'] = 'all'
15− params = gigablast.request(query, dicto)
16− self.assertTrue('url' in params)
17− self.assertTrue(query in params['url'])
18− self.assertTrue('gigablast.com' in params['url'])
19− self.assertTrue('xx' in params['url'])
20−
21− dicto['language'] = 'en-US'
22− params = gigablast.request(query, dicto)
23− self.assertTrue('en' in params['url'])
24− self.assertFalse('en-US' in params['url'])
25−
26− def test_response(self):
27− self.assertRaises(AttributeError, gigablast.response, None)
28− self.assertRaises(AttributeError, gigablast.response, [])
29− self.assertRaises(AttributeError, gigablast.response, '')
30− self.assertRaises(AttributeError, gigablast.response, '[]')
31−
32− response = mock.Mock(text='{"results": []}')
33− self.assertEqual(gigablast.response(response), [])
34−
35− json = """{"results": [
36− {
37− "title":"South by Southwest 2016",
38− "dmozEntry":{
39− "dmozCatId":1041152,
40− "directCatId":1,
41− "dmozCatStr":"Top: Regional: North America: United States",
42− "dmozTitle":"South by Southwest (SXSW)",
43− "dmozSum":"Annual music, film, and interactive conference.",
44− "dmozAnchor":""
45− },
46− "dmozEntry":{
47− "dmozCatId":763945,
48− "directCatId":1,
49− "dmozCatStr":"Top: Regional: North America: United States",
50− "dmozTitle":"South by Southwest (SXSW)",
51− "dmozSum":"",
52− "dmozAnchor":"www.sxsw.com"
53− },
54− "dmozEntry":{
55− "dmozCatId":761446,
56− "directCatId":1,
57− "dmozCatStr":"Top: Regional: North America: United States",
58− "dmozTitle":"South by Southwest (SXSW)",
59− "dmozSum":"Music, film, and interactive conference and festival.",
60− "dmozAnchor":""
61− },
62− "indirectDmozCatId":1041152,
63− "indirectDmozCatId":763945,
64− "indirectDmozCatId":761446,
65− "contentType":"html",
66− "sum":"This should be the content.",
67− "url":"www.sxsw.com",
68− "hopCount":0,
69− "size":" 102k",
70− "sizeInBytes":104306,
71− "bytesUsedToComputeSummary":70000,
72− "docId":269411794364,
73− "docScore":586571136.000000,
74− "summaryGenTimeMS":12,
75− "summaryTagdbLookupTimeMS":0,
76− "summaryTitleRecLoadTimeMS":1,
77− "site":"www.sxsw.com",
78− "spidered":1452203608,
79− "firstIndexedDateUTC":1444167123,
80− "contentHash32":2170650347,
81− "language":"English",
82− "langAbbr":"en"
83− }
84−]}
85− """
86− response = mock.Mock(text=json)
87− results = gigablast.response(response)
88− self.assertEqual(type(results), list)
89− self.assertEqual(len(results), 1)
90− self.assertEqual(results[0]['title'], 'South by Southwest 2016')
91− self.assertEqual(results[0]['url'], 'www.sxsw.com')
92− self.assertEqual(results[0]['content'], 'This should be the content.')
93−
94− def test_fetch_supported_languages(self):
95− html = """<html></html>"""
96− response = mock.Mock(text=html)
97− results = gigablast._fetch_supported_languages(response)
98− self.assertEqual(type(results), list)
99− self.assertEqual(len(results), 0)
100−
101− html = """
102− <html>
103− <body>
104− <span id="menu2">
105− <a href="/search?&rxikd=1&qlang=xx"></a>
106− <a href="/search?&rxikd=1&qlang=en"></a>
107− <a href="/search?&rxikd=1&prepend=gblang%3Aen"></a>
108− <a href="/search?&rxikd=1&qlang=zh_"></a>
109− <a href="/search?&rxikd=1&prepend=gblang%3Azh_tw"></a>
110− </span>
111− </body>
112− </html>
113− """
114− response = mock.Mock(text=html)
115− languages = gigablast._fetch_supported_languages(response)
116− self.assertEqual(type(languages), list)
117− self.assertEqual(len(languages), 2)
118− self.assertIn('en', languages)
119− self.assertIn('zh-TW', languages)
tests/unit/engines/test_github.pydeleted−61
@@ -1,61 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import github
4−from searx.testing import SearxTestCase
5−
6−
7−class TestGitHubEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− params = github.request(query, defaultdict(dict))
12− self.assertTrue('url' in params)
13− self.assertTrue(query in params['url'])
14− self.assertTrue('github.com' in params['url'])
15− self.assertEqual(params['headers']['Accept'], github.accept_header)
16−
17− def test_response(self):
18− self.assertRaises(AttributeError, github.response, None)
19− self.assertRaises(AttributeError, github.response, [])
20− self.assertRaises(AttributeError, github.response, '')
21− self.assertRaises(AttributeError, github.response, '[]')
22−
23− response = mock.Mock(text='{}')
24− self.assertEqual(github.response(response), [])
25−
26− response = mock.Mock(text='{"items": []}')
27− self.assertEqual(github.response(response), [])
28−
29− json = """
30− {
31− "items": [
32− {
33− "name": "title",
34− "html_url": "url",
35− "description": ""
36− }
37− ]
38− }
39− """
40− response = mock.Mock(text=json)
41− results = github.response(response)
42− self.assertEqual(type(results), list)
43− self.assertEqual(len(results), 1)
44− self.assertEqual(results[0]['title'], 'title')
45− self.assertEqual(results[0]['url'], 'url')
46− self.assertEqual(results[0]['content'], '')
47−
48− json = """
49− {
50− "items": [
51− {
52− "name": "title",
53− "html_url": "url",
54− "description": "desc"
55− }
56− ]
57− }
58− """
59− response = mock.Mock(text=json)
60− results = github.response(response)
61− self.assertEqual(results[0]['content'], "desc")
tests/unit/engines/test_google.pydeleted−237
@@ -1,237 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−import lxml
5−from searx.engines import google
6−from searx.testing import SearxTestCase
7−
8−
9−class TestGoogleEngine(SearxTestCase):
10−
11− def mock_response(self, text):
12− response = mock.Mock(text=text, url='https://www.google.com/search?q=test&start=0&gbv=1&gws_rd=cr')
13− response.search_params = mock.Mock()
14− response.search_params.get = mock.Mock(return_value='www.google.com')
15− return response
16−
17− def test_request(self):
18− google.supported_languages = ['en', 'fr', 'zh-CN', 'iw']
19− google.language_aliases = {'he': 'iw'}
20−
21− query = 'test_query'
22− dicto = defaultdict(dict)
23− dicto['pageno'] = 1
24− dicto['language'] = 'fr-FR'
25− dicto['time_range'] = ''
26− params = google.request(query, dicto)
27− self.assertIn('url', params)
28− self.assertIn(query, params['url'])
29− self.assertIn('google.fr', params['url'])
30− self.assertIn('fr', params['url'])
31− self.assertIn('fr', params['headers']['Accept-Language'])
32−
33− dicto['language'] = 'en-US'
34− params = google.request(query, dicto)
35− self.assertIn('google.com', params['url'])
36− self.assertIn('en', params['url'])
37− self.assertIn('en', params['headers']['Accept-Language'])
38−
39− dicto['language'] = 'zh'
40− params = google.request(query, dicto)
41− self.assertIn('google.com', params['url'])
42− self.assertIn('zh-CN', params['url'])
43− self.assertIn('zh-CN', params['headers']['Accept-Language'])
44−
45− dicto['language'] = 'he'
46− params = google.request(query, dicto)
47− self.assertIn('google.com', params['url'])
48− self.assertIn('iw', params['url'])
49− self.assertIn('iw', params['headers']['Accept-Language'])
50−
51− def test_response(self):
52− self.assertRaises(AttributeError, google.response, None)
53− self.assertRaises(AttributeError, google.response, [])
54− self.assertRaises(AttributeError, google.response, '')
55− self.assertRaises(AttributeError, google.response, '[]')
56−
57− response = self.mock_response('<html></html>')
58− self.assertEqual(google.response(response), [])
59−
60− html = """
61− <div class="g">
62− <h3 class="r">
63− <a href="http://this.should.be.the.link/">
64− <b>This</b> is <b>the</b> title
65− </a>
66− </h3>
67− <div class="s">
68− <div class="kv" style="margin-bottom:2px">
69− <cite>
70− <b>test</b>.psychologies.com/
71− </cite>
72− <div class="_nBb"><U+200E>
73− <div style="display:inline" onclick="google.sham(this);" aria-expanded="false"
74− aria-haspopup="true" tabindex="0" data-ved="0CBUQ7B0wAA">
75− <span class="_O0">
76− </span>
77− </div>
78− <div style="display:none" class="am-dropdown-menu" role="menu" tabindex="-1">
79− <ul>
80− <li class="_Ykb">
81− <a class="_Zkb" href="http://www.google.fr/url?url=http://webcache.googleusercontent
82− .com/search%3Fcache:R1Z_4pGXjuIJ:http://test.psychologies.com/">
83− En cache
84− </a>
85− </li>
86− <li class="_Ykb">
87− <a class="_Zkb" href="/search?safe=off&amp;q=related:test.psy.com/">
88− Pages similaires
89− </a>
90− </li>
91− </ul>
92− </div>
93− </div>
94− </div>
95− <span class="st">
96− This should be the content.
97− </span>
98− <br>
99− <div class="osl"><U+200E>
100− <a href="http://www.google.fr/url?url=http://test.psychologies.com/tests/">
101− Test Personnalité
102− </a> - <U+200E>
103− <a href="http://www.google.fr/url?url=http://test.psychologies.com/test/">
104− Tests - Moi
105− </a> - <U+200E>
106− <a href="http://www.google.fr/url?url=http://test.psychologies.com/test/tests-couple">
107− Test Couple
108− </a>
109− - <U+200E>
110− <a href="http://www.google.fr/url?url=http://test.psychologies.com/tests/tests-amour">
111− Test Amour
112− </a>
113− </div>
114− </div>
115− </div>
116− <div class="g">
117− <h3 class="r">
118− <a href="http://www.google.com/images?q=toto">
119− <b>This</b>
120− </a>
121− </h3>
122− </div>
123− <div class="g">
124− <h3 class="r">
125− <a href="http://www.google.com/search?q=toto">
126− <b>This</b> is
127− </a>
128− </h3>
129− </div>
130− <div class="g">
131− <h3 class="r">
132− <a href="€">
133− <b>This</b> is <b>the</b>
134− </a>
135− </h3>
136− </div>
137− <div class="g">
138− <h3 class="r">
139− <a href="/url?q=url">
140− <b>This</b> is <b>the</b>
141− </a>
142− </h3>
143− </div>
144− <p class="_Bmc" style="margin:3px 8px">
145− <a href="/search?num=20&amp;safe=off&amp;q=t&amp;revid=1754833769&amp;sa=X&amp;ei=-&amp;ved=">
146− suggestion <b>title</b>
147− </a>
148− </p>
149− """
150− response = self.mock_response(html)
151− results = google.response(response)
152− self.assertEqual(type(results), list)
153− self.assertEqual(len(results), 2)
154− self.assertEqual(results[0]['title'], 'This is the title')
155− self.assertEqual(results[0]['url'], 'http://this.should.be.the.link/')
156− self.assertEqual(results[0]['content'], 'This should be the content.')
157− self.assertEqual(results[1]['suggestion'], 'suggestion title')
158−
159− html = """
160− <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
161− </li>
162− """
163− response = self.mock_response(html)
164− results = google.response(response)
165− self.assertEqual(type(results), list)
166− self.assertEqual(len(results), 0)
167−
168− response = mock.Mock(text='<html></html>', url='https://sorry.google.com')
169− response.search_params = mock.Mock()
170− response.search_params.get = mock.Mock(return_value='www.google.com')
171− self.assertRaises(RuntimeWarning, google.response, response)
172−
173− response = mock.Mock(text='<html></html>', url='https://www.google.com/sorry/IndexRedirect')
174− response.search_params = mock.Mock()
175− response.search_params.get = mock.Mock(return_value='www.google.com')
176− self.assertRaises(RuntimeWarning, google.response, response)
177−
178− def test_parse_images(self):
179− html = """
180− <li>
181− <div>
182− <a href="http://www.google.com/url?q=http://this.is.the.url/">
183− <img style="margin:3px 0;margin-right:6px;padding:0" height="90"
184− src="https://this.is.the.image/image.jpg" width="60" align="middle" alt="" border="0">
185− </a>
186− </div>
187− </li>
188− """
189− dom = lxml.html.fromstring(html)
190− results = google.parse_images(dom, 'www.google.com')
191− self.assertEqual(type(results), list)
192− self.assertEqual(len(results), 1)
193− self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
194− self.assertEqual(results[0]['title'], '')
195− self.assertEqual(results[0]['content'], '')
196− self.assertEqual(results[0]['img_src'], 'https://this.is.the.image/image.jpg')
197−
198− def test_fetch_supported_languages(self):
199− html = """<html></html>"""
200− response = mock.Mock(text=html)
201− languages = google._fetch_supported_languages(response)
202− self.assertEqual(type(languages), dict)
203− self.assertEqual(len(languages), 0)
204−
205− html = u"""
206− <html>
207− <body>
208− <div id="langSec">
209− <div>
210− <input name="lr" data-name="english" value="lang_en" />
211− <input name="lr" data-name="中文 (简体)" value="lang_zh-CN" />
212− <input name="lr" data-name="中文 (繁體)" value="lang_zh-TW" />
213− </div>
214− </div>
215− </body>
216− </html>
217− """
218− response = mock.Mock(text=html)
219− languages = google._fetch_supported_languages(response)
220− self.assertEqual(type(languages), dict)
221− self.assertEqual(len(languages), 3)
222−
223− self.assertIn('en', languages)
224− self.assertIn('zh-CN', languages)
225− self.assertIn('zh-TW', languages)
226−
227− self.assertEquals(type(languages['en']), dict)
228− self.assertEquals(type(languages['zh-CN']), dict)
229− self.assertEquals(type(languages['zh-TW']), dict)
230−
231− self.assertIn('name', languages['en'])
232− self.assertIn('name', languages['zh-CN'])
233− self.assertIn('name', languages['zh-TW'])
234−
235− self.assertEquals(languages['en']['name'], 'English')
236− self.assertEquals(languages['zh-CN']['name'], u'中文 (简体)')
237− self.assertEquals(languages['zh-TW']['name'], u'中文 (繁體)')
tests/unit/engines/test_google_images.pydeleted−27
@@ -1,27 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import google_images
4−from searx.testing import SearxTestCase
5−
6−
7−class TestGoogleImagesEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 1
13− dicto['safesearch'] = 1
14− dicto['time_range'] = ''
15− params = google_images.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn(query, params['url'])
18−
19− dicto['safesearch'] = 0
20− params = google_images.request(query, dicto)
21− self.assertNotIn('safe', params['url'])
22−
23− def test_response(self):
24− self.assertRaises(AttributeError, google_images.response, None)
25− self.assertRaises(AttributeError, google_images.response, [])
26− self.assertRaises(AttributeError, google_images.response, '')
27− self.assertRaises(AttributeError, google_images.response, '[]')
tests/unit/engines/test_google_news.pydeleted−102
@@ -1,102 +0,0 @@
1−# -*- coding: utf-8 -*-
2−
3−from collections import defaultdict
4−import mock
5−from searx.engines import google_news
6−from searx.testing import SearxTestCase
7−
8−
9−class TestGoogleNewsEngine(SearxTestCase):
10−
11− def test_request(self):
12− google_news.supported_languages = ['en-US', 'fr-FR']
13− google_news.language_aliases = {}
14− query = 'test_query'
15− dicto = defaultdict(dict)
16− dicto['pageno'] = 1
17− dicto['language'] = 'fr-FR'
18− dicto['time_range'] = 'w'
19− params = google_news.request(query, dicto)
20− self.assertIn('url', params)
21− self.assertIn(query, params['url'])
22− self.assertIn('fr', params['url'])
23−
24− dicto['language'] = 'all'
25− params = google_news.request(query, dicto)
26− self.assertIn('url', params)
27− self.assertNotIn('fr', params['url'])
28−
29− def test_response(self):
30− self.assertRaises(AttributeError, google_news.response, None)
31− self.assertRaises(AttributeError, google_news.response, [])
32− self.assertRaises(AttributeError, google_news.response, '')
33− self.assertRaises(AttributeError, google_news.response, '[]')
34−
35− response = mock.Mock(text='{}')
36− self.assertEqual(google_news.response(response), [])
37−
38− response = mock.Mock(text='{"data": []}')
39− self.assertEqual(google_news.response(response), [])
40−
41− html = u"""
42−<h2 class="hd">Search Results</h2>
43−<div data-async-context="query:searx" id="ires">
44− <div eid="oC2oWcGXCafR6ASkwoCwDA" id="rso">
45− <div class="_NId">
46− <!--m-->
47− <div class="g _cy">
48− <div class="ts _JGs _JHs _tJs _KGs _jHs">
49− <div class="_hJs">
50− <h3 class="r _gJs">
51− <a class="l lLrAF" href="https://example.com/" onmousedown="return rwt(this,'','','','11','AFQjCNEyehpzD5cJK1KUfXBx9RmsbqqG9g','','0ahUKEwjB58OR54HWAhWnKJoKHSQhAMY4ChCpAggiKAAwAA','','',event)">Example title</a>
52− </h3>
53− <div class="slp">
54− <span class="_OHs _PHs">
55− Mac &amp; i</span>
56− <span class="_QGs">
57− -</span>
58− <span class="f nsa _QHs">
59− Mar 21, 2016</span>
60− </div>
61− <div class="st">Example description</div>
62− </div>
63− </div>
64− </div>
65− <div class="g _cy">
66− <div class="ts _JGs _JHs _oGs _KGs _jHs">
67− <a class="top _xGs _SHs" href="https://example2.com/" onmousedown="return rwt(this,'','','','12','AFQjCNHObfH7sYmLWI1SC-YhWXKZFRzRjw','','0ahUKEwjB58OR54HWAhWnKJoKHSQhAMY4ChC8iAEIJDAB','','',event)">
68− <img class="th _RGs" src="https://example2.com/image.jpg" alt="Story image for searx from Golem.de" onload="typeof google==='object'&&google.aft&&google.aft(this)">
69− </a>
70− <div class="_hJs">
71− <h3 class="r _gJs">
72− <a class="l lLrAF" href="https://example2.com/" onmousedown="return rwt(this,'','','','12','AFQjCNHObfH7sYmLWI1SC-YhWXKZFRzRjw','','0ahUKEwjB58OR54HWAhWnKJoKHSQhAMY4ChCpAgglKAAwAQ','','',event)">Example title 2</a>
73− </h3>
74− <div class="slp">
75− <span class="_OHs _PHs">
76− Golem.de</span>
77− <span class="_QGs">
78− -</span>
79− <span class="f nsa _QHs">
80− Oct 4, 2016</span>
81− </div>
82− <div class="st">Example description 2</div>
83− </div>
84− </div>
85− </div>
86− </div>
87− </div>
88−</div>
89−
90−
91− """ # noqa
92− response = mock.Mock(text=html)
93− results = google_news.response(response)
94− self.assertEqual(type(results), list)
95− self.assertEqual(len(results), 2)
96− self.assertEqual(results[0]['title'], u'Example title')
97− self.assertEqual(results[0]['url'], 'https://example.com/')
98− self.assertEqual(results[0]['content'], 'Example description')
99− self.assertEqual(results[1]['title'], u'Example title 2')
100− self.assertEqual(results[1]['url'], 'https://example2.com/')
101− self.assertEqual(results[1]['content'], 'Example description 2')
102− self.assertEqual(results[1]['img_src'], 'https://example2.com/image.jpg')
tests/unit/engines/test_google_videos.pydeleted−79
@@ -1,79 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import google_videos
4−from searx.testing import SearxTestCase
5−
6−
7−class TestGoogleVideosEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 1
13− dicto['safesearch'] = 1
14− dicto['time_range'] = ''
15− params = google_videos.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn(query, params['url'])
18−
19− dicto['safesearch'] = 0
20− params = google_videos.request(query, dicto)
21− self.assertNotIn('safe', params['url'])
22−
23− def test_response(self):
24− self.assertRaises(AttributeError, google_videos.response, None)
25− self.assertRaises(AttributeError, google_videos.response, [])
26− self.assertRaises(AttributeError, google_videos.response, '')
27− self.assertRaises(AttributeError, google_videos.response, '[]')
28−
29− html = r"""
30− <div>
31− <div>
32− <div class="g">
33− <div class="r">
34− <a href="url_1"><h3>Title 1</h3></a>
35− </div>
36− <div class="s">
37− <div>
38− <a>
39− <g-img>
40− <img id="vidthumb1">
41− </g-img>
42− </a>
43− </div>
44− </div>
45− <div>
46− <span class="st">Content 1</span>
47− </div>
48− </div>
49− <div class="g">
50− <div class="r">
51− <a href="url_2"><h3>Title 2</h3></a>
52− </div>
53− <div class="s">
54− <div>
55− <a>
56− <g-img>
57− <img id="vidthumb2">
58− </g-img>
59− </a>
60− </div>
61− </div>
62− <div>
63− <span class="st">Content 2</span>
64− </div>
65− </div>
66− </div>
67− </div>
68− <script>function _setImagesSrc(c,d,e){}</script>
69− """
70− response = mock.Mock(text=html)
71− results = google_videos.response(response)
72− self.assertEqual(type(results), list)
73− self.assertEqual(len(results), 2)
74− self.assertEqual(results[0]['url'], u'url_1')
75− self.assertEqual(results[0]['title'], u'Title 1')
76− self.assertEqual(results[0]['content'], u'Content 1')
77− self.assertEqual(results[1]['url'], u'url_2')
78− self.assertEqual(results[1]['title'], u'Title 2')
79− self.assertEqual(results[1]['content'], u'Content 2')
tests/unit/engines/test_ina.pydeleted−64
@@ -1,64 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import ina
4−from searx.testing import SearxTestCase
5−
6−
7−class TestInaEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = ina.request(query, dicto)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('ina.fr' in params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, ina.response, None)
20− self.assertRaises(AttributeError, ina.response, [])
21− self.assertRaises(AttributeError, ina.response, '')
22− self.assertRaises(AttributeError, ina.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(ina.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(ina.response(response), [])
29−
30− json = """
31− {"content":"\\t<div class=\\"container\\">\\n\\t\\n\
32− <!-- DEBUT CONTENU PRINCIPAL -->\\n<div class=\\"row\\">\\n\
33− <div class=\\"search-results--list\\"><div class=\\"media\\">\\n\
34− \\t\\t\\t\\t<a class=\\"media-left media-video premium xiti_click_action\\" \
35− data-xiti-params=\\"recherche_v4::resultats_conference_de_presse_du_general_de_gaulle::N\\" \
36− href=\\"\\/video\\/CAF89035682\\/conference-de-presse-du-general-de-gaulle-video.html\\">\\n\
37− <img src=\\"https:\\/\\/www.ina.fr\\/images_v2\\/140x105\\/CAF89035682.jpeg\\" \
38− alt=\\"Conf\\u00e9rence de presse du G\\u00e9n\\u00e9ral de Gaulle \\">\\n\
39− \\t\\t\\t\\t\\t<\\/a>\\n\
40− \\t\\t\\t\\t\\t<div class=\\"media-body\\">\\n\\t\\t\\t\\t\\t\\t<h3 class=\\"h3--title media-heading\\">\\n\
41− \\t\\t\\t\\t\\t\\t\\t<a class=\\"xiti_click_action\\" \
42− data-xiti-params=\\"recherche_v4::resultats_conference_de_presse_du_general_de_gaulle::N\\" \
43− href=\\"\\/video\\/CAF89035682\\/conference-de-presse-du-general-de-gaulle-video.html\\">\
44− Conf\\u00e9rence de presse du G\\u00e9n\\u00e9ral de Gaulle <\\/a>\\n\
45− <\\/h3>\\n\
46− <div class=\\"media-body__info\\">\\n<span class=\\"broadcast\\">27\\/11\\/1967<\\/span>\\n\
47− <span class=\\"views\\">29321 vues<\\/span>\\n\
48− <span class=\\"duration\\">01h 33m 07s<\\/span>\\n\
49− <\\/div>\\n\
50− <p class=\\"media-body__summary\\">VERSION INTEGRALE DE LA CONFERENCE DE PRESSE DU GENERAL DE GAULLE . \
51− - PA le Pr\\u00e9sident DE GAULLE : il ouvre les bras et s'assied. DP journalis...<\\/p>\\n\
52− <\\/div>\\n<\\/div><!-- \\/.media -->\\n"
53− }
54− """
55− response = mock.Mock(text=json)
56− results = ina.response(response)
57− self.assertEqual(type(results), list)
58− self.assertEqual(len(results), 1)
59− self.assertEqual(results[0]['title'], u'Conf\xe9rence de presse du G\xe9n\xe9ral de Gaulle')
60− self.assertEqual(results[0]['url'],
61− 'https://www.ina.fr/video/CAF89035682/conference-de-presse-du-general-de-gaulle-video.html')
62− self.assertEqual(results[0]['content'],
63− u"VERSION INTEGRALE DE LA CONFERENCE DE PRESSE DU GENERAL DE GAULLE ."
64− u" - PA le Pr\u00e9sident DE GAULLE : il ouvre les bras et s'assied. DP journalis...")
tests/unit/engines/test_kickass.pydeleted−397
@@ -1,397 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import kickass
5−from searx.testing import SearxTestCase
6−
7−
8−class TestKickassEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = kickass.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn(query, params['url'])
17− self.assertIn('kickass.cd', params['url'])
18− self.assertFalse(params['verify'])
19−
20− def test_response(self):
21− self.assertRaises(AttributeError, kickass.response, None)
22− self.assertRaises(AttributeError, kickass.response, [])
23− self.assertRaises(AttributeError, kickass.response, '')
24− self.assertRaises(AttributeError, kickass.response, '[]')
25−
26− response = mock.Mock(text='<html></html>')
27− self.assertEqual(kickass.response(response), [])
28−
29− html = """
30− <table cellpadding="0" cellspacing="0" class="data" style="width: 100%">
31− <tr class="firstr">
32− <th class="width100perc nopad">torrent name</th>
33− <th class="center">
34− <a href="/search/test/?field=size&sorder=desc" rel="nofollow">size</a>
35− </th>
36− <th class="center"><span class="files">
37− <a href="/search/test/?field=files_count&sorder=desc" rel="nofollow">files</a></span>
38− </th>
39− <th class="center"><span>
40− <a href="/search/test/?field=time_add&sorder=desc" rel="nofollow">age</a></span>
41− </th>
42− <th class="center"><span class="seed">
43− <a href="/search/test/?field=seeders&sorder=desc" rel="nofollow">seed</a></span>
44− </th>
45− <th class="lasttd nobr center">
46− <a href="/search/test/?field=leechers&sorder=desc" rel="nofollow">leech</a>
47− </th>
48− </tr>
49− <tr class="even" id="torrent_test6478745">
50− <td>
51− <div class="iaconbox center floatright">
52− <a rel="6478745,0" class="icommentjs icon16" href="/test-t6478745.html#comment">
53− <em style="font-size: 12px; margin: 0 4px 0 4px;" class="iconvalue">3</em>
54− <i class="ka ka-comment"></i>
55− </a>
56− <a class="iverify icon16" href="/test-t6478745.html" title="Verified Torrent">
57− <i class="ka ka16 ka-verify ka-green"></i>
58− </a>
59− <a href="#" onclick="_scq.push([]); return false;" class="partner1Button idownload icon16">
60− <i class="ka ka16 ka-arrow-down partner1Button"></i>
61− </a>
62− <a title="Torrent magnet link"
63− href="magnet:?xt=urn:btih:MAGNETURL&dn=test" class="imagnet icon16">
64− <i class="ka ka16 ka-magnet"></i>
65− </a>
66− <a title="Download torrent file"
67− href="http://torcache.net/torrent/53917.torrent?title=test" class="idownload icon16">
68− <i class="ka ka16 ka-arrow-down"></i>
69− </a>
70− </div>
71− <div class="torrentname">
72− <a href="/test-t6478745.html" class="torType txtType"></a>
73− <a href="/test-t6478745.html" class="normalgrey font12px plain bold"></a>
74− <div class="markeredBlock torType txtType">
75− <a href="/url.html" class="cellMainLink">
76− <strong class="red">This should be the title</strong>
77− </a>
78− <span class="font11px lightgrey block">
79− Posted by <i class="ka ka-verify" style="font-size: 16px;color:orange;"></i>
80− <a class="plain" href="/user/riri/">riri</a> in
81− <span id="cat_6478745">
82− <strong><a href="/other/">Other</a> > <a href="/unsorted/">Unsorted</a></strong>
83− </span>
84− </span>
85− </div>
86− </td>
87− <td class="nobr center">449 bytes</td>
88− <td class="center">4</td>
89− <td class="center">2&nbsp;years</td>
90− <td class="green center">10</td>
91− <td class="red lasttd center">1</td>
92− </tr>
93− </table>
94− """
95− response = mock.Mock(text=html)
96− results = kickass.response(response)
97− self.assertEqual(type(results), list)
98− self.assertEqual(len(results), 1)
99− self.assertEqual(results[0]['title'], 'This should be the title')
100− self.assertEqual(results[0]['url'], 'https://kickass.cd/url.html')
101− self.assertEqual(results[0]['content'], 'Posted by riri in Other > Unsorted')
102− self.assertEqual(results[0]['seed'], 10)
103− self.assertEqual(results[0]['leech'], 1)
104− self.assertEqual(results[0]['filesize'], 449)
105− self.assertEqual(results[0]['files'], 4)
106− self.assertEqual(results[0]['magnetlink'], 'magnet:?xt=urn:btih:MAGNETURL&dn=test')
107− self.assertEqual(results[0]['torrentfile'], 'http://torcache.net/torrent/53917.torrent?title=test')
108−
109− html = """
110− <table cellpadding="0" cellspacing="0" class="data" style="width: 100%">
111− <tr class="firstr">
112− <th class="width100perc nopad">torrent name</th>
113− <th class="center">
114− <a href="/search/test/?field=size&sorder=desc" rel="nofollow">size</a>
115− </th>
116− <th class="center"><span class="files">
117− <a href="/search/test/?field=files_count&sorder=desc" rel="nofollow">files</a></span>
118− </th>
119− <th class="center"><span>
120− <a href="/search/test/?field=time_add&sorder=desc" rel="nofollow">age</a></span>
121− </th>
122− <th class="center"><span class="seed">
123− <a href="/search/test/?field=seeders&sorder=desc" rel="nofollow">seed</a></span>
124− </th>
125− <th class="lasttd nobr center">
126− <a href="/search/test/?field=leechers&sorder=desc" rel="nofollow">leech</a>
127− </th>
128− </tr>
129− </table>
130− """
131− response = mock.Mock(text=html)
132− results = kickass.response(response)
133− self.assertEqual(type(results), list)
134− self.assertEqual(len(results), 0)
135−
136− html = """
137− <table cellpadding="0" cellspacing="0" class="data" style="width: 100%">
138− <tr class="firstr">
139− <th class="width100perc nopad">torrent name</th>
140− <th class="center">
141− <a href="/search/test/?field=size&sorder=desc" rel="nofollow">size</a>
142− </th>
143− <th class="center"><span class="files">
144− <a href="/search/test/?field=files_count&sorder=desc" rel="nofollow">files</a></span>
145− </th>
146− <th class="center"><span>
147− <a href="/search/test/?field=time_add&sorder=desc" rel="nofollow">age</a></span>
148− </th>
149− <th class="center"><span class="seed">
150− <a href="/search/test/?field=seeders&sorder=desc" rel="nofollow">seed</a></span>
151− </th>
152− <th class="lasttd nobr center">
153− <a href="/search/test/?field=leechers&sorder=desc" rel="nofollow">leech</a>
154− </th>
155− </tr>
156− <tr class="even" id="torrent_test6478745">
157− <td>
158− <div class="iaconbox center floatright">
159− <a rel="6478745,0" class="icommentjs icon16" href="/test-t6478745.html#comment">
160− <em style="font-size: 12px; margin: 0 4px 0 4px;" class="iconvalue">3</em>
161− <i class="ka ka-comment"></i>
162− </a>
163− <a class="iverify icon16" href="/test-t6478745.html" title="Verified Torrent">
164− <i class="ka ka16 ka-verify ka-green"></i>
165− </a>
166− <a href="#" onclick="_scq.push([]); return false;" class="partner1Button idownload icon16">
167− <i class="ka ka16 ka-arrow-down partner1Button"></i>
168− </a>
169− <a title="Torrent magnet link"
170− href="magnet:?xt=urn:btih:MAGNETURL&dn=test" class="imagnet icon16">
171− <i class="ka ka16 ka-magnet"></i>
172− </a>
173− <a title="Download torrent file"
174− href="http://torcache.net/torrent/53917.torrent?title=test" class="idownload icon16">
175− <i class="ka ka16 ka-arrow-down"></i>
176− </a>
177− </div>
178− <div class="torrentname">
179− <a href="/test-t6478745.html" class="torType txtType"></a>
180− <a href="/test-t6478745.html" class="normalgrey font12px plain bold"></a>
181− <div class="markeredBlock torType txtType">
182− <a href="/url.html" class="cellMainLink">
183− <strong class="red">This should be the title</strong>
184− </a>
185− <span class="font11px lightgrey block">
186− Posted by <i class="ka ka-verify" style="font-size: 16px;color:orange;"></i>
187− <a class="plain" href="/user/riri/">riri</a> in
188− <span id="cat_6478745">
189− <strong><a href="/other/">Other</a> > <a href="/unsorted/">Unsorted</a></strong>
190− </span>
191− </span>
192− </div>
193− </td>
194− <td class="nobr center">1 KiB</td>
195− <td class="center">4</td>
196− <td class="center">2&nbsp;years</td>
197− <td class="green center">10</td>
198− <td class="red lasttd center">1</td>
199− </tr>
200− <tr class="even" id="torrent_test6478745">
201− <td>
202− <div class="iaconbox center floatright">
203− <a rel="6478745,0" class="icommentjs icon16" href="/test-t6478745.html#comment">
204− <em style="font-size: 12px; margin: 0 4px 0 4px;" class="iconvalue">3</em>
205− <i class="ka ka-comment"></i>
206− </a>
207− <a class="iverify icon16" href="/test-t6478745.html" title="Verified Torrent">
208− <i class="ka ka16 ka-verify ka-green"></i>
209− </a>
210− <a href="#" onclick="_scq.push([]); return false;" class="partner1Button idownload icon16">
211− <i class="ka ka16 ka-arrow-down partner1Button"></i>
212− </a>
213− <a title="Torrent magnet link"
214− href="magnet:?xt=urn:btih:MAGNETURL&dn=test" class="imagnet icon16">
215− <i class="ka ka16 ka-magnet"></i>
216− </a>
217− <a title="Download torrent file"
218− href="http://torcache.net/torrent/53917.torrent?title=test" class="idownload icon16">
219− <i class="ka ka16 ka-arrow-down"></i>
220− </a>
221− </div>
222− <div class="torrentname">
223− <a href="/test-t6478745.html" class="torType txtType"></a>
224− <a href="/test-t6478745.html" class="normalgrey font12px plain bold"></a>
225− <div class="markeredBlock torType txtType">
226− <a href="/url.html" class="cellMainLink">
227− <strong class="red">This should be the title</strong>
228− </a>
229− <span class="font11px lightgrey block">
230− Posted by <i class="ka ka-verify" style="font-size: 16px;color:orange;"></i>
231− <a class="plain" href="/user/riri/">riri</a> in
232− <span id="cat_6478745">
233− <strong><a href="/other/">Other</a> > <a href="/unsorted/">Unsorted</a></strong>
234− </span>
235− </span>
236− </div>
237− </td>
238− <td class="nobr center">1 MiB</td>
239− <td class="center">4</td>
240− <td class="center">2&nbsp;years</td>
241− <td class="green center">9</td>
242− <td class="red lasttd center">1</td>
243− </tr>
244− <tr class="even" id="torrent_test6478745">
245− <td>
246− <div class="iaconbox center floatright">
247− <a rel="6478745,0" class="icommentjs icon16" href="/test-t6478745.html#comment">
248− <em style="font-size: 12px; margin: 0 4px 0 4px;" class="iconvalue">3</em>
249− <i class="ka ka-comment"></i>
250− </a>
251− <a class="iverify icon16" href="/test-t6478745.html" title="Verified Torrent">
252− <i class="ka ka16 ka-verify ka-green"></i>
253− </a>
254− <a href="#" onclick="_scq.push([]); return false;" class="partner1Button idownload icon16">
255− <i class="ka ka16 ka-arrow-down partner1Button"></i>
256− </a>
257− <a title="Torrent magnet link"
258− href="magnet:?xt=urn:btih:MAGNETURL&dn=test" class="imagnet icon16">
259− <i class="ka ka16 ka-magnet"></i>
260− </a>
261− <a title="Download torrent file"
262− href="http://torcache.net/torrent/53917.torrent?title=test" class="idownload icon16">
263− <i class="ka ka16 ka-arrow-down"></i>
264− </a>
265− </div>
266− <div class="torrentname">
267− <a href="/test-t6478745.html" class="torType txtType"></a>
268− <a href="/test-t6478745.html" class="normalgrey font12px plain bold"></a>
269− <div class="markeredBlock torType txtType">
270− <a href="/url.html" class="cellMainLink">
271− <strong class="red">This should be the title</strong>
272− </a>
273− <span class="font11px lightgrey block">
274− Posted by <i class="ka ka-verify" style="font-size: 16px;color:orange;"></i>
275− <a class="plain" href="/user/riri/">riri</a> in
276− <span id="cat_6478745">
277− <strong><a href="/other/">Other</a> > <a href="/unsorted/">Unsorted</a></strong>
278− </span>
279− </span>
280− </div>
281− </td>
282− <td class="nobr center">1 GiB</td>
283− <td class="center">4</td>
284− <td class="center">2&nbsp;years</td>
285− <td class="green center">8</td>
286− <td class="red lasttd center">1</td>
287− </tr>
288− <tr class="even" id="torrent_test6478745">
289− <td>
290− <div class="iaconbox center floatright">
291− <a rel="6478745,0" class="icommentjs icon16" href="/test-t6478745.html#comment">
292− <em style="font-size: 12px; margin: 0 4px 0 4px;" class="iconvalue">3</em>
293− <i class="ka ka-comment"></i>
294− </a>
295− <a class="iverify icon16" href="/test-t6478745.html" title="Verified Torrent">
296− <i class="ka ka16 ka-verify ka-green"></i>
297− </a>
298− <a href="#" onclick="_scq.push([]); return false;" class="partner1Button idownload icon16">
299− <i class="ka ka16 ka-arrow-down partner1Button"></i>
300− </a>
301− <a title="Torrent magnet link"
302− href="magnet:?xt=urn:btih:MAGNETURL&dn=test" class="imagnet icon16">
303− <i class="ka ka16 ka-magnet"></i>
304− </a>
305− <a title="Download torrent file"
306− href="http://torcache.net/torrent/53917.torrent?title=test" class="idownload icon16">
307− <i class="ka ka16 ka-arrow-down"></i>
308− </a>
309− </div>
310− <div class="torrentname">
311− <a href="/test-t6478745.html" class="torType txtType"></a>
312− <a href="/test-t6478745.html" class="normalgrey font12px plain bold"></a>
313− <div class="markeredBlock torType txtType">
314− <a href="/url.html" class="cellMainLink">
315− <strong class="red">This should be the title</strong>
316− </a>
317− <span class="font11px lightgrey block">
318− Posted by <i class="ka ka-verify" style="font-size: 16px;color:orange;"></i>
319− <a class="plain" href="/user/riri/">riri</a> in
320− <span id="cat_6478745">
321− <strong><a href="/other/">Other</a> > <a href="/unsorted/">Unsorted</a></strong>
322− </span>
323− </span>
324− </div>
325− </td>
326− <td class="nobr center">1 TiB</td>
327− <td class="center">4</td>
328− <td class="center">2&nbsp;years</td>
329− <td class="green center">7</td>
330− <td class="red lasttd center">1</td>
331− </tr>
332− <tr class="even" id="torrent_test6478745">
333− <td>
334− <div class="iaconbox center floatright">
335− <a rel="6478745,0" class="icommentjs icon16" href="/test-t6478745.html#comment">
336− <em style="font-size: 12px; margin: 0 4px 0 4px;" class="iconvalue">3</em>
337− <i class="ka ka-comment"></i>
338− </a>
339− <a class="iverify icon16" href="/test-t6478745.html" title="Verified Torrent">
340− <i class="ka ka16 ka-verify ka-green"></i>
341− </a>
342− <a href="#" onclick="_scq.push([]); return false;" class="partner1Button idownload icon16">
343− <i class="ka ka16 ka-arrow-down partner1Button"></i>
344− </a>
345− <a title="Torrent magnet link"
346− href="magnet:?xt=urn:btih:MAGNETURL&dn=test" class="imagnet icon16">
347− <i class="ka ka16 ka-magnet"></i>
348− </a>
349− <a title="Download torrent file"
350− href="http://torcache.net/torrent/53917.torrent?title=test" class="idownload icon16">
351− <i class="ka ka16 ka-arrow-down"></i>
352− </a>
353− </div>
354− <div class="torrentname">
355− <a href="/test-t6478745.html" class="torType txtType"></a>
356− <a href="/test-t6478745.html" class="normalgrey font12px plain bold"></a>
357− <div class="markeredBlock torType txtType">
358− <a href="/url.html" class="cellMainLink">
359− <strong class="red">This should be the title</strong>
360− </a>
361− <span class="font11px lightgrey block">
362− Posted by <i class="ka ka-verify" style="font-size: 16px;color:orange;"></i>
363− <a class="plain" href="/user/riri/">riri</a> in
364− <span id="cat_6478745">
365− <strong><a href="/other/">Other</a> > <a href="/unsorted/">Unsorted</a></strong>
366− </span>
367− </span>
368− </div>
369− </td>
370− <td class="nobr center">z bytes</td>
371− <td class="center">r</td>
372− <td class="center">2&nbsp;years</td>
373− <td class="green center">a</td>
374− <td class="red lasttd center">t</td>
375− </tr>
376− </table>
377− """
378− response = mock.Mock(text=html)
379− results = kickass.response(response)
380− self.assertEqual(type(results), list)
381− self.assertEqual(len(results), 5)
382− self.assertEqual(results[0]['title'], 'This should be the title')
383− self.assertEqual(results[0]['url'], 'https://kickass.cd/url.html')
384− self.assertEqual(results[0]['content'], 'Posted by riri in Other > Unsorted')
385− self.assertEqual(results[0]['seed'], 10)
386− self.assertEqual(results[0]['leech'], 1)
387− self.assertEqual(results[0]['files'], 4)
388− self.assertEqual(results[0]['magnetlink'], 'magnet:?xt=urn:btih:MAGNETURL&dn=test')
389− self.assertEqual(results[0]['torrentfile'], 'http://torcache.net/torrent/53917.torrent?title=test')
390− self.assertEqual(results[0]['filesize'], 1000)
391− self.assertEqual(results[1]['filesize'], 1000000)
392− self.assertEqual(results[2]['filesize'], 1000000000)
393− self.assertEqual(results[3]['filesize'], 1000000000000)
394− self.assertEqual(results[4]['seed'], 0)
395− self.assertEqual(results[4]['leech'], 0)
396− self.assertEqual(results[4]['files'], None)
397− self.assertEqual(results[4]['filesize'], None)
tests/unit/engines/test_mediawiki.pydeleted−130
@@ -1,130 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import mediawiki
5−from searx.testing import SearxTestCase
6−
7−
8−class TestMediawikiEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− dicto['language'] = 'fr_FR'
15− params = mediawiki.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn(query, params['url'])
18− self.assertIn('wikipedia.org', params['url'])
19− self.assertIn('fr', params['url'])
20−
21− dicto['language'] = 'all'
22− params = mediawiki.request(query, dicto)
23− self.assertIn('en', params['url'])
24−
25− mediawiki.base_url = "http://test.url/"
26− mediawiki.search_url = mediawiki.base_url +\
27− 'w/api.php?action=query'\
28− '&list=search'\
29− '&{query}'\
30− '&srprop=timestamp'\
31− '&format=json'\
32− '&sroffset={offset}'\
33− '&srlimit={limit}' # noqa
34− params = mediawiki.request(query, dicto)
35− self.assertIn('test.url', params['url'])
36−
37− def test_response(self):
38− dicto = defaultdict(dict)
39− dicto['language'] = 'fr'
40− mediawiki.base_url = "https://{language}.wikipedia.org/"
41−
42− self.assertRaises(AttributeError, mediawiki.response, None)
43− self.assertRaises(AttributeError, mediawiki.response, [])
44− self.assertRaises(AttributeError, mediawiki.response, '')
45− self.assertRaises(AttributeError, mediawiki.response, '[]')
46−
47− response = mock.Mock(text='{}', search_params=dicto)
48− self.assertEqual(mediawiki.response(response), [])
49−
50− response = mock.Mock(text='{"data": []}', search_params=dicto)
51− self.assertEqual(mediawiki.response(response), [])
52−
53− json = """
54− {
55− "query-continue": {
56− "search": {
57− "sroffset": 1
58− }
59− },
60− "query": {
61− "searchinfo": {
62− "totalhits": 29721
63− },
64− "search": [
65− {
66− "ns": 0,
67− "title": "This is the title étude",
68− "timestamp": "2014-12-19T17:42:52Z"
69− }
70− ]
71− }
72− }
73− """
74− response = mock.Mock(text=json, search_params=dicto)
75− results = mediawiki.response(response)
76− self.assertEqual(type(results), list)
77− self.assertEqual(len(results), 1)
78− self.assertEqual(results[0]['title'], u'This is the title étude')
79− self.assertIn('fr.wikipedia.org', results[0]['url'])
80− self.assertIn('This_is_the_title', results[0]['url'])
81− self.assertIn('%C3%A9tude', results[0]['url'])
82− self.assertEqual(results[0]['content'], '')
83−
84− json = """
85− {
86− "query-continue": {
87− "search": {
88− "sroffset": 1
89− }
90− },
91− "query": {
92− "searchinfo": {
93− "totalhits": 29721
94− },
95− "search": [
96− ]
97− }
98− }
99− """
100− response = mock.Mock(text=json, search_params=dicto)
101− results = mediawiki.response(response)
102− self.assertEqual(type(results), list)
103− self.assertEqual(len(results), 0)
104−
105− json = """
106− {
107− "query-continue": {
108− "search": {
109− "sroffset": 1
110− }
111− },
112− "query": {
113− }
114− }
115− """
116− response = mock.Mock(text=json, search_params=dicto)
117− results = mediawiki.response(response)
118− self.assertEqual(type(results), list)
119− self.assertEqual(len(results), 0)
120−
121− json = r"""
122− {"toto":[
123− {"id":200,"name":"Artist Name",
124− "link":"http:\/\/www.mediawiki.com\/artist\/1217","type":"artist"}
125− ]}
126− """
127− response = mock.Mock(text=json, search_params=dicto)
128− results = mediawiki.response(response)
129− self.assertEqual(type(results), list)
130− self.assertEqual(len(results), 0)
tests/unit/engines/test_mixcloud.pydeleted−67
@@ -1,67 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import mixcloud
4−from searx.testing import SearxTestCase
5−
6−
7−class TestMixcloudEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = mixcloud.request(query, dicto)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('mixcloud.com' in params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, mixcloud.response, None)
20− self.assertRaises(AttributeError, mixcloud.response, [])
21− self.assertRaises(AttributeError, mixcloud.response, '')
22− self.assertRaises(AttributeError, mixcloud.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(mixcloud.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(mixcloud.response(response), [])
29−
30− json = """
31− {"data":[
32− {
33− "user": {
34− "url": "http://www.mixcloud.com/user/",
35− "username": "user",
36− "name": "User",
37− "key": "/user/"
38− },
39− "key": "/user/this-is-the-url/",
40− "created_time": "2014-11-14T13:30:02Z",
41− "audio_length": 3728,
42− "slug": "this-is-the-url",
43− "name": "Title of track",
44− "url": "http://www.mixcloud.com/user/this-is-the-url/",
45− "updated_time": "2014-11-14T13:14:10Z"
46− }
47− ]}
48− """
49− response = mock.Mock(text=json)
50− results = mixcloud.response(response)
51− self.assertEqual(type(results), list)
52− self.assertEqual(len(results), 1)
53− self.assertEqual(results[0]['title'], 'Title of track')
54− self.assertEqual(results[0]['url'], 'http://www.mixcloud.com/user/this-is-the-url/')
55− self.assertEqual(results[0]['content'], 'User')
56− self.assertTrue('http://www.mixcloud.com/user/this-is-the-url/' in results[0]['embedded'])
57−
58− json = r"""
59− {"toto":[
60− {"id":200,"name":"Artist Name",
61− "link":"http:\/\/www.mixcloud.com\/artist\/1217","type":"artist"}
62− ]}
63− """
64− response = mock.Mock(text=json)
65− results = mixcloud.response(response)
66− self.assertEqual(type(results), list)
67− self.assertEqual(len(results), 0)
tests/unit/engines/test_nyaa.pydeleted−124
@@ -1,124 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import nyaa
4−from searx.testing import SearxTestCase
5−
6−
7−class TestNyaaEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dic = defaultdict(dict)
12− dic['pageno'] = 1
13− params = nyaa.request(query, dic)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('nyaa.si' in params['url'])
17−
18− def test_response(self):
19− resp = mock.Mock(text='<html></html>')
20− self.assertEqual(nyaa.response(resp), [])
21−
22− html = """
23− <table class="table table-bordered table-hover table-striped torrent-list">
24− <thead>
25− <tr>
26− <th class="hdr-category text-center" style="width:80px;">
27− <div>Category</div>
28− </th>
29− <th class="hdr-name" style="width:auto;">
30− <div>Name</div>
31− </th>
32− <th class="hdr-comments sorting text-center" title="Comments" style="width:50px;">
33− <a href="/?f=0&amp;c=0_0&amp;q=Death+Parade&amp;s=comments&amp;o=desc"></a>
34− <i class="fa fa-comments-o"></i>
35− </th>
36− <th class="hdr-link text-center" style="width:70px;">
37− <div>Link</div>
38− </th>
39− <th class="hdr-size sorting text-center" style="width:100px;">
40− <a href="/?f=0&amp;c=0_0&amp;q=Death+Parade&amp;s=size&amp;o=desc"></a>
41− <div>Size</div>
42− </th>
43− <th class="hdr-date sorting_desc text-center" title="In local time" style="width:140px;">
44− <a href="/?f=0&amp;c=0_0&amp;q=Death+Parade&amp;s=id&amp;o=asc"></a>
45− <div>Date</div>
46− </th>
47− <th class="hdr-seeders sorting text-center" title="Seeders" style="width:50px;">
48− <a href="/?f=0&amp;c=0_0&amp;q=Death+Parade&amp;s=seeders&amp;o=desc"></a>
49− <i class="fa fa-arrow-up" aria-hidden="true"></i>
50− </th>
51− <th class="hdr-leechers sorting text-center" title="Leechers" style="width:50px;">
52− <a href="/?f=0&amp;c=0_0&amp;q=Death+Parade&amp;s=leechers&amp;o=desc"></a>
53− <i class="fa fa-arrow-down" aria-hidden="true"></i>
54− </th>
55− <th class="hdr-downloads sorting text-center" title="Completed downloads" style="width:50px;">
56− <a href="/?f=0&amp;c=0_0&amp;q=Death+Parade&amp;s=downloads&amp;o=desc"></a>
57− <i class="fa fa-check" aria-hidden="true"></i>
58− </th>
59− </tr>
60− </thead>
61− <tbody>
62− <tr class="default">
63− <td style="padding:0 4px;">
64− <a href="/?c=1_2" title="Anime - English-translated">
65− <img src="/static/img/icons/nyaa/1_2.png" alt="Anime - English-translated">
66− </a>
67− </td>
68− <td colspan="2">
69− <a href="/view/1" title="Sample title 1">Sample title 1</a>
70− </td>
71− <td class="text-center" style="white-space: nowrap;">
72− <a href="/download/1.torrent"><i class="fa fa-fw fa-download"></i></a>
73− <a href="magnet:?xt=urn:btih:2"><i class="fa fa-fw fa-magnet"></i></a>
74− </td>
75− <td class="text-center">723.7 MiB</td>
76− <td class="text-center" data-timestamp="1503307456" title="1 week 3
77− days 9 hours 44 minutes 39 seconds ago">2017-08-21 11:24</td>
78− <td class="text-center" style="color: green;">1</td>
79− <td class="text-center" style="color: red;">3</td>
80− <td class="text-center">12</td>
81− </tr>
82− <tr class="default">
83− <td style="padding:0 4px;">
84− <a href="/?c=1_2" title="Anime - English-translated">
85− <img src="/static/img/icons/nyaa/1_2.png" alt="Anime - English-translated">
86− </a>
87− </td>
88− <td colspan="2">
89− <a href="/view/2" title="Sample title 2">Sample title 2</a>
90− </td>
91− <td class="text-center" style="white-space: nowrap;">
92− <a href="magnet:?xt=urn:btih:2"><i class="fa fa-fw fa-magnet"></i></a>
93− </td>
94− <td class="text-center">8.2 GiB</td>
95− <td class="text-center" data-timestamp="1491608400" title="4 months 3
96− weeks 4 days 19 hours 28 minutes 55 seconds ago">2017-04-08 01:40</td>
97− <td class="text-center" style="color: green;">10</td>
98− <td class="text-center" style="color: red;">1</td>
99− <td class="text-center">206</td>
100− </tr>
101− </tbody>
102− </table>
103− """
104−
105− resp = mock.Mock(text=html)
106− results = nyaa.response(resp)
107−
108− self.assertEqual(type(results), list)
109− self.assertEqual(len(results), 2)
110−
111− r = results[0]
112− self.assertTrue(r['url'].find('1') >= 0)
113− self.assertTrue(r['torrentfile'].find('1.torrent') >= 0)
114− self.assertTrue(r['content'].find('Anime - English-translated') >= 0)
115− self.assertTrue(r['content'].find('Downloaded 12 times.') >= 0)
116−
117− self.assertEqual(r['title'], 'Sample title 1')
118− self.assertEqual(r['seed'], 1)
119− self.assertEqual(r['leech'], 3)
120− self.assertEqual(r['filesize'], 723700000)
121−
122− r = results[1]
123− self.assertTrue(r['url'].find('2') >= 0)
124− self.assertTrue(r['magnetlink'].find('magnet:') >= 0)
tests/unit/engines/test_openstreetmap.pydeleted−199
@@ -1,199 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import openstreetmap
5−from searx.testing import SearxTestCase
6−
7−
8−class TestOpenstreetmapEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = openstreetmap.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn(query, params['url'])
17− self.assertIn('openstreetmap.org', params['url'])
18−
19− def test_response(self):
20− self.assertRaises(AttributeError, openstreetmap.response, None)
21− self.assertRaises(AttributeError, openstreetmap.response, [])
22− self.assertRaises(AttributeError, openstreetmap.response, '')
23− self.assertRaises(AttributeError, openstreetmap.response, '[]')
24−
25− response = mock.Mock(text='{}')
26− self.assertEqual(openstreetmap.response(response), [])
27−
28− response = mock.Mock(text='{"data": []}')
29− self.assertEqual(openstreetmap.response(response), [])
30−
31− json = """
32− [
33− {
34− "place_id": "127732055",
35− "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
36− "osm_type": "relation",
37− "osm_id": "7444",
38− "boundingbox": [
39− "48.8155755",
40− "48.902156",
41− "2.224122",
42− "2.4697602"
43− ],
44− "lat": "48.8565056",
45− "lon": "2.3521334",
46− "display_name": "This is the title",
47− "class": "place",
48− "type": "city",
49− "importance": 0.96893459932191,
50− "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
51− "address": {
52− "city": "Paris",
53− "county": "Paris",
54− "state": "Île-de-France",
55− "country": "France",
56− "country_code": "fr"
57− },
58− "geojson": {
59− "type": "Polygon",
60− "coordinates": [
61− [
62− [
63− 2.224122,
64− 48.854199
65− ]
66− ]
67− ]
68− }
69− }
70− ]
71− """
72− response = mock.Mock(text=json)
73− results = openstreetmap.response(response)
74− self.assertEqual(type(results), list)
75− self.assertEqual(len(results), 1)
76− self.assertEqual(results[0]['title'], 'This is the title')
77− self.assertEqual(results[0]['url'], 'https://openstreetmap.org/relation/7444')
78− self.assertIn('coordinates', results[0]['geojson'])
79− self.assertEqual(results[0]['geojson']['coordinates'][0][0][0], 2.224122)
80− self.assertEqual(results[0]['geojson']['coordinates'][0][0][1], 48.854199)
81− self.assertEqual(results[0]['address'], None)
82− self.assertIn('48.8155755', results[0]['boundingbox'])
83− self.assertIn('48.902156', results[0]['boundingbox'])
84− self.assertIn('2.224122', results[0]['boundingbox'])
85− self.assertIn('2.4697602', results[0]['boundingbox'])
86−
87− json = """
88− [
89− {
90− "place_id": "127732055",
91− "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
92− "osm_type": "relation",
93− "osm_id": "7444",
94− "boundingbox": [
95− "48.8155755",
96− "48.902156",
97− "2.224122",
98− "2.4697602"
99− ],
100− "lat": "48.8565056",
101− "lon": "2.3521334",
102− "display_name": "This is the title",
103− "class": "tourism",
104− "type": "city",
105− "importance": 0.96893459932191,
106− "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
107− "address": {
108− "city": "Paris",
109− "county": "Paris",
110− "state": "Île-de-France",
111− "country": "France",
112− "country_code": "fr",
113− "address29": "Address"
114− },
115− "geojson": {
116− "type": "Polygon",
117− "coordinates": [
118− [
119− [
120− 2.224122,
121− 48.854199
122− ]
123− ]
124− ]
125− }
126− },
127− {
128− "place_id": "127732055",
129− "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
130− "osm_type": "relation",
131− "osm_id": "7444",
132− "boundingbox": [
133− "48.8155755",
134− "48.902156",
135− "2.224122",
136− "2.4697602"
137− ],
138− "lat": "48.8565056",
139− "lon": "2.3521334",
140− "display_name": "This is the title",
141− "class": "tourism",
142− "type": "city",
143− "importance": 0.96893459932191,
144− "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
145− "address": {
146− "city": "Paris",
147− "county": "Paris",
148− "state": "Île-de-France",
149− "country": "France",
150− "postcode": 75000,
151− "country_code": "fr"
152− },
153− "geojson": {
154− "type": "Polygon",
155− "coordinates": [
156− [
157− [
158− 2.224122,
159− 48.854199
160− ]
161− ]
162− ]
163− }
164− },
165− {
166− "place_id": "127732055",
167− "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
168− "osm_type": "node",
169− "osm_id": "7444",
170− "boundingbox": [
171− "48.8155755",
172− "48.902156",
173− "2.224122",
174− "2.4697602"
175− ],
176− "lat": "48.8565056",
177− "lon": "2.3521334",
178− "display_name": "This is the title",
179− "class": "tourism",
180− "type": "city",
181− "importance": 0.96893459932191,
182− "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
183− "address": {
184− "city": "Paris",
185− "county": "Paris",
186− "state": "Île-de-France",
187− "country": "France",
188− "country_code": "fr",
189− "address29": "Address"
190− }
191− }
192− ]
193− """
194− response = mock.Mock(text=json)
195− results = openstreetmap.response(response)
196− self.assertEqual(type(results), list)
197− self.assertEqual(len(results), 3)
198− self.assertIn('48.8565056', results[2]['geojson']['coordinates'])
199− self.assertIn('2.3521334', results[2]['geojson']['coordinates'])
tests/unit/engines/test_pdbe.pydeleted−109
@@ -1,109 +0,0 @@
1−import mock
2−from collections import defaultdict
3−from searx.engines import pdbe
4−from searx.testing import SearxTestCase
5−
6−
7−class TestPdbeEngine(SearxTestCase):
8− def test_request(self):
9− query = 'test_query'
10− dicto = defaultdict(dict)
11− params = pdbe.request(query, dicto)
12− self.assertTrue('url' in params)
13− self.assertTrue('ebi.ac.uk' in params['url'])
14− self.assertTrue('data' in params)
15− self.assertTrue('q' in params['data'])
16− self.assertTrue(query in params['data']['q'])
17− self.assertTrue('wt' in params['data'])
18− self.assertTrue('json' in params['data']['wt'])
19− self.assertTrue('method' in params)
20− self.assertTrue(params['method'] == 'POST')
21−
22− def test_response(self):
23− self.assertRaises(AttributeError, pdbe.response, None)
24− self.assertRaises(AttributeError, pdbe.response, [])
25− self.assertRaises(AttributeError, pdbe.response, '')
26− self.assertRaises(AttributeError, pdbe.response, '[]')
27−
28− json = """
29−{
30− "response": {
31− "docs": [
32− {
33− "citation_title": "X-ray crystal structure of ferric Aplysia limacina myoglobin in different liganded states.",
34− "citation_year": 1993,
35− "entry_author_list": [
36− "Conti E, Moser C, Rizzi M, Mattevi A, Lionetti C, Coda A, Ascenzi P, Brunori M, Bolognesi M"
37− ],
38− "journal": "J. Mol. Biol.",
39− "journal_page": "498-508",
40− "journal_volume": "233",
41− "pdb_id": "2fal",
42− "status": "REL",
43− "title": "X-RAY CRYSTAL STRUCTURE OF FERRIC APLYSIA LIMACINA MYOGLOBIN IN DIFFERENT LIGANDED STATES"
44− }
45− ],
46− "numFound": 1,
47− "start": 0
48− },
49− "responseHeader": {
50− "QTime": 0,
51− "params": {
52− "q": "2fal",
53− "wt": "json"
54− },
55− "status": 0
56− }
57−}
58−"""
59−
60− response = mock.Mock(text=json)
61− results = pdbe.response(response)
62− self.assertEqual(type(results), list)
63− self.assertEqual(len(results), 1)
64− self.assertEqual(results[0]['title'],
65− 'X-RAY CRYSTAL STRUCTURE OF FERRIC APLYSIA LIMACINA MYOGLOBIN IN DIFFERENT LIGANDED STATES')
66− self.assertEqual(results[0]['url'], pdbe.pdbe_entry_url.format(pdb_id='2fal'))
67− self.assertEqual(results[0]['img_src'], pdbe.pdbe_preview_url.format(pdb_id='2fal'))
68− self.assertTrue('Conti E' in results[0]['content'])
69− self.assertTrue('X-ray crystal structure of ferric Aplysia limacina myoglobin in different liganded states.' in
70− results[0]['content'])
71− self.assertTrue('1993' in results[0]['content'])
72−
73− # Testing proper handling of PDB entries marked as obsolete
74− json = """
75−{
76− "response": {
77− "docs": [
78− {
79− "citation_title": "Obsolete entry test",
80− "citation_year": 2016,
81− "entry_author_list": ["Doe J"],
82− "journal": "J. Obs.",
83− "journal_page": "1-2",
84− "journal_volume": "1",
85− "pdb_id": "xxxx",
86− "status": "OBS",
87− "title": "OBSOLETE ENTRY TEST",
88− "superseded_by": "yyyy"
89− }
90− ],
91− "numFound": 1,
92− "start": 0
93− },
94− "responseHeader": {
95− "QTime": 0,
96− "params": {
97− "q": "xxxx",
98− "wt": "json"
99− },
100− "status": 0
101− }
102−}
103−"""
104− response = mock.Mock(text=json)
105− results = pdbe.response(response)
106− self.assertEqual(type(results), list)
107− self.assertEqual(len(results), 1)
108− self.assertEqual(results[0]['title'], 'OBSOLETE ENTRY TEST (OBSOLETE)')
109− self.assertTrue(results[0]['content'].startswith('This entry has been superseded by'))
tests/unit/engines/test_photon.pydeleted−166
@@ -1,166 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import photon
5−from searx.testing import SearxTestCase
6−
7−
8−class TestPhotonEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− dicto['language'] = 'all'
15− params = photon.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn(query, params['url'])
18− self.assertIn('photon.komoot.de', params['url'])
19−
20− dicto['language'] = 'all'
21− params = photon.request(query, dicto)
22− self.assertNotIn('lang', params['url'])
23−
24− dicto['language'] = 'al'
25− params = photon.request(query, dicto)
26− self.assertNotIn('lang', params['url'])
27−
28− dicto['language'] = 'fr'
29− params = photon.request(query, dicto)
30− self.assertIn('fr', params['url'])
31−
32− def test_response(self):
33− self.assertRaises(AttributeError, photon.response, None)
34− self.assertRaises(AttributeError, photon.response, [])
35− self.assertRaises(AttributeError, photon.response, '')
36− self.assertRaises(AttributeError, photon.response, '[]')
37−
38− response = mock.Mock(text='{}')
39− self.assertEqual(photon.response(response), [])
40−
41− response = mock.Mock(text='{"data": []}')
42− self.assertEqual(photon.response(response), [])
43−
44− json = """
45− {
46− "features": [
47− {
48− "properties": {
49− "osm_key": "waterway",
50− "extent": [
51− -1.4508446,
52− 51.1614997,
53− -1.4408036,
54− 51.1525635
55− ],
56− "name": "This is the title",
57− "state": "England",
58− "osm_id": 114823817,
59− "osm_type": "W",
60− "osm_value": "river",
61− "city": "Test Valley",
62− "country": "United Kingdom"
63− },
64− "type": "Feature",
65− "geometry": {
66− "type": "Point",
67− "coordinates": [
68− -1.4458571,
69− 51.1576661
70− ]
71− }
72− },
73− {
74− "properties": {
75− "osm_key": "place",
76− "street": "Rue",
77− "state": "Ile-de-France",
78− "osm_id": 129211377,
79− "osm_type": "R",
80− "housenumber": "10",
81− "postcode": "75011",
82− "osm_value": "house",
83− "city": "Paris",
84− "country": "France"
85− },
86− "type": "Feature",
87− "geometry": {
88− "type": "Point",
89− "coordinates": [
90− 2.3725025,
91− 48.8654481
92− ]
93− }
94− },
95− {
96− "properties": {
97− "osm_key": "amenity",
98− "street": "Allée",
99− "name": "Bibliothèque",
100− "state": "Ile-de-France",
101− "osm_id": 1028573132,
102− "osm_type": "N",
103− "postcode": "75001",
104− "osm_value": "library",
105− "city": "Paris",
106− "country": "France"
107− },
108− "type": "Feature",
109− "geometry": {
110− "type": "Point",
111− "coordinates": [
112− 2.3445634,
113− 48.862494
114− ]
115− }
116− },
117− {
118− "properties": {
119− "osm_key": "amenity",
120− "osm_id": 1028573132,
121− "osm_type": "Y",
122− "postcode": "75001",
123− "osm_value": "library",
124− "city": "Paris",
125− "country": "France"
126− },
127− "type": "Feature",
128− "geometry": {
129− "type": "Point",
130− "coordinates": [
131− 2.3445634,
132− 48.862494
133− ]
134− }
135− },
136− {
137− }
138− ],
139− "type": "FeatureCollection"
140− }
141− """
142− response = mock.Mock(text=json)
143− results = photon.response(response)
144− self.assertEqual(type(results), list)
145− self.assertEqual(len(results), 3)
146− self.assertEqual(results[0]['title'], 'This is the title')
147− self.assertEqual(results[0]['content'], '')
148− self.assertEqual(results[0]['longitude'], -1.4458571)
149− self.assertEqual(results[0]['latitude'], 51.1576661)
150− self.assertIn(-1.4508446, results[0]['boundingbox'])
151− self.assertIn(51.1614997, results[0]['boundingbox'])
152− self.assertIn(-1.4408036, results[0]['boundingbox'])
153− self.assertIn(51.1525635, results[0]['boundingbox'])
154− self.assertIn('type', results[0]['geojson'])
155− self.assertEqual(results[0]['geojson']['type'], 'Point')
156− self.assertEqual(results[0]['address'], None)
157− self.assertEqual(results[0]['osm']['type'], 'way')
158− self.assertEqual(results[0]['osm']['id'], 114823817)
159− self.assertEqual(results[0]['url'], 'https://openstreetmap.org/way/114823817')
160− self.assertEqual(results[1]['osm']['type'], 'relation')
161− self.assertEqual(results[2]['address']['name'], u'Bibliothèque')
162− self.assertEqual(results[2]['address']['house_number'], None)
163− self.assertEqual(results[2]['address']['locality'], 'Paris')
164− self.assertEqual(results[2]['address']['postcode'], '75001')
165− self.assertEqual(results[2]['address']['country'], 'France')
166− self.assertEqual(results[2]['osm']['type'], 'node')
tests/unit/engines/test_piratebay.pydeleted−166
@@ -1,166 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import piratebay
5−from searx.testing import SearxTestCase
6−
7−
8−class TestPiratebayEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− dicto['category'] = 'Toto'
15− params = piratebay.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn(query, params['url'])
18− self.assertIn('piratebay.org', params['url'])
19− self.assertIn('0', params['url'])
20−
21− dicto['category'] = 'music'
22− params = piratebay.request(query, dicto)
23− self.assertIn('100', params['url'])
24−
25− def test_response(self):
26− self.assertRaises(AttributeError, piratebay.response, None)
27− self.assertRaises(AttributeError, piratebay.response, [])
28− self.assertRaises(AttributeError, piratebay.response, '')
29− self.assertRaises(AttributeError, piratebay.response, '[]')
30−
31− response = mock.Mock(text='<html></html>')
32− self.assertEqual(piratebay.response(response), [])
33−
34− html = """
35− <table id="searchResult">
36− <tr>
37− </tr>
38− <tr>
39− <td class="vertTh">
40− <center>
41− <a href="#" title="More from this category">Anime</a><br/>
42− (<a href="#" title="More from this category">Anime</a>)
43− </center>
44− </td>
45− <td>
46− <div class="detName">
47− <a href="/this.is.the.link" class="detLink" title="Title">
48− This is the title
49− </a>
50− </div>
51− <a href="magnet:?xt=urn:btih:MAGNETLINK" title="Download this torrent using magnet">
52− <img src="/static/img/icon-magnet.gif" alt="Magnet link"/>
53− </a>
54− <a href="http://torcache.net/torrent/TORRENTFILE.torrent" title="Download this torrent">
55− <img src="/static/img/dl.gif" class="dl" alt="Download"/>
56− </a>
57− <a href="/user/HorribleSubs">
58− <img src="/static/img/vip.gif" alt="VIP" title="VIP" style="width:11px;" border='0'/>
59− </a>
60− <img src="/static/img/11x11p.png"/>
61− <font class="detDesc">
62− This is the content <span>and should be</span> OK
63− </font>
64− </td>
65− <td align="right">13</td>
66− <td align="right">334</td>
67− </tr>
68− <tr>
69− <td class="vertTh">
70− <center>
71− <a href="#" title="More from this category">Anime</a><br/>
72− (<a href="#" title="More from this category">Anime</a>)
73− </center>
74− </td>
75− <td>
76− <div class="detName">
77− <a href="/this.is.the.link" class="detLink" title="Title">
78− This is the title
79− </a>
80− </div>
81− <a href="magnet:?xt=urn:btih:MAGNETLINK" title="Download this torrent using magnet">
82− <img src="/static/img/icon-magnet.gif" alt="Magnet link"/>
83− </a>
84− <a href="/user/HorribleSubs">
85− <img src="/static/img/vip.gif" alt="VIP" title="VIP" style="width:11px;" border='0'/>
86− </a>
87− <img src="/static/img/11x11p.png"/>
88− <font class="detDesc">
89− This is the content <span>and should be</span> OK
90− </font>
91− </td>
92− <td align="right">13</td>
93− <td align="right">334</td>
94− </tr>
95− </table>
96− """
97− response = mock.Mock(text=html)
98− results = piratebay.response(response)
99− self.assertEqual(type(results), list)
100− self.assertEqual(len(results), 2)
101− self.assertEqual(results[0]['title'], 'This is the title')
102− self.assertEqual(results[0]['url'], 'https://thepiratebay.org/this.is.the.link')
103− self.assertEqual(results[0]['content'], 'This is the content and should be OK')
104− self.assertEqual(results[0]['seed'], 13)
105− self.assertEqual(results[0]['leech'], 334)
106− self.assertEqual(results[0]['magnetlink'], 'magnet:?xt=urn:btih:MAGNETLINK')
107− self.assertEqual(results[0]['torrentfile'], 'http://torcache.net/torrent/TORRENTFILE.torrent')
108−
109− self.assertEqual(results[1]['torrentfile'], None)
110−
111− html = """
112− <table id="searchResult">
113− <tr>
114− </tr>
115− <tr>
116− <td class="vertTh">
117− <center>
118− <a href="#" title="More from this category">Anime</a><br/>
119− (<a href="#" title="More from this category">Anime</a>)
120− </center>
121− </td>
122− <td>
123− <div class="detName">
124− <a href="/this.is.the.link" class="detLink" title="Title">
125− This is the title
126− </a>
127− </div>
128− <a href="magnet:?xt=urn:btih:MAGNETLINK" title="Download this torrent using magnet">
129− <img src="/static/img/icon-magnet.gif" alt="Magnet link"/>
130− </a>
131− <a href="http://torcache.net/torrent/TORRENTFILE.torrent" title="Download this torrent">
132− <img src="/static/img/dl.gif" class="dl" alt="Download"/>
133− </a>
134− <a href="/user/HorribleSubs">
135− <img src="/static/img/vip.gif" alt="VIP" title="VIP" style="width:11px;" border='0'/>
136− </a>
137− <img src="/static/img/11x11p.png"/>
138− <font class="detDesc">
139− This is the content <span>and should be</span> OK
140− </font>
141− </td>
142− <td align="right">s</td>
143− <td align="right">d</td>
144− </tr>
145− </table>
146− """
147− response = mock.Mock(text=html)
148− results = piratebay.response(response)
149− self.assertEqual(type(results), list)
150− self.assertEqual(len(results), 1)
151− self.assertEqual(results[0]['title'], 'This is the title')
152− self.assertEqual(results[0]['url'], 'https://thepiratebay.org/this.is.the.link')
153− self.assertEqual(results[0]['content'], 'This is the content and should be OK')
154− self.assertEqual(results[0]['seed'], 0)
155− self.assertEqual(results[0]['leech'], 0)
156− self.assertEqual(results[0]['magnetlink'], 'magnet:?xt=urn:btih:MAGNETLINK')
157− self.assertEqual(results[0]['torrentfile'], 'http://torcache.net/torrent/TORRENTFILE.torrent')
158−
159− html = """
160− <table id="searchResult">
161− </table>
162− """
163− response = mock.Mock(text=html)
164− results = piratebay.response(response)
165− self.assertEqual(type(results), list)
166− self.assertEqual(len(results), 0)
tests/unit/engines/test_qwant.pydeleted−339
@@ -1,339 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import qwant
4−from searx.testing import SearxTestCase
5−
6−
7−class TestQwantEngine(SearxTestCase):
8−
9− def test_request(self):
10− qwant.supported_languages = ['en-US', 'fr-CA', 'fr-FR']
11− qwant.language_aliases = {}
12− query = 'test_query'
13− dicto = defaultdict(dict)
14− dicto['pageno'] = 0
15− dicto['language'] = 'fr-FR'
16− qwant.categories = ['']
17− params = qwant.request(query, dicto)
18− self.assertIn('url', params)
19− self.assertIn(query, params['url'])
20− self.assertIn('web', params['url'])
21− self.assertIn('qwant.com', params['url'])
22− self.assertIn('fr_fr', params['url'])
23−
24− dicto['language'] = 'all'
25− qwant.categories = ['news']
26− params = qwant.request(query, dicto)
27− self.assertFalse('fr' in params['url'])
28− self.assertIn('news', params['url'])
29−
30− dicto['language'] = 'fr'
31− params = qwant.request(query, dicto)
32− self.assertIn('fr_fr', params['url'])
33−
34− def test_response(self):
35− self.assertRaises(AttributeError, qwant.response, None)
36− self.assertRaises(AttributeError, qwant.response, [])
37− self.assertRaises(AttributeError, qwant.response, '')
38− self.assertRaises(AttributeError, qwant.response, '[]')
39−
40− response = mock.Mock(text='{}')
41− self.assertEqual(qwant.response(response), [])
42−
43− response = mock.Mock(text='{"data": {}}')
44− self.assertEqual(qwant.response(response), [])
45−
46− json = """
47− {
48− "status": "success",
49− "data": {
50− "query": {
51− "locale": "en_us",
52− "query": "Test",
53− "offset": 10
54− },
55− "result": {
56− "items": [
57− {
58− "title": "Title",
59− "score": 9999,
60− "url": "http://www.url.xyz",
61− "source": "...",
62− "desc": "Description",
63− "date": "",
64− "_id": "db0aadd62c2a8565567ffc382f5c61fa",
65− "favicon": "https://s.qwant.com/fav.ico"
66− }
67− ],
68− "filters": []
69− },
70− "cache": {
71− "key": "e66aa864c00147a0e3a16ff7a5efafde",
72− "created": 1433092754,
73− "expiration": 259200,
74− "status": "miss",
75− "age": 0
76− }
77− }
78− }
79− """
80− response = mock.Mock(text=json)
81− qwant.categories = ['general']
82− results = qwant.response(response)
83− self.assertEqual(type(results), list)
84− self.assertEqual(len(results), 1)
85− self.assertEqual(results[0]['title'], 'Title')
86− self.assertEqual(results[0]['url'], 'http://www.url.xyz')
87− self.assertEqual(results[0]['content'], 'Description')
88−
89− json = """
90− {
91− "status": "success",
92− "data": {
93− "query": {
94− "locale": "en_us",
95− "query": "Test",
96− "offset": 10
97− },
98− "result": {
99− "items": [
100− {
101− "title": "Title",
102− "score": 9999,
103− "url": "http://www.url.xyz",
104− "source": "...",
105− "media": "http://image.jpg",
106− "desc": "",
107− "thumbnail": "http://thumbnail.jpg",
108− "date": "",
109− "_id": "db0aadd62c2a8565567ffc382f5c61fa",
110− "favicon": "https://s.qwant.com/fav.ico"
111− }
112− ],
113− "filters": []
114− },
115− "cache": {
116− "key": "e66aa864c00147a0e3a16ff7a5efafde",
117− "created": 1433092754,
118− "expiration": 259200,
119− "status": "miss",
120− "age": 0
121− }
122− }
123− }
124− """
125− response = mock.Mock(text=json)
126− qwant.categories = ['images']
127− results = qwant.response(response)
128− self.assertEqual(type(results), list)
129− self.assertEqual(len(results), 1)
130− self.assertEqual(results[0]['title'], 'Title')
131− self.assertEqual(results[0]['url'], 'http://www.url.xyz')
132− self.assertEqual(results[0]['content'], '')
133− self.assertEqual(results[0]['thumbnail_src'], 'http://thumbnail.jpg')
134− self.assertEqual(results[0]['img_src'], 'http://image.jpg')
135−
136− json = """
137− {
138− "status": "success",
139− "data": {
140− "query": {
141− "locale": "en_us",
142− "query": "Test",
143− "offset": 10
144− },
145− "result": {
146− "items": [
147− {
148− "title": "Title",
149− "score": 9999,
150− "url": "http://www.url.xyz",
151− "source": "...",
152− "desc": "Description",
153− "date": 1433260920,
154− "_id": "db0aadd62c2a8565567ffc382f5c61fa",
155− "favicon": "https://s.qwant.com/fav.ico"
156− }
157− ],
158− "filters": []
159− },
160− "cache": {
161− "key": "e66aa864c00147a0e3a16ff7a5efafde",
162− "created": 1433092754,
163− "expiration": 259200,
164− "status": "miss",
165− "age": 0
166− }
167− }
168− }
169− """
170− response = mock.Mock(text=json)
171− qwant.categories = ['news']
172− results = qwant.response(response)
173− self.assertEqual(type(results), list)
174− self.assertEqual(len(results), 1)
175− self.assertEqual(results[0]['title'], 'Title')
176− self.assertEqual(results[0]['url'], 'http://www.url.xyz')
177− self.assertEqual(results[0]['content'], 'Description')
178− self.assertIn('publishedDate', results[0])
179−
180− json = """
181− {
182− "status": "success",
183− "data": {
184− "query": {
185− "locale": "en_us",
186− "query": "Test",
187− "offset": 10
188− },
189− "result": {
190− "items": [
191− {
192− "title": "Title",
193− "score": 9999,
194− "url": "http://www.url.xyz",
195− "source": "...",
196− "desc": "Description",
197− "date": 1433260920,
198− "_id": "db0aadd62c2a8565567ffc382f5c61fa",
199− "favicon": "https://s.qwant.com/fav.ico"
200− }
201− ],
202− "filters": []
203− },
204− "cache": {
205− "key": "e66aa864c00147a0e3a16ff7a5efafde",
206− "created": 1433092754,
207− "expiration": 259200,
208− "status": "miss",
209− "age": 0
210− }
211− }
212− }
213− """
214− response = mock.Mock(text=json)
215− qwant.categories = ['social media']
216− results = qwant.response(response)
217− self.assertEqual(type(results), list)
218− self.assertEqual(len(results), 1)
219− self.assertEqual(results[0]['title'], 'Title')
220− self.assertEqual(results[0]['url'], 'http://www.url.xyz')
221− self.assertEqual(results[0]['content'], 'Description')
222− self.assertIn('publishedDate', results[0])
223−
224− json = """
225− {
226− "status": "success",
227− "data": {
228− "query": {
229− "locale": "en_us",
230− "query": "Test",
231− "offset": 10
232− },
233− "result": {
234− "items": [
235− {
236− "title": "Title",
237− "score": 9999,
238− "url": "http://www.url.xyz",
239− "source": "...",
240− "desc": "Description",
241− "date": 1433260920,
242− "_id": "db0aadd62c2a8565567ffc382f5c61fa",
243− "favicon": "https://s.qwant.com/fav.ico"
244− }
245− ],
246− "filters": []
247− },
248− "cache": {
249− "key": "e66aa864c00147a0e3a16ff7a5efafde",
250− "created": 1433092754,
251− "expiration": 259200,
252− "status": "miss",
253− "age": 0
254− }
255− }
256− }
257− """
258− response = mock.Mock(text=json)
259− qwant.categories = ['']
260− results = qwant.response(response)
261− self.assertEqual(type(results), list)
262− self.assertEqual(len(results), 0)
263−
264− json = """
265− {
266− "status": "success",
267− "data": {
268− "query": {
269− "locale": "en_us",
270− "query": "Test",
271− "offset": 10
272− },
273− "result": {
274− "filters": []
275− },
276− "cache": {
277− "key": "e66aa864c00147a0e3a16ff7a5efafde",
278− "created": 1433092754,
279− "expiration": 259200,
280− "status": "miss",
281− "age": 0
282− }
283− }
284− }
285− """
286− response = mock.Mock(text=json)
287− results = qwant.response(response)
288− self.assertEqual(type(results), list)
289− self.assertEqual(len(results), 0)
290−
291− json = """
292− {
293− "status": "success",
294− "data": {
295− "query": {
296− "locale": "en_us",
297− "query": "Test",
298− "offset": 10
299− },
300− "cache": {
301− "key": "e66aa864c00147a0e3a16ff7a5efafde",
302− "created": 1433092754,
303− "expiration": 259200,
304− "status": "miss",
305− "age": 0
306− }
307− }
308− }
309− """
310− response = mock.Mock(text=json)
311− results = qwant.response(response)
312− self.assertEqual(type(results), list)
313− self.assertEqual(len(results), 0)
314−
315− json = """
316− {
317− "status": "success"
318− }
319− """
320− response = mock.Mock(text=json)
321− results = qwant.response(response)
322− self.assertEqual(type(results), list)
323− self.assertEqual(len(results), 0)
324−
325− def test_fetch_supported_languages(self):
326− page = """some code...
327− config_set('project.regionalisation', {"continents":{},"languages":
328− {"de":{"code":"de","name":"Deutsch","countries":["DE","CH","AT"]},
329− "it":{"code":"it","name":"Italiano","countries":["IT","CH"]}}});
330− some more code..."""
331− response = mock.Mock(text=page)
332− languages = qwant._fetch_supported_languages(response)
333− self.assertEqual(type(languages), list)
334− self.assertEqual(len(languages), 5)
335− self.assertIn('de-DE', languages)
336− self.assertIn('de-CH', languages)
337− self.assertIn('de-AT', languages)
338− self.assertIn('it-IT', languages)
339− self.assertIn('it-CH', languages)
tests/unit/engines/test_reddit.pydeleted−71
@@ -1,71 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import reddit
4−from searx.testing import SearxTestCase
5−from datetime import datetime
6−
7−
8−class TestRedditEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dic = defaultdict(dict)
13− params = reddit.request(query, dic)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('reddit.com' in params['url'])
17−
18− def test_response(self):
19− resp = mock.Mock(text='{}')
20− self.assertEqual(reddit.response(resp), [])
21−
22− json = """
23− {
24− "kind": "Listing",
25− "data": {
26− "children": [{
27− "data": {
28− "url": "http://google2.com/",
29− "permalink": "http://google.com/",
30− "title": "Title number one",
31− "selftext": "Sample",
32− "created_utc": 1401219957.0,
33− "thumbnail": "http://image.com/picture.jpg"
34− }
35− }, {
36− "data": {
37− "url": "https://reddit2.com/",
38− "permalink": "https://reddit.com/",
39− "title": "Title number two",
40− "selftext": "Dominus vobiscum",
41− "created_utc": 1438792533.0,
42− "thumbnail": "self"
43− }
44− }]
45− }
46− }
47− """
48−
49− resp = mock.Mock(text=json)
50− results = reddit.response(resp)
51−
52− self.assertEqual(len(results), 2)
53− self.assertEqual(type(results), list)
54−
55− # testing first result (picture)
56− r = results[0]
57− self.assertEqual(r['url'], 'http://google.com/')
58− self.assertEqual(r['title'], 'Title number one')
59− self.assertEqual(r['template'], 'images.html')
60− self.assertEqual(r['img_src'], 'http://google2.com/')
61− self.assertEqual(r['thumbnail_src'], 'http://image.com/picture.jpg')
62−
63− # testing second result (self-post)
64− r = results[1]
65− self.assertEqual(r['url'], 'https://reddit.com/')
66− self.assertEqual(r['title'], 'Title number two')
67− self.assertEqual(r['content'], 'Dominus vobiscum')
68− created = datetime.fromtimestamp(1438792533.0)
69− self.assertEqual(r['publishedDate'], created)
70− self.assertTrue('thumbnail_src' not in r)
71− self.assertTrue('img_src' not in r)
tests/unit/engines/test_scanr_structures.pydeleted−175
@@ -1,175 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import scanr_structures
4−from searx.testing import SearxTestCase
5−
6−
7−class TestScanrStructuresEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 1
13− params = scanr_structures.request(query, dicto)
14− self.assertIn('url', params)
15− self.assertIn(query, params['data'])
16− self.assertIn('scanr.enseignementsup-recherche.gouv.fr', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, scanr_structures.response, None)
20− self.assertRaises(AttributeError, scanr_structures.response, [])
21− self.assertRaises(AttributeError, scanr_structures.response, '')
22− self.assertRaises(AttributeError, scanr_structures.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(scanr_structures.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(scanr_structures.response(response), [])
29−
30− json = u"""
31− {
32− "request":
33− {
34− "query":"test_query",
35− "page":1,
36− "pageSize":20,
37− "sortOrder":"RELEVANCY",
38− "sortDirection":"ASC",
39− "searchField":"ALL",
40− "from":0
41− },
42− "total":2471,
43− "results":[
44− {
45− "id":"200711886U",
46− "label":"Laboratoire d'Informatique de Grenoble",
47− "kind":"RNSR",
48− "publicEntity":true,
49− "address":{"city":"Grenoble","departement":"38"},
50− "logo":"/static/logos/200711886U.png",
51− "acronym":"LIG",
52− "type":{"code":"UR","label":"Unit\xe9 de recherche"},
53− "level":2,
54− "institutions":[
55− {
56− "id":"193819125",
57− "label":"Grenoble INP",
58− "acronym":"IPG",
59− "code":"UMR 5217"
60− },
61− {
62− "id":"130021397",
63− "label":"Universit\xe9 de Grenoble Alpes",
64− "acronym":"UGA",
65− "code":"UMR 5217"
66− },
67− {
68− "id":"180089013",
69− "label":"Centre national de la recherche scientifique",
70− "acronym":"CNRS",
71− "code":"UMR 5217"
72− },
73− {
74− "id":"180089047",
75− "label":"Institut national de recherche en informatique et en automatique",
76− "acronym":"Inria",
77− "code":"UMR 5217"
78− }
79− ],
80− "highlights":[
81− {
82− "type":"projects",
83− "value":"linguicielles d\xe9velopp\xe9s jusqu'ici par le GETALP\
84− du <strong>LIG</strong> en tant que prototypes op\xe9rationnels.\
85−\\r\\nDans le contexte"
86− },
87− {
88− "type":"acronym",
89− "value":"<strong>LIG</strong>"
90− },
91− {
92− "type":"websiteContents",
93− "value":"S\xe9lection\\nListe structures\\nD\xe9tail\\n\
94− Accueil\\n200711886U : <strong>LIG</strong>\
95− Laboratoire d'Informatique de Grenoble Unit\xe9 de recherche"},
96− {
97− "type":"publications",
98− "value":"de noms. Nous avons d'abord d\xe9velopp\xe9 LOOV \
99− (pour <strong>Lig</strong> Overlaid OCR in Vid\xe9o), \
100− un outil d'extraction des"
101− }
102− ]
103− },
104− {
105− "id":"199511665F",
106− "label":"Laboratoire Bordelais de Recherche en Informatique",
107− "kind":"RNSR",
108− "publicEntity":true,
109− "address":{"city":"Talence","departement":"33"},
110− "logo":"/static/logos/199511665F.png",
111− "acronym":"LaBRI",
112− "type":{"code":"UR","label":"Unit\xe9 de recherche"},
113− "level":2,
114− "institutions":[
115− {
116− "id":"130006356",
117− "label":"Institut polytechnique de Bordeaux",
118− "acronym":"IPB",
119− "code":"UMR 5800"
120− },
121− {
122− "id":"130018351",
123− "label":"Universit\xe9 de Bordeaux",
124− "acronym":null,
125− "code":"UMR 5800"
126− },
127− {
128− "id":"180089013",
129− "label":"Centre national de la recherche scientifique",
130− "acronym":"CNRS",
131− "code":"UMR 5800"
132− },
133− {
134− "id":"180089047",
135− "label":"Institut national de recherche en informatique et en automatique",
136− "acronym":"Inria",
137− "code":"UMR 5800"
138− }
139− ],
140− "highlights":[
141− {
142− "type":"websiteContents",
143− "value":"Samia Kerdjoudj\\n2016-07-05\\nDouble-exponential\
144− and <strong>triple</strong>-exponential bounds for\
145− choosability problems parameterized"
146− },
147− {
148− "type":"publications",
149− "value":"de cam\xe9ras install\xe9es dans les lieux publiques \
150− a <strong>tripl\xe9</strong> en 2009, passant de 20 000 \
151− \xe0 60 000. Malgr\xe9 le"
152− }
153− ]
154− }
155− ]
156− }
157− """
158− response = mock.Mock(text=json)
159− results = scanr_structures.response(response)
160− self.assertEqual(type(results), list)
161− self.assertEqual(len(results), 2)
162− self.assertEqual(results[0]['title'], u"Laboratoire d'Informatique de Grenoble")
163− self.assertEqual(results[0]['url'], 'https://scanr.enseignementsup-recherche.gouv.fr/structure/200711886U')
164− self.assertEqual(results[0]['content'],
165− u"linguicielles d\xe9velopp\xe9s jusqu'ici par le GETALP "
166− u"du LIG en tant que prototypes "
167− u"op\xe9rationnels. Dans le contexte")
168− self.assertEqual(results[1]['img_src'],
169− 'https://scanr.enseignementsup-recherche.gouv.fr//static/logos/199511665F.png')
170− self.assertEqual(results[1]['content'],
171− "Samia Kerdjoudj 2016-07-05 Double-exponential and"
172− " triple-exponential bounds for "
173− "choosability problems parameterized")
174− self.assertEqual(results[1]['url'], 'https://scanr.enseignementsup-recherche.gouv.fr/structure/199511665F')
175− self.assertEqual(results[1]['title'], u"Laboratoire Bordelais de Recherche en Informatique")
tests/unit/engines/test_searchcode_code.pydeleted−75
@@ -1,75 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import searchcode_code
4−from searx.testing import SearxTestCase
5−
6−
7−class TestSearchcodeCodeEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = searchcode_code.request(query, dicto)
14− self.assertIn('url', params)
15− self.assertIn(query, params['url'])
16− self.assertIn('searchcode.com', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, searchcode_code.response, None)
20− self.assertRaises(AttributeError, searchcode_code.response, [])
21− self.assertRaises(AttributeError, searchcode_code.response, '')
22− self.assertRaises(AttributeError, searchcode_code.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(searchcode_code.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(searchcode_code.response(response), [])
29−
30− json = """
31− {
32− "matchterm": "test",
33− "previouspage": null,
34− "searchterm": "test",
35− "query": "test",
36− "total": 1000,
37− "page": 0,
38− "nextpage": 1,
39− "results": [
40− {
41− "repo": "https://repo",
42− "linescount": 1044,
43− "location": "/tests",
44− "name": "Name",
45− "url": "https://url",
46− "md5hash": "ecac6e479edd2b9406c9e08603cec655",
47− "lines": {
48− "1": "// Test 011",
49− "2": "// Source: "
50− },
51− "id": 51223527,
52− "filename": "File.CPP"
53− }
54− ]
55− }
56− """
57− response = mock.Mock(text=json)
58− results = searchcode_code.response(response)
59− self.assertEqual(type(results), list)
60− self.assertEqual(len(results), 1)
61− self.assertEqual(results[0]['title'], 'Name - File.CPP')
62− self.assertEqual(results[0]['url'], 'https://url')
63− self.assertEqual(results[0]['repository'], 'https://repo')
64− self.assertEqual(results[0]['code_language'], 'cpp')
65−
66− json = r"""
67− {"toto":[
68− {"id":200,"name":"Artist Name",
69− "link":"http:\/\/www.searchcode_code.com\/artist\/1217","type":"artist"}
70− ]}
71− """
72− response = mock.Mock(text=json)
73− results = searchcode_code.response(response)
74− self.assertEqual(type(results), list)
75− self.assertEqual(len(results), 0)
tests/unit/engines/test_searchcode_doc.pydeleted−70
@@ -1,70 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import searchcode_doc
4−from searx.testing import SearxTestCase
5−
6−
7−class TestSearchcodeDocEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = searchcode_doc.request(query, dicto)
14− self.assertIn('url', params)
15− self.assertIn(query, params['url'])
16− self.assertIn('searchcode.com', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, searchcode_doc.response, None)
20− self.assertRaises(AttributeError, searchcode_doc.response, [])
21− self.assertRaises(AttributeError, searchcode_doc.response, '')
22− self.assertRaises(AttributeError, searchcode_doc.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(searchcode_doc.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(searchcode_doc.response(response), [])
29−
30− json = """
31− {
32− "matchterm": "test",
33− "previouspage": null,
34− "searchterm": "test",
35− "query": "test",
36− "total": 60,
37− "page": 0,
38− "nextpage": 1,
39− "results": [
40− {
41− "synopsis": "Synopsis",
42− "displayname": null,
43− "name": "test",
44− "url": "http://url",
45− "type": "Type",
46− "icon": null,
47− "namespace": "Namespace",
48− "description": "Description"
49− }
50− ]
51− }
52− """
53− response = mock.Mock(text=json)
54− results = searchcode_doc.response(response)
55− self.assertEqual(type(results), list)
56− self.assertEqual(len(results), 1)
57− self.assertEqual(results[0]['title'], '[Type] Namespace test')
58− self.assertEqual(results[0]['url'], 'http://url')
59− self.assertIn('Description', results[0]['content'])
60−
61− json = r"""
62− {"toto":[
63− {"id":200,"name":"Artist Name",
64− "link":"http:\/\/www.searchcode_doc.com\/artist\/1217","type":"artist"}
65− ]}
66− """
67− response = mock.Mock(text=json)
68− results = searchcode_doc.response(response)
69− self.assertEqual(type(results), list)
70− self.assertEqual(len(results), 0)
tests/unit/engines/test_soundcloud.pydeleted−192
@@ -1,192 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import soundcloud
4−from searx.testing import SearxTestCase
5−from searx.url_utils import quote_plus
6−
7−
8−class TestSoundcloudEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− params = soundcloud.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn(query, params['url'])
17− self.assertIn('soundcloud.com', params['url'])
18−
19− def test_response(self):
20− self.assertRaises(AttributeError, soundcloud.response, None)
21− self.assertRaises(AttributeError, soundcloud.response, [])
22− self.assertRaises(AttributeError, soundcloud.response, '')
23− self.assertRaises(AttributeError, soundcloud.response, '[]')
24−
25− response = mock.Mock(text='{}')
26− self.assertEqual(soundcloud.response(response), [])
27−
28− response = mock.Mock(text='{"data": []}')
29− self.assertEqual(soundcloud.response(response), [])
30−
31− json = """
32− {
33− "collection": [
34− {
35− "kind": "track",
36− "id": 159723640,
37− "created_at": "2014/07/22 00:51:21 +0000",
38− "user_id": 2976616,
39− "duration": 303780,
40− "commentable": true,
41− "state": "finished",
42− "original_content_size": 13236349,
43− "last_modified": "2015/01/31 15:14:50 +0000",
44− "sharing": "public",
45− "tag_list": "seekae flume",
46− "permalink": "seekae-test-recognise-flume-re-work",
47− "streamable": true,
48− "embeddable_by": "all",
49− "downloadable": true,
50− "purchase_url": "http://www.facebook.com/seekaemusic",
51− "label_id": null,
52− "purchase_title": "Seekae",
53− "genre": "freedownload",
54− "title": "This is the title",
55− "description": "This is the content",
56− "label_name": "Future Classic",
57− "release": "",
58− "track_type": "remix",
59− "key_signature": "",
60− "isrc": "",
61− "video_url": null,
62− "bpm": null,
63− "release_year": 2014,
64− "release_month": 7,
65− "release_day": 22,
66− "original_format": "mp3",
67− "license": "all-rights-reserved",
68− "uri": "https://api.soundcloud.com/tracks/159723640",
69− "user": {
70− "id": 2976616,
71− "kind": "user",
72− "permalink": "flume",
73− "username": "Flume",
74− "last_modified": "2014/11/24 19:21:29 +0000",
75− "uri": "https://api.soundcloud.com/users/2976616",
76− "permalink_url": "http://soundcloud.com/flume",
77− "avatar_url": "https://i1.sndcdn.com/avatars-000044475439-4zi7ii-large.jpg"
78− },
79− "permalink_url": "http://soundcloud.com/this.is.the.url",
80− "artwork_url": "https://i1.sndcdn.com/artworks-000085857162-xdxy5c-large.jpg",
81− "waveform_url": "https://w1.sndcdn.com/DWrL1lAN8BkP_m.png",
82− "stream_url": "https://api.soundcloud.com/tracks/159723640/stream",
83− "download_url": "https://api.soundcloud.com/tracks/159723640/download",
84− "playback_count": 2190687,
85− "download_count": 54856,
86− "favoritings_count": 49061,
87− "comment_count": 826,
88− "likes_count": 49061,
89− "reposts_count": 15910,
90− "attachments_uri": "https://api.soundcloud.com/tracks/159723640/attachments",
91− "policy": "ALLOW"
92− }
93− ],
94− "total_results": 375750,
95− "next_href": "https://api.soundcloud.com/search?&q=test",
96− "tx_id": ""
97− }
98− """
99− response = mock.Mock(text=json)
100− results = soundcloud.response(response)
101− self.assertEqual(type(results), list)
102− self.assertEqual(len(results), 1)
103− self.assertEqual(results[0]['title'], 'This is the title')
104− self.assertEqual(results[0]['url'], 'http://soundcloud.com/this.is.the.url')
105− self.assertEqual(results[0]['content'], 'This is the content')
106− self.assertIn(quote_plus('https://api.soundcloud.com/tracks/159723640'), results[0]['embedded'])
107−
108− json = """
109− {
110− "collection": [
111− {
112− "kind": "user",
113− "id": 159723640,
114− "created_at": "2014/07/22 00:51:21 +0000",
115− "user_id": 2976616,
116− "duration": 303780,
117− "commentable": true,
118− "state": "finished",
119− "original_content_size": 13236349,
120− "last_modified": "2015/01/31 15:14:50 +0000",
121− "sharing": "public",
122− "tag_list": "seekae flume",
123− "permalink": "seekae-test-recognise-flume-re-work",
124− "streamable": true,
125− "embeddable_by": "all",
126− "downloadable": true,
127− "purchase_url": "http://www.facebook.com/seekaemusic",
128− "label_id": null,
129− "purchase_title": "Seekae",
130− "genre": "freedownload",
131− "title": "This is the title",
132− "description": "This is the content",
133− "label_name": "Future Classic",
134− "release": "",
135− "track_type": "remix",
136− "key_signature": "",
137− "isrc": "",
138− "video_url": null,
139− "bpm": null,
140− "release_year": 2014,
141− "release_month": 7,
142− "release_day": 22,
143− "original_format": "mp3",
144− "license": "all-rights-reserved",
145− "uri": "https://api.soundcloud.com/tracks/159723640",
146− "user": {
147− "id": 2976616,
148− "kind": "user",
149− "permalink": "flume",
150− "username": "Flume",
151− "last_modified": "2014/11/24 19:21:29 +0000",
152− "uri": "https://api.soundcloud.com/users/2976616",
153− "permalink_url": "http://soundcloud.com/flume",
154− "avatar_url": "https://i1.sndcdn.com/avatars-000044475439-4zi7ii-large.jpg"
155− },
156− "permalink_url": "http://soundcloud.com/this.is.the.url",
157− "artwork_url": "https://i1.sndcdn.com/artworks-000085857162-xdxy5c-large.jpg",
158− "waveform_url": "https://w1.sndcdn.com/DWrL1lAN8BkP_m.png",
159− "stream_url": "https://api.soundcloud.com/tracks/159723640/stream",
160− "download_url": "https://api.soundcloud.com/tracks/159723640/download",
161− "playback_count": 2190687,
162− "download_count": 54856,
163− "favoritings_count": 49061,
164− "comment_count": 826,
165− "likes_count": 49061,
166− "reposts_count": 15910,
167− "attachments_uri": "https://api.soundcloud.com/tracks/159723640/attachments",
168− "policy": "ALLOW"
169− }
170− ],
171− "total_results": 375750,
172− "next_href": "https://api.soundcloud.com/search?&q=test",
173− "tx_id": ""
174− }
175− """
176− response = mock.Mock(text=json)
177− results = soundcloud.response(response)
178− self.assertEqual(type(results), list)
179− self.assertEqual(len(results), 0)
180−
181− json = """
182− {
183− "collection": [],
184− "total_results": 375750,
185− "next_href": "https://api.soundcloud.com/search?&q=test",
186− "tx_id": ""
187− }
188− """
189− response = mock.Mock(text=json)
190− results = soundcloud.response(response)
191− self.assertEqual(type(results), list)
192− self.assertEqual(len(results), 0)
tests/unit/engines/test_spotify.pydeleted−124
@@ -1,124 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import spotify
4−from searx.testing import SearxTestCase
5−
6−
7−class TestSpotifyEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = spotify.request(query, dicto)
14− self.assertIn('url', params)
15− self.assertIn(query, params['url'])
16− self.assertIn('spotify.com', params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, spotify.response, None)
20− self.assertRaises(AttributeError, spotify.response, [])
21− self.assertRaises(AttributeError, spotify.response, '')
22− self.assertRaises(AttributeError, spotify.response, '[]')
23−
24− response = mock.Mock(text='{}')
25− self.assertEqual(spotify.response(response), [])
26−
27− response = mock.Mock(text='{"data": []}')
28− self.assertEqual(spotify.response(response), [])
29−
30− json = """
31− {
32− "tracks": {
33− "href": "https://api.spotify.com/v1/search?query=nosfell&offset=0&limit=20&type=track",
34− "items": [
35− {
36− "album": {
37− "album_type": "album",
38− "external_urls": {
39− "spotify": "https://open.spotify.com/album/5c9ap1PBkSGLxT3J73toxA"
40− },
41− "href": "https://api.spotify.com/v1/albums/5c9ap1PBkSGLxT3J73toxA",
42− "id": "5c9ap1PBkSGLxT3J73toxA",
43− "name": "Album Title",
44− "type": "album",
45− "uri": "spotify:album:5c9ap1PBkSGLxT3J73toxA"
46− },
47− "artists": [
48− {
49− "external_urls": {
50− "spotify": "https://open.spotify.com/artist/0bMc6b75FfZEpQHG1jifKu"
51− },
52− "href": "https://api.spotify.com/v1/artists/0bMc6b75FfZEpQHG1jifKu",
53− "id": "0bMc6b75FfZEpQHG1jifKu",
54− "name": "Artist Name",
55− "type": "artist",
56− "uri": "spotify:artist:0bMc6b75FfZEpQHG1jifKu"
57− }
58− ],
59− "disc_number": 1,
60− "duration_ms": 202386,
61− "explicit": false,
62− "external_ids": {
63− "isrc": "FRV640600067"
64− },
65− "external_urls": {
66− "spotify": "https://open.spotify.com/track/2GzvFiedqW8hgqUpWcASZa"
67− },
68− "href": "https://api.spotify.com/v1/tracks/2GzvFiedqW8hgqUpWcASZa",
69− "id": "1000",
70− "is_playable": true,
71− "name": "Title of track",
72− "popularity": 6,
73− "preview_url": "https://p.scdn.co/mp3-preview/7b8ecda580965a066b768c2647f877e43f7b1a0a",
74− "track_number": 3,
75− "type": "track",
76− "uri": "spotify:track:2GzvFiedqW8hgqUpWcASZa"
77− }
78− ],
79− "limit": 20,
80− "next": "https://api.spotify.com/v1/search?query=nosfell&offset=20&limit=20&type=track",
81− "offset": 0,
82− "previous": null,
83− "total": 107
84− }
85− }
86− """
87− response = mock.Mock(text=json)
88− results = spotify.response(response)
89− self.assertEqual(type(results), list)
90− self.assertEqual(len(results), 1)
91− self.assertEqual(results[0]['title'], 'Title of track')
92− self.assertEqual(results[0]['url'], 'https://open.spotify.com/track/2GzvFiedqW8hgqUpWcASZa')
93− self.assertEqual(results[0]['content'], 'Artist Name - Album Title - Title of track')
94− self.assertIn('1000', results[0]['embedded'])
95−
96− json = """
97− {
98− "tracks": {
99− "href": "https://api.spotify.com/v1/search?query=nosfell&offset=0&limit=20&type=track",
100− "items": [
101− {
102− "href": "https://api.spotify.com/v1/tracks/2GzvFiedqW8hgqUpWcASZa",
103− "id": "1000",
104− "is_playable": true,
105− "name": "Title of track",
106− "popularity": 6,
107− "preview_url": "https://p.scdn.co/mp3-preview/7b8ecda580965a066b768c2647f877e43f7b1a0a",
108− "track_number": 3,
109− "type": "album",
110− "uri": "spotify:track:2GzvFiedqW8hgqUpWcASZa"
111− }
112− ],
113− "limit": 20,
114− "next": "https://api.spotify.com/v1/search?query=nosfell&offset=20&limit=20&type=track",
115− "offset": 0,
116− "previous": null,
117− "total": 107
118− }
119− }
120− """
121− response = mock.Mock(text=json)
122− results = spotify.response(response)
123− self.assertEqual(type(results), list)
124− self.assertEqual(len(results), 0)
tests/unit/engines/test_stackoverflow.pydeleted−106
@@ -1,106 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import stackoverflow
4−from searx.testing import SearxTestCase
5−
6−
7−class TestStackoverflowEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− params = stackoverflow.request(query, dicto)
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16− self.assertTrue('stackoverflow.com' in params['url'])
17−
18− def test_response(self):
19− self.assertRaises(AttributeError, stackoverflow.response, None)
20− self.assertRaises(AttributeError, stackoverflow.response, [])
21− self.assertRaises(AttributeError, stackoverflow.response, '')
22− self.assertRaises(AttributeError, stackoverflow.response, '[]')
23−
24− response = mock.Mock(text='<html></html>')
25− self.assertEqual(stackoverflow.response(response), [])
26−
27− html = """
28− <div class="question-summary search-result" id="answer-id-1783426">
29− <div class="statscontainer">
30− <div class="statsarrow"></div>
31− <div class="stats">
32− <div class="vote">
33− <div class="votes answered">
34− <span class="vote-count-post "><strong>2583</strong></span>
35− <div class="viewcount">votes</div>
36− </div>
37− </div>
38− </div>
39− </div>
40− <div class="summary">
41− <div class="result-link">
42− <span>
43− <a href="/questions/this.is.the.url"
44− data-searchsession="/questions"
45− title="Checkout remote Git branch">
46− This is the title
47− </a>
48− </span>
49− </div>
50− <div class="excerpt">
51− This is the content
52− </div>
53− <div class="tags user-tags t-git t-git-checkout t-remote-branch">
54− </div>
55− <div class="started fr">
56− answered <span title="2009-11-23 14:26:08Z" class="relativetime">nov 23 '09</span> by
57− <a href="/users/214090/hallski">hallski</a>
58− </div>
59− </div>
60− </div>
61− """
62− response = mock.Mock(text=html)
63− results = stackoverflow.response(response)
64− self.assertEqual(type(results), list)
65− self.assertEqual(len(results), 1)
66− self.assertEqual(results[0]['title'], 'This is the title')
67− self.assertEqual(results[0]['url'], 'https://stackoverflow.com/questions/this.is.the.url')
68− self.assertEqual(results[0]['content'], 'This is the content')
69−
70− html = """
71− <div class="statscontainer">
72− <div class="statsarrow"></div>
73− <div class="stats">
74− <div class="vote">
75− <div class="votes answered">
76− <span class="vote-count-post "><strong>2583</strong></span>
77− <div class="viewcount">votes</div>
78− </div>
79− </div>
80− </div>
81− </div>
82− <div class="summary">
83− <div class="result-link">
84− <span>
85− <a href="/questions/this.is.the.url"
86− data-searchsession="/questions"
87− title="Checkout remote Git branch">
88− This is the title
89− </a>
90− </span>
91− </div>
92− <div class="excerpt">
93− This is the content
94− </div>
95− <div class="tags user-tags t-git t-git-checkout t-remote-branch">
96− </div>
97− <div class="started fr">
98− answered <span title="2009-11-23 14:26:08Z" class="relativetime">nov 23 '09</span> by
99− <a href="/users/214090/hallski">hallski</a>
100− </div>
101− </div>
102− """
103− response = mock.Mock(text=html)
104− results = stackoverflow.response(response)
105− self.assertEqual(type(results), list)
106− self.assertEqual(len(results), 0)
tests/unit/engines/test_startpage.pydeleted−140
@@ -1,140 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import startpage
5−from searx.testing import SearxTestCase
6−
7−
8−class TestStartpageEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 1
14− dicto['language'] = 'fr_FR'
15− params = startpage.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn('startpage.com', params['url'])
18− self.assertIn('data', params)
19− self.assertIn('query', params['data'])
20− self.assertIn(query, params['data']['query'])
21− self.assertIn('with_language', params['data'])
22− self.assertIn('lang_fr', params['data']['with_language'])
23−
24− dicto['language'] = 'all'
25− params = startpage.request(query, dicto)
26− self.assertNotIn('with_language', params['data'])
27−
28− def test_response(self):
29− self.assertRaises(AttributeError, startpage.response, None)
30− self.assertRaises(AttributeError, startpage.response, [])
31− self.assertRaises(AttributeError, startpage.response, '')
32− self.assertRaises(AttributeError, startpage.response, '[]')
33−
34− response = mock.Mock(text='<html></html>')
35− self.assertEqual(startpage.response(response), [])
36−
37− html = """
38− <li class="search-result search-item">
39− <h3>
40− <a href='http://this.should.be.the.link/' id='title_2' name='title_2' >
41− This should be the title
42− </a>
43− <span id='title_stars_2' name='title_stars_2'> </span>
44− </h3>
45− <p class="search-item__body">
46− This should be the content.
47− </p>
48− <p>
49− <span class='url'>www.speed<b>test</b>.net/fr/
50− </span>
51− -
52− <A class="proxy" id="proxy_link" HREF="https://ixquick-proxy.com/do/spg/proxy?ep=&edata=&ek=&ekdata="
53− class='proxy'>
54− Navigation avec Ixquick Proxy
55− </A>
56− -
57− <A HREF="https://ixquick-proxy.com/do/spg/highlight.pl?l=francais&c=hf&cat=web&q=test&rl=NONE&rid=
58− &hlq=https://startpage.com/do/search&mtabp=-1&mtcmd=process_search&mtlanguage=francais&mtengine0=
59− &mtcat=web&u=http:%2F%2Fwww.speedtest.net%2Ffr%2F" class='proxy'>
60− Mis en surbrillance
61− </A>
62− </p>
63− </li>
64− """
65− response = mock.Mock(text=html.encode('utf-8'))
66− results = startpage.response(response)
67− self.assertEqual(type(results), list)
68− self.assertEqual(len(results), 1)
69− self.assertEqual(results[0]['title'], 'This should be the title')
70− self.assertEqual(results[0]['url'], 'http://this.should.be.the.link/')
71− self.assertEqual(results[0]['content'], 'This should be the content.')
72−
73− html = """
74− <li class="search-result search-item">
75− <h3>
76− <a href='http://www.google.com/aclk?sa=l&ai=C' id='title_2' name='title_2' >
77− This should be the title
78− </a>
79− <span id='title_stars_2' name='title_stars_2'> </span>
80− </h3>
81− <p class="search-item__body">
82− This should be the content.
83− </p>
84− <p>
85− <span class='url'>www.speed<b>test</b>.net/fr/
86− </span>
87− -
88− <A class="proxy" id="proxy_link" HREF="https://ixquick-proxy.com/do/spg/proxy?ep=&edata=&ek=&ekdata="
89− class='proxy'>
90− Navigation avec Ixquick Proxy
91− </A>
92− -
93− <A HREF="https://ixquick-proxy.com/do/spg/highlight.pl?l=francais&c=hf&cat=web&q=test&rl=NONE&rid=
94− &hlq=https://startpage.com/do/search&mtabp=-1&mtcmd=process_search&mtlanguage=francais&mtengine0=
95− &mtcat=web&u=http:%2F%2Fwww.speedtest.net%2Ffr%2F" class='proxy'>
96− Mis en surbrillance
97− </A>
98− </p>
99− </li>
100− <li class="search-result search-item">
101− <h3>
102− <span id='title_stars_2' name='title_stars_2'> </span>
103− </h3>
104− <p class="search-item__body">
105− This should be the content.
106− </p>
107− <p>
108− <span class='url'>www.speed<b>test</b>.net/fr/
109− </span>
110− </p>
111− </li>
112− <li class="search-result search-item">
113− <h3>
114− <a href='http://this.should.be.the.link/' id='title_2' name='title_2' >
115− This should be the title
116− </a>
117− <span id='title_stars_2' name='title_stars_2'> </span>
118− </h3>
119− <p>
120− <span class='url'>www.speed<b>test</b>.net/fr/
121− </span>
122− -
123− <A class="proxy" id="proxy_link" HREF="https://ixquick-proxy.com/do/spg/proxy?ep=&edata=&ek=&ekdata="
124− class='proxy'>
125− Navigation avec Ixquick Proxy
126− </A>
127− -
128− <A HREF="https://ixquick-proxy.com/do/spg/highlight.pl?l=francais&c=hf&cat=web&q=test&rl=NONE&rid=
129− &hlq=https://startpage.com/do/search&mtabp=-1&mtcmd=process_search&mtlanguage=francais&mtengine0=
130− &mtcat=web&u=http:%2F%2Fwww.speedtest.net%2Ffr%2F" class='proxy'>
131− Mis en surbrillance
132− </A>
133− </p>
134− </li>
135− """
136− response = mock.Mock(text=html.encode('utf-8'))
137− results = startpage.response(response)
138− self.assertEqual(type(results), list)
139− self.assertEqual(len(results), 1)
140− self.assertEqual(results[0]['content'], '')
tests/unit/engines/test_tokyotoshokan.pydeleted−110
@@ -1,110 +0,0 @@
1−import mock
2−from collections import defaultdict
3−from searx.engines import tokyotoshokan
4−from searx.testing import SearxTestCase
5−from datetime import datetime
6−
7−
8−class TestTokyotoshokanEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dic = defaultdict(dict)
13− dic['pageno'] = 1
14− params = tokyotoshokan.request(query, dic)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertTrue('tokyotosho.info' in params['url'])
18−
19− def test_response(self):
20− resp = mock.Mock(text='<html></html>')
21− self.assertEqual(tokyotoshokan.response(resp), [])
22−
23− html = """
24− <table class="listing">
25− <tbody>
26− <tr class="shade category_0">
27− <td rowspan="2">
28− <a href="/?cat=7"><span class="sprite_cat-raw"></span></a>
29− </td>
30− <td class="desc-top">
31− <a href="magnet:?xt=urn:btih:4c19eb46b5113685fbd2288ed2531b0b">
32− <span class="sprite_magnet"></span>
33− </a>
34− <a rel="nofollow" type="application/x-bittorrent" href="http://www.nyaa.se/f">
35− Koyomimonogatari
36− </a>
37− </td>
38− <td class="web"><a rel="nofollow" href="details.php?id=975700">Details</a></td>
39− </tr>
40− <tr class="shade category_0">
41− <td class="desc-bot">
42− Authorized: <span class="auth_ok">Yes</span>
43− Submitter: <a href="?username=Ohys">Ohys</a> |
44− Size: 10.5MB |
45− Date: 2016-03-26 16:41 UTC |
46− Comment: sample comment
47− </td>
48− <td style="color: #BBB; font-family: monospace" class="stats" align="right">
49− S: <span style="color: red">53</span>
50− L: <span style="color: red">18</span>
51− C: <span style="color: red">0</span>
52− ID: 975700
53− </td>
54− </tr>
55−
56− <tr class="category_0">
57− <td rowspan="2">
58− <a href="/?cat=7"><span class="sprite_cat-raw"></span></a>
59− </td>
60− <td class="desc-top">
61− <a rel="nofollow" type="application/x-bittorrent" href="http://google.com/q">
62− Owarimonogatari
63− </a>
64− </td>
65− <td class="web"><a rel="nofollow" href="details.php?id=975700">Details</a></td>
66− </tr>
67− <tr class="category_0">
68− <td class="desc-bot">
69− Submitter: <a href="?username=Ohys">Ohys</a> |
70− Size: 932.84EB |
71− Date: QWERTY-03-26 16:41 UTC
72− </td>
73− <td style="color: #BBB; font-family: monospace" class="stats" align="right">
74− S: <span style="color: red">0</span>
75− </td>
76− </tr>
77− </tbody>
78− </table>
79− """
80−
81− resp = mock.Mock(text=html)
82− results = tokyotoshokan.response(resp)
83−
84− self.assertEqual(type(results), list)
85− self.assertEqual(len(results), 2)
86−
87− # testing the first result, which has correct format
88− # and should have all information fields filled
89− r = results[0]
90− self.assertEqual(r['url'], 'http://www.nyaa.se/f')
91− self.assertEqual(r['title'], 'Koyomimonogatari')
92− self.assertEqual(r['magnetlink'], 'magnet:?xt=urn:btih:4c19eb46b5113685fbd2288ed2531b0b')
93− self.assertEqual(r['filesize'], int(1024 * 1024 * 10.5))
94− self.assertEqual(r['publishedDate'], datetime(2016, 3, 26, 16, 41))
95− self.assertEqual(r['content'], 'Comment: sample comment')
96− self.assertEqual(r['seed'], 53)
97− self.assertEqual(r['leech'], 18)
98−
99− # testing the second result, which does not include magnet link,
100− # seed & leech info, and has incorrect size & creation date
101− r = results[1]
102− self.assertEqual(r['url'], 'http://google.com/q')
103− self.assertEqual(r['title'], 'Owarimonogatari')
104−
105− self.assertFalse('magnetlink' in r)
106− self.assertFalse('filesize' in r)
107− self.assertFalse('content' in r)
108− self.assertFalse('publishedDate' in r)
109− self.assertFalse('seed' in r)
110− self.assertFalse('leech' in r)
tests/unit/engines/test_torrentz.pydeleted−87
@@ -1,87 +0,0 @@
1−import mock
2−from collections import defaultdict
3−from searx.engines import torrentz
4−from searx.testing import SearxTestCase
5−from datetime import datetime
6−
7−
8−class TestTorrentzEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dic = defaultdict(dict)
13− dic['pageno'] = 1
14− params = torrentz.request(query, dic)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertTrue('torrentz2.eu' in params['url'])
18−
19− def test_response(self):
20− resp = mock.Mock(text='<html></html>')
21− self.assertEqual(torrentz.response(resp), [])
22−
23− html = """
24− <div class="results">
25− <dl>
26− <dt>
27− <a href="/4362e08b1d80e1820fb2550b752f9f3126fe76d6">
28− Completely valid info
29− </a>
30− books ebooks
31− </dt>
32− <dd>
33− <span>1</span>
34− <span title="1503595924">5 hours</span>
35− <span>30 MB</span>
36− <span>14</span>
37− <span>1</span>
38− </dd>
39− </dl>
40−
41− <dl>
42− <dt>
43− <a href="/poaskdpokaspod">
44− Invalid hash and date and filesize
45− </a>
46− books ebooks
47− </dt>
48− <dd>
49− <span>1</span>
50− <span title="1503595924 aaa">5 hours</span>
51− <span>30MB</span>
52− <span>5,555</span>
53− <span>1,234,567</span>
54− </dd>
55− </dl>
56− </div>
57− """
58−
59− resp = mock.Mock(text=html)
60− results = torrentz.response(resp)
61−
62− self.assertEqual(type(results), list)
63− self.assertEqual(len(results), 2)
64−
65− # testing against the first result
66− r = results[0]
67− self.assertEqual(r['url'], 'https://torrentz2.eu/4362e08b1d80e1820fb2550b752f9f3126fe76d6')
68− self.assertEqual(r['title'], 'Completely valid info books ebooks')
69− # 22 Nov 2015 03:01:42
70− self.assertEqual(r['publishedDate'], datetime.fromtimestamp(1503595924))
71− self.assertEqual(r['seed'], 14)
72− self.assertEqual(r['leech'], 1)
73− self.assertEqual(r['filesize'], 30 * 1024 * 1024)
74− self.assertEqual(r['magnetlink'], 'magnet:?xt=urn:btih:4362e08b1d80e1820fb2550b752f9f3126fe76d6')
75−
76− # testing against the second result
77− r = results[1]
78− self.assertEqual(r['url'], 'https://torrentz2.eu/poaskdpokaspod')
79− self.assertEqual(r['title'], 'Invalid hash and date and filesize books ebooks')
80− self.assertEqual(r['seed'], 5555)
81− self.assertEqual(r['leech'], 1234567)
82−
83− # in the second result we have invalid hash, creation date & torrent size,
84− # so these tests should fail
85− self.assertFalse('magnetlink' in r)
86− self.assertFalse('filesize' in r)
87− self.assertFalse('publishedDate' in r)
tests/unit/engines/test_twitter.pydeleted−502
@@ -1,502 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import twitter
5−from searx.testing import SearxTestCase
6−
7−
8−class TestTwitterEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 0
14− dicto['language'] = 'fr_FR'
15− params = twitter.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn(query, params['url'])
18− self.assertIn('twitter.com', params['url'])
19− self.assertIn('cookies', params)
20− self.assertIn('lang', params['cookies'])
21− self.assertIn('fr', params['cookies']['lang'])
22−
23− dicto['language'] = 'all'
24− params = twitter.request(query, dicto)
25− self.assertIn('cookies', params)
26− self.assertIn('lang', params['cookies'])
27− self.assertIn('en', params['cookies']['lang'])
28−
29− def test_response(self):
30− self.assertRaises(AttributeError, twitter.response, None)
31− self.assertRaises(AttributeError, twitter.response, [])
32− self.assertRaises(AttributeError, twitter.response, '')
33− self.assertRaises(AttributeError, twitter.response, '[]')
34−
35− response = mock.Mock(text='<html></html>')
36− self.assertEqual(twitter.response(response), [])
37−
38− html = """
39− <li class="js-stream-item stream-item stream-item expanding-stream-item" data-item-id="563005573290287105"
40− id="stream-item-tweet-563005573290287105" data-item-type="tweet">
41− <div class="tweet original-tweet js-stream-tweet js-actionable-tweet js-profile-popup-actionable
42− js-original-tweet has-cards has-native-media" data-tweet-id="563005573290287105" data-disclosure-type=""
43− data-item-id="563005573290287105" data-screen-name="Jalopnik" data-name="Jalopnik"
44− data-user-id="3060631" data-has-native-media="true" data-has-cards="true" data-card-type="photo"
45− data-expanded-footer="&lt;div class=&quot;js-tweet-details-fixer
46− tweet-details-fixer&quot;&gt;&#10;&#10;&#10;
47− &lt;div class=&quot;cards-media-container js-media-container&quot;&gt;&lt;div
48− data-card-url=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot; data-card-type=&quot;
49− photo&quot; class=&quot;cards-base cards-multimedia&quot; data-element-context=&quot;platform_photo_card
50− &quot;&gt;&#10;&#10;&#10; &lt;a class=&quot;media media-thumbnail twitter-timeline-link is-preview
51− &quot; data-url=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
52− data-resolved-url-large=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
53− href=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot;&gt;&#10;
54− &lt;div class=&quot;&quot;&gt;&#10; &lt;img src=&quot;
55− https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg&quot;
56− alt=&quot;Embedded image permalink&quot; width=&quot;636&quot; height=&quot;309&quot;&gt;&#10;
57− &lt;/div&gt;&#10;&#10; &lt;/a&gt;&#10;&#10; &lt;div class=&quot;cards-content&quot;&gt;&#10;
58− &lt;div class=&quot;byline&quot;&gt;&#10; &#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;
59− &#10;&lt;/div&gt;&#10;&#10;&#10;&#10;&#10;&lt;/div&gt;&#10;&#10;&#10;&#10; &lt;div
60− class=&quot;js-machine-translated-tweet-container&quot;&gt;&lt;/div&gt;&#10; &lt;div
61− class=&quot;js-tweet-stats-container tweet-stats-container &quot;&gt;&#10; &lt;/div&gt;&#10;&#10;
62− &lt;div class=&quot;client-and-actions&quot;&gt;&#10; &lt;span class=&quot;metadata&quot;&gt;&#10;
63− &lt;span&gt;5:06 PM - 4 Feb 2015&lt;/span&gt;&#10;&#10; &amp;middot; &lt;a
64− class=&quot;permalink-link js-permalink js-nav&quot; href=&quot;/Jalopnik/status/563005573290287105
65− &quot;tabindex=&quot;-1&quot;&gt;Details&lt;/a&gt;&#10; &#10;&#10; &#10; &#10;
66− &#10;&#10; &lt;/span&gt;&#10;&lt;/div&gt;&#10;&#10;&#10;&lt;/div&gt;&#10;" data-you-follow="false"
67− data-you-block="false">
68− <div class="context">
69− </div>
70− <div class="content">
71− <div class="stream-item-header">
72− <a class="account-group js-account-group js-action-profile js-user-profile-link js-nav"
73− href="/Jalopnik" data-user-id="3060631">
74− <img class="avatar js-action-profile-avatar"
75− src="https://pbs.twimg.com/profile_images/2976430168/5cd4a59_bigger.jpeg" alt="">
76− <strong class="fullname js-action-profile-name show-popup-with-id" data-aria-label-part>
77− Jalopnik
78− </strong>
79− <span>&rlm;</span>
80− <span class="username js-action-profile-name" data-aria-label-part>
81− <s>@</s><b>TitleName</b>
82− </span>
83− </a>
84− <small class="time">
85− <a href="/this.is.the.url"
86− class="tweet-timestamp js-permalink js-nav js-tooltip" title="5:06 PM - 4 Feb 2015" >
87− <span class="u-hiddenVisually" data-aria-label-part="last">17 minutes ago</span>
88− </a>
89− </small>
90− </div>
91− <p class="js-tweet-text tweet-text" lang="en" data-aria-label-part="0">
92− This is the content étude à€
93− <a href="http://t.co/nRWsqQAwBL" rel="nofollow" dir="ltr"
94− data-expanded-url="http://jalo.ps/ReMENu4" class="twitter-timeline-link"
95− target="_blank" title="http://jalo.ps/ReMENu4" >
96− <span class="tco-ellipsis">
97− </span>
98− <span class="invisible">http://</span><span class="js-display-url">link.in.tweet</span>
99− <span class="invisible"></span>
100− <span class="tco-ellipsis">
101− <span class="invisible">&nbsp;</span>
102− </span>
103− </a>
104− <a href="http://t.co/rbFsfeE0l3" class="twitter-timeline-link u-hidden"
105− data-pre-embedded="true" dir="ltr">
106− pic.twitter.com/rbFsfeE0l3
107− </a>
108− </p>
109− <div class="expanded-content js-tweet-details-dropdown">
110− </div>
111− <div class="stream-item-footer">
112− <a class="details with-icn js-details" href="/Jalopnik/status/563005573290287105">
113− <span class="Icon Icon--photo">
114− </span>
115− <b>
116− <span class="expand-stream-item js-view-details">
117− View photo
118− </span>
119− <span class="collapse-stream-item js-hide-details">
120− Hide photo
121− </span>
122− </b>
123− </a>
124− <span class="ProfileTweet-action--reply u-hiddenVisually">
125− <span class="ProfileTweet-actionCount" aria-hidden="true" data-tweet-stat-count="0">
126− <span class="ProfileTweet-actionCountForAria" >0 replies</span>
127− </span>
128− </span>
129− <span class="ProfileTweet-action--retweet u-hiddenVisually">
130− <span class="ProfileTweet-actionCount" data-tweet-stat-count="8">
131− <span class="ProfileTweet-actionCountForAria" data-aria-label-part>8 retweets</span>
132− </span>
133− </span>
134− <span class="ProfileTweet-action--favorite u-hiddenVisually">
135− <span class="ProfileTweet-actionCount" data-tweet-stat-count="14">
136− <span class="ProfileTweet-actionCountForAria" data-aria-label-part>14 favorites</span>
137− </span>
138− </span>
139− <div role="group" aria-label="Tweet actions" class="ProfileTweet-actionList u-cf js-actions">
140− <div class="ProfileTweet-action ProfileTweet-action--reply">
141− <button class="ProfileTweet-actionButton u-textUserColorHover js-actionButton
142− js-actionReply" data-modal="ProfileTweet-reply" type="button" title="Reply">
143− <span class="Icon Icon--reply">
144− </span>
145− <span class="u-hiddenVisually">Reply</span>
146− <span class="ProfileTweet-actionCount u-textUserColorHover
147− ProfileTweet-actionCount--isZero">
148− <span class="ProfileTweet-actionCountForPresentation" aria-hidden="true">
149− </span>
150− </span>
151− </button>
152− </div>
153− <div class="ProfileTweet-action ProfileTweet-action--retweet js-toggleState js-toggleRt">
154− <button class="ProfileTweet-actionButton js-actionButton js-actionRetweet js-tooltip"
155− title="Retweet" data-modal="ProfileTweet-retweet" type="button">
156− <span class="Icon Icon--retweet">
157− </span>
158− <span class="u-hiddenVisually">Retweet</span>
159− <span class="ProfileTweet-actionCount">
160− <span class="ProfileTweet-actionCountForPresentation">8</span>
161− </span>
162− </button>
163− <button class="ProfileTweet-actionButtonUndo js-actionButton js-actionRetweet"
164− data-modal="ProfileTweet-retweet" title="Undo retweet" type="button">
165− <span class="Icon Icon--retweet">
166− </span>
167− <span class="u-hiddenVisually">Retweeted</span>
168− <span class="ProfileTweet-actionCount">
169− <span class="ProfileTweet-actionCountForPresentation">8</span>
170− </span>
171− </button>
172− </div>
173− <div class="ProfileTweet-action ProfileTweet-action--favorite js-toggleState">
174− <button class="ProfileTweet-actionButton js-actionButton js-actionFavorite js-tooltip"
175− title="Favorite" type="button">
176− <span class="Icon Icon--favorite">
177− </span>
178− <span class="u-hiddenVisually">Favorite</span>
179− <span class="ProfileTweet-actionCount">
180− <span class="ProfileTweet-actionCountForPresentation">14</span>
181− </span>
182− </button>
183− <button class="ProfileTweet-actionButtonUndo u-linkClean js-actionButton
184− js-actionFavorite" title="Undo favorite" type="button">
185− <span class="Icon Icon--favorite">
186− </span>
187− <span class="u-hiddenVisually">Favorited</span>
188− <span class="ProfileTweet-actionCount">
189− <span class="ProfileTweet-actionCountForPresentation">
190− 14
191− </span>
192− </span>
193− </button>
194− </div>
195− <div class="ProfileTweet-action ProfileTweet-action--more js-more-ProfileTweet-actions">
196− <div class="dropdown">
197− <button class="ProfileTweet-actionButton u-textUserColorHover dropdown-toggle
198− js-tooltip js-dropdown-toggle" type="button" title="More">
199− <span class="Icon Icon--dots">
200− </span>
201− <span class="u-hiddenVisually">More</span>
202− </button>
203− <div class="dropdown-menu">
204− <div class="dropdown-caret">
205− <div class="caret-outer">
206− </div>
207− <div class="caret-inner">
208− </div>
209− </div>
210− <ul>
211− <li class="share-via-dm js-actionShareViaDM" data-nav="share_tweet_dm">
212− <button type="button" class="dropdown-link">
213− Share via Direct Message
214− </button>
215− </li>
216− <li class="embed-link js-actionEmbedTweet" data-nav="embed_tweet">
217− <button type="button" class="dropdown-link">
218− Embed Tweet
219− </button>
220− </li>
221− <li class="mute-user-item pretty-link">
222− <button type="button" class="dropdown-link">
223− Mute
224− </button>
225− </li>
226− <li class="unmute-user-item pretty-link">
227− <button type="button" class="dropdown-link">
228− Unmute
229− </button>
230− </li>
231− <li class="block-or-report-link js-actionBlockOrReport"
232− data-nav="block_or_report">
233− <button type="button" class="dropdown-link">
234− Block or report
235− </button>
236− </li>
237− </ul>
238− </div>
239− </div>
240− </div>
241− </div>
242− </div>
243− </div>
244− </div>
245− </li>
246− """
247− response = mock.Mock(text=html)
248− results = twitter.response(response)
249− self.assertEqual(type(results), list)
250− self.assertEqual(len(results), 1)
251− self.assertEqual(results[0]['title'], '@TitleName')
252− self.assertEqual(results[0]['url'], 'https://twitter.com/this.is.the.url')
253− self.assertIn(u'This is the content', results[0]['content'])
254− # self.assertIn(u'This is the content étude à€', results[0]['content'])
255−
256− html = """
257− <li class="js-stream-item stream-item stream-item expanding-stream-item" data-item-id="563005573290287105"
258− id="stream-item-tweet-563005573290287105" data-item-type="tweet">
259− <div class="tweet original-tweet js-stream-tweet js-actionable-tweet js-profile-popup-actionable
260− js-original-tweet has-cards has-native-media" data-tweet-id="563005573290287105" data-disclosure-type=""
261− data-item-id="563005573290287105" data-screen-name="Jalopnik" data-name="Jalopnik"
262− data-user-id="3060631" data-has-native-media="true" data-has-cards="true" data-card-type="photo"
263− data-expanded-footer="&lt;div class=&quot;js-tweet-details-fixer
264− tweet-details-fixer&quot;&gt;&#10;&#10;&#10;
265− &lt;div class=&quot;cards-media-container js-media-container&quot;&gt;&lt;div
266− data-card-url=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot; data-card-type=&quot;
267− photo&quot; class=&quot;cards-base cards-multimedia&quot; data-element-context=&quot;platform_photo_card
268− &quot;&gt;&#10;&#10;&#10; &lt;a class=&quot;media media-thumbnail twitter-timeline-link is-preview
269− &quot; data-url=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
270− data-resolved-url-large=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
271− href=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot;&gt;&#10;
272− &lt;div class=&quot;&quot;&gt;&#10; &lt;img src=&quot;
273− https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg&quot;
274− alt=&quot;Embedded image permalink&quot; width=&quot;636&quot; height=&quot;309&quot;&gt;&#10;
275− &lt;/div&gt;&#10;&#10; &lt;/a&gt;&#10;&#10; &lt;div class=&quot;cards-content&quot;&gt;&#10;
276− &lt;div class=&quot;byline&quot;&gt;&#10; &#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;
277− &#10;&lt;/div&gt;&#10;&#10;&#10;&#10;&#10;&lt;/div&gt;&#10;&#10;&#10;&#10; &lt;div
278− class=&quot;js-machine-translated-tweet-container&quot;&gt;&lt;/div&gt;&#10; &lt;div
279− class=&quot;js-tweet-stats-container tweet-stats-container &quot;&gt;&#10; &lt;/div&gt;&#10;&#10;
280− &lt;div class=&quot;client-and-actions&quot;&gt;&#10; &lt;span class=&quot;metadata&quot;&gt;&#10;
281− &lt;span&gt;5:06 PM - 4 Feb 2015&lt;/span&gt;&#10;&#10; &amp;middot; &lt;a
282− class=&quot;permalink-link js-permalink js-nav&quot; href=&quot;/Jalopnik/status/563005573290287105
283− &quot;tabindex=&quot;-1&quot;&gt;Details&lt;/a&gt;&#10; &#10;&#10; &#10; &#10;
284− &#10;&#10; &lt;/span&gt;&#10;&lt;/div&gt;&#10;&#10;&#10;&lt;/div&gt;&#10;" data-you-follow="false"
285− data-you-block="false">
286− <div class="context">
287− </div>
288− <div class="content">
289− <div class="stream-item-header">
290− <a class="account-group js-account-group js-action-profile js-user-profile-link js-nav"
291− href="/Jalopnik" data-user-id="3060631">
292− <img class="avatar js-action-profile-avatar"
293− src="https://pbs.twimg.com/profile_images/2976430168/5cd4a59_bigger.jpeg" alt="">
294− <strong class="fullname js-action-profile-name show-popup-with-id" data-aria-label-part>
295− Jalopnik
296− </strong>
297− <span>&rlm;</span>
298− <span class="username js-action-profile-name" data-aria-label-part>
299− <s>@</s><b>TitleName</b>
300− </span>
301− </a>
302− <small class="time">
303− <a href="/this.is.the.url"
304− class="tweet-timestamp js-permalink js-nav js-tooltip" title="5:06 PM - 4 Feb 2015" >
305− <span class="_timestamp js-short-timestamp js-relative-timestamp" data-time="1423065963"
306− data-time-ms="1423065963000" data-long-form="true" aria-hidden="true">
307− 17m
308− </span>
309− <span class="u-hiddenVisually" data-aria-label-part="last">17 minutes ago</span>
310− </a>
311− </small>
312− </div>
313− <p class="js-tweet-text tweet-text" lang="en" data-aria-label-part="0">
314− This is the content étude à€
315− <a href="http://t.co/nRWsqQAwBL" rel="nofollow" dir="ltr"
316− data-expanded-url="http://jalo.ps/ReMENu4" class="twitter-timeline-link"
317− target="_blank" title="http://jalo.ps/ReMENu4" >
318− <span class="tco-ellipsis">
319− </span>
320− <span class="invisible">http://</span><span class="js-display-url">link.in.tweet</span>
321− <span class="invisible"></span>
322− <span class="tco-ellipsis">
323− <span class="invisible">&nbsp;</span>
324− </span>
325− </a>
326− <a href="http://t.co/rbFsfeE0l3" class="twitter-timeline-link u-hidden"
327− data-pre-embedded="true" dir="ltr">
328− pic.twitter.com/rbFsfeE0l3
329− </a>
330− </p>
331− <div class="expanded-content js-tweet-details-dropdown">
332− </div>
333− <div class="stream-item-footer">
334− <a class="details with-icn js-details" href="/Jalopnik/status/563005573290287105">
335− <span class="Icon Icon--photo">
336− </span>
337− <b>
338− <span class="expand-stream-item js-view-details">
339− View photo
340− </span>
341− <span class="collapse-stream-item js-hide-details">
342− Hide photo
343− </span>
344− </b>
345− </a>
346− <span class="ProfileTweet-action--reply u-hiddenVisually">
347− <span class="ProfileTweet-actionCount" aria-hidden="true" data-tweet-stat-count="0">
348− <span class="ProfileTweet-actionCountForAria" >0 replies</span>
349− </span>
350− </span>
351− <span class="ProfileTweet-action--retweet u-hiddenVisually">
352− <span class="ProfileTweet-actionCount" data-tweet-stat-count="8">
353− <span class="ProfileTweet-actionCountForAria" data-aria-label-part>8 retweets</span>
354− </span>
355− </span>
356− <span class="ProfileTweet-action--favorite u-hiddenVisually">
357− <span class="ProfileTweet-actionCount" data-tweet-stat-count="14">
358− <span class="ProfileTweet-actionCountForAria" data-aria-label-part>14 favorites</span>
359− </span>
360− </span>
361− <div role="group" aria-label="Tweet actions" class="ProfileTweet-actionList u-cf js-actions">
362− <div class="ProfileTweet-action ProfileTweet-action--reply">
363− <button class="ProfileTweet-actionButton u-textUserColorHover js-actionButton
364− js-actionReply" data-modal="ProfileTweet-reply" type="button" title="Reply">
365− <span class="Icon Icon--reply">
366− </span>
367− <span class="u-hiddenVisually">Reply</span>
368− <span class="ProfileTweet-actionCount u-textUserColorHover
369− ProfileTweet-actionCount--isZero">
370− <span class="ProfileTweet-actionCountForPresentation" aria-hidden="true">
371− </span>
372− </span>
373− </button>
374− </div>
375− <div class="ProfileTweet-action ProfileTweet-action--retweet js-toggleState js-toggleRt">
376− <button class="ProfileTweet-actionButton js-actionButton js-actionRetweet js-tooltip"
377− title="Retweet" data-modal="ProfileTweet-retweet" type="button">
378− <span class="Icon Icon--retweet">
379− </span>
380− <span class="u-hiddenVisually">Retweet</span>
381− <span class="ProfileTweet-actionCount">
382− <span class="ProfileTweet-actionCountForPresentation">8</span>
383− </span>
384− </button>
385− <button class="ProfileTweet-actionButtonUndo js-actionButton js-actionRetweet"
386− data-modal="ProfileTweet-retweet" title="Undo retweet" type="button">
387− <span class="Icon Icon--retweet">
388− </span>
389− <span class="u-hiddenVisually">Retweeted</span>
390− <span class="ProfileTweet-actionCount">
391− <span class="ProfileTweet-actionCountForPresentation">8</span>
392− </span>
393− </button>
394− </div>
395− <div class="ProfileTweet-action ProfileTweet-action--favorite js-toggleState">
396− <button class="ProfileTweet-actionButton js-actionButton js-actionFavorite js-tooltip"
397− title="Favorite" type="button">
398− <span class="Icon Icon--favorite">
399− </span>
400− <span class="u-hiddenVisually">Favorite</span>
401− <span class="ProfileTweet-actionCount">
402− <span class="ProfileTweet-actionCountForPresentation">14</span>
403− </span>
404− </button>
405− <button class="ProfileTweet-actionButtonUndo u-linkClean js-actionButton
406− js-actionFavorite" title="Undo favorite" type="button">
407− <span class="Icon Icon--favorite">
408− </span>
409− <span class="u-hiddenVisually">Favorited</span>
410− <span class="ProfileTweet-actionCount">
411− <span class="ProfileTweet-actionCountForPresentation">
412− 14
413− </span>
414− </span>
415− </button>
416− </div>
417− <div class="ProfileTweet-action ProfileTweet-action--more js-more-ProfileTweet-actions">
418− <div class="dropdown">
419− <button class="ProfileTweet-actionButton u-textUserColorHover dropdown-toggle
420− js-tooltip js-dropdown-toggle" type="button" title="More">
421− <span class="Icon Icon--dots">
422− </span>
423− <span class="u-hiddenVisually">More</span>
424− </button>
425− <div class="dropdown-menu">
426− <div class="dropdown-caret">
427− <div class="caret-outer">
428− </div>
429− <div class="caret-inner">
430− </div>
431− </div>
432− <ul>
433− <li class="share-via-dm js-actionShareViaDM" data-nav="share_tweet_dm">
434− <button type="button" class="dropdown-link">
435− Share via Direct Message
436− </button>
437− </li>
438− <li class="embed-link js-actionEmbedTweet" data-nav="embed_tweet">
439− <button type="button" class="dropdown-link">
440− Embed Tweet
441− </button>
442− </li>
443− <li class="mute-user-item pretty-link">
444− <button type="button" class="dropdown-link">
445− Mute
446− </button>
447− </li>
448− <li class="unmute-user-item pretty-link">
449− <button type="button" class="dropdown-link">
450− Unmute
451− </button>
452− </li>
453− <li class="block-or-report-link js-actionBlockOrReport"
454− data-nav="block_or_report">
455− <button type="button" class="dropdown-link">
456− Block or report
457− </button>
458− </li>
459− </ul>
460− </div>
461− </div>
462− </div>
463− </div>
464− </div>
465− </div>
466− </div>
467− </li>
468− """
469− response = mock.Mock(text=html)
470− results = twitter.response(response)
471− self.assertEqual(type(results), list)
472− self.assertEqual(len(results), 1)
473− self.assertEqual(results[0]['title'], '@TitleName')
474− self.assertEqual(results[0]['url'], 'https://twitter.com/this.is.the.url')
475− self.assertIn(u'This is the content', results[0]['content'])
476−
477− html = """
478− <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
479− <div Class="sa_mc">
480− <div class="sb_tlst">
481− <h2>
482− <a href="http://this.should.be.the.link/" h="ID=SERP,5124.1">
483− <strong>This</strong> should be the title</a>
484− </h2>
485− </div>
486− <div class="sb_meta">
487− <cite>
488− <strong>this</strong>.meta.com</cite>
489− <span class="c_tlbxTrg">
490− <span class="c_tlbxH" H="BASE:CACHEDPAGEDEFAULT" K="SERP,5125.1">
491− </span>
492− </span>
493− </div>
494− <p>
495− <strong>This</strong> should be the content.</p>
496− </div>
497− </li>
498− """
499− response = mock.Mock(text=html)
500− results = twitter.response(response)
501− self.assertEqual(type(results), list)
502− self.assertEqual(len(results), 0)
tests/unit/engines/test_unsplash.pydeleted−38
@@ -1,38 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.testing import SearxTestCase
4−from searx.engines import unsplash
5−
6−
7−class TestUnsplashEngine(SearxTestCase):
8− def test_request(self):
9− query = 'penguin'
10− _dict = defaultdict(dict)
11− _dict['pageno'] = 1
12− params = unsplash.request(query, _dict)
13−
14− self.assertTrue('url' in params)
15− self.assertTrue(query in params['url'])
16−
17− def test_response(self):
18− resp = mock.Mock(text='{}')
19− result = unsplash.response(resp)
20− self.assertEqual([], result)
21−
22− resp.text = '{"results": []}'
23− result = unsplash.response(resp)
24− self.assertEqual([], result)
25−
26− # Sourced from https://unsplash.com/napi/search/photos?query=penguin&xp=&per_page=20&page=2
27− with open('./tests/unit/engines/unsplash_fixture.json') as fixture:
28− resp.text = fixture.read()
29−
30− result = unsplash.response(resp)
31− self.assertEqual(len(result), 2)
32− self.assertEqual(result[0]['title'], 'low angle photography of swimming penguin')
33− self.assertEqual(result[0]['url'], 'https://unsplash.com/photos/FY8d721UO_4')
34− self.assertEqual(result[0]['thumbnail_src'], 'https://images.unsplash.com/photo-1523557148507-1b77641c7e7c?ixlib=rb-0.3.5&q=80\
35−&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max')
36− self.assertEqual(result[0]['img_src'], 'https://images.unsplash.com/photo-1523557148507-1b77641c7e7c\
37−?ixlib=rb-0.3.5')
38− self.assertEqual(result[0]['content'], '')
tests/unit/engines/test_vimeo.pydeleted−36
@@ -1,36 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import vimeo
5−from searx.testing import SearxTestCase
6−
7−
8−class TestVimeoEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 0
14− params = vimeo.request(query, dicto)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertTrue('vimeo.com' in params['url'])
18−
19− def test_response(self):
20− self.assertRaises(AttributeError, vimeo.response, None)
21− self.assertRaises(AttributeError, vimeo.response, [])
22− self.assertRaises(AttributeError, vimeo.response, '')
23− self.assertRaises(AttributeError, vimeo.response, '[]')
24−
25− json = u"""
26−{"filtered":{"total":274641,"page":1,"per_page":18,"paging":{"next":"?sizes=590x332&page=2","previous":null,"first":"?sizes=590x332&page=1","last":"?sizes=590x332&page=15258"},"data":[{"is_staffpick":false,"is_featured":true,"type":"clip","clip":{"uri":"\\/videos\\/106557563","name":"Hot Rod Revue: The South","link":"https:\\/\\/vimeo.com\\/106557563","duration":4069,"created_time":"2014-09-19T03:38:04+00:00","privacy":{"view":"ptv"},"pictures":{"sizes":[{"width":"590","height":"332","link":"https:\\/\\/i.vimeocdn.com\\/video\\/489717884_590x332.jpg?r=pad","link_with_play_button":"https:\\/\\/i.vimeocdn.com\\/filter\\/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F489717884_590x332.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"}]},"stats":{"plays":null},"metadata":{"connections":{"comments":{"total":0},"likes":{"total":5}},"interactions":[]},"user":{"name":"Cal Thorley","link":"https:\\/\\/vimeo.com\\/calthorley","pictures":{"sizes":[{"width":30,"height":30,"link":"https:\\/\\/i.vimeocdn.com\\/portrait\\/2545308_30x30?r=pad"},{"width":75,"height":75,"link":"https:\\/\\/i.vimeocdn.com\\/portrait\\/2545308_75x75?r=pad"},{"width":100,"height":100,"link":"https:\\/\\/i.vimeocdn.com\\/portrait\\/2545308_100x100?r=pad"},{"width":300,"height":300,"link":"https:\\/\\/i.vimeocdn.com\\/portrait\\/2545308_300x300?r=pad"}]}}}}]}};
27−
28−""" # noqa
29− response = mock.Mock(text=json)
30− results = vimeo.response(response)
31− self.assertEqual(type(results), list)
32− self.assertEqual(len(results), 1)
33− self.assertEqual(results[0]['title'], u'Hot Rod Revue: The South')
34− self.assertEqual(results[0]['url'], 'https://vimeo.com/106557563')
35− self.assertEqual(results[0]['content'], '')
36− self.assertEqual(results[0]['thumbnail'], 'https://i.vimeocdn.com/video/489717884_590x332.jpg?r=pad')
tests/unit/engines/test_wikidata.pydeleted−514
@@ -1,514 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from lxml.html import fromstring
3−from lxml import etree
4−from collections import defaultdict
5−import mock
6−from searx.engines import wikidata
7−from searx.testing import SearxTestCase
8−
9−
10−class TestWikidataEngine(SearxTestCase):
11−
12− def test_request(self):
13− query = 'test_query'
14− dicto = defaultdict(dict)
15− dicto['language'] = 'all'
16− params = wikidata.request(query, dicto)
17− self.assertIn('url', params)
18− self.assertIn(query, params['url'])
19− self.assertIn('wikidata.org', params['url'])
20−
21− dicto['language'] = 'es_ES'
22− params = wikidata.request(query, dicto)
23− self.assertIn(query, params['url'])
24−
25− # successful cases are not tested here to avoid sending additional requests
26− def test_response(self):
27− self.assertRaises(AttributeError, wikidata.response, None)
28− self.assertRaises(AttributeError, wikidata.response, [])
29− self.assertRaises(AttributeError, wikidata.response, '')
30− self.assertRaises(AttributeError, wikidata.response, '[]')
31−
32− wikidata.supported_languages = ['en', 'es']
33− wikidata.language_aliases = {}
34− response = mock.Mock(content='<html></html>'.encode("utf-8"), search_params={"language": "en"})
35− self.assertEqual(wikidata.response(response), [])
36−
37− def test_getDetail(self):
38− response = {}
39− results = wikidata.getDetail(response, "Q123", "en", "en-US", etree.HTMLParser())
40− self.assertEqual(results, [])
41−
42− title_html = '<div><div class="wikibase-title-label">Test</div></div>'
43− html = """
44− <div>
45− <div class="wikibase-entitytermsview-heading-description">
46− </div>
47− <div>
48− <ul class="wikibase-sitelinklistview-listview">
49− <li data-wb-siteid="enwiki"><a href="http://en.wikipedia.org/wiki/Test">Test</a></li>
50− </ul>
51− </div>
52− </div>
53− """
54− response = {"parse": {"displaytitle": title_html, "text": html}}
55−
56− results = wikidata.getDetail(response, "Q123", "en", "en-US", etree.HTMLParser())
57− self.assertEqual(len(results), 1)
58− self.assertEqual(results[0]['url'], 'https://en.wikipedia.org/wiki/Test')
59−
60− title_html = """
61− <div>
62− <div class="wikibase-title-label">
63− <span lang="en">Test</span>
64− <sup class="wb-language-fallback-indicator">English</sup>
65− </div>
66− </div>
67− """
68− html = """
69− <div>
70− <div class="wikibase-entitytermsview-heading-description">
71− <span lang="en">Description</span>
72− <sup class="wb-language-fallback-indicator">English</sup>
73− </div>
74− <div id="P856">
75− <div class="wikibase-statementgroupview-property-label">
76− <a href="/wiki/Property:P856">
77− <span lang="en">official website</span>
78− <sup class="wb-language-fallback-indicator">English</sup>
79− </a>
80− </div>
81− <div class="wikibase-statementview-mainsnak">
82− <a class="external free" href="https://officialsite.com">
83− https://officialsite.com
84− </a>
85− </div>
86− </div>
87− <div>
88− <ul class="wikibase-sitelinklistview-listview">
89− <li data-wb-siteid="enwiki"><a href="http://en.wikipedia.org/wiki/Test">Test</a></li>
90− </ul>
91− </div>
92− </div>
93− """
94− response = {"parse": {"displaytitle": title_html, "text": html}}
95−
96− results = wikidata.getDetail(response, "Q123", "yua", "yua_MX", etree.HTMLParser())
97− self.assertEqual(len(results), 2)
98− self.assertEqual(results[0]['title'], 'Official website')
99− self.assertEqual(results[0]['url'], 'https://officialsite.com')
100−
101− self.assertEqual(results[1]['infobox'], 'Test')
102− self.assertEqual(results[1]['id'], None)
103− self.assertEqual(results[1]['content'], 'Description')
104− self.assertEqual(results[1]['attributes'], [])
105− self.assertEqual(results[1]['urls'][0]['title'], 'Official website')
106− self.assertEqual(results[1]['urls'][0]['url'], 'https://officialsite.com')
107− self.assertEqual(results[1]['urls'][1]['title'], 'Wikipedia (en)')
108− self.assertEqual(results[1]['urls'][1]['url'], 'https://en.wikipedia.org/wiki/Test')
109−
110− def test_add_image(self):
111− image_src = wikidata.add_image(fromstring("<div></div>"))
112− self.assertEqual(image_src, None)
113−
114− html = u"""
115− <div>
116− <div id="P18">
117− <div class="wikibase-statementgroupview-property-label">
118− <a href="/wiki/Property:P18">
119− image
120− </a>
121− </div>
122− <div class="wikibase-statementlistview">
123− <div class="wikibase-statementview listview-item">
124− <div class="wikibase-statementview-rankselector">
125− <span class="wikibase-rankselector-normal"></span>
126− </div>
127− <div class="wikibase-statementview-mainsnak">
128− <div>
129− <div class="wikibase-snakview-value">
130− <div class="commons-media-caption">
131− <a href="https://commons.wikimedia.org/wiki/File:image.png">image.png</a>
132− <br/>2,687 &#215; 3,356; 1.22 MB
133− </div>
134− </div>
135− </div>
136− </div>
137− </div>
138− </div>
139− </div>
140− </div>
141− """
142− html_etree = fromstring(html)
143− id_cache = wikidata.get_id_cache(html_etree)
144− image_src = wikidata.add_image(id_cache)
145− self.assertEqual(image_src,
146− "https://commons.wikimedia.org/wiki/Special:FilePath/image.png?width=500&height=400")
147−
148− html = u"""
149− <div>
150− <div id="P2910">
151− <div class="wikibase-statementgroupview-property-label">
152− <a href="/wiki/Property:P2910">
153− icon
154− </a>
155− </div>
156− <div class="wikibase-statementlistview">
157− <div class="wikibase-statementview listview-item">
158− <div class="wikibase-statementview-rankselector">
159− <span class="wikibase-rankselector-normal"></span>
160− </div>
161− <div class="wikibase-statementview-mainsnak">
162− <div>
163− <div class="wikibase-snakview-value">
164− <div class="commons-media-caption">
165− <a href="https://commons.wikimedia.org/wiki/File:icon.png">icon.png</a>
166− <br/>671 &#215; 671; 18 KB</div>
167− </div>
168− </div>
169− </div>
170− </div>
171− </div>
172− </div>
173− </div>
174− <div id="P154">
175− <div class="wikibase-statementgroupview-property-label">
176− <a href="/wiki/Property:P154">
177− logo
178− </a>
179− </div>
180− <div class="wikibase-statementlistview">
181− <div class="wikibase-statementview listview-item">
182− <div class="wikibase-statementview-rankselector">
183− <span class="wikibase-rankselector-normal"></span>
184− </div>
185− <div class="wikibase-statementview-mainsnak">
186− <div>
187− <div class="wikibase-snakview-value">
188− <div class="commons-media-caption">
189− <a href="https://commons.wikimedia.org/wiki/File:logo.png">logo.png</a>
190− <br/>170 &#215; 170; 1 KB
191− </div>
192− </div>
193− </div>
194− </div>
195− </div>
196− </div>
197− </div>
198− </div>
199− """
200− html_etree = fromstring(html)
201− id_cache = wikidata.get_id_cache(html_etree)
202−
203− image_src = wikidata.add_image(id_cache)
204− self.assertEqual(image_src,
205− "https://commons.wikimedia.org/wiki/Special:FilePath/logo.png?width=500&height=400")
206−
207− def test_add_attribute(self):
208− html = u"""
209− <div>
210− <div id="P27">
211− <div class="wikibase-statementgroupview-property-label">
212− <a href="/wiki/Property:P27">
213− country of citizenship
214− </a>
215− </div>
216− <div class="wikibase-statementlistview">
217− <div class="wikibase-statementview listview-item">
218− <div class="wikibase-statementview-rankselector">
219− <span class="wikibase-rankselector-normal"></span>
220− </div>
221− <div class="wikibase-statementview-mainsnak">
222− <div>
223− <div class="wikibase-snakview-value">
224− <a href="/wiki/Q145">
225− United Kingdom
226− </a>
227− </div>
228− </div>
229− </div>
230− </div>
231− </div>
232− </div>
233− </div>
234− """
235− attributes = []
236− html_etree = fromstring(html)
237− id_cache = wikidata.get_id_cache(html_etree)
238−
239− wikidata.add_attribute(attributes, id_cache, "Fail")
240− self.assertEqual(attributes, [])
241−
242− wikidata.add_attribute(attributes, id_cache, "P27")
243− self.assertEqual(len(attributes), 1)
244− self.assertEqual(attributes[0]["label"], "Country of citizenship")
245− self.assertEqual(attributes[0]["value"], "United Kingdom")
246−
247− html = u"""
248− <div>
249− <div id="P569">
250− <div class="wikibase-statementgroupview-property-label">
251− <a href="/wiki/Property:P569">
252− date of birth
253− </a>
254− </div>
255− <div class="wikibase-statementlistview">
256− <div class="wikibase-statementview listview-item">
257− <div class="wikibase-statementview-rankselector">
258− <span class="wikibase-rankselector-normal"></span>
259− </div>
260− <div class="wikibase-statementview-mainsnak">
261− <div>
262− <div class="wikibase-snakview-value">
263− 27 January 1832
264− <sup class="wb-calendar-name">
265− Gregorian
266− </sup>
267− </div>
268− </div>
269− </div>
270− </div>
271− </div>
272− </div>
273− </div>
274− """
275− attributes = []
276− html_etree = fromstring(html)
277− id_cache = wikidata.get_id_cache(html_etree)
278− wikidata.add_attribute(attributes, id_cache, "P569", date=True)
279− self.assertEqual(len(attributes), 1)
280− self.assertEqual(attributes[0]["label"], "Date of birth")
281− self.assertEqual(attributes[0]["value"], "27 January 1832")
282−
283− html = u"""
284− <div>
285− <div id="P6">
286− <div class="wikibase-statementgroupview-property-label">
287− <a href="/wiki/Property:P27">
288− head of government
289− </a>
290− </div>
291− <div class="wikibase-statementlistview">
292− <div class="wikibase-statementview listview-item">
293− <div class="wikibase-statementview-rankselector">
294− <span class="wikibase-rankselector-normal"></span>
295− </div>
296− <div class="wikibase-statementview-mainsnak">
297− <div>
298− <div class="wikibase-snakview-value">
299− <a href="/wiki/Q206">
300− Old Prime Minister
301− </a>
302− </div>
303− </div>
304− </div>
305− </div>
306− <div class="wikibase-statementview listview-item">
307− <div class="wikibase-statementview-rankselector">
308− <span class="wikibase-rankselector-preferred"></span>
309− </div>
310− <div class="wikibase-statementview-mainsnak">
311− <div>
312− <div class="wikibase-snakview-value">
313− <a href="/wiki/Q3099714">
314− Actual Prime Minister
315− </a>
316− </div>
317− </div>
318− </div>
319− </div>
320− </div>
321− </div>
322− </div>
323− """
324− attributes = []
325− html_etree = fromstring(html)
326− id_cache = wikidata.get_id_cache(html_etree)
327− wikidata.add_attribute(attributes, id_cache, "P6")
328− self.assertEqual(len(attributes), 1)
329− self.assertEqual(attributes[0]["label"], "Head of government")
330− self.assertEqual(attributes[0]["value"], "Old Prime Minister, Actual Prime Minister")
331−
332− attributes = []
333− html_etree = fromstring(html)
334− id_cache = wikidata.get_id_cache(html_etree)
335− wikidata.add_attribute(attributes, id_cache, "P6", trim=True)
336− self.assertEqual(len(attributes), 1)
337− self.assertEqual(attributes[0]["value"], "Actual Prime Minister")
338−
339− def test_add_url(self):
340− html = u"""
341− <div>
342− <div id="P856">
343− <div class="wikibase-statementgroupview-property-label">
344− <a href="/wiki/Property:P856">
345− official website
346− </a>
347− </div>
348− <div class="wikibase-statementlistview">
349− <div class="wikibase-statementview listview-item">
350− <div class="wikibase-statementview-mainsnak">
351− <div>
352− <div class="wikibase-snakview-value">
353− <a class="external free" href="https://searx.me">
354− https://searx.me/
355− </a>
356− </div>
357− </div>
358− </div>
359− </div>
360− </div>
361− </div>
362− </div>
363− """
364− urls = []
365− html_etree = fromstring(html)
366− id_cache = wikidata.get_id_cache(html_etree)
367− wikidata.add_url(urls, html_etree, id_cache, 'P856')
368− self.assertEquals(len(urls), 1)
369− self.assertIn({'title': 'Official website', 'url': 'https://searx.me/'}, urls)
370− urls = []
371− results = []
372− wikidata.add_url(urls, html_etree, id_cache, 'P856', 'custom label', results=results)
373− self.assertEquals(len(urls), 1)
374− self.assertEquals(len(results), 1)
375− self.assertIn({'title': 'custom label', 'url': 'https://searx.me/'}, urls)
376− self.assertIn({'title': 'custom label', 'url': 'https://searx.me/'}, results)
377−
378− html = u"""
379− <div>
380− <div id="P856">
381− <div class="wikibase-statementgroupview-property-label">
382− <a href="/wiki/Property:P856">
383− official website
384− </a>
385− </div>
386− <div class="wikibase-statementlistview">
387− <div class="wikibase-statementview listview-item">
388− <div class="wikibase-statementview-mainsnak">
389− <div>
390− <div class="wikibase-snakview-value">
391− <a class="external free" href="http://www.worldofwarcraft.com">
392− http://www.worldofwarcraft.com
393− </a>
394− </div>
395− </div>
396− </div>
397− </div>
398− <div class="wikibase-statementview listview-item">
399− <div class="wikibase-statementview-mainsnak">
400− <div>
401− <div class="wikibase-snakview-value">
402− <a class="external free" href="http://eu.battle.net/wow/en/">
403− http://eu.battle.net/wow/en/
404− </a>
405− </div>
406− </div>
407− </div>
408− </div>
409− </div>
410− </div>
411− </div>
412− """
413− urls = []
414− html_etree = fromstring(html)
415− id_cache = wikidata.get_id_cache(html_etree)
416− wikidata.add_url(urls, html_etree, id_cache, 'P856')
417− self.assertEquals(len(urls), 2)
418− self.assertIn({'title': 'Official website', 'url': 'http://www.worldofwarcraft.com'}, urls)
419− self.assertIn({'title': 'Official website', 'url': 'http://eu.battle.net/wow/en/'}, urls)
420−
421− def test_get_imdblink(self):
422− html = u"""
423− <div>
424− <div class="wikibase-statementview-mainsnak">
425− <div>
426− <div class="wikibase-snakview-value">
427− <a class="wb-external-id" href="http://www.imdb.com/tt0433664">
428− tt0433664
429− </a>
430− </div>
431− </div>
432− </div>
433− </div>
434− """
435− html_etree = fromstring(html)
436− imdblink = wikidata.get_imdblink(html_etree, 'https://www.imdb.com/')
437−
438− html = u"""
439− <div>
440− <div class="wikibase-statementview-mainsnak">
441− <div>
442− <div class="wikibase-snakview-value">
443− <a class="wb-external-id"
444− href="href="http://tools.wmflabs.org/...http://www.imdb.com/&id=nm4915994"">
445− nm4915994
446− </a>
447− </div>
448− </div>
449− </div>
450− </div>
451− """
452− html_etree = fromstring(html)
453− imdblink = wikidata.get_imdblink(html_etree, 'https://www.imdb.com/')
454− self.assertIn('https://www.imdb.com/name/nm4915994', imdblink)
455−
456− def test_get_geolink(self):
457− html = u"""
458− <div>
459− <div class="wikibase-statementview-mainsnak">
460− <div>
461− <div class="wikibase-snakview-value">
462− 60°N, 40°E
463− </div>
464− </div>
465− </div>
466− </div>
467− """
468− html_etree = fromstring(html)
469− geolink = wikidata.get_geolink(html_etree)
470− self.assertIn('https://www.openstreetmap.org/', geolink)
471− self.assertIn('lat=60&lon=40', geolink)
472−
473− html = u"""
474− <div>
475− <div class="wikibase-statementview-mainsnak">
476− <div>
477− <div class="wikibase-snakview-value">
478− 34°35'59"S, 58°22'55"W
479− </div>
480− </div>
481− </div>
482− </div>
483− """
484− html_etree = fromstring(html)
485− geolink = wikidata.get_geolink(html_etree)
486− self.assertIn('https://www.openstreetmap.org/', geolink)
487− self.assertIn('lat=-34.59', geolink)
488− self.assertIn('lon=-58.38', geolink)
489−
490− def test_get_wikilink(self):
491− html = """
492− <div>
493− <div>
494− <ul class="wikibase-sitelinklistview-listview">
495− <li data-wb-siteid="arwiki"><a href="http://ar.wikipedia.org/wiki/Test">Test</a></li>
496− <li data-wb-siteid="enwiki"><a href="http://en.wikipedia.org/wiki/Test">Test</a></li>
497− </ul>
498− </div>
499− <div>
500− <ul class="wikibase-sitelinklistview-listview">
501− <li data-wb-siteid="enwikiquote"><a href="https://en.wikiquote.org/wiki/Test">Test</a></li>
502− </ul>
503− </div>
504− </div>
505− """
506− html_etree = fromstring(html)
507− wikilink = wikidata.get_wikilink(html_etree, 'nowiki')
508− self.assertEqual(wikilink, None)
509− wikilink = wikidata.get_wikilink(html_etree, 'enwiki')
510− self.assertEqual(wikilink, 'https://en.wikipedia.org/wiki/Test')
511− wikilink = wikidata.get_wikilink(html_etree, 'arwiki')
512− self.assertEqual(wikilink, 'https://ar.wikipedia.org/wiki/Test')
513− wikilink = wikidata.get_wikilink(html_etree, 'enwikiquote')
514− self.assertEqual(wikilink, 'https://en.wikiquote.org/wiki/Test')
tests/unit/engines/test_wikipedia.pydeleted−263
@@ -1,263 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import wikipedia
5−from searx.testing import SearxTestCase
6−
7−
8−class TestWikipediaEngine(SearxTestCase):
9−
10− def test_request(self):
11− wikipedia.supported_languages = ['fr', 'en', 'no']
12− wikipedia.language_aliases = {'nb': 'no'}
13−
14− query = 'test_query'
15− dicto = defaultdict(dict)
16− dicto['language'] = 'fr-FR'
17− params = wikipedia.request(query.encode('utf-8'), dicto)
18− self.assertIn('url', params)
19− self.assertIn(query, params['url'])
20− self.assertIn('test_query', params['url'])
21− self.assertIn('Test_Query', params['url'])
22− self.assertIn('fr.wikipedia.org', params['url'])
23−
24− query = u'Test_Query'
25− params = wikipedia.request(query.encode('utf-8'), dicto)
26− self.assertIn('Test_Query', params['url'])
27− self.assertNotIn('test_query', params['url'])
28−
29− dicto['language'] = 'nb'
30− params = wikipedia.request(query, dicto)
31− self.assertIn('no.wikipedia.org', params['url'])
32− dicto['language'] = 'all'
33− params = wikipedia.request(query, dicto)
34− self.assertIn('en', params['url'])
35−
36− dicto['language'] = 'xx'
37− params = wikipedia.request(query, dicto)
38− self.assertIn('en.wikipedia.org', params['url'])
39−
40− def test_response(self):
41− dicto = defaultdict(dict)
42− dicto['language'] = 'fr'
43−
44− self.assertRaises(AttributeError, wikipedia.response, None)
45− self.assertRaises(AttributeError, wikipedia.response, [])
46− self.assertRaises(AttributeError, wikipedia.response, '')
47− self.assertRaises(AttributeError, wikipedia.response, '[]')
48−
49− # page not found
50− json = """
51− {
52− "batchcomplete": "",
53− "query": {
54− "normalized": [],
55− "pages": {
56− "-1": {
57− "ns": 0,
58− "title": "",
59− "missing": ""
60− }
61− }
62− }
63− }"""
64− response = mock.Mock(text=json, search_params=dicto)
65− self.assertEqual(wikipedia.response(response), [])
66−
67− # normal case
68− json = """
69− {
70− "batchcomplete": "",
71− "query": {
72− "normalized": [],
73− "pages": {
74− "12345": {
75− "pageid": 12345,
76− "ns": 0,
77− "title": "The Title",
78− "extract": "The Title is...",
79− "thumbnail": {
80− "source": "img_src.jpg"
81− },
82− "pageimage": "img_name.jpg"
83− }
84− }
85− }
86− }"""
87− response = mock.Mock(text=json, search_params=dicto)
88− results = wikipedia.response(response)
89− self.assertEqual(type(results), list)
90− self.assertEqual(len(results), 2)
91− self.assertEqual(results[0]['title'], u'The Title')
92− self.assertIn('fr.wikipedia.org/wiki/The_Title', results[0]['url'])
93− self.assertEqual(results[1]['infobox'], u'The Title')
94− self.assertIn('fr.wikipedia.org/wiki/The_Title', results[1]['id'])
95− self.assertIn('The Title is...', results[1]['content'])
96− self.assertEqual(results[1]['img_src'], 'img_src.jpg')
97−
98− # disambiguation page
99− json = """
100− {
101− "batchcomplete": "",
102− "query": {
103− "normalized": [],
104− "pages": {
105− "12345": {
106− "pageid": 12345,
107− "ns": 0,
108− "title": "The Title",
109− "extract": "The Title can be:\\nThe Title 1\\nThe Title 2\\nThe Title 3\\nThe Title 4......................................................................................................................................." """ # noqa
110− json += """
111− }
112− }
113− }
114− }"""
115− response = mock.Mock(text=json, search_params=dicto)
116− results = wikipedia.response(response)
117− self.assertEqual(type(results), list)
118− self.assertEqual(len(results), 2)
119−
120− # no image
121− json = """
122− {
123− "batchcomplete": "",
124− "query": {
125− "normalized": [],
126− "pages": {
127− "12345": {
128− "pageid": 12345,
129− "ns": 0,
130− "title": "The Title",
131− "extract": "The Title is......................................................................................................................................................................................." """ # noqa
132− json += """
133− }
134− }
135− }
136− }"""
137− response = mock.Mock(text=json, search_params=dicto)
138− results = wikipedia.response(response)
139− self.assertEqual(type(results), list)
140− self.assertEqual(len(results), 2)
141− self.assertIn('The Title is...', results[1]['content'])
142− self.assertEqual(results[1]['img_src'], None)
143−
144− # title not in first paragraph
145− json = u"""
146− {
147− "batchcomplete": "",
148− "query": {
149− "normalized": [],
150− "pages": {
151− "12345": {
152− "pageid": 12345,
153− "ns": 0,
154− "title": "披頭四樂隊",
155− "extract": "披头士乐队....................................................................................................................................................................................................\\n披頭四樂隊...", """ # noqa
156− json += """
157− "thumbnail": {
158− "source": "img_src.jpg"
159− },
160− "pageimage": "img_name.jpg"
161− }
162− }
163− }
164− }"""
165− response = mock.Mock(text=json, search_params=dicto)
166− results = wikipedia.response(response)
167− self.assertEqual(type(results), list)
168− self.assertEqual(len(results), 2)
169− self.assertEqual(results[1]['infobox'], u'披頭四樂隊')
170− self.assertIn(u'披头士乐队...', results[1]['content'])
171−
172− def test_fetch_supported_languages(self):
173− html = u"""<html></html>"""
174− response = mock.Mock(text=html)
175− languages = wikipedia._fetch_supported_languages(response)
176− self.assertEqual(type(languages), dict)
177− self.assertEqual(len(languages), 0)
178−
179− html = u"""
180− <html>
181− <body>
182− <div>
183− <div>
184− <h3>Table header</h3>
185− <table class="sortable jquery-tablesorter">
186− <thead>
187− <tr>
188− <th>N</th>
189− <th>Language</th>
190− <th>Language (local)</th>
191− <th>Wiki</th>
192− <th>Articles</th>
193− </tr>
194− </thead>
195− <tbody>
196− <tr>
197− <td>2</td>
198− <td><a>Swedish</a></td>
199− <td><a>Svenska</a></td>
200− <td><a>sv</a></td>
201− <td><a><b>3000000</b></a></td>
202− </tr>
203− <tr>
204− <td>3</td>
205− <td><a>Cebuano</a></td>
206− <td><a>Sinugboanong Binisaya</a></td>
207− <td><a>ceb</a></td>
208− <td><a><b>3000000</b></a></td>
209− </tr>
210− </tbody>
211− </table>
212− <h3>Table header</h3>
213− <table class="sortable jquery-tablesorter">
214− <thead>
215− <tr>
216− <th>N</th>
217− <th>Language</th>
218− <th>Language (local)</th>
219− <th>Wiki</th>
220− <th>Articles</th>
221− </tr>
222− </thead>
223− <tbody>
224− <tr>
225− <td>2</td>
226− <td><a>Norwegian (Bokmål)</a></td>
227− <td><a>Norsk (Bokmål)</a></td>
228− <td><a>no</a></td>
229− <td><a><b>100000</b></a></td>
230− </tr>
231− </tbody>
232− </table>
233− </div>
234− </div>
235− </body>
236− </html>
237− """
238− response = mock.Mock(text=html)
239− languages = wikipedia._fetch_supported_languages(response)
240− self.assertEqual(type(languages), dict)
241− self.assertEqual(len(languages), 3)
242−
243− self.assertIn('sv', languages)
244− self.assertIn('ceb', languages)
245− self.assertIn('no', languages)
246−
247− self.assertEqual(type(languages['sv']), dict)
248− self.assertEqual(type(languages['ceb']), dict)
249− self.assertEqual(type(languages['no']), dict)
250−
251− self.assertIn('name', languages['sv'])
252− self.assertIn('english_name', languages['sv'])
253− self.assertIn('articles', languages['sv'])
254−
255− self.assertEqual(languages['sv']['name'], 'Svenska')
256− self.assertEqual(languages['sv']['english_name'], 'Swedish')
257− self.assertEqual(languages['sv']['articles'], 3000000)
258− self.assertEqual(languages['ceb']['name'], 'Sinugboanong Binisaya')
259− self.assertEqual(languages['ceb']['english_name'], 'Cebuano')
260− self.assertEqual(languages['ceb']['articles'], 3000000)
261− self.assertEqual(languages['no']['name'], u'Norsk (Bokmål)')
262− self.assertEqual(languages['no']['english_name'], u'Norwegian (Bokmål)')
263− self.assertEqual(languages['no']['articles'], 100000)
tests/unit/engines/test_wolframalpha_api.pydeleted−166
@@ -1,166 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from requests import Request
5−from searx.engines import wolframalpha_api
6−from searx.testing import SearxTestCase
7−
8−
9−class TestWolframAlphaAPIEngine(SearxTestCase):
10−
11− def test_request(self):
12− query = 'test_query'
13− dicto = defaultdict(dict)
14− params = wolframalpha_api.request(query, dicto)
15−
16− # TODO: test api_key
17− self.assertIn('url', params)
18− self.assertIn('https://api.wolframalpha.com/v2/query?', params['url'])
19− self.assertIn(query, params['url'])
20− self.assertEqual('https://www.wolframalpha.com/input/?i=test_query', params['headers']['Referer'])
21−
22− def test_replace_pua_chars(self):
23− self.assertEqual('i', wolframalpha_api.replace_pua_chars(u'\uf74e'))
24−
25− def test_response(self):
26− self.assertRaises(AttributeError, wolframalpha_api.response, None)
27− self.assertRaises(AttributeError, wolframalpha_api.response, [])
28− self.assertRaises(AttributeError, wolframalpha_api.response, '')
29− self.assertRaises(AttributeError, wolframalpha_api.response, '[]')
30−
31− referer_url = 'referer_url'
32− request = Request(headers={'Referer': referer_url})
33−
34− # test failure
35− xml = '''<?xml version='1.0' encoding='UTF-8'?>
36− <queryresult success='false' error='false' />
37− '''
38− response = mock.Mock(content=xml.encode('utf-8'))
39− self.assertEqual(wolframalpha_api.response(response), [])
40−
41− # test basic case
42− xml = b"""<?xml version='1.0' encoding='UTF-8'?>
43− <queryresult success='true'
44− error='false'
45− numpods='3'
46− datatypes='Math'
47− id='queryresult_id'
48− host='http://www4c.wolframalpha.com'
49− related='related_url'
50− version='2.6'>
51− <pod title='Input'
52− scanner='Identity'
53− id='Input'
54− numsubpods='1'>
55− <subpod title=''>
56− <img src='input_img_src.gif'
57− alt='input_img_alt'
58− title='input_img_title' />
59− <plaintext>input_plaintext</plaintext>
60− </subpod>
61− </pod>
62− <pod title='Result'
63− scanner='Simplification'
64− id='Result'
65− numsubpods='1'
66− primary='true'>
67− <subpod title=''>
68− <img src='result_img_src.gif'
69− alt='result_img_alt'
70− title='result_img_title' />
71− <plaintext>result_plaintext</plaintext>
72− </subpod>
73− </pod>
74− <pod title='Manipulatives illustration'
75− scanner='Arithmetic'
76− id='Illustration'
77− numsubpods='1'>
78− <subpod title=''>
79− <img src='illustration_img_src.gif'
80− alt='illustration_img_alt' />
81− <plaintext>illustration_plaintext</plaintext>
82− </subpod>
83− </pod>
84− </queryresult>
85− """
86− response = mock.Mock(content=xml, request=request)
87− results = wolframalpha_api.response(response)
88− self.assertEqual(type(results), list)
89− self.assertEqual(len(results), 2)
90− self.assertEqual('input_plaintext', results[0]['infobox'])
91−
92− self.assertEqual(len(results[0]['attributes']), 3)
93− self.assertEqual('Input', results[0]['attributes'][0]['label'])
94− self.assertEqual('input_plaintext', results[0]['attributes'][0]['value'])
95− self.assertEqual('Result', results[0]['attributes'][1]['label'])
96− self.assertEqual('result_plaintext', results[0]['attributes'][1]['value'])
97− self.assertEqual('Manipulatives illustration', results[0]['attributes'][2]['label'])
98− self.assertEqual('illustration_img_src.gif', results[0]['attributes'][2]['image']['src'])
99− self.assertEqual('illustration_img_alt', results[0]['attributes'][2]['image']['alt'])
100−
101− self.assertEqual(len(results[0]['urls']), 1)
102−
103− self.assertEqual(referer_url, results[0]['urls'][0]['url'])
104− self.assertEqual('Wolfram|Alpha', results[0]['urls'][0]['title'])
105− self.assertEqual(referer_url, results[1]['url'])
106− self.assertEqual('Wolfram|Alpha (input_plaintext)', results[1]['title'])
107− self.assertIn('result_plaintext', results[1]['content'])
108−
109− # test calc
110− xml = b"""<?xml version='1.0' encoding='UTF-8'?>
111− <queryresult success='true'
112− error='false'
113− numpods='2'
114− datatypes=''
115− parsetimedout='false'
116− id='queryresult_id'
117− host='http://www5b.wolframalpha.com'
118− related='related_url'
119− version='2.6' >
120− <pod title='Indefinite integral'
121− scanner='Integral'
122− id='IndefiniteIntegral'
123− error='false'
124− numsubpods='1'
125− primary='true'>
126− <subpod title=''>
127− <img src='integral_image.gif'
128− alt='integral_img_alt'
129− title='integral_img_title' />
130− <plaintext>integral_plaintext</plaintext>
131− </subpod>
132− </pod>
133− <pod title='Plot of the integral'
134− scanner='Integral'
135− id='Plot'
136− error='false'
137− numsubpods='1'>
138− <subpod title=''>
139− <img src='plot.gif'
140− alt='plot_alt'
141− title='' />
142− <plaintext></plaintext>
143− </subpod>
144− </pod>
145− </queryresult>
146− """
147− response = mock.Mock(content=xml, request=request)
148− results = wolframalpha_api.response(response)
149− self.assertEqual(type(results), list)
150− self.assertEqual(len(results), 2)
151− self.assertEqual('integral_plaintext', results[0]['infobox'])
152−
153− self.assertEqual(len(results[0]['attributes']), 2)
154− self.assertEqual('Indefinite integral', results[0]['attributes'][0]['label'])
155− self.assertEqual('integral_plaintext', results[0]['attributes'][0]['value'])
156− self.assertEqual('Plot of the integral', results[0]['attributes'][1]['label'])
157− self.assertEqual('plot.gif', results[0]['attributes'][1]['image']['src'])
158− self.assertEqual('plot_alt', results[0]['attributes'][1]['image']['alt'])
159−
160− self.assertEqual(len(results[0]['urls']), 1)
161−
162− self.assertEqual(referer_url, results[0]['urls'][0]['url'])
163− self.assertEqual('Wolfram|Alpha', results[0]['urls'][0]['title'])
164− self.assertEqual(referer_url, results[1]['url'])
165− self.assertEqual('Wolfram|Alpha (integral_plaintext)', results[1]['title'])
166− self.assertIn('integral_plaintext', results[1]['content'])
tests/unit/engines/test_wolframalpha_noapi.pydeleted−224
@@ -1,224 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from requests import Request
5−from searx.engines import wolframalpha_noapi
6−from searx.testing import SearxTestCase
7−
8−
9−class TestWolframAlphaNoAPIEngine(SearxTestCase):
10−
11− def test_request(self):
12− query = 'test_query'
13− dicto = defaultdict(dict)
14− params = wolframalpha_noapi.request(query, dicto)
15−
16− self.assertIn('url', params)
17− self.assertIn('https://www.wolframalpha.com/input/json.jsp', params['url'])
18− self.assertIn(query, params['url'])
19− self.assertEqual('https://www.wolframalpha.com/input/?i=test_query', params['headers']['Referer'])
20−
21− def test_response(self):
22− self.assertRaises(AttributeError, wolframalpha_noapi.response, None)
23− self.assertRaises(AttributeError, wolframalpha_noapi.response, [])
24− self.assertRaises(AttributeError, wolframalpha_noapi.response, '')
25− self.assertRaises(AttributeError, wolframalpha_noapi.response, '[]')
26−
27− referer_url = 'referer_url'
28− request = Request(headers={'Referer': referer_url})
29−
30− # test failure
31− json = r'''
32− {"queryresult" : {
33− "success" : false,
34− "error" : false,
35− "numpods" : 0,
36− "id" : "",
37− "host" : "https:\/\/www5a.wolframalpha.com",
38− "didyoumeans" : {}
39− }}
40− '''
41− response = mock.Mock(text=json, request=request)
42− self.assertEqual(wolframalpha_noapi.response(response), [])
43−
44− # test basic case
45− json = r'''
46− {"queryresult" : {
47− "success" : true,
48− "error" : false,
49− "numpods" : 6,
50− "datatypes" : "Math",
51− "id" : "queryresult_id",
52− "host" : "https:\/\/www5b.wolframalpha.com",
53− "related" : "related_url",
54− "version" : "2.6",
55− "pods" : [
56− {
57− "title" : "Input",
58− "scanners" : [
59− "Identity"
60− ],
61− "id" : "Input",
62− "error" : false,
63− "numsubpods" : 1,
64− "subpods" : [
65− {
66− "title" : "",
67− "img" : {
68− "src" : "input_img_src.gif",
69− "alt" : "input_img_alt",
70− "title" : "input_img_title"
71− },
72− "plaintext" : "input_plaintext",
73− "minput" : "input_minput"
74− }
75− ]
76− },
77− {
78− "title" : "Result",
79− "scanners" : [
80− "Simplification"
81− ],
82− "id" : "Result",
83− "error" : false,
84− "numsubpods" : 1,
85− "primary" : true,
86− "subpods" : [
87− {
88− "title" : "",
89− "img" : {
90− "src" : "result_img_src.gif",
91− "alt" : "result_img_alt",
92− "title" : "result_img_title"
93− },
94− "plaintext" : "result_plaintext",
95− "moutput" : "result_moutput"
96− }
97− ]
98− },
99− {
100− "title" : "Manipulatives illustration",
101− "scanners" : [
102− "Arithmetic"
103− ],
104− "id" : "Illustration",
105− "error" : false,
106− "numsubpods" : 1,
107− "subpods" : [
108− {
109− "title" : "",
110− "CDFcontent" : "Resizeable",
111− "img" : {
112− "src" : "illustration_img_src.gif",
113− "alt" : "illustration_img_alt",
114− "title" : "illustration_img_title"
115− },
116− "plaintext" : "illustration_img_plaintext"
117− }
118− ]
119− }
120− ]
121− }}
122− '''
123− response = mock.Mock(text=json, request=request)
124− results = wolframalpha_noapi.response(response)
125− self.assertEqual(type(results), list)
126− self.assertEqual(len(results), 2)
127− self.assertEqual('input_plaintext', results[0]['infobox'])
128−
129− self.assertEqual(len(results[0]['attributes']), 3)
130− self.assertEqual('Input', results[0]['attributes'][0]['label'])
131− self.assertEqual('input_plaintext', results[0]['attributes'][0]['value'])
132− self.assertEqual('Result', results[0]['attributes'][1]['label'])
133− self.assertEqual('result_plaintext', results[0]['attributes'][1]['value'])
134− self.assertEqual('Manipulatives illustration', results[0]['attributes'][2]['label'])
135− self.assertEqual('illustration_img_src.gif', results[0]['attributes'][2]['image']['src'])
136− self.assertEqual('illustration_img_alt', results[0]['attributes'][2]['image']['alt'])
137−
138− self.assertEqual(len(results[0]['urls']), 1)
139−
140− self.assertEqual(referer_url, results[0]['urls'][0]['url'])
141− self.assertEqual('Wolfram|Alpha', results[0]['urls'][0]['title'])
142− self.assertEqual(referer_url, results[1]['url'])
143− self.assertEqual('Wolfram|Alpha (input_plaintext)', results[1]['title'])
144− self.assertIn('result_plaintext', results[1]['content'])
145−
146− # test calc
147− json = r"""
148− {"queryresult" : {
149− "success" : true,
150− "error" : false,
151− "numpods" : 2,
152− "datatypes" : "",
153− "id" : "queryresult_id",
154− "host" : "https:\/\/www4b.wolframalpha.com",
155− "related" : "related_url",
156− "version" : "2.6",
157− "pods" : [
158− {
159− "title" : "Indefinite integral",
160− "scanners" : [
161− "Integral"
162− ],
163− "id" : "IndefiniteIntegral",
164− "error" : false,
165− "numsubpods" : 1,
166− "primary" : true,
167− "subpods" : [
168− {
169− "title" : "",
170− "img" : {
171− "src" : "integral_img_src.gif",
172− "alt" : "integral_img_alt",
173− "title" : "integral_img_title"
174− },
175− "plaintext" : "integral_plaintext",
176− "minput" : "integral_minput",
177− "moutput" : "integral_moutput"
178− }
179− ]
180− },
181− {
182− "title" : "Plot of the integral",
183− "scanners" : [
184− "Integral"
185− ],
186− "id" : "Plot",
187− "error" : false,
188− "numsubpods" : 1,
189− "subpods" : [
190− {
191− "title" : "",
192− "img" : {
193− "src" : "plot.gif",
194− "alt" : "plot_alt",
195− "title" : "plot_title"
196− },
197− "plaintext" : "",
198− "minput" : "plot_minput"
199− }
200− ]
201− }
202− ]
203− }}
204− """
205− response = mock.Mock(text=json, request=request)
206− results = wolframalpha_noapi.response(response)
207− self.assertEqual(type(results), list)
208− self.assertEqual(len(results), 2)
209− self.assertEqual('integral_plaintext', results[0]['infobox'])
210−
211− self.assertEqual(len(results[0]['attributes']), 2)
212− self.assertEqual('Indefinite integral', results[0]['attributes'][0]['label'])
213− self.assertEqual('integral_plaintext', results[0]['attributes'][0]['value'])
214− self.assertEqual('Plot of the integral', results[0]['attributes'][1]['label'])
215− self.assertEqual('plot.gif', results[0]['attributes'][1]['image']['src'])
216− self.assertEqual('plot_alt', results[0]['attributes'][1]['image']['alt'])
217−
218− self.assertEqual(len(results[0]['urls']), 1)
219−
220− self.assertEqual(referer_url, results[0]['urls'][0]['url'])
221− self.assertEqual('Wolfram|Alpha', results[0]['urls'][0]['title'])
222− self.assertEqual(referer_url, results[1]['url'])
223− self.assertEqual('Wolfram|Alpha (integral_plaintext)', results[1]['title'])
224− self.assertIn('integral_plaintext', results[1]['content'])
tests/unit/engines/test_www1x.pydeleted−57
@@ -1,57 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import www1x
4−from searx.testing import SearxTestCase
5−
6−
7−class TestWww1xEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− params = www1x.request(query, defaultdict(dict))
12− self.assertTrue('url' in params)
13− self.assertTrue(query in params['url'])
14− self.assertTrue('1x.com' in params['url'])
15−
16− def test_response(self):
17− self.assertRaises(AttributeError, www1x.response, None)
18− self.assertRaises(AttributeError, www1x.response, [])
19− self.assertRaises(AttributeError, www1x.response, '')
20− self.assertRaises(AttributeError, www1x.response, '[]')
21−
22− response = mock.Mock(text='<html></html>')
23− self.assertEqual(www1x.response(response), [])
24− html = """
25− <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE characters
26− [
27− <!ELEMENT characters (character*) >
28− <!ELEMENT character (#PCDATA ) >
29−
30− <!ENTITY iexcl "&#161;" >
31− <!ENTITY cent "&#162;" >
32− <!ENTITY pound "&#163;" >
33− ]
34− ><root><searchresult><![CDATA[<table border="0" cellpadding="0" cellspacing="0" width="100%">
35− <tr>
36− <td style="min-width: 220px;" valign="top">
37− <div style="font-size: 30px; margin: 0px 0px 20px 0px;">Photos</div>
38− <div>
39− <a href="/photo/123456" class="dynamiclink">
40−<img border="0" class="searchresult" src="/images/user/testimage-123456.jpg" style="width: 125px; height: 120px;">
41− </a>
42− <a title="sjoerd lammers street photography" href="/member/sjoerdlammers" class="dynamiclink">
43−<img border="0" class="searchresult" src="/images/profile/60c48b394c677d2fa4d9e7d263aabf44-square.jpg">
44− </a>
45− </div>
46− </td>
47− </table>
48− ]]></searchresult></root>
49− """
50− response = mock.Mock(text=html)
51− results = www1x.response(response)
52− self.assertEqual(type(results), list)
53− self.assertEqual(len(results), 1)
54− self.assertEqual(results[0]['url'], 'https://1x.com/photo/123456')
55− self.assertEqual(results[0]['thumbnail_src'], 'https://1x.com/images/user/testimage-123456.jpg')
56− self.assertEqual(results[0]['content'], '')
57− self.assertEqual(results[0]['template'], 'images.html')
tests/unit/engines/test_yacy.pydeleted−96
@@ -1,96 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import yacy
4−from searx.testing import SearxTestCase
5−
6−
7−class TestYacyEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 1
13− dicto['language'] = 'fr_FR'
14− params = yacy.request(query, dicto)
15− self.assertIn('url', params)
16− self.assertIn(query, params['url'])
17− self.assertIn('localhost', params['url'])
18− self.assertIn('fr', params['url'])
19−
20− dicto['language'] = 'all'
21− params = yacy.request(query, dicto)
22− self.assertIn('url', params)
23− self.assertNotIn('lr=lang_', params['url'])
24−
25− def test_response(self):
26− self.assertRaises(AttributeError, yacy.response, None)
27− self.assertRaises(AttributeError, yacy.response, [])
28− self.assertRaises(AttributeError, yacy.response, '')
29− self.assertRaises(AttributeError, yacy.response, '[]')
30−
31− response = mock.Mock(text='{}')
32− self.assertEqual(yacy.response(response), [])
33−
34− response = mock.Mock(text='{"data": []}')
35− self.assertEqual(yacy.response(response), [])
36−
37− json = """
38− {
39− "channels": [
40− {
41− "title": "YaCy P2P-Search for test",
42− "description": "Search for test",
43− "link": "http://search.yacy.de:7001/yacysearch.html?query=test&amp;resource=global&amp;contentdom=0",
44− "image": {
45− "url": "http://search.yacy.de:7001/env/grafics/yacy.png",
46− "title": "Search for test",
47− "link": "http://search.yacy.de:7001/yacysearch.html?query=test&amp;resource=global&amp;contentdom=0"
48− },
49− "totalResults": "249",
50− "startIndex": "0",
51− "itemsPerPage": "5",
52− "searchTerms": "test",
53− "items": [
54− {
55− "title": "This is the title",
56− "link": "http://this.is.the.url",
57− "code": "",
58− "description": "This should be the content",
59− "pubDate": "Sat, 08 Jun 2013 02:00:00 +0200",
60− "size": "44213",
61− "sizename": "43 kbyte",
62− "guid": "lzh_1T_5FP-A",
63− "faviconCode": "XTS4uQ_5FP-A",
64− "host": "www.gamestar.de",
65− "path": "/spiele/city-of-heroes-freedom/47019.html",
66− "file": "47019.html",
67− "urlhash": "lzh_1T_5FP-A",
68− "ranking": "0.20106804"
69− },
70− {
71− "title": "This is the title2",
72− "icon": "/ViewImage.png?maxwidth=96&amp;maxheight=96&amp;code=7EbAbW6BpPOA",
73− "image": "http://image.url/image.png",
74− "cache": "/ViewImage.png?quadratic=&amp;url=http://golem.ivwbox.de/cgi-bin/ivw/CP/G_INET?d=14071378",
75− "url": "http://this.is.the.url",
76− "urlhash": "7EbAbW6BpPOA",
77− "host": "www.golem.de",
78− "width": "-1",
79− "height": "-1"
80− }
81− ]
82− }
83− ]
84− }
85− """
86− response = mock.Mock(text=json)
87− results = yacy.response(response)
88− self.assertEqual(type(results), list)
89− self.assertEqual(len(results), 2)
90− self.assertEqual(results[0]['title'], 'This is the title')
91− self.assertEqual(results[0]['url'], 'http://this.is.the.url')
92− self.assertEqual(results[0]['content'], 'This should be the content')
93− self.assertEqual(results[1]['img_src'], 'http://image.url/image.png')
94− self.assertEqual(results[1]['content'], '')
95− self.assertEqual(results[1]['url'], 'http://this.is.the.url')
96− self.assertEqual(results[1]['title'], 'This is the title2')
tests/unit/engines/test_yahoo.pydeleted−190
@@ -1,190 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import yahoo
5−from searx.testing import SearxTestCase
6−
7−
8−class TestYahooEngine(SearxTestCase):
9−
10− def test_parse_url(self):
11− test_url = 'http://r.search.yahoo.com/_ylt=A0LEb9JUSKcAEGRXNyoA;_ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb' +\
12− '2xvA2Jm2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10/RU=https%3a%2f%2fthis.is.the.url%2f/RK=0/RS=' +\
13− 'dtcJsfP4mEeBOjnVfUQ-'
14− url = yahoo.parse_url(test_url)
15− self.assertEqual('https://this.is.the.url/', url)
16−
17− test_url = 'http://r.search.yahoo.com/_ylt=A0LElb9JUSKcAEGRXNyoA;_ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb' +\
18− '2xvA2Jm2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10/RU=https%3a%2f%2fthis.is.the.url%2f/RS=' +\
19− 'dtcJsfP4mEeBOjnVfUQ-'
20− url = yahoo.parse_url(test_url)
21− self.assertEqual('https://this.is.the.url/', url)
22−
23− test_url = 'https://this.is.the.url/'
24− url = yahoo.parse_url(test_url)
25− self.assertEqual('https://this.is.the.url/', url)
26−
27− def test_request(self):
28− yahoo.supported_languages = ['en', 'fr', 'zh-CHT', 'zh-CHS']
29− query = 'test_query'
30− dicto = defaultdict(dict)
31− dicto['pageno'] = 1
32− dicto['time_range'] = ''
33− dicto['language'] = 'fr-FR'
34− params = yahoo.request(query, dicto)
35− self.assertIn('url', params)
36− self.assertIn(query, params['url'])
37− self.assertIn('search.yahoo.com', params['url'])
38− self.assertIn('fr', params['url'])
39− self.assertIn('cookies', params)
40− self.assertIn('sB', params['cookies'])
41− self.assertIn('fr', params['cookies']['sB'])
42−
43− dicto['language'] = 'zh'
44− params = yahoo.request(query, dicto)
45− self.assertIn('zh_chs', params['url'])
46− self.assertIn('zh_chs', params['cookies']['sB'])
47−
48− dicto['language'] = 'zh-TW'
49− params = yahoo.request(query, dicto)
50− self.assertIn('zh_cht', params['url'])
51− self.assertIn('zh_cht', params['cookies']['sB'])
52−
53− dicto['language'] = 'all'
54− params = yahoo.request(query, dicto)
55− self.assertIn('cookies', params)
56− self.assertIn('sB', params['cookies'])
57− self.assertIn('en', params['cookies']['sB'])
58− self.assertIn('en', params['url'])
59−
60− def test_no_url_in_request_year_time_range(self):
61− dicto = defaultdict(dict)
62− query = 'test_query'
63− dicto['time_range'] = 'year'
64− params = yahoo.request(query, dicto)
65− self.assertEqual({}, params['url'])
66−
67− def test_response(self):
68− self.assertRaises(AttributeError, yahoo.response, None)
69− self.assertRaises(AttributeError, yahoo.response, [])
70− self.assertRaises(AttributeError, yahoo.response, '')
71− self.assertRaises(AttributeError, yahoo.response, '[]')
72−
73− response = mock.Mock(text='<html></html>')
74− self.assertEqual(yahoo.response(response), [])
75−
76− html = """
77−<ol class="reg mb-15 searchCenterMiddle">
78− <li class="first">
79− <div class="dd algo fst Sr">
80− <div class="compTitle">
81− <h3 class="title"><a class=" td-u" href="http://r.search.yahoo.com/_ylt=A0LEb9JUSKcAEGRXNyoA;
82− _ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb2xvA2Jm2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10
83− /RU=https%3a%2f%2fthis.is.the.url%2f/RK=0/RS=dtcJsfP4mEeBOjnVfUQ-"
84− target="_blank" data-bid="54e712e13671c">
85− <b><b>This is the title</b></b></a>
86− </h3>
87− </div>
88− <div class="compText aAbs">
89− <p class="lh-18"><b><b>This is the </b>content</b>
90− </p>
91− </div>
92− </div>
93− </li>
94− <li>
95− <div class="dd algo lst Sr">
96− <div class="compTitle">
97− </div>
98− <div class="compText aAbs">
99− <p class="lh-18">This is the second content</p>
100− </div>
101− </div>
102− </li>
103−</ol>
104−<div class="dd assist fst lst AlsoTry" data-bid="54e712e138d04">
105− <div class="compTitle mb-4 h-17">
106− <h3 class="title">Also Try</h3> </div>
107− <table class="compTable m-0 ac-1st td-u fz-ms">
108− <tbody>
109− <tr>
110− <td class="w-50p pr-28"><a href="https://search.yahoo.com/"><B>This is the </B>suggestion<B></B></a>
111− </td>
112− </tr>
113− </table>
114−</div>
115− """
116− response = mock.Mock(text=html)
117− results = yahoo.response(response)
118− self.assertEqual(type(results), list)
119− self.assertEqual(len(results), 2)
120− self.assertEqual(results[0]['title'], 'This is the title')
121− self.assertEqual(results[0]['url'], 'https://this.is.the.url/')
122− self.assertEqual(results[0]['content'], 'This is the content')
123− self.assertEqual(results[1]['suggestion'], 'This is the suggestion')
124−
125− html = """
126−<ol class="reg mb-15 searchCenterMiddle">
127− <li class="first">
128− <div class="dd algo fst Sr">
129− <div class="compTitle">
130− <h3 class="title"><a class=" td-u" href="http://r.search.yahoo.com/_ylt=A0LEb9JUSKcAEGRXNyoA;
131− _ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb2xvA2Jm2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10
132− /RU=https%3a%2f%2fthis.is.the.url%2f/RK=0/RS=dtcJsfP4mEeBOjnVfUQ-"
133− target="_blank" data-bid="54e712e13671c">
134− <b><b>This is the title</b></b></a>
135− </h3>
136− </div>
137− <div class="compText aAbs">
138− <p class="lh-18"><b><b>This is the </b>content</b>
139− </p>
140− </div>
141− </div>
142− </li>
143−</ol>
144− """
145− response = mock.Mock(text=html)
146− results = yahoo.response(response)
147− self.assertEqual(type(results), list)
148− self.assertEqual(len(results), 1)
149− self.assertEqual(results[0]['title'], 'This is the title')
150− self.assertEqual(results[0]['url'], 'https://this.is.the.url/')
151− self.assertEqual(results[0]['content'], 'This is the content')
152−
153− html = """
154− <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
155− </li>
156− """
157− response = mock.Mock(text=html)
158− results = yahoo.response(response)
159− self.assertEqual(type(results), list)
160− self.assertEqual(len(results), 0)
161−
162− def test_fetch_supported_languages(self):
163− html = """<html></html>"""
164− response = mock.Mock(text=html)
165− results = yahoo._fetch_supported_languages(response)
166− self.assertEqual(type(results), list)
167− self.assertEqual(len(results), 0)
168−
169− html = """
170− <html>
171− <div>
172− <div id="yschlang">
173− <span>
174− <label><input value="lang_ar"></input></label>
175− </span>
176− <span>
177− <label><input value="lang_zh_chs"></input></label>
178− <label><input value="lang_zh_cht"></input></label>
179− </span>
180− </div>
181− </div>
182− </html>
183− """
184− response = mock.Mock(text=html)
185− languages = yahoo._fetch_supported_languages(response)
186− self.assertEqual(type(languages), list)
187− self.assertEqual(len(languages), 3)
188− self.assertIn('ar', languages)
189− self.assertIn('zh-CHS', languages)
190− self.assertIn('zh-CHT', languages)
tests/unit/engines/test_yahoo_news.pydeleted−150
@@ -1,150 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−from datetime import datetime
4−import mock
5−from searx.engines import yahoo_news
6−from searx.testing import SearxTestCase
7−
8−
9−class TestYahooNewsEngine(SearxTestCase):
10−
11− def test_request(self):
12− yahoo_news.supported_languages = ['en', 'fr']
13− query = 'test_query'
14− dicto = defaultdict(dict)
15− dicto['pageno'] = 1
16− dicto['language'] = 'fr-FR'
17− params = yahoo_news.request(query, dicto)
18− self.assertIn('url', params)
19− self.assertIn(query, params['url'])
20− self.assertIn('news.search.yahoo.com', params['url'])
21− self.assertIn('fr', params['url'])
22− self.assertIn('cookies', params)
23− self.assertIn('sB', params['cookies'])
24− self.assertIn('fr', params['cookies']['sB'])
25−
26− dicto['language'] = 'all'
27− params = yahoo_news.request(query, dicto)
28− self.assertIn('cookies', params)
29− self.assertIn('sB', params['cookies'])
30− self.assertIn('en', params['cookies']['sB'])
31− self.assertIn('en', params['url'])
32−
33− def test_sanitize_url(self):
34− url = "test.url"
35− self.assertEqual(url, yahoo_news.sanitize_url(url))
36−
37− url = "www.yahoo.com/;_ylt=test"
38− self.assertEqual("www.yahoo.com/", yahoo_news.sanitize_url(url))
39−
40− def test_response(self):
41− self.assertRaises(AttributeError, yahoo_news.response, None)
42− self.assertRaises(AttributeError, yahoo_news.response, [])
43− self.assertRaises(AttributeError, yahoo_news.response, '')
44− self.assertRaises(AttributeError, yahoo_news.response, '[]')
45−
46− response = mock.Mock(text='<html></html>')
47− self.assertEqual(yahoo_news.response(response), [])
48−
49− html = """
50− <ol class=" reg searchCenterMiddle">
51− <li class="first">
52− <div class="compTitle">
53− <h3>
54− <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
55− This is
56− the <b>title</b>...
57− </a>
58− </h3>
59− </div>
60− <div>
61− <span class="cite">Business via Yahoo!</span>
62− <span class="tri fc-2nd ml-10">May 01 10:00 AM</span>
63− </div>
64− <div class="compText">
65− This is the content
66− </div>
67− </li>
68− <li class="first">
69− <div class="compTitle">
70− <h3>
71− <a class="yschttl spt" target="_blank">
72− </a>
73− </h3>
74− </div>
75− <div class="compText">
76− </div>
77− </li>
78− </ol>
79− """
80− response = mock.Mock(text=html)
81− results = yahoo_news.response(response)
82− self.assertEqual(type(results), list)
83− self.assertEqual(len(results), 1)
84− self.assertEqual(results[0]['title'], 'This is the title...')
85− self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
86− self.assertEqual(results[0]['content'], 'This is the content')
87−
88− html = """
89− <ol class=" reg searchCenterMiddle">
90− <li class="first">
91− <div class="compTitle">
92− <h3>
93− <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
94− This is
95− the <b>title</b>...
96− </a>
97− </h3>
98− </div>
99− <div>
100− <span class="cite">Business via Yahoo!</span>
101− <span class="tri fc-2nd ml-10">2 hours, 22 minutes ago</span>
102− </div>
103− <div class="compText">
104− This is the content
105− </div>
106− </li>
107− <li>
108− <div class="compTitle">
109− <h3>
110− <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
111− This is
112− the <b>title</b>...
113− </a>
114− </h3>
115− </div>
116− <div>
117− <span class="cite">Business via Yahoo!</span>
118− <span class="tri fc-2nd ml-10">22 minutes ago</span>
119− </div>
120− <div class="compText">
121− This is the content
122− </div>
123− </li>
124− <li>
125− <div class="compTitle">
126− <h3>
127− <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
128− This is
129− the <b>title</b>...
130− </a>
131− </h3>
132− </div>
133− <div>
134− <span class="cite">Business via Yahoo!</span>
135− <span class="tri fc-2nd ml-10">Feb 03 09:45AM 1900</span>
136− </div>
137− <div class="compText">
138− This is the content
139− </div>
140− </li>
141− </ol>
142− """
143− response = mock.Mock(text=html)
144− results = yahoo_news.response(response)
145− self.assertEqual(type(results), list)
146− self.assertEqual(len(results), 3)
147− self.assertEqual(results[0]['title'], 'This is the title...')
148− self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
149− self.assertEqual(results[0]['content'], 'This is the content')
150− self.assertEqual(results[2]['publishedDate'].year, datetime.now().year)
tests/unit/engines/test_youtube_api.pydeleted−111
@@ -1,111 +0,0 @@
1−from collections import defaultdict
2−import mock
3−from searx.engines import youtube_api
4−from searx.testing import SearxTestCase
5−
6−
7−class TestYoutubeAPIEngine(SearxTestCase):
8−
9− def test_request(self):
10− query = 'test_query'
11− dicto = defaultdict(dict)
12− dicto['pageno'] = 0
13− dicto['language'] = 'fr_FR'
14− params = youtube_api.request(query, dicto)
15− self.assertTrue('url' in params)
16− self.assertTrue(query in params['url'])
17− self.assertIn('googleapis.com', params['url'])
18− self.assertIn('youtube', params['url'])
19− self.assertIn('fr', params['url'])
20−
21− dicto['language'] = 'all'
22− params = youtube_api.request(query, dicto)
23− self.assertFalse('fr' in params['url'])
24−
25− def test_response(self):
26− self.assertRaises(AttributeError, youtube_api.response, None)
27− self.assertRaises(AttributeError, youtube_api.response, [])
28− self.assertRaises(AttributeError, youtube_api.response, '')
29− self.assertRaises(AttributeError, youtube_api.response, '[]')
30−
31− response = mock.Mock(text='{}')
32− self.assertEqual(youtube_api.response(response), [])
33−
34− response = mock.Mock(text='{"data": []}')
35− self.assertEqual(youtube_api.response(response), [])
36−
37− json = """
38− {
39− "kind": "youtube#searchListResponse",
40− "etag": "xmg9xJZuZD438sF4hb-VcBBREXc/YJQDcTBCDcaBvl-sRZJoXdvy1ME",
41− "nextPageToken": "CAUQAA",
42− "pageInfo": {
43− "totalResults": 1000000,
44− "resultsPerPage": 20
45− },
46− "items": [
47− {
48− "kind": "youtube#searchResult",
49− "etag": "xmg9xJZuZD438sF4hb-VcBBREXc/IbLO64BMhbHIgWLwLw7MDYe7Hs4",
50− "id": {
51− "kind": "youtube#video",
52− "videoId": "DIVZCPfAOeM"
53− },
54− "snippet": {
55− "publishedAt": "2015-05-29T22:41:04.000Z",
56− "channelId": "UCNodmx1ERIjKqvcJLtdzH5Q",
57− "title": "Title",
58− "description": "Description",
59− "thumbnails": {
60− "default": {
61− "url": "https://i.ytimg.com/vi/DIVZCPfAOeM/default.jpg"
62− },
63− "medium": {
64− "url": "https://i.ytimg.com/vi/DIVZCPfAOeM/mqdefault.jpg"
65− },
66− "high": {
67− "url": "https://i.ytimg.com/vi/DIVZCPfAOeM/hqdefault.jpg"
68− }
69− },
70− "channelTitle": "MinecraftUniverse",
71− "liveBroadcastContent": "none"
72− }
73− }
74− ]
75− }
76− """
77− response = mock.Mock(text=json)
78− results = youtube_api.response(response)
79− self.assertEqual(type(results), list)
80− self.assertEqual(len(results), 1)
81− self.assertEqual(results[0]['title'], 'Title')
82− self.assertEqual(results[0]['url'], 'https://www.youtube.com/watch?v=DIVZCPfAOeM')
83− self.assertEqual(results[0]['content'], 'Description')
84− self.assertEqual(results[0]['thumbnail'], 'https://i.ytimg.com/vi/DIVZCPfAOeM/hqdefault.jpg')
85− self.assertTrue('DIVZCPfAOeM' in results[0]['embedded'])
86−
87− json = """
88− {
89− "kind": "youtube#searchListResponse",
90− "etag": "xmg9xJZuZD438sF4hb-VcBBREXc/YJQDcTBCDcaBvl-sRZJoXdvy1ME",
91− "nextPageToken": "CAUQAA",
92− "pageInfo": {
93− "totalResults": 1000000,
94− "resultsPerPage": 20
95− }
96− }
97− """
98− response = mock.Mock(text=json)
99− results = youtube_api.response(response)
100− self.assertEqual(type(results), list)
101− self.assertEqual(len(results), 0)
102−
103− json = """
104− {"toto":{"entry":[]
105− }
106− }
107− """
108− response = mock.Mock(text=json)
109− results = youtube_api.response(response)
110− self.assertEqual(type(results), list)
111− self.assertEqual(len(results), 0)
tests/unit/engines/test_youtube_noapi.pydeleted−124
@@ -1,124 +0,0 @@
1−# -*- coding: utf-8 -*-
2−from collections import defaultdict
3−import mock
4−from searx.engines import youtube_noapi
5−from searx.testing import SearxTestCase
6−
7−
8−class TestYoutubeNoAPIEngine(SearxTestCase):
9−
10− def test_request(self):
11− query = 'test_query'
12− dicto = defaultdict(dict)
13− dicto['pageno'] = 0
14− dicto['time_range'] = ''
15− params = youtube_noapi.request(query, dicto)
16− self.assertIn('url', params)
17− self.assertIn(query, params['url'])
18− self.assertIn('youtube.com', params['url'])
19−
20− def test_time_range_search(self):
21− dicto = defaultdict(dict)
22− query = 'test_query'
23− dicto['time_range'] = 'year'
24− params = youtube_noapi.request(query, dicto)
25− self.assertIn('&sp=EgIIBQ%253D%253D', params['url'])
26−
27− dicto['time_range'] = 'month'
28− params = youtube_noapi.request(query, dicto)
29− self.assertIn('&sp=EgIIBA%253D%253D', params['url'])
30−
31− dicto['time_range'] = 'week'
32− params = youtube_noapi.request(query, dicto)
33− self.assertIn('&sp=EgIIAw%253D%253D', params['url'])
34−
35− dicto['time_range'] = 'day'
36− params = youtube_noapi.request(query, dicto)
37− self.assertIn('&sp=EgIIAg%253D%253D', params['url'])
38−
39− def test_response(self):
40− self.assertRaises(AttributeError, youtube_noapi.response, None)
41− self.assertRaises(AttributeError, youtube_noapi.response, [])
42− self.assertRaises(AttributeError, youtube_noapi.response, '')
43− self.assertRaises(AttributeError, youtube_noapi.response, '[]')
44−
45− response = mock.Mock(text='<html></html>')
46− self.assertEqual(youtube_noapi.response(response), [])
47−
48− html = """
49− <div></div>
50− <script>
51− window["ytInitialData"] = {
52− "contents": {
53− "twoColumnSearchResultsRenderer": {
54− "primaryContents": {
55− "sectionListRenderer": {
56− "contents": [
57− {
58− "itemSectionRenderer": {
59− "contents": [
60− {
61− "videoRenderer": {
62− "videoId": "DIVZCPfAOeM",
63− "title": {
64− "simpleText": "Title"
65− },
66− "descriptionSnippet": {
67− "runs": [
68− {
69− "text": "Des"
70− },
71− {
72− "text": "cription"
73− }
74− ]
75− }
76− }
77− },
78− {
79− "videoRenderer": {
80− "videoId": "9C_HReR_McQ",
81− "title": {
82− "simpleText": "Title"
83− },
84− "descriptionSnippet": {
85− "simpleText": "Description"
86− }
87− }
88− }
89− ]
90− }
91− }
92− ]
93− }
94− }
95− }
96− }
97− };
98− </script>
99− """
100− response = mock.Mock(text=html)
101− results = youtube_noapi.response(response)
102− self.assertEqual(type(results), list)
103− self.assertEqual(len(results), 2)
104− self.assertEqual(results[0]['title'], 'Title')
105− self.assertEqual(results[0]['url'], 'https://www.youtube.com/watch?v=DIVZCPfAOeM')
106− self.assertEqual(results[0]['content'], 'Description')
107− self.assertEqual(results[0]['thumbnail'], 'https://i.ytimg.com/vi/DIVZCPfAOeM/hqdefault.jpg')
108− self.assertTrue('DIVZCPfAOeM' in results[0]['embedded'])
109− self.assertEqual(results[1]['title'], 'Title')
110− self.assertEqual(results[1]['url'], 'https://www.youtube.com/watch?v=9C_HReR_McQ')
111− self.assertEqual(results[1]['content'], 'Description')
112− self.assertEqual(results[1]['thumbnail'], 'https://i.ytimg.com/vi/9C_HReR_McQ/hqdefault.jpg')
113− self.assertTrue('9C_HReR_McQ' in results[1]['embedded'])
114−
115− html = """
116− <ol id="item-section-063864" class="item-section">
117− <li>
118− </li>
119− </ol>
120− """
121− response = mock.Mock(text=html)
122− results = youtube_noapi.response(response)
123− self.assertEqual(type(results), list)
124− self.assertEqual(len(results), 0)
tests/unit/engines/unsplash_fixture.jsondeleted−241
@@ -1,241 +0,0 @@
1−{
2− "total": 2,
3− "total_pages": 1,
4− "results": [
5− {
6− "id": "FY8d721UO_4",
7− "created_at": "2018-04-12T14:20:35-04:00",
8− "updated_at": "2018-08-28T20:58:33-04:00",
9− "width": 3891,
10− "height": 5829,
11− "color": "#152C33",
12− "description": "low angle photography of swimming penguin",
13− "urls": {
14− "raw": "https://images.unsplash.com/photo-1523557148507-1b77641c7e7c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=095c5fc319c5a77c705f49ad63e0f195",
15− "full": "https://images.unsplash.com/photo-1523557148507-1b77641c7e7c?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjEyMDd9&s=74be977849c173d6929636d491a760c3",
16− "regular": "https://images.unsplash.com/photo-1523557148507-1b77641c7e7c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9&s=ad65df26970bd010085f0ca25434de33",
17− "small": "https://images.unsplash.com/photo-1523557148507-1b77641c7e7c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9&s=5d2edfd073c31eb8ee7b305222bdc5a2",
18− "thumb": "https://images.unsplash.com/photo-1523557148507-1b77641c7e7c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9&s=a9b9e56e63efc6f4611a87ce7e9a48f8"
19− },
20− "links": {
21− "self": "https://api.unsplash.com/photos/FY8d721UO_4",
22− "html": "https://unsplash.com/photos/FY8d721UO_4",
23− "download": "https://unsplash.com/photos/FY8d721UO_4/download",
24− "download_location": "https://api.unsplash.com/photos/FY8d721UO_4/download"
25− },
26− "categories": [],
27− "sponsored": false,
28− "likes": 31,
29− "liked_by_user": false,
30− "current_user_collections": [],
31− "slug": null,
32− "user": {
33− "id": "N4gE4mrG8lE",
34− "updated_at": "2018-10-03T02:51:19-04:00",
35− "username": "gaspanik",
36− "name": "Masaaki Komori",
37− "first_name": "Masaaki",
38− "last_name": "Komori",
39− "twitter_username": "cipher",
40− "portfolio_url": "https://www.instagram.com/cipher/",
41− "bio": null,
42− "location": "Tokyo, JAPAN",
43− "links": {
44− "self": "https://api.unsplash.com/users/gaspanik",
45− "html": "https://unsplash.com/@gaspanik",
46− "photos": "https://api.unsplash.com/users/gaspanik/photos",
47− "likes": "https://api.unsplash.com/users/gaspanik/likes",
48− "portfolio": "https://api.unsplash.com/users/gaspanik/portfolio",
49− "following": "https://api.unsplash.com/users/gaspanik/following",
50− "followers": "https://api.unsplash.com/users/gaspanik/followers"
51− },
52− "profile_image": {
53− "small": "https://images.unsplash.com/profile-fb-1502270358-e7c86c1011ce.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=9fe12f6d177bd6fdbd56d233a80c01a3",
54− "medium": "https://images.unsplash.com/profile-fb-1502270358-e7c86c1011ce.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=6ad7d156b62e438ae9dc794cba712fff",
55− "large": "https://images.unsplash.com/profile-fb-1502270358-e7c86c1011ce.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=13a08a2e72e7d11632410e92bd3a9406"
56− },
57− "instagram_username": "cipher",
58− "total_collections": 0,
59− "total_likes": 406,
60− "total_photos": 196
61− },
62− "tags": [
63− {
64− "title": "animal"
65− },
66− {
67− "title": "water"
68− },
69− {
70− "title": "swim"
71− },
72− {
73− "title": "aquarium"
74− },
75− {
76− "title": "wallpaper"
77− },
78− {
79− "title": "blue"
80− },
81− {
82− "title": "sealife"
83− },
84− {
85− "title": "wildlife"
86− },
87− {
88− "title": "bird"
89− },
90− {
91− "title": "deep sea"
92− },
93− {
94− "title": "fish"
95− },
96− {
97− "title": "water life"
98− }
99− ],
100− "photo_tags": [
101− {
102− "title": "animal"
103− },
104− {
105− "title": "water"
106− },
107− {
108− "title": "swim"
109− },
110− {
111− "title": "aquarium"
112− },
113− {
114− "title": "wallpaper"
115− }
116− ]
117− },
118− {
119− "id": "ayKyc01xLWA",
120− "created_at": "2018-02-16T23:14:31-05:00",
121− "updated_at": "2018-08-28T20:48:27-04:00",
122− "width": 4928,
123− "height": 3264,
124− "color": "#161618",
125− "description": "black and white penguins on ice field",
126− "urls": {
127− "raw": "https://images.unsplash.com/photo-1518840801558-9770b4a34eeb?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=4e107a2bc49ab561ba6272eea2ec725d",
128− "full": "https://images.unsplash.com/photo-1518840801558-9770b4a34eeb?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjEyMDd9&s=f9b1e4d4572ab44efb2cf3d601d2b4d9",
129− "regular": "https://images.unsplash.com/photo-1518840801558-9770b4a34eeb?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9&s=4430cedb63841f1fe055d5005316cc96",
130− "small": "https://images.unsplash.com/photo-1518840801558-9770b4a34eeb?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9&s=ee73c7af22ce445d408e240821ce07af",
131− "thumb": "https://images.unsplash.com/photo-1518840801558-9770b4a34eeb?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9&s=934302390d383cad8c571905e3a80bac"
132− },
133− "links": {
134− "self": "https://api.unsplash.com/photos/ayKyc01xLWA",
135− "html": "https://unsplash.com/photos/ayKyc01xLWA",
136− "download": "https://unsplash.com/photos/ayKyc01xLWA/download",
137− "download_location": "https://api.unsplash.com/photos/ayKyc01xLWA/download"
138− },
139− "categories": [],
140− "sponsored": false,
141− "likes": 37,
142− "liked_by_user": false,
143− "current_user_collections": [],
144− "slug": null,
145− "user": {
146− "id": "tRb_KGw60Xk",
147− "updated_at": "2018-09-20T11:51:54-04:00",
148− "username": "ghost_cat",
149− "name": "Danielle Barnes",
150− "first_name": "Danielle",
151− "last_name": "Barnes",
152− "twitter_username": null,
153− "portfolio_url": null,
154− "bio": null,
155− "location": null,
156− "links": {
157− "self": "https://api.unsplash.com/users/ghost_cat",
158− "html": "https://unsplash.com/@ghost_cat",
159− "photos": "https://api.unsplash.com/users/ghost_cat/photos",
160− "likes": "https://api.unsplash.com/users/ghost_cat/likes",
161− "portfolio": "https://api.unsplash.com/users/ghost_cat/portfolio",
162− "following": "https://api.unsplash.com/users/ghost_cat/following",
163− "followers": "https://api.unsplash.com/users/ghost_cat/followers"
164− },
165− "profile_image": {
166− "small": "https://images.unsplash.com/profile-fb-1508491082-ae77f53e9ac3.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=751bf6a557763648d52ffd7e60e79436",
167− "medium": "https://images.unsplash.com/profile-fb-1508491082-ae77f53e9ac3.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=e46cd1c8713035f045130e1b093b981e",
168− "large": "https://images.unsplash.com/profile-fb-1508491082-ae77f53e9ac3.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=352eabcf107c3ce95fe51a18485f116b"
169− },
170− "instagram_username": null,
171− "total_collections": 0,
172− "total_likes": 0,
173− "total_photos": 21
174− },
175− "tags": [
176− {
177− "title": "ice"
178− },
179− {
180− "title": "bird"
181− },
182− {
183− "title": "ice field"
184− },
185− {
186− "title": "iceberg"
187− },
188− {
189− "title": "snow"
190− },
191− {
192− "title": "frozen"
193− },
194− {
195− "title": "animal"
196− },
197− {
198− "title": "wildlife"
199− },
200− {
201− "title": "wild"
202− },
203− {
204− "title": "antarctica"
205− },
206− {
207− "title": "sunshine"
208− },
209− {
210− "title": "daylight"
211− },
212− {
213− "title": "wilderness"
214− },
215− {
216− "title": "south pole"
217− },
218− {
219− "title": "flock"
220− }
221− ],
222− "photo_tags": [
223− {
224− "title": "ice"
225− },
226− {
227− "title": "bird"
228− },
229− {
230− "title": "ice field"
231− },
232− {
233− "title": "iceberg"
234− },
235− {
236− "title": "snow"
237− }
238− ]
239− }
240− ]
241−}
utils/makefile.includenew file+128
@@ -0,0 +1,128 @@
1+# -*- coding: utf-8; mode: makefile-gmake -*-
2+
3+make-help:
4+ @echo ' make V=0|1 [targets] 0 => quiet build (default), 1 => verbose build'
5+ @echo ' make V=2 [targets] 2 => give reason for rebuild of target'
6+
7+quiet_cmd_common_clean = CLEAN $@
8+ cmd_common_clean = \
9+ rm -rf tests/build ;\
10+ find . -name '*.orig' -exec rm -f {} + ;\
11+ find . -name '*.rej' -exec rm -f {} + ;\
12+ find . -name '*~' -exec rm -f {} + ;\
13+ find . -name '*.bak' -exec rm -f {} + ;\
14+
15+FMT = cat
16+ifeq ($(shell which fmt >/dev/null 2>&1; echo $$?), 0)
17+FMT = fmt
18+endif
19+
20+# MS-Windows
21+#
22+# For a minimal *make-environment*, I'am using the gnu-tools from:
23+#
24+# - GNU MCU Eclipse Windows Build Tools, which brings 'make', 'rm' etc.
25+# https://github.com/gnu-mcu-eclipse/windows-build-tools/releases
26+#
27+# - git for Windows, which brings 'find', 'grep' etc.
28+# https://git-scm.com/download/win
29+
30+
31+# normpath
32+#
33+# System-dependent normalization of the path name
34+#
35+# usage: $(call normpath,/path/to/file)
36+
37+normpath = $1
38+ifeq ($(OS),Windows_NT)
39+ normpath = $(subst /,\,$1)
40+endif
41+
42+
43+# stolen from linux/Makefile
44+#
45+
46+ifeq ("$(origin V)", "command line")
47+ KBUILD_VERBOSE = $(V)
48+endif
49+ifndef KBUILD_VERBOSE
50+ KBUILD_VERBOSE = 0
51+endif
52+
53+ifeq ($(KBUILD_VERBOSE),1)
54+ quiet =
55+ Q =
56+else
57+ quiet=quiet_
58+ Q = @
59+endif
60+
61+# stolen from linux/scripts/Kbuild.include
62+#
63+
64+# Convenient variables
65+comma := ,
66+quote := "
67+#" this comment is only for emacs highlighting
68+squote := '
69+#' this comment is only for emacs highlighting
70+empty :=
71+space := $(empty) $(empty)
72+space_escape := _-_SPACE_-_
73+
74+# Find any prerequisites that is newer than target or that does not exist.
75+# PHONY targets skipped in both cases.
76+any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)
77+#
78+###
79+# why - tell why a a target got build
80+# enabled by make V=2
81+# Output (listed in the order they are checked):
82+# (1) - due to target is PHONY
83+# (2) - due to target missing
84+# (3) - due to: file1.h file2.h
85+# (4) - due to command line change
86+# (5) - due to missing .cmd file
87+# (6) - due to target not in $(targets)
88+# (1) PHONY targets are always build
89+# (2) No target, so we better build it
90+# (3) Prerequisite is newer than target
91+# (4) The command line stored in the file named dir/.target.cmd
92+# differed from actual command line. This happens when compiler
93+# options changes
94+# (5) No dir/.target.cmd file (used to store command line)
95+# (6) No dir/.target.cmd file and target not listed in $(targets)
96+# This is a good hint that there is a bug in the kbuild file
97+ifeq ($(KBUILD_VERBOSE),2)
98+why = \
99+ $(if $(filter $@, $(PHONY)),- due to target is PHONY, \
100+ $(if $(wildcard $@), \
101+ $(if $(strip $(any-prereq)),- due to: $(any-prereq), \
102+ $(if $(arg-check), \
103+ $(if $(cmd_$@),- due to command line change, \
104+ $(if $(filter $@, $(targets)), \
105+ - due to missing .cmd file, \
106+ - due to $(notdir $@) not in $$(targets) \
107+ ) \
108+ ) \
109+ ) \
110+ ), \
111+ - due to target missing \
112+ ) \
113+ )
114+
115+echo-why = $(call escsq, $(strip $(why)))
116+endif
117+#
118+###
119+# Escape single quote for use in echo statements
120+escsq = $(subst $(squote),'\$(squote)',$1)
121+#
122+# echo command.
123+# Short version is used, if $(quiet) equals `quiet_', otherwise full one.
124+echo-cmd = $(if $($(quiet)cmd_$(1)),echo '$(call escsq,$($(quiet)cmd_$(1)))$(echo-why)';)
125+#
126+# printing commands
127+cmd = @$(echo-cmd) $(cmd_$(1))
128+
utils/makefile.pythonnew file+290
@@ -0,0 +1,290 @@
1+# -*- coding: utf-8; mode: makefile-gmake -*-
2+
3+# list of python packages (folders) or modules (files) of this build
4+PYOBJECTS ?=
5+
6+SITE_PYTHON ?=$(dir $(abspath $(lastword $(MAKEFILE_LIST))))site-python
7+export PYTHONPATH := $(SITE_PYTHON):$$PYTHONPATH
8+
9+# folder where the python distribution takes place
10+PYDIST ?= ./py_dist
11+# folder where the python intermediate build files take place
12+PYBUILD ?= ./py_build
13+# python version to use
14+PY ?=3
15+PYTHON ?= python$(PY)
16+PIP ?= pip$(PY)
17+PIP_INST ?= --user
18+
19+# https://www.python.org/dev/peps/pep-0508/#extras
20+#PY_SETUP_EXTRAS ?= \[develop,test\]
21+PY_SETUP_EXTRAS ?=
22+
23+PYDEBUG ?= --pdb
24+PYLINT_RC ?= .pylintrc
25+
26+TEST_FOLDER ?= ./tests
27+TEST ?= .
28+
29+VTENV_OPTS = "--no-site-packages"
30+PY_ENV = ./local/py$(PY)
31+PY_ENV_BIN = $(PY_ENV)/bin
32+PY_ENV_ACT = . $(PY_ENV_BIN)/activate
33+
34+ifeq ($(OS),Windows_NT)
35+ PYTHON = python
36+ PY_ENV_BIN = $(PY_ENV)/Scripts
37+ PY_ENV_ACT = $(PY_ENV_BIN)/activate
38+endif
39+
40+ifeq ($(PYTHON),python)
41+ VIRTUALENV = virtualenv
42+else
43+ VIRTUALENV = virtualenv --python=$(PYTHON)
44+endif
45+
46+ifeq ($(KBUILD_VERBOSE),1)
47+ PIP_VERBOSE =
48+ VIRTUALENV_VERBOSE =
49+else
50+ PIP_VERBOSE = "-q"
51+ VIRTUALENV_VERBOSE = "-q"
52+endif
53+
54+python-help::
55+ @echo 'makefile.python:'
56+ @echo ' pyenv | pyenv[un]install'
57+ @echo ' build $(PY_ENV) & [un]install python objects'
58+ @echo ' targts using pyenv $(PY_ENV):'
59+ @echo ' pylint - run pylint *linting*'
60+ @echo ' pytest - run *tox* test on python objects'
61+ @echo ' pydebug - run tests within a PDB debug session'
62+ @echo ' pybuild - build python packages'
63+ @echo ' pyclean - clean intermediate python objects'
64+ @echo ' targets using system users environment:'
65+ @echo ' py[un]install - [un]install python objects in editable mode'
66+ @echo ' upload-pypi - upload $(PYDIST)/* files to PyPi'
67+ @echo 'options:'
68+ @echo ' make PY=2 [targets] => to eval targets with python 2 ($(PY))'
69+ @echo ' make PIP_INST= => to set/unset pip install options ($(PIP_INST))'
70+ @echo ' make TEST=. => choose test from $(TEST_FOLDER) (default "." runs all)'
71+ @echo ' make DEBUG= => target "debug": do not invoke PDB on errors'
72+ @echo ' make PY_SETUP_EXTRAS => also install extras_require from setup.py \[develop,test\]'
73+ @echo ' when using target "pydebug", set breakpoints within py-source by adding::'
74+ @echo ' DEBUG()'
75+
76+# ------------------------------------------------------------------------------
77+# OS requirements
78+# ------------------------------------------------------------------------------
79+
80+PHONY += msg-python-exe python-exe
81+msg-python-exe:
82+ @echo "\n $(PYTHON) is required\n\n\
83+ Make sure you have $(PYTHON) installed, grab it from\n\
84+ https://www.python.org or install it from your package\n\
85+ manager. On debian based OS these requirements are\n\
86+ installed by::\n\n\
87+ sudo -H apt-get install $(PYTHON)\n" | $(FMT)
88+
89+ifeq ($(shell which $(PYTHON) >/dev/null 2>&1; echo $$?), 1)
90+python-exe: msg-python-exe
91+ $(error The '$(PYTHON)' command was not found)
92+else
93+python-exe:
94+ @:
95+endif
96+
97+msg-pip-exe:
98+ @echo "\n $(PIP) is required\n\n\
99+ Make sure you have updated pip installed, grab it from\n\
100+ https://pip.pypa.io or install it from your package\n\
101+ manager. On debian based OS these requirements are\n\
102+ installed by::\n\n\
103+ sudo -H apt-get install python$(PY)-pip\n" | $(FMT)
104+
105+ifeq ($(shell which $(PIP) >/dev/null 2>&1; echo $$?), 1)
106+pip-exe: msg-pip-exe
107+ $(error The '$(PIP)' command was not found)
108+else
109+pip-exe:
110+ @:
111+endif
112+
113+PHONY += msg-virtualenv-exe virtualenv-exe
114+msg-virtualenv-exe:
115+ @echo "\n virtualenv is required\n\n\
116+ Make sure you have an updated virtualenv installed, grab it from\n\
117+ https://virtualenv.pypa.io/en/stable/installation/ or install it\n\
118+ via pip by::\n\n\
119+ pip install --user https://github.com/pypa/virtualenv/tarball/master\n" | $(FMT)
120+
121+ifeq ($(shell which virtualenv >/dev/null 2>&1; echo $$?), 1)
122+virtualenv-exe: msg-virtualenv-exe
123+ $(error The 'virtualenv' command was not found)
124+else
125+virtualenv-exe:
126+ @:
127+endif
128+
129+# ------------------------------------------------------------------------------
130+# commands
131+# ------------------------------------------------------------------------------
132+
133+# $2 path to folder with setup.py, this uses pip from the OS
134+quiet_cmd_pyinstall = INSTALL $2
135+ cmd_pyinstall = $(PIP) $(PIP_VERBOSE) install $(PIP_INST) -e $2$(PY_SETUP_EXTRAS)
136+
137+# $2 path to folder with setup.py, this uses pip from pyenv (not OS!)
138+quiet_cmd_pyenvinstall = PYENV install $2
139+ cmd_pyenvinstall = $(PY_ENV_BIN)/pip $(PIP_VERBOSE) install -e $2$(PY_SETUP_EXTRAS)
140+
141+# Uninstall the package. Since pip does not uninstall the no longer needed
142+# depencies (something like autoremove) the depencies remain.
143+
144+# $2 package name to uninstall, this uses pip from the OS.
145+quiet_cmd_pyuninstall = UNINSTALL $2
146+ cmd_pyuninstall = $(PIP) $(PIP_VERBOSE) uninstall --yes $2
147+
148+# $2 path to folder with setup.py, this uses pip from pyenv (not OS!)
149+quiet_cmd_pyenvuninstall = PYENV uninstall $2
150+ cmd_pyenvuninstall = $(PY_ENV_BIN)/pip $(PIP_VERBOSE) uninstall --yes $2
151+
152+# $2 path to folder where virtualenv take place
153+quiet_cmd_virtualenv = PYENV usage: $ source ./$@/bin/activate
154+ cmd_virtualenv = \
155+ if [ ! -d "./$(PY_ENV)" ];then \
156+ $(VIRTUALENV) $(VIRTUALENV_VERBOSE) $(VTENV_OPTS) $2; \
157+ else \
158+ echo "PYENV using virtualenv from $2"; \
159+ fi
160+
161+# $2 path to lint
162+quiet_cmd_pylint = LINT $@
163+ cmd_pylint = $(PY_ENV_BIN)/pylint --rcfile $(PYLINT_RC) $2
164+
165+quiet_cmd_pytest = TEST $@
166+ cmd_pytest = $(PY_ENV_BIN)/tox -vv
167+
168+# setuptools, pip, easy_install its a mess full of cracks, a documentation hell
169+# and broken by design ... all sucks, I really, really hate all this ... aaargh!
170+#
171+# About python packaging see `Python Packaging Authority`_. Most of the names
172+# here are mapped to ``setup(<name1>=..., <name2>=...)`` arguments in
173+# ``setup.py``. See `Packaging and distributing projects`_ about ``setup(...)``
174+# arguments. If this is all new for you, start with `PyPI Quick and Dirty`_.
175+#
176+# Further read:
177+#
178+# - pythonwheels_
179+# - setuptools_
180+# - packaging_
181+# - sdist_
182+# - installing_
183+#
184+# .. _`Python Packaging Authority`: https://www.pypa.io
185+# .. _`Packaging and distributing projects`: https://packaging.python.org/guides/distributing-packages-using-setuptools/
186+# .. _`PyPI Quick and Dirty`: https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/
187+# .. _pythonwheels: https://pythonwheels.com/
188+# .. _setuptools: https://setuptools.readthedocs.io/en/latest/setuptools.html
189+# .. _packaging: https://packaging.python.org/guides/distributing-packages-using-setuptools/#packaging-and-distributing-projects
190+# .. _sdist: https://packaging.python.org/guides/distributing-packages-using-setuptools/#source-distributions
191+# .. _bdist_wheel: https://packaging.python.org/guides/distributing-packages-using-setuptools/#pure-python-wheels
192+# .. _installing: https://packaging.python.org/tutorials/installing-packages/
193+#
194+quiet_cmd_pybuild = BUILD $@
195+ cmd_pybuild = $(PY_ENV_BIN)/$(PYTHON) setup.py \
196+ sdist -d $(PYDIST) \
197+ bdist_wheel --bdist-dir $(PYBUILD) -d $(PYDIST)
198+
199+quiet_cmd_pyclean = CLEAN $@
200+# remove 'build' folder since bdist_wheel does not care the --bdist-dir
201+ cmd_pyclean = \
202+ rm -rf $(PYDIST) $(PYBUILD) ./local ./.tox *.egg-info ;\
203+ find . -name '*.pyc' -exec rm -f {} + ;\
204+ find . -name '*.pyo' -exec rm -f {} + ;\
205+ find . -name __pycache__ -exec rm -rf {} +
206+
207+# ------------------------------------------------------------------------------
208+# targets
209+# ------------------------------------------------------------------------------
210+
211+# for installation use the pip from the OS!
212+PHONY += pyinstall
213+pyinstall: pip-exe
214+ $(call cmd,pyinstall,.)
215+
216+PHONY += pyuninstall
217+pyuninstall: pip-exe
218+ $(call cmd,pyuninstall,$(PYOBJECTS))
219+
220+# for installation use the pip from PY_ENV (not the OS)!
221+PHONY += pyenvinstall
222+pyenvinstall: $(PY_ENV)
223+ $(call cmd,pyenvinstall,.)
224+
225+PHONY += pyenvuninstall
226+pyenvuninstall: $(PY_ENV)
227+ $(call cmd,pyenvuninstall,$(PYOBJECTS))
228+
229+PHONY += pyclean
230+pyclean:
231+ $(call cmd,pyclean)
232+
233+# to build *local* environment, python and virtualenv from the OS is needed!
234+pyenv: $(PY_ENV)
235+$(PY_ENV): virtualenv-exe python-exe
236+ $(call cmd,virtualenv,$(PY_ENV))
237+ @$(PY_ENV_BIN)/pip install $(PIP_VERBOSE) -r requirements.txt
238+
239+PHONY += pylint-exe
240+pylint-exe: $(PY_ENV)
241+ @$(PY_ENV_BIN)/pip $(PIP_VERBOSE) install pylint
242+
243+PHONY += pylint
244+pylint: pylint-exe
245+ $(call cmd,pylint,$(PYOBJECTS))
246+
247+PHONY += pybuild
248+pybuild: $(PY_ENV)
249+ $(call cmd,pybuild)
250+
251+PHONY += pytest
252+pytest: $(PY_ENV)
253+ $(call cmd,pytest)
254+
255+PHONY += pydebug
256+# set breakpoint with:
257+# DEBUG()
258+# e.g. to run tests in debug mode in emacs use:
259+# 'M-x pdb' ... 'make pydebug'
260+pydebug: $(PY_ENV)
261+ DEBUG=$(DEBUG) $(PY_ENV_BIN)/pytest $(DEBUG) -v $(TEST_FOLDER)/$(TEST)
262+
263+# install / uninstall python objects into virtualenv (PYENV)
264+pyenv-install: $(PY_ENV)
265+ @$(PY_ENV_BIN)/pip $(PIP_VERBOSE) install -e .
266+ @echo "ACTIVATE $(call normpath,$(PY_ENV_ACT)) "
267+
268+pyenv-uninstall: $(PY_ENV)
269+ @$(PY_ENV_BIN)/pip $(PIP_VERBOSE) uninstall --yes .
270+
271+# runs python interpreter from ./local/py<N>/bin/python
272+pyenv-python: pyenv-install
273+ cd ./local; ../$(PY_ENV_BIN)/python -i
274+
275+# With 'dependency_links=' setuptools supports dependencies on packages hosted
276+# on other reposetories then PyPi, see "Packages Not On PyPI" [1]. The big
277+# drawback is, due to security reasons (I don't know where the security gate on
278+# PyPi is), this feature is not supported by pip [2]. Thats why an upload to
279+# PyPi is required and since uploads via setuptools is not recommended, we have
280+# to imstall / use twine ... its really a mess.
281+#
282+# [1] http://python-packaging.readthedocs.io/en/latest/dependencies.html#packages-not-on-pypi
283+# [2] https://github.com/pypa/pip/pull/1519
284+
285+# https://github.com/pypa/twine
286+PHONY += upload-pypi
287+upload-pypi: pyclean pybuild
288+ @$(PY_ENV_BIN)/twine upload $(PYDIST)/*
289+
290+.PHONY: $(PHONY)
utils/makefile.sphinxnew file+215
@@ -0,0 +1,215 @@
1+# -*- coding: utf-8; mode: makefile-gmake -*-
2+
3+# You can set these variables from the command line.
4+SPHINXOPTS ?=
5+SPHINXBUILD ?= $(PY_ENV_BIN)/sphinx-build
6+SPHINX_CONF ?= conf.py
7+
8+DOCS_FOLDER ?= docs
9+DOCS_BUILD ?= build/docs
10+DOCS_DIST ?= dist/docs
11+GH_PAGES ?= gh-pages
12+
13+BOOKS_FOLDER ?= docs
14+BOOKS_DIST ?= dist/books
15+
16+ifeq ($(KBUILD_VERBOSE),1)
17+ SPHINX_VERBOSE = "-v"
18+else
19+ SPHINX_VERBOSE =
20+endif
21+
22+## SPHINXVERS variable
23+## ===================
24+##
25+## .. _requirement-specifiers: https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers
26+##
27+## Sphinx version to use, when building documentation. Set this when calling
28+## build target. The default value is empty (install latest), to select a
29+## specific version use a requirement-specifiers_. E.g. to build your target
30+## 'doc' with a select sphinx-doc_ version 1.7.9::
31+##
32+## make SPHINXVERS='==1.7.9' docs
33+##
34+## To build with latest 1.7::
35+##
36+## make SPHINXVERS='>=1.7,<1.8' docs
37+##
38+SPHINXVERS ?=
39+
40+docs-help:
41+ @echo 'makefile.sphinx:'
42+ @echo ' docs-clean - clean intermediate doc objects'
43+ @echo ' $(GH_PAGES) - create & upload github pages'
44+ @echo ' sphinx-pdf - run sphinx latex & pdf targets'
45+ echo ''
46+ @echo ' books/{name}.html : build only the HTML of document {name}'
47+ @echo ' valid values for books/{name}.html are:'
48+ @echo ' $(BOOKS_HTML)' | $(FMT)
49+ @echo ' books/{name}.pdf : build only the PDF of document {name}'
50+ @echo ' valid values for books/{name}.pdf are:'
51+ @echo ' $(BOOKS_PDF) ' | $(FMT)
52+
53+# ------------------------------------------------------------------------------
54+# requirements
55+# ------------------------------------------------------------------------------
56+
57+sphinx-doc: $(PY_ENV)
58+ @echo "PYENV installing Sphinx$(SPHINXVERS)"
59+ $(Q)$(PY_ENV_BIN)/pip install $(PIP_VERBOSE) 'Sphinx$(SPHINXVERS)'
60+
61+sphinx-live: $(PY_ENV)
62+ @echo "PYENV installing Sphinx$(SPHINXVERS)"
63+ $(Q)$(PY_ENV_BIN)/pip install $(PIP_VERBOSE) 'Sphinx$(SPHINXVERS)' sphinx-autobuild
64+
65+
66+PHONY += msg-texlive texlive
67+
68+ifeq ($(shell which xelatex >/dev/null 2>&1; echo $$?), 1)
69+texlive: msg-TeXLive
70+ $(error The 'xelatex' command was not found)
71+else
72+texlive:
73+ @:
74+endif
75+
76+msg-texlive:
77+ $(Q)echo "\n\
78+The TeX/PDF output and the *math* extension require TexLive and latexmk:\n\n\
79+ Make sure you have a updated TeXLive with XeTeX engine installed, grab it\n\
80+ it from https://www.tug.org/texlive or install it from your package manager.\n\n\
81+ Install latexmk from your package manager or visit https://ctan.org/pkg/latexmk\n\n\
82+ Sphinx-doc produce (Xe)LaTeX files which might use additional TeX-packages\n\
83+ and fonts. To process these LaTeX files, a TexLive installation with the\n\
84+ additional packages is required. On debian based OS these requirements\n\
85+ are installed by::\n\n\
86+ sudo -H apt-get install\n\
87+ latexmk\n\
88+ texlive-base texlive-xetex texlive-latex-recommended\n\
89+ texlive-extra-utils dvipng ttf-dejavu\n"
90+
91+# ------------------------------------------------------------------------------
92+# commands
93+# ------------------------------------------------------------------------------
94+
95+# $2 sphinx builder e.g. "html"
96+# $3 path where configuration file (conf.py) is located
97+# $4 sourcedir
98+# $5 dest subfolder e.g. "man" for man pages at $(DOCS_DIST)/man
99+
100+quiet_cmd_sphinx = SPHINX $@ --> file://$(abspath $(DOCS_DIST)/$5)
101+ cmd_sphinx = SPHINX_CONF=$(abspath $4/$(SPHINX_CONF))\
102+ $(SPHINXBUILD) $(SPHINX_VERBOSE) $(SPHINXOPTS)\
103+ -b $2 -c $3 -d $(DOCS_BUILD)/.doctrees $4 $(DOCS_DIST)/$5
104+
105+quiet_cmd_sphinx_autobuild = SPHINX $@ --> file://$(abspath $(DOCS_DIST)/$5)
106+ cmd_sphinx_autobuild = PATH="$(PY_ENV_BIN):$(PATH)" $(PY_ENV_BIN)/sphinx-autobuild $(SPHINX_VERBOSE) --poll -B --host 0.0.0.0 --port 8080 $(SPHINXOPTS)\
107+ -b $2 -c $3 -d $(DOCS_BUILD)/.doctrees $4 $(DOCS_DIST)/$5
108+
109+quiet_cmd_sphinx_clean = CLEAN $@
110+ cmd_sphinx_clean = rm -rf $(DOCS_BUILD) $(DOCS_DIST) $(GH_PAGES)/* $(GH_PAGES)/.buildinfo
111+
112+# ------------------------------------------------------------------------------
113+# targets
114+# ------------------------------------------------------------------------------
115+
116+# build PDF of whole documentation in: $(DOCS_DIST)/pdf
117+
118+PHONY += sphinx-pdf
119+sphinx-pdf: sphinx-latex
120+ $(Q)cd $(DOCS_BUILD)/latex/; make all-pdf
121+ $(Q)mkdir -p $(DOCS_DIST)/pdf
122+ $(Q)cp $(DOCS_BUILD)/latex/*.pdf $(DOCS_DIST)/pdf
123+ @echo "SPHINX *.pdf --> file://$(abspath $(DOCS_DIST)/pdf)"
124+
125+PHONY += sphinx-latex
126+sphinx-latex: texlive sphinx-doc
127+ $(SPHINXBUILD) $(SPHINX_VERBOSE) $(SPHINXOPTS)\
128+ -b latex \
129+ -c $(DOCS_FOLDER) \
130+ -d $(DOCS_BUILD)/.doctrees \
131+ $(DOCS_FOLDER) \
132+ $(DOCS_BUILD)/latex
133+
134+# Sphinx projects, we call them *books* (what is more common). Books are
135+# folders under $(BOOKS_FOLDER) containing a conf.py file. The HTML output goes
136+# to folder $(BOOKS_DIST)/<name> while PDF is placed (BOOKS_DIST)/<name>/pdf
137+
138+BOOKS=$(patsubst $(BOOKS_FOLDER)/%/conf.py,books/%,$(wildcard $(BOOKS_FOLDER)/*/conf.py))
139+
140+# fine grained targets
141+BOOKS_HTML = $(patsubst %,%.html,$(BOOKS))
142+BOOKS_CLEAN = $(patsubst %,%.clean,$(BOOKS))
143+BOOKS_LATEX = $(patsubst %,%.latex,$(BOOKS))
144+BOOKS_PDF = $(patsubst %,%.pdf,$(BOOKS))
145+BOOKS_LIVE = $(patsubst %,%.live,$(BOOKS))
146+
147+$(BOOKS_DIST):
148+ mkdir -p $(BOOKS_DIST)
149+
150+PHONY += $(BOOKS_HTML)
151+$(BOOKS_HTML): sphinx-doc | $(BOOKS_DIST)
152+ SPHINX_CONF=$(patsubst books/%.html,%,$@)/conf.py \
153+ $(SPHINXBUILD) $(SPHINX_VERBOSE) $(SPHINXOPTS)\
154+ -b html \
155+ -c $(DOCS_FOLDER) \
156+ -d $(DOCS_BUILD)/books/$(patsubst books/%.html,%,$@)/.doctrees \
157+ $(patsubst books/%.html,%,$@) \
158+ $(BOOKS_DIST)/$(patsubst books/%.html,%,$@)
159+ @echo "SPHINX $@ --> file://$(abspath $(BOOKS_DIST)/$(patsubst books/%.html,%,$@))"
160+
161+PHONY += $(BOOKS_HTML)
162+$(BOOKS_LIVE): sphinx-live | $(BOOKS_DIST)
163+ PATH="$(PY_ENV_BIN):$(PATH)" \
164+ SPHINX_CONF=$(patsubst books/%.live,%,$@)/conf.py \
165+ $(PY_ENV_BIN)/sphinx-autobuild --poll -B --host 0.0.0.0 --port 8080 $(SPHINX_VERBOSE) $(SPHINXOPTS)\
166+ -b html \
167+ -c $(DOCS_FOLDER) \
168+ -d $(DOCS_BUILD)/books/$(patsubst books/%.live,%,$@)/.doctrees \
169+ $(patsubst books/%.live,%,$@) \
170+ $(BOOKS_DIST)/$(patsubst books/%.live,%,$@)
171+
172+$(BOOKS_PDF): %.pdf : %.latex
173+ $(Q)cd $(DOCS_BUILD)/latex/$(patsubst books/%.pdf,%,$@); make all-pdf
174+ $(Q)mkdir -p $(BOOKS_DIST)/$(patsubst books/%.pdf,%,$@)/pdf
175+ $(Q)cp -v $(DOCS_BUILD)/latex/$(patsubst books/%.pdf,%,$@)/*.pdf $(BOOKS_DIST)/$(patsubst books/%.pdf,%,$@)/pdf
176+ @echo "SPHINX $@ --> file://$(abspath $(BOOKS_DIST)/$(patsubst books/%.pdf,%,$@))/pdf"
177+
178+PHONY += $(BOOKS_LATEX)
179+$(BOOKS_LATEX): sphinx-doc | $(BOOKS_DIST)
180+ SPHINX_CONF=$(patsubst books/%.latex,%,$@)/conf.py \
181+ $(SPHINXBUILD) $(SPHINX_VERBOSE) $(SPHINXOPTS)\
182+ -b latex \
183+ -c $(DOCS_FOLDER) \
184+ -d $(DOCS_BUILD)/books/$(patsubst books/%.latex,%,$@)/.doctrees \
185+ $(patsubst books/%.latex,%,$@) \
186+ $(DOCS_BUILD)/latex/$(patsubst books/%.latex,%,$@)
187+ @echo "SPHINX $@ --> file://$(abspath $(DOCS_BUILD)/latex/$(patsubst books/%.latex,%,$@))"
188+
189+$(BOOKS_CLEAN):
190+ $(Q)rm -rf $(BOOKS_DIST)/$(patsubst books/%.clean,%,$@) \
191+ $(DOCS_BUILD)/books/$(patsubst books/%.clean,%,$@) \
192+ $(DOCS_BUILD)/latex/$(patsubst books/%.clean,%,$@)
193+
194+# github pages
195+
196+PHONY += $(GH_PAGES)
197+$(GH_PAGES)::
198+ $(MAKE) docs
199+ [ -d "gh-pages/.git" ] || git clone $(GIT_URL) gh-pages
200+ -cd $(GH_PAGES); git checkout gh-pages >/dev/null
201+ -cd $(GH_PAGES); ls -A | grep -v '.git$$' | xargs rm -rf
202+ cp -r $(DOCS_DIST)/* $(GH_PAGES)/
203+ touch $(GH_PAGES)/.nojekyll
204+ echo "<html><head><META http-equiv='refresh' content='0;URL=index.html'></head></html>" > $(GH_PAGES)/404.html
205+ cd $(GH_PAGES);\
206+ git add --all . ;\
207+ git commit -m "gh-pages: updated" ;\
208+ git push origin gh-pages
209+
210+
211+PHONY += docs-clean
212+docs-clean: $(BOOKS_CLEAN)
213+ $(call cmd,sphinx_clean)
214+
215+.PHONY: $(PHONY)