mirror of
https://github.com/searxng/searxng.git
synced 2026-09-11 16:56:05 +00:00
Compare commits
22 Commits
22056605a6
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b2f750b58 | ||
|
|
ffe96f8a6f | ||
|
|
931fd9787b | ||
|
|
42e1d61296 | ||
|
|
765a9999df | ||
|
|
ba055b3e09 | ||
|
|
3fdc6d753a | ||
|
|
3e454637fb | ||
|
|
c7f3080aac | ||
|
|
072311b5e0 | ||
|
|
c06e9f0889 | ||
|
|
4781754dc4 | ||
|
|
28b61729c7 | ||
|
|
14a9f84c6c | ||
|
|
8b01679e8f | ||
|
|
eaf1fcb349 | ||
|
|
e20e370353 | ||
|
|
3605a2d58b | ||
|
|
aef258321c | ||
|
|
a303e9c0ca | ||
|
|
ccffbfc164 | ||
|
|
242dc6e398 |
5
.github/workflows/container.yml
vendored
5
.github/workflows/container.yml
vendored
@@ -105,8 +105,9 @@ jobs:
|
||||
arch: amd64
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: arm64
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: armv7
|
||||
# FIXME: https://github.com/searxng/searxng/pull/6655#issuecomment-5550293085
|
||||
# - runner: ubuntu-26.04-arm
|
||||
# arch: armv7
|
||||
|
||||
steps:
|
||||
- name: Login to GHCR
|
||||
|
||||
847
client/simple/package-lock.json
generated
847
client/simple/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,14 +29,14 @@
|
||||
"swiped-events": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.10",
|
||||
"@types/node": "^26.3.0",
|
||||
"@biomejs/biome": "2.5.11",
|
||||
"@types/node": "^26.4.0",
|
||||
"browserslist": "^4.28.8",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"edge.js": "^6.5.1",
|
||||
"less": "^4.9.0",
|
||||
"mathjs": "^15.2.0",
|
||||
"sharp": "~0.35.3",
|
||||
"sharp": "~0.35.4",
|
||||
"sort-package-json": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"stylelint-config-standard-less": "^4.1.0",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.. _cara engine:
|
||||
|
||||
===========
|
||||
Cara Images
|
||||
===========
|
||||
|
||||
.. automodule:: searx.engines.cara
|
||||
:members:
|
||||
8
docs/dev/engines/online/europepmc.rst
Normal file
8
docs/dev/engines/online/europepmc.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _europepmc engine:
|
||||
|
||||
==========
|
||||
Europe PMC
|
||||
==========
|
||||
|
||||
.. automodule:: searx.engines.europepmc
|
||||
:members:
|
||||
@@ -2,9 +2,9 @@ mock==5.2.0
|
||||
nose2[coverage_plugin]==0.16.0
|
||||
cov-core==1.15.0
|
||||
black==25.9.0
|
||||
pylint==4.0.7
|
||||
pylint==4.0.8
|
||||
splinter==0.21.0
|
||||
selenium==4.47.0
|
||||
selenium==4.48.0
|
||||
Sphinx==8.2.3;python_version <= "3.11"
|
||||
Sphinx==9.1.0; python_version > "3.11"
|
||||
sphinx-issues==6.0.0
|
||||
@@ -18,11 +18,11 @@ myst-parser==5.0.0
|
||||
linuxdoc==20260504
|
||||
aiounittest==1.5.0
|
||||
yamllint==1.38.0
|
||||
wlc==2.1.1
|
||||
wlc==2.2.0
|
||||
coloredlogs==15.0.1
|
||||
docutils>=0.21.2;python_version <= "3.11"
|
||||
docutils>=0.22.4; python_version > "3.11"
|
||||
parameterized==0.9.0
|
||||
granian[reload]==2.8.2
|
||||
basedpyright==1.39.10
|
||||
basedpyright==1.40.0
|
||||
types-lxml==2026.2.16
|
||||
|
||||
@@ -3,15 +3,15 @@ babel==2.18.0
|
||||
flask-babel==4.0.0
|
||||
flask==3.1.3
|
||||
jinja2==3.1.6
|
||||
lxml==6.1.2
|
||||
lxml==6.1.3
|
||||
pygments==2.21.0
|
||||
python-dateutil==2.9.0.post0
|
||||
pyyaml==6.0.3
|
||||
curl_cffi==0.16.1
|
||||
curl_cffi==0.16.3
|
||||
valkey==6.1.1
|
||||
markdown-it-py==4.2.0
|
||||
msgspec==0.21.1
|
||||
typer==0.27.1
|
||||
typer==0.27.2
|
||||
isodate==0.7.2
|
||||
whitenoise==6.12.0
|
||||
typing-extensions==4.16.0
|
||||
|
||||
@@ -72,43 +72,21 @@ def get_locale_params(engine_region: str | None) -> dict[str, str] | None:
|
||||
return {"mkt": engine_region}
|
||||
|
||||
|
||||
def override_accept_language(params: "OnlineParams", engine_region: str | None) -> None:
|
||||
"""Override the ``Accept-Language`` header.
|
||||
|
||||
The default header built by :py:class:`~searx.search.processors.online.OnlineProcessor`
|
||||
appends ``en;q=0.3`` as a fallback language::
|
||||
|
||||
Accept-Language: de,de-DE;q=0.7,en;q=0.3
|
||||
|
||||
Bing seems to better select the results locale based on the
|
||||
``Accept-Language`` value header.
|
||||
|
||||
This function is shared with :py:mod:`searx.engines.bing_images`,
|
||||
:py:mod:`searx.engines.bing_news`, and :py:mod:`searx.engines.bing_videos`.
|
||||
"""
|
||||
|
||||
if not engine_region or engine_region == "clear":
|
||||
return
|
||||
|
||||
lang = engine_region.split("-")[0]
|
||||
params["headers"]["Accept-Language"] = f"{engine_region},{lang};q=0.9"
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
"""Assemble a Bing-Web request."""
|
||||
|
||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||
|
||||
override_accept_language(params, engine_region)
|
||||
|
||||
query_params: dict[str, str | int] = {
|
||||
"q": query,
|
||||
"adlt": _safesearch_map.get(params.get("safesearch", 0), "off"),
|
||||
}
|
||||
|
||||
locale_params = get_locale_params(engine_region)
|
||||
if locale_params:
|
||||
query_params.update(locale_params)
|
||||
if engine_region and engine_region != "clear":
|
||||
lang, _, cc = engine_region.partition("-")
|
||||
query_params["setlang"] = lang
|
||||
if cc and cc not in ("us", "cn", "ru"): # bing just sends junk for these
|
||||
query_params["cc"] = cc
|
||||
|
||||
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from lxml import html
|
||||
from searx.engines.bing import ( # pylint: disable=unused-import
|
||||
fetch_traits,
|
||||
get_locale_params,
|
||||
override_accept_language,
|
||||
)
|
||||
|
||||
# about
|
||||
@@ -44,8 +43,6 @@ def request(query, params):
|
||||
|
||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||
|
||||
override_accept_language(params, engine_region)
|
||||
|
||||
# build URL query
|
||||
# - example: https://www.bing.com/images/async?q=foo&async=1&first=1&count=35
|
||||
query_params = {
|
||||
|
||||
@@ -12,10 +12,7 @@ from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.engines.bing import (
|
||||
get_locale_params,
|
||||
override_accept_language,
|
||||
)
|
||||
from searx.engines.bing import get_locale_params
|
||||
from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text
|
||||
|
||||
# about
|
||||
@@ -54,8 +51,6 @@ def request(query, params):
|
||||
|
||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||
|
||||
override_accept_language(params, engine_region)
|
||||
|
||||
# build URL query
|
||||
# - example: https://www.bing.com/news/infinitescrollajax?q=london&first=1
|
||||
page = int(params.get("pageno", 1)) - 1
|
||||
|
||||
@@ -9,7 +9,6 @@ from lxml import html
|
||||
from searx.engines.bing import ( # pylint: disable=unused-import
|
||||
fetch_traits,
|
||||
get_locale_params,
|
||||
override_accept_language,
|
||||
)
|
||||
from searx.engines.bing_images import time_map
|
||||
from searx.utils import eval_xpath, eval_xpath_getindex
|
||||
@@ -39,8 +38,6 @@ def request(query, params):
|
||||
|
||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||
|
||||
override_accept_language(params, engine_region)
|
||||
|
||||
# build URL query
|
||||
# - example: https://www.bing.com/videos/asyncv2?q=foo&async=content&first=1&count=35
|
||||
query_params = {
|
||||
|
||||
@@ -248,13 +248,13 @@ def extract_json_data(text: str) -> dict[str, t.Any]:
|
||||
# node_ids: [0, 19],
|
||||
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
text = text[text.index("<script") : text.index("</script")]
|
||||
if not text:
|
||||
raise ValueError("can't find JS/JSON data in the given text")
|
||||
# form: null,
|
||||
# error: null
|
||||
# });
|
||||
start = text.index("data: [{")
|
||||
end = text.rindex("}}]")
|
||||
js_obj_str = text[start:end]
|
||||
js_obj_str = "{" + js_obj_str + "}}]}"
|
||||
newline = text.index("\n", start)
|
||||
end = text.rindex("}}]", start, newline)
|
||||
js_obj_str = "{" + text[start:end] + "}}]}"
|
||||
# js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for
|
||||
# js_obj_str = js_obj_str.replace(r"\u003C", "<").replace(r"\u003c", "<") # fix broken HTML tags in strings
|
||||
json_str = js_obj_str_to_json_str(js_obj_str)
|
||||
@@ -354,14 +354,14 @@ def _parse_news(resp: SXNG_Response) -> EngineResults:
|
||||
res = EngineResults()
|
||||
dom = html.fromstring(resp.text)
|
||||
|
||||
for result in eval_xpath_list(dom, "//div[contains(@class, 'results')]//div[@data-type='news']"):
|
||||
url = eval_xpath_getindex(result, ".//a[contains(@class, 'result-header')]/@href", 0, default=None)
|
||||
for result in eval_xpath_list(dom, "//div[@data-type='news']"):
|
||||
url = eval_xpath_getindex(result, ".//a/@href", 0, default=None)
|
||||
if url is None:
|
||||
continue
|
||||
|
||||
title = eval_xpath_list(result, ".//span[contains(@class, 'snippet-title')]")
|
||||
content = eval_xpath_list(result, ".//p[contains(@class, 'desc')]")
|
||||
thumbnail = eval_xpath_getindex(result, ".//div[contains(@class, 'image-wrapper')]//img/@src", 0, default="")
|
||||
title = eval_xpath_list(result, ".//div[contains(@class, 'title')]")
|
||||
content = eval_xpath_list(result, ".//div[contains(@class, 'description')]")
|
||||
thumbnail = eval_xpath_getindex(result, ".//a[contains(@class, 'thumbnail')]//img/@src", 0, default="")
|
||||
|
||||
item = res.types.LegacyResult(
|
||||
template="default.html",
|
||||
|
||||
@@ -91,6 +91,7 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
params["url"] = f"{base_url}?{urlencode(search_args)}"
|
||||
params["headers"]["X-Subscription-Token"] = api_key
|
||||
params["headers"]["Accept"] = "application/json"
|
||||
|
||||
|
||||
def _extract_published_date(published_date_raw: str):
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=invalid-name
|
||||
"""Cara_ is a social media and portfolio-sharing platform for artists and art
|
||||
enthusiasts.
|
||||
|
||||
With the widespread use of generative AI, Cara_ decided to build a place that
|
||||
filters out gen AI images so that people searching for authentic creatives and
|
||||
images can do so easily.
|
||||
|
||||
.. _Cara: https://cara.app/about
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import typing as t
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
|
||||
about = {
|
||||
"website": "https://cara.app",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
base_url = "https://cara.app"
|
||||
images_url = "https://images.cara.app"
|
||||
|
||||
categories = ["images"]
|
||||
paging = True
|
||||
results_per_page = 24
|
||||
|
||||
# if using HTTP2, we get blocked immediately
|
||||
enable_http2 = False
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
args = {
|
||||
"q": query,
|
||||
"sortBy": "Top",
|
||||
"take": results_per_page,
|
||||
"skip": (params["pageno"] - 1) * results_per_page,
|
||||
}
|
||||
params["url"] = f"{base_url}/api/search/portfolio-posts?{urlencode(args)}"
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response"):
|
||||
res = EngineResults()
|
||||
json_data: list[dict[str, t.Any]] = resp.json()
|
||||
|
||||
for result in json_data:
|
||||
thumbnail, img = None, None
|
||||
|
||||
i: dict[str, str]
|
||||
for i in result["images"]:
|
||||
if thumbnail is None or i["isCoverImg"]:
|
||||
thumbnail = i
|
||||
|
||||
if img is None or not i["isCoverImg"]:
|
||||
img = i
|
||||
|
||||
if not thumbnail or not img:
|
||||
continue
|
||||
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
{
|
||||
"template": "images.html",
|
||||
"url": f"{base_url}/post/{result['id']}",
|
||||
"thumbnail_src": f"{images_url}/{thumbnail['src']}?height=256",
|
||||
"img_src": f"{images_url}/{img['src']}",
|
||||
"title": result["title"],
|
||||
"content": result["content"],
|
||||
"author": result["name"],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -84,6 +84,7 @@ def request(query: str, params: "OnlineParams"):
|
||||
|
||||
params["url"] = f"{base_url}/api/v1/_search"
|
||||
params["method"] = "POST"
|
||||
params["impersonate"] = "none"
|
||||
|
||||
json_data = {
|
||||
"query": query,
|
||||
|
||||
@@ -141,12 +141,13 @@ def response(resp: "SXNG_Response") -> EngineResults:
|
||||
if name:
|
||||
authors.add(name)
|
||||
|
||||
tag = result.get("fieldOfStudy")
|
||||
res.add(
|
||||
res.types.Paper(
|
||||
title=result.get("title"),
|
||||
url=url,
|
||||
content=result.get("fullText", "") or "",
|
||||
tags=result.get("fieldOfStudy", []),
|
||||
tags=[tag] if tag else [],
|
||||
publishedDate=published_date,
|
||||
type=result.get("documentType", "") or "",
|
||||
authors=authors,
|
||||
|
||||
@@ -8,6 +8,9 @@ import typing as t
|
||||
from datetime import datetime, timezone
|
||||
import html
|
||||
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.network import post
|
||||
from searx.utils import format_duration, html_to_text, humanize_number
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
@@ -35,15 +38,36 @@ dogpile_categ = "search"
|
||||
base_url = "https://www.dogpile.com"
|
||||
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Cache for the API token from dogpile"""
|
||||
|
||||
|
||||
def setup(_: dict[str, t.Any]) -> bool | None:
|
||||
if dogpile_categ not in ("search", "images", "videos", "news"):
|
||||
raise ValueError("invalid search type: %s" % dogpile_categ)
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache("dogpile") # one token for images/videos/news
|
||||
return True
|
||||
|
||||
|
||||
def _obtain_token() -> str:
|
||||
token = CACHE.get("token")
|
||||
if token:
|
||||
return token
|
||||
resp = post(f"{base_url}/api/token/refresh", headers={"Origin": base_url}, cookies={"dp_api_token": "1"})
|
||||
if not resp.ok:
|
||||
raise SearxEngineAPIException("failed to obtain dogpile token")
|
||||
token = resp.json()["token"]
|
||||
CACHE.set("token", token, expire=240) # 300s ttl
|
||||
return token
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
params["url"] = f"{base_url}/api/{dogpile_categ}"
|
||||
params["headers"]["Origin"] = base_url
|
||||
params["cookies"]["dp_api_token"] = "1"
|
||||
if dogpile_categ != "search": # web doesnt need token
|
||||
params["headers"]["x-dogpile-token"] = _obtain_token()
|
||||
|
||||
params["method"] = "POST"
|
||||
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
|
||||
|
||||
@@ -14,11 +14,12 @@ can't build it ourselves and must scrape it from the HTML pages.
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
import re
|
||||
|
||||
from urllib.parse import quote_plus
|
||||
from urllib.parse import quote_plus, urljoin
|
||||
from lxml import html
|
||||
|
||||
from searx.utils import html_to_text, gen_useragent, extract_text, eval_xpath
|
||||
from searx.utils import html_to_text, extract_text, eval_xpath
|
||||
from searx.result_types import EngineResults
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.network import get
|
||||
@@ -38,7 +39,6 @@ about = {
|
||||
# engine dependent config
|
||||
categories = ["general"]
|
||||
paging = True
|
||||
_HTTP_User_Agent: str = gen_useragent()
|
||||
|
||||
base_url = "https://duckduckgo.com"
|
||||
|
||||
@@ -73,6 +73,8 @@ def _fetch_first_page_link(
|
||||
resp = get(
|
||||
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
|
||||
headers=headers,
|
||||
impersonate="firefox",
|
||||
default_headers=False,
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
@@ -96,6 +98,43 @@ def _cache_key(query: str, pageno: int) -> str:
|
||||
return f"nextpage_url|{query}|{pageno}"
|
||||
|
||||
|
||||
def _solve_jsa(resp: "SXNG_Response") -> "SXNG_Response":
|
||||
"""Duckduckgo sometimes issues a challenge instead of json."""
|
||||
|
||||
# length that a real browser would report for where the broken snippet is
|
||||
html_len = {
|
||||
"<p><div></p><p></div": 32,
|
||||
"<li><div></li><li></div": 29,
|
||||
"<div><div></div><div></div": 33,
|
||||
"<br><div></br><br></div": 23,
|
||||
}
|
||||
|
||||
js = resp.text or ""
|
||||
jsa_match = re.search(r"let jsa = (\d+);.*?DDG\.deep\.initialize\('([^']+)'", js, re.S)
|
||||
if not jsa_match:
|
||||
return resp
|
||||
|
||||
js_functions = dict(re.findall(r"let (\w+) = function\(num\) \{([^}]*)\};", js))
|
||||
jsa = int(jsa_match.group(1))
|
||||
try:
|
||||
for name in re.findall(r"jsa = (\w+)\(jsa\);", js):
|
||||
body = js_functions[name]
|
||||
mul = re.search(r"num \* (\d+)", body)
|
||||
jsa = jsa * int(mul.group(1)) if mul else jsa + html_len[re.search(r"`([^`]+)`", body).group(1)]
|
||||
except (KeyError, AttributeError):
|
||||
return resp
|
||||
|
||||
params = resp.search_params
|
||||
follow = get(
|
||||
urljoin("https://links.duckduckgo.com", jsa_match.group(2) + str(jsa)),
|
||||
headers=params["headers"],
|
||||
impersonate="firefox",
|
||||
default_headers=False,
|
||||
)
|
||||
follow.search_params = params
|
||||
return follow
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
if len(query) >= 500:
|
||||
@@ -103,25 +142,15 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
params["url"] = None
|
||||
return
|
||||
|
||||
headers = params["headers"]
|
||||
|
||||
# The vqd value is generated from the query and the UA header. To be able
|
||||
# to reuse the vqd value, the UA header must be static.
|
||||
headers["User-Agent"] = _HTTP_User_Agent
|
||||
headers["Accept"] = "*/*"
|
||||
headers["Referer"] = f"{base_url}/"
|
||||
headers["Host"] = "duckduckgo.com"
|
||||
|
||||
# Sec-Fetch headers are required to not get blocked when sending a Firefox user agent
|
||||
headers["Sec-Fetch-Dest"] = "script"
|
||||
headers["Sec-Fetch-Mode"] = "no-cors"
|
||||
headers["Sec-Fetch-Site"] = "same-site"
|
||||
# firefox TLS only
|
||||
params["impersonate"] = "firefox"
|
||||
params["default_headers"] = False
|
||||
|
||||
api_url = ""
|
||||
if params["pageno"] > 1:
|
||||
api_url = CACHE.get(_cache_key(query, params["pageno"]))
|
||||
else:
|
||||
api_url = _fetch_first_page_link(query, headers)
|
||||
api_url = _fetch_first_page_link(query, params["headers"])
|
||||
|
||||
if not api_url:
|
||||
params["url"] = None
|
||||
@@ -129,14 +158,27 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
|
||||
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
|
||||
|
||||
# loads as a script
|
||||
headers = params["headers"]
|
||||
headers["Accept"] = "*/*"
|
||||
headers["Sec-Fetch-Dest"] = "script"
|
||||
headers["Sec-Fetch-Mode"] = "no-cors"
|
||||
headers["Sec-Fetch-Site"] = "same-site"
|
||||
headers["Referer"] = f"{base_url}/"
|
||||
|
||||
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response"):
|
||||
res = EngineResults()
|
||||
res_json = resp.json()
|
||||
|
||||
for result in res_json["results"]:
|
||||
# check if ddg returns a challenge
|
||||
# e.g. 'site:github.com searxng'
|
||||
if "let jsa =" in (resp.text or ""):
|
||||
resp = _solve_jsa(resp)
|
||||
|
||||
results = resp.json()["results"]
|
||||
for result in results:
|
||||
if "u" not in result:
|
||||
continue
|
||||
|
||||
@@ -144,13 +186,13 @@ def response(resp: "SXNG_Response"):
|
||||
res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"]))
|
||||
)
|
||||
|
||||
# link to next page
|
||||
next_page_path = res_json["results"][-1].get("n")
|
||||
if next_page_path:
|
||||
CACHE.set(
|
||||
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
|
||||
base_url + next_page_path,
|
||||
expire=60 * 60,
|
||||
)
|
||||
if results:
|
||||
next_page_path = results[-1].get("n")
|
||||
if next_page_path:
|
||||
CACHE.set(
|
||||
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
|
||||
base_url + next_page_path,
|
||||
expire=60 * 60,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
150
searx/engines/europepmc.py
Normal file
150
searx/engines/europepmc.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""`Europe PMC`_ provides comprehensive access to life sciences literature from
|
||||
trusted sources. With Europe PMC you can search and read millions of
|
||||
publications, preprints and other documents enriched with links to supporting
|
||||
data, reviews, protocols, and other relevant resources.
|
||||
|
||||
.. _Europe PMC: https://europepmc.org/
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: europepmc
|
||||
engine: europepmc
|
||||
shortcut: epmc
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from dateutil.parser import isoparse
|
||||
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import html_to_text
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.search.processors import OnlineParams
|
||||
|
||||
|
||||
about = {
|
||||
"website": "https://europepmc.org/",
|
||||
"wikidata_id": "Q5412157",
|
||||
"official_api_documentation": "https://europepmc.org/RestfulWebService",
|
||||
"use_official_api": True,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
categories = ["science", "scientific publications"]
|
||||
paging = True
|
||||
|
||||
# engine dependent config
|
||||
search_url = "https://www.ebi.ac.uk/europepmc/webservices/rest/search"
|
||||
article_url = "https://europepmc.org/article/"
|
||||
|
||||
page_size = 20
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Cache for storing the pagination cursor."""
|
||||
|
||||
|
||||
def setup(engine_settings: dict[str, t.Any]):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"])
|
||||
|
||||
|
||||
def _cache_key(query: str, page: int) -> str:
|
||||
return f"{query}|{page}"
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
args = {
|
||||
"query": query,
|
||||
"format": "json",
|
||||
"resultType": "core",
|
||||
"pageSize": page_size,
|
||||
}
|
||||
|
||||
if params["pageno"] > 1:
|
||||
if cursor := CACHE.get(_cache_key(query, params["pageno"])):
|
||||
args["cursorMark"] = cursor
|
||||
else:
|
||||
# no cached cursor for that page
|
||||
params["url"] = None
|
||||
return
|
||||
|
||||
params["url"] = f"{search_url}?{urlencode(args)}"
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
res = EngineResults()
|
||||
|
||||
json_resp = resp.json()
|
||||
|
||||
# store pagination cursor for loading next pages in cache
|
||||
if next_cursor := json_resp.get("nextCursorMark"):
|
||||
next_page = resp.search_params["pageno"] + 1
|
||||
query = resp.search_params["query"]
|
||||
CACHE.set(_cache_key(query, next_page), next_cursor)
|
||||
|
||||
all_results = json_resp.get("resultList", {}).get("result", [])
|
||||
|
||||
for item in all_results:
|
||||
source = item.get("source", "")
|
||||
identifier = item.get("id", "")
|
||||
url = f"{article_url}{source}/{identifier}" if source and identifier else ""
|
||||
|
||||
journal_info: dict[str, t.Any] = item.get("journalInfo", {})
|
||||
journal: dict[str, t.Any] = journal_info.get("journal", {})
|
||||
|
||||
res.add(
|
||||
res.types.Paper(
|
||||
url=url,
|
||||
title=html_to_text(item.get("title", "")),
|
||||
content=html_to_text(item.get("abstractText", "")),
|
||||
journal=journal.get("title", ""),
|
||||
issn=[journal.get("issn", "")],
|
||||
authors=_get_authors(item),
|
||||
doi=item.get("doi", ""),
|
||||
publishedDate=_get_published_date(item),
|
||||
type=", ".join((item.get("pubTypeList", {})).get("pubType", [])),
|
||||
pdf_url=_get_pdf_url(item),
|
||||
html_url=url,
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _get_authors(item: dict[str, t.Any]) -> list:
|
||||
"""Extract the list of authors from the item."""
|
||||
if authors := item.get("authorString", None):
|
||||
authors = [author.strip().rstrip(".") for author in authors.split(",") if author.strip()]
|
||||
else:
|
||||
authors = []
|
||||
return authors
|
||||
|
||||
|
||||
def _get_pdf_url(item: dict[str, t.Any]) -> str:
|
||||
"""Extract the PDF URL in case it is open access."""
|
||||
for url_info in (item.get("fullTextUrlList", {})).get("fullTextUrl", []):
|
||||
if url_info.get("documentStyle") == "pdf" and url_info.get("availabilityCode") == "OA":
|
||||
return url_info.get("url", "")
|
||||
return ""
|
||||
|
||||
|
||||
def _get_published_date(item: dict[str, t.Any]) -> datetime | None:
|
||||
"""Extract the published date from the item and convert it to a datetime object."""
|
||||
if unformatted_date := item.get("firstPublicationDate"):
|
||||
return isoparse(unformatted_date)
|
||||
return None
|
||||
@@ -37,17 +37,13 @@ about = {
|
||||
}
|
||||
|
||||
categories = []
|
||||
safeseach = True
|
||||
safesearch = True
|
||||
|
||||
base_url = "https://luxxle.com"
|
||||
|
||||
luxxle_categ = "search"
|
||||
"""Supported categories: "search", "news", "images", "videos"."""
|
||||
|
||||
# otherwise all requests get blocked (http2-fingerprinted probably)
|
||||
enable_http2 = False
|
||||
|
||||
|
||||
safe_search_map = {0: "Off", 1: "Moderate", 2: "Strict"}
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
|
||||
from json import loads
|
||||
import typing as t
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from lxml import html
|
||||
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.network import get
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import eval_xpath, extract_text
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
@@ -25,18 +29,33 @@ about = {
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
paging = False
|
||||
enable_http3 = True
|
||||
|
||||
base_url = "https://neosearch.org"
|
||||
categories = ["general"]
|
||||
|
||||
paging = False
|
||||
|
||||
def _obtain_xsrf_token() -> str:
|
||||
resp = get(base_url)
|
||||
doc = html.fromstring(resp.text)
|
||||
|
||||
xsrf_token = extract_text(eval_xpath(doc, "//meta[@name='xsrf-token']/@content"))
|
||||
if not xsrf_token:
|
||||
raise SearxEngineAPIException("failed to obtain xsrf token")
|
||||
return xsrf_token
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
params["url"] = f"{base_url}/search"
|
||||
params["headers"]["X-XSRF-TOKEN"] = _obtain_xsrf_token()
|
||||
params["method"] = "POST"
|
||||
|
||||
args = {"q": query, "generate": "auto"}
|
||||
countrycode = params["searxng_locale"].split("-")[-1].upper()
|
||||
if countrycode in traits.custom["countrycodes"]:
|
||||
args["loc"] = countrycode
|
||||
params["url"] = f"{base_url}/search?{urlencode(args)}"
|
||||
params["json"] = args
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
@@ -67,7 +86,6 @@ def response(resp: "SXNG_Response") -> EngineResults:
|
||||
|
||||
def fetch_traits(engine_traits: "EngineTraits") -> None:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from searx.network import get
|
||||
from searx.utils import extr, js_obj_str_to_python
|
||||
from babel.core import get_global
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import eval_xpath_list, gen_useragent
|
||||
from searx.utils import eval_xpath_list
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException
|
||||
from searx.network import get
|
||||
@@ -44,8 +44,6 @@ SECRET_KEY_DB_KEY = "secret-key"
|
||||
CACHE: EngineCache
|
||||
"""Cache to store the secret API key for the engine."""
|
||||
|
||||
enable_http2 = False
|
||||
|
||||
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
global CACHE # pylint: disable=global-statement
|
||||
@@ -56,13 +54,7 @@ def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
def _get_secret_key():
|
||||
resp = get(
|
||||
base_url,
|
||||
headers={
|
||||
# circumvents Cloudflare bot protections
|
||||
"User-Agent": gen_useragent(),
|
||||
"Referer": base_url,
|
||||
"Sec-GPC": "1",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
headers={"Referer": base_url},
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
@@ -105,8 +97,6 @@ def request(query, params):
|
||||
|
||||
params["headers"]["secret-key"] = secret_key
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
res = EngineResults()
|
||||
|
||||
@@ -27,9 +27,6 @@ time_range_support = True
|
||||
safesearch_map = {0: 'off', 1: '1', 2: '1'}
|
||||
time_range_map = {'day': '1d', 'week': '1w', 'month': '1m', 'year': '1y'}
|
||||
|
||||
# using http2 returns forbidden errors
|
||||
enable_http2 = False
|
||||
|
||||
|
||||
def request(query, params):
|
||||
args = {
|
||||
@@ -50,8 +47,6 @@ def request(query, params):
|
||||
# prevent automatic redirects to first page on pagination
|
||||
params['allow_redirects'] = False
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def _image_result(result):
|
||||
return {
|
||||
|
||||
@@ -10,6 +10,7 @@ from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
|
||||
from searx import locales
|
||||
from searx.exceptions import SearxEngineResponseException
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import eval_xpath_list, eval_xpath, extract_text
|
||||
|
||||
@@ -52,6 +53,7 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
"q": query,
|
||||
"search_type": resulthunter_categ,
|
||||
"offset": params["pageno"] - 1,
|
||||
"search_source": "other",
|
||||
}
|
||||
|
||||
# uses Brave's engine traits
|
||||
@@ -111,6 +113,11 @@ def _image_results(doc: "ElementBase") -> EngineResults:
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
doc = html.fromstring(resp.text)
|
||||
|
||||
# if the request was wrong (e.g. missing params), the site doesn't contain a result container
|
||||
# and instead shows an "Installation required" page to download the resulthunter browser extension
|
||||
if not eval_xpath(doc, "//div[contains(@class, 'organic-results-container')]"):
|
||||
raise SearxEngineResponseException()
|
||||
|
||||
match resulthunter_categ:
|
||||
case "web":
|
||||
return _general_results(doc)
|
||||
|
||||
88
searx/engines/searchrockit.py
Normal file
88
searx/engines/searchrockit.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""SearchRockit is an American search engine. It allegedly has its own index,
|
||||
but the results seem to come from Google."""
|
||||
|
||||
import typing as t
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
from dateutil import parser
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import (
|
||||
eval_xpath_list,
|
||||
extract_text,
|
||||
eval_xpath,
|
||||
)
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.search.processors import OnlineParams
|
||||
from searx.extended_types import SXNG_Response
|
||||
|
||||
about = {
|
||||
"website": "https://searchrockit.com",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "HTML",
|
||||
}
|
||||
|
||||
categories = ["general"]
|
||||
paging = True
|
||||
|
||||
SearchrockitCateg = t.Literal["web", "images", "news"]
|
||||
searchrockit_categ: SearchrockitCateg = "web"
|
||||
|
||||
base_url = "https://searchrockit.com"
|
||||
|
||||
|
||||
def setup(_):
|
||||
if searchrockit_categ not in t.get_args(SearchrockitCateg):
|
||||
raise ValueError("invalid search category: %s" % searchrockit_categ)
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
args = {"q": query, "p": params["pageno"]}
|
||||
params["url"] = f"{base_url}/results/{searchrockit_categ}?{urlencode(args)}"
|
||||
|
||||
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
doc = html.fromstring(resp.text)
|
||||
res = EngineResults()
|
||||
|
||||
match searchrockit_categ:
|
||||
case "web" | "news":
|
||||
for result in eval_xpath_list(
|
||||
doc, "//div[contains(@class, 'results-list')]/div[contains(@class, 'result-item')]"
|
||||
):
|
||||
publishedDate = None
|
||||
try:
|
||||
d = extract_text(eval_xpath(result, ".//span[contains(@class, 'result-item--publishedAt')]")) or ""
|
||||
publishedDate = parser.parse(d)
|
||||
except parser.ParserError:
|
||||
pass
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
url=extract_text(eval_xpath(result, ".//a[contains(@class, 'result-item--title')]/@href")),
|
||||
title=extract_text(eval_xpath(result, ".//a[contains(@class, 'result-item--title')]")) or "",
|
||||
content=extract_text(eval_xpath(result, ".//a[contains(@class, 'result-item--desc')]")) or "",
|
||||
thumbnail=extract_text(
|
||||
eval_xpath(result, ".//a[contains(@class, 'result-item--thumb')]/img/@src")
|
||||
)
|
||||
or "",
|
||||
publishedDate=publishedDate,
|
||||
)
|
||||
)
|
||||
case "images":
|
||||
for result in eval_xpath_list(
|
||||
doc, "//div[contains(@class, 'image-grid')]/a[contains(@class, 'image-card')]"
|
||||
):
|
||||
res.add(
|
||||
res.types.Image(
|
||||
url=extract_text(eval_xpath(result, "./@href")),
|
||||
title=extract_text(eval_xpath(result, "./div[contains(@class, 'image-title')]")) or "",
|
||||
thumbnail_src=extract_text(eval_xpath(result, "./img/@src")) or "",
|
||||
img_src=extract_text(eval_xpath(result, "./@data-full-url")) or "",
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
@@ -4,13 +4,11 @@ independent search infrastructure."""
|
||||
|
||||
import typing as t
|
||||
from urllib.parse import urlencode
|
||||
import uuid
|
||||
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx.network import get
|
||||
from searx.result_types import EngineResults
|
||||
from searx.utils import extr, html_to_text
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.utils import html_to_text
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.search.processors import OnlineParams
|
||||
@@ -34,43 +32,19 @@ SearchzeeCategType = t.Literal["web", "news"]
|
||||
searchzee_categ: SearchzeeCategType = None # type: ignore[reportAssignmentType]
|
||||
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Cache for storing the scraped API Token."""
|
||||
|
||||
base_url = "https://searchzee.com"
|
||||
|
||||
# only supports for news
|
||||
time_range_map = {"day": "pd", "week": "pw", "month": "pm", "year": "py"}
|
||||
|
||||
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
def setup(_: dict[str, t.Any]):
|
||||
if searchzee_categ not in t.get_args(SearchzeeCategType):
|
||||
raise ValueError("invalid category: %s" % searchzee_categ)
|
||||
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"]) # type: ignore[reportAny]
|
||||
return True
|
||||
|
||||
|
||||
def _obtain_api_token() -> str:
|
||||
token: str | None = CACHE.get("token") # type: ignore[reportAny]
|
||||
if token:
|
||||
return token
|
||||
|
||||
token_resp = get(
|
||||
f"{base_url}/app.js",
|
||||
)
|
||||
if not token_resp.ok:
|
||||
raise SearxEngineAPIException("failed to obtain api key")
|
||||
|
||||
token = extr(token_resp.text, "const SEARCHZEE_API_TOKEN = \"", "\";")
|
||||
CACHE.set("token", token, expire=3600)
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def request(query: str, params: "OnlineParams"):
|
||||
params["headers"]["X-SearchZee-Token"] = _obtain_api_token()
|
||||
params["cookies"]["szs"] = str(uuid.uuid4())
|
||||
|
||||
args = {"q": query, "type": searchzee_categ, "offset": params["pageno"] - 1}
|
||||
if params["time_range"]:
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Searx (all)
|
||||
"""
|
||||
|
||||
from json import loads
|
||||
from searx.engines import categories as searx_categories
|
||||
|
||||
# about
|
||||
about = {
|
||||
"website": 'https://github.com/searxng/searxng',
|
||||
"wikidata_id": 'Q17639196',
|
||||
"official_api_documentation": 'https://docs.searxng.org/dev/search_api.html',
|
||||
"use_official_api": True,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
}
|
||||
|
||||
categories = searx_categories.keys()
|
||||
|
||||
# search-url
|
||||
instance_urls = []
|
||||
instance_index = 0
|
||||
|
||||
|
||||
# do search-request
|
||||
def request(query, params):
|
||||
global instance_index # pylint: disable=global-statement
|
||||
params['url'] = instance_urls[instance_index % len(instance_urls)]
|
||||
params['method'] = 'POST'
|
||||
|
||||
instance_index += 1
|
||||
|
||||
params['data'] = {
|
||||
'q': query,
|
||||
'pageno': params['pageno'],
|
||||
'language': params['language'],
|
||||
'time_range': params['time_range'],
|
||||
'category': params['category'],
|
||||
'format': 'json',
|
||||
}
|
||||
|
||||
return params
|
||||
|
||||
|
||||
# get response from search-request
|
||||
def response(resp):
|
||||
|
||||
response_json = loads(resp.text)
|
||||
results = response_json['results']
|
||||
|
||||
for i in ('answers', 'infoboxes'):
|
||||
results.extend(response_json[i])
|
||||
|
||||
results.extend({'suggestion': s} for s in response_json['suggestions'])
|
||||
|
||||
return results
|
||||
@@ -1,5 +1,8 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Startpage's language & region selectors are a mess ..
|
||||
"""Startpage requires solving an Anubis POW captcha (difficulty 4).
|
||||
Solving it requires a lot of CPU, so the engine is set inactive by default.
|
||||
|
||||
Startpage's language & region selectors are a mess ..
|
||||
|
||||
.. _startpage regions:
|
||||
|
||||
@@ -84,6 +87,7 @@ Startpage's category (for Web-search, News, Videos, ..) is set by
|
||||
"""
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import typing as t
|
||||
from collections import OrderedDict
|
||||
@@ -99,7 +103,7 @@ from searx.enginelib import EngineCache
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.exceptions import SearxEngineCaptchaException
|
||||
from searx.locales import region_tag
|
||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||
from searx.network import get, post # see https://github.com/searxng/searxng/issues/762
|
||||
from searx.utils import (
|
||||
eval_xpath,
|
||||
extr,
|
||||
@@ -176,6 +180,45 @@ def setup(_: dict[str, t.Any]) -> bool | None:
|
||||
sc_code_cache_sec = 3600
|
||||
"""Time in seconds the sc-code is cached in memory :py:obj:`get_sc_code`."""
|
||||
|
||||
# startpage's anubis difficulty is set to 4
|
||||
max_difficulty = 4
|
||||
|
||||
|
||||
def _solve_anubis(resp) -> str:
|
||||
"""Anubis POW solver"""
|
||||
payload = loads(extr(resp.text, '<script id="anubis_challenge" type="application/json">', "</script>"))
|
||||
challenge = payload["challenge"]
|
||||
difficulty = int(payload["rules"]["difficulty"])
|
||||
if difficulty > max_difficulty:
|
||||
raise SearxEngineCaptchaException(message="startpage: Anubis difficulty too high")
|
||||
prefix = "0" * difficulty
|
||||
blob = challenge["randomData"].encode()
|
||||
for nonce in range(16**difficulty * 8): # max search is 8x average search, e^-8 = 0.034% will fail
|
||||
digest = hashlib.sha256(blob + str(nonce).encode()).hexdigest()
|
||||
if digest.startswith(prefix):
|
||||
break
|
||||
else:
|
||||
raise SearxEngineCaptchaException(message="startpage: Anubis failed")
|
||||
|
||||
pass_resp = get(
|
||||
f"{base_url}/.within.website/x/cmd/anubis/api/pass-challenge",
|
||||
params={
|
||||
"id": challenge["id"],
|
||||
"response": digest,
|
||||
"nonce": nonce,
|
||||
"redir": str(resp.url),
|
||||
"elapsedTime": "1",
|
||||
},
|
||||
cookies=resp.cookies,
|
||||
allow_redirects=False,
|
||||
)
|
||||
auth = pass_resp.cookies.get("spchal-auth")
|
||||
if not auth:
|
||||
raise SearxEngineCaptchaException(message="startpage: Anubis pass-challenge failed")
|
||||
auth = str(auth)
|
||||
CACHE.set("SPCHAL_AUTH", auth, expire=240)
|
||||
return auth
|
||||
|
||||
|
||||
def get_sc_code(params):
|
||||
"""Get an actual ``sc`` argument from Startpage's search form (HTML page).
|
||||
@@ -201,6 +244,9 @@ def get_sc_code(params):
|
||||
logger.debug("get_sc_code: request headers: %s", headers)
|
||||
resp = get(get_sc_url, headers=headers)
|
||||
|
||||
if 'id="anubis_challenge"' in resp.text:
|
||||
resp = get(get_sc_url, headers=headers, cookies={"spchal-auth": _solve_anubis(resp)})
|
||||
|
||||
# ?? x = network.get('https://www.startpage.com/sp/cdn/images/filter-chevron.svg', headers=headers)
|
||||
# ?? https://www.startpage.com/sp/cdn/images/filter-chevron.svg
|
||||
# ?? ping-back URL: https://www.startpage.com/sp/pb?sc=TLsB0oITjZ8F21
|
||||
@@ -239,8 +285,8 @@ def request(query, params):
|
||||
Additionally the arguments form Startpage's search form needs to be set in
|
||||
HTML POST data / compare ``<input>`` elements: :py:obj:`search_form_xpath`.
|
||||
"""
|
||||
engine_region = traits.get_region(params["searxng_locale"], "en-US")
|
||||
engine_language = traits.get_language(params["searxng_locale"], "en")
|
||||
engine_region = traits.get_region(params["searxng_locale"], "en_US")
|
||||
engine_language = traits.get_language(params["searxng_locale"], "english")
|
||||
|
||||
params["headers"]["Origin"] = base_url
|
||||
params["headers"]["Referer"] = base_url + "/"
|
||||
@@ -262,9 +308,9 @@ def request(query, params):
|
||||
args["language"] = engine_language
|
||||
args["lui"] = engine_language
|
||||
|
||||
args["segment"] = "startpage.udog"
|
||||
if params["pageno"] > 1:
|
||||
args["page"] = params["pageno"]
|
||||
args["segment"] = "startpage.udog"
|
||||
|
||||
# Build cookie
|
||||
lang_homepage = "en"
|
||||
@@ -289,6 +335,8 @@ def request(query, params):
|
||||
cookie["search_results_region"] = engine_region
|
||||
|
||||
params["cookies"]["preferences"] = "N1N".join(["%sEEE%s" % x for x in cookie.items()])
|
||||
if auth := CACHE.get("SPCHAL_AUTH"):
|
||||
params["cookies"]["spchal-auth"] = auth
|
||||
logger.debug("cookie preferences: %s", params["cookies"]["preferences"])
|
||||
|
||||
logger.debug("data: %s", args)
|
||||
@@ -400,6 +448,18 @@ def _get_image_result(result) -> dict[str, t.Any] | None:
|
||||
|
||||
|
||||
def response(resp):
|
||||
if 'id="anubis_challenge"' in resp.text:
|
||||
params = resp.search_params
|
||||
params["cookies"]["spchal-auth"] = _solve_anubis(resp)
|
||||
resp = post(
|
||||
params["url"] or search_url,
|
||||
data=params["data"],
|
||||
headers=params["headers"],
|
||||
cookies=params["cookies"],
|
||||
)
|
||||
if 'id="anubis_challenge"' in resp.text:
|
||||
raise SearxEngineCaptchaException()
|
||||
|
||||
categ = startpage_categ.capitalize()
|
||||
results_raw = "{" + extr(resp.text, f"React.createElement(UIStartpage.AppSerp{categ}, {{", "}})") + "}}"
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ about = {
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
# otherwise all requests get blocked, probably HTTP2 fingerprinting
|
||||
enable_http2 = False
|
||||
|
||||
base_url = "https://stocksnap.io"
|
||||
cdn_url = "https://cdn.stocksnap.io"
|
||||
|
||||
@@ -13,7 +13,7 @@ from dateutil import parser
|
||||
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.network import get
|
||||
from searx.utils import gen_useragent, html_to_text
|
||||
from searx.utils import html_to_text
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
@@ -52,7 +52,7 @@ def _obtain_x_sid() -> tuple[str, str]:
|
||||
The header key is usually called `x-sid-{UUIDv4}`, and the value is
|
||||
usually a plain UUIDv4 (but a different one than in the header key).
|
||||
"""
|
||||
resp = get(f"{api_url}/revcontent/embed.js", headers={"User-Agent": gen_useragent()})
|
||||
resp = get(f"{api_url}/revcontent/embed.js", headers={"Referer": "https://tusksearch.com/"})
|
||||
if not resp.ok:
|
||||
raise SearxEngineAPIException("failed to obtain request x-sid token")
|
||||
|
||||
@@ -95,6 +95,7 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
# required - we send a random longitude and latitude instead of the actual user location
|
||||
"x-lon": str(round(random.random() * 90, 4)),
|
||||
"x-lat": str(round(random.random() * 90, 4)),
|
||||
"Referer": "https://tusksearch.com/",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ about = {
|
||||
categories = ['images', 'icons']
|
||||
|
||||
base_url = "https://uxwing.com"
|
||||
enable_http2 = False
|
||||
|
||||
|
||||
def request(query, params):
|
||||
params['url'] = f"{base_url}/?s={quote_plus(query)}"
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
|
||||
@@ -30,8 +30,6 @@ web_base_url = "https://yep.com"
|
||||
safesearch = True
|
||||
safesearch_map = {0: "off", 1: "moderate", 2: "strict"}
|
||||
|
||||
enable_http2 = False
|
||||
|
||||
results_per_page = 20
|
||||
|
||||
_IMPORT_RE = re.compile(r"import\"(.*?)\";")
|
||||
@@ -50,9 +48,6 @@ def request(query: str, params: "OnlineParams") -> None:
|
||||
{
|
||||
"Referer": f"{web_base_url}/",
|
||||
"Origin": web_base_url,
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-site",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -88,6 +88,9 @@ class HTTPParams(t.TypedDict):
|
||||
impersonate: t.NotRequired[str]
|
||||
"""curl_cffi impersonate target. Default: ``chrome``."""
|
||||
|
||||
default_headers: t.NotRequired[bool]
|
||||
"""Ask curl_cffi to skip setting the impersonated browser's default headers."""
|
||||
|
||||
curl_options: t.NotRequired[dict[int, t.Any]]
|
||||
"""Any extra libcurl options for the request."""
|
||||
|
||||
@@ -169,8 +172,8 @@ class OnlineProcessor(EngineProcessor):
|
||||
"cookies": params["cookies"],
|
||||
"auth": params["auth"],
|
||||
}
|
||||
for key in ("curl_options", "impersonate"):
|
||||
if params.get(key):
|
||||
for key in ("curl_options", "impersonate", "default_headers"):
|
||||
if params.get(key) is not None:
|
||||
request_args[key] = params[key]
|
||||
|
||||
verify = params.get("verify")
|
||||
|
||||
@@ -634,11 +634,6 @@ engines:
|
||||
shortcut: cos
|
||||
disabled: true
|
||||
|
||||
- name: cara
|
||||
engine: cara
|
||||
shortcut: ca
|
||||
disabled: true
|
||||
|
||||
# - name: chatnoir
|
||||
# engine: chatnoir
|
||||
# shortcut: cha
|
||||
@@ -797,6 +792,10 @@ engines:
|
||||
require_api_key: false
|
||||
results: JSON
|
||||
|
||||
- name: europepmc
|
||||
engine: europepmc
|
||||
shortcut: epmc
|
||||
|
||||
- name: erowid
|
||||
engine: xpath
|
||||
paging: true
|
||||
@@ -2171,6 +2170,32 @@ engines:
|
||||
about:
|
||||
website: https://searchmysite.net
|
||||
|
||||
- name: searchrockit
|
||||
engine: searchrockit
|
||||
categories: general
|
||||
searchrockit_categ: web
|
||||
shortcut: sr
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: searchrockit images
|
||||
engine: searchrockit
|
||||
categories: images
|
||||
searchrockit_categ: images
|
||||
shortcut: sri
|
||||
# took up to 60s when testing
|
||||
timeout: 10
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: searchrockit news
|
||||
engine: searchrockit
|
||||
categories: news
|
||||
searchrockit_categ: news
|
||||
shortcut: srn
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: selfhst icons
|
||||
engine: selfhst
|
||||
shortcut: si
|
||||
@@ -2275,14 +2300,6 @@ engines:
|
||||
shortcut: pwv
|
||||
disabled: true
|
||||
|
||||
# - name: searx
|
||||
# engine: searx_engine
|
||||
# shortcut: se
|
||||
# instance_urls :
|
||||
# - http://127.0.0.1:8888/
|
||||
# - ...
|
||||
# disabled: true
|
||||
|
||||
- name: seekninja
|
||||
engine: seekninja
|
||||
shortcut: sen
|
||||
@@ -2326,18 +2343,21 @@ engines:
|
||||
shortcut: sp
|
||||
startpage_categ: web
|
||||
categories: [general, web]
|
||||
inactive: true # uses a Proof Of Work captcha https://github.com/searxng/searxng/pull/6669
|
||||
|
||||
- name: startpage news
|
||||
engine: startpage
|
||||
startpage_categ: news
|
||||
categories: [news, web]
|
||||
shortcut: spn
|
||||
inactive: true # uses a Proof Of Work captcha https://github.com/searxng/searxng/pull/6669
|
||||
|
||||
- name: startpage images
|
||||
engine: startpage
|
||||
startpage_categ: images
|
||||
categories: [images, web]
|
||||
shortcut: spi
|
||||
inactive: true # uses a Proof Of Work captcha https://github.com/searxng/searxng/pull/6669
|
||||
|
||||
- name: steam
|
||||
engine: steam
|
||||
|
||||
11
searx/static/themes/simple/chunk/BVVCO4vX.min.js
vendored
Normal file
11
searx/static/themes/simple/chunk/BVVCO4vX.min.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";
|
||||
/*!
|
||||
* swiped-events.js - v@version@
|
||||
* Pure JavaScript swipe events
|
||||
* https://github.com/john-doherty/swiped-events
|
||||
* @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
|
||||
* @author John Doherty <www.johndoherty.info>
|
||||
* @license MIT
|
||||
*/
|
||||
(function(e,t){typeof e.CustomEvent!=`function`&&(e.CustomEvent=function(e,n){n||={bubbles:!1,cancelable:!1,detail:void 0};var r=t.createEvent(`CustomEvent`);return r.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),r},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener(`touchstart`,u,!1),t.addEventListener(`touchmove`,d,!1),t.addEventListener(`touchend`,l,!1);var n=null,r=null,i=null,a=null,o=null,s=null,c=0;function l(e){if(s===e.target){var l=parseInt(f(s,`data-swipe-threshold`,`20`),10),u=f(s,`data-swipe-unit`,`px`),d=parseInt(f(s,`data-swipe-timeout`,`500`),10),p=Date.now()-o,m=``,h=e.changedTouches||e.touches||[];if(u===`vh`&&(l=Math.round(l/100*t.documentElement.clientHeight)),u===`vw`&&(l=Math.round(l/100*t.documentElement.clientWidth)),Math.abs(i)>Math.abs(a)?Math.abs(i)>l&&p<d&&(m=i>0?`swiped-left`:`swiped-right`):Math.abs(a)>l&&p<d&&(m=a>0?`swiped-up`:`swiped-down`),m!==``){var g={dir:m.replace(/swiped-/,``),touchType:(h[0]||{}).touchType||`direct`,fingers:c,xStart:parseInt(n,10),xEnd:parseInt((h[0]||{}).clientX||-1,10),yStart:parseInt(r,10),yEnd:parseInt((h[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent(`swiped`,{bubbles:!0,cancelable:!0,detail:g})),s.dispatchEvent(new CustomEvent(m,{bubbles:!0,cancelable:!0,detail:g}))}n=null,r=null,o=null}}function u(e){e.target.getAttribute(`data-swipe-ignore`)!==`true`&&(s=e.target,o=Date.now(),n=e.touches[0].clientX,r=e.touches[0].clientY,i=0,a=0,c=e.touches.length)}function d(e){if(n&&r){var t=e.touches[0].clientX,o=e.touches[0].clientY;i=n-t,a=r-o}}function f(e,n,r){for(;e&&e!==t.documentElement;){var i=e.getAttribute(n);if(i)return i;e=e.parentNode}return r}})(window,document);var i,a=t=>{i&&clearTimeout(i);let n=t.querySelector(`.result-images-source img`);if(!n)return;let r=t.querySelector(`.image_thumbnail`);if(r){if(r.src===`${e.theme_static_path}/img/img_load_error.svg`)return;n.onerror=()=>{n.src=r.src},n.src=r.src}let a=n.getAttribute(`data-src`);a&&(i=setTimeout(()=>{n.src=a,n.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail, img.thumbnail`);for(let t of o)t.complete&&t.naturalWidth===0&&(t.src=`${e.theme_static_path}/img/img_load_error.svg`),t.onerror=()=>{t.src=`${e.theme_static_path}/img/img_load_error.svg`};document.querySelector(`#search_url button#copy_url`)?.style.setProperty(`display`,`block`),n.selectImage=e=>{document.getElementById(`results`)?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,n.scrollPageToSelected?.(),e&&a(e)},n.closeDetail=()=>{document.getElementById(`results`)?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),n.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let e=this.getAttribute(`data-btn-text-collapsed`),t=this.getAttribute(`data-btn-text-not-collapsed`),n=this.getAttribute(`data-target`);if(!(n&&e&&t))return;let i=document.querySelector(n);r(i);let a=this.classList.contains(`collapsed`),o=a?t:e,s=a?e:t;this.innerHTML=this.innerHTML.replace(s,o),this.classList.toggle(`collapsed`),i.classList.toggle(`invisible`)}),t(`click`,`.media-loader`,function(){let e=this.getAttribute(`data-target`);if(!e)return;let t=document.querySelector(`${e} > iframe`);if(r(t),!t.getAttribute(`src`)){let e=t.getAttribute(`data-src`);e&&t.setAttribute(`src`,e)}}),t(`click`,`#copy_url`,async function(){let e=this.parentElement?.querySelector(`pre`);if(r(e),window.isSecureContext)await navigator.clipboard.writeText(e.innerText);else{let t=window.getSelection();if(t){let n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),document.execCommand(`copy`)}}this.dataset.copiedText&&(this.innerText=this.dataset.copiedText)}),t(`click`,`.result-detail-close`,e=>{e.preventDefault(),n.closeDetail?.()}),t(`click`,`.result-detail-previous`,e=>{e.preventDefault(),n.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,e=>{e.preventDefault(),n.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&n.closeDetail?.()});var s=document.querySelectorAll(`.swipe-horizontal`);for(let e of s)t(`swiped-left`,e,()=>{n.selectNext?.(!1)}),t(`swiped-right`,e,()=>{n.selectPrevious?.(!1)});window.addEventListener(`scroll`,()=>{let e=document.getElementById(`backToTop`),t=document.getElementById(`results`);if(e&&t){let e=(document.documentElement.scrollTop||document.body.scrollTop)>=100;t.classList.toggle(`scrolling`,e)}},!0);
|
||||
//# sourceMappingURL=BVVCO4vX.min.js.map
|
||||
File diff suppressed because one or more lines are too long
15
searx/static/themes/simple/chunk/BhYx0_Gb.min.js
vendored
15
searx/static/themes/simple/chunk/BhYx0_Gb.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
15
searx/static/themes/simple/chunk/CdVmyKrK.min.js
vendored
Normal file
15
searx/static/themes/simple/chunk/CdVmyKrK.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11
searx/static/themes/simple/chunk/od7pNHfk.min.js
vendored
11
searx/static/themes/simple/chunk/od7pNHfk.min.js
vendored
@@ -1,11 +0,0 @@
|
||||
import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";
|
||||
/*!
|
||||
* swiped-events.js - v@version@
|
||||
* Pure JavaScript swipe events
|
||||
* https://github.com/john-doherty/swiped-events
|
||||
* @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
|
||||
* @author John Doherty <www.johndoherty.info>
|
||||
* @license MIT
|
||||
*/
|
||||
(function(e,t){typeof e.CustomEvent!=`function`&&(e.CustomEvent=function(e,n){n||={bubbles:!1,cancelable:!1,detail:void 0};var r=t.createEvent(`CustomEvent`);return r.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),r},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener(`touchstart`,u,!1),t.addEventListener(`touchmove`,d,!1),t.addEventListener(`touchend`,l,!1);var n=null,r=null,i=null,a=null,o=null,s=null,c=0;function l(e){if(s===e.target){var l=parseInt(f(s,`data-swipe-threshold`,`20`),10),u=f(s,`data-swipe-unit`,`px`),d=parseInt(f(s,`data-swipe-timeout`,`500`),10),p=Date.now()-o,m=``,h=e.changedTouches||e.touches||[];if(u===`vh`&&(l=Math.round(l/100*t.documentElement.clientHeight)),u===`vw`&&(l=Math.round(l/100*t.documentElement.clientWidth)),Math.abs(i)>Math.abs(a)?Math.abs(i)>l&&p<d&&(m=i>0?`swiped-left`:`swiped-right`):Math.abs(a)>l&&p<d&&(m=a>0?`swiped-up`:`swiped-down`),m!==``){var g={dir:m.replace(/swiped-/,``),touchType:(h[0]||{}).touchType||`direct`,fingers:c,xStart:parseInt(n,10),xEnd:parseInt((h[0]||{}).clientX||-1,10),yStart:parseInt(r,10),yEnd:parseInt((h[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent(`swiped`,{bubbles:!0,cancelable:!0,detail:g})),s.dispatchEvent(new CustomEvent(m,{bubbles:!0,cancelable:!0,detail:g}))}n=null,r=null,o=null}}function u(e){e.target.getAttribute(`data-swipe-ignore`)!==`true`&&(s=e.target,o=Date.now(),n=e.touches[0].clientX,r=e.touches[0].clientY,i=0,a=0,c=e.touches.length)}function d(e){if(!(!n||!r)){var t=e.touches[0].clientX,o=e.touches[0].clientY;i=n-t,a=r-o}}function f(e,n,r){for(;e&&e!==t.documentElement;){var i=e.getAttribute(n);if(i)return i;e=e.parentNode}return r}})(window,document);var i,a=t=>{i&&clearTimeout(i);let n=t.querySelector(`.result-images-source img`);if(!n)return;let r=t.querySelector(`.image_thumbnail`);if(r){if(r.src===`${e.theme_static_path}/img/img_load_error.svg`)return;n.onerror=()=>{n.src=r.src},n.src=r.src}let a=n.getAttribute(`data-src`);a&&(i=setTimeout(()=>{n.src=a,n.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail, img.thumbnail`);for(let t of o)t.complete&&t.naturalWidth===0&&(t.src=`${e.theme_static_path}/img/img_load_error.svg`),t.onerror=()=>{t.src=`${e.theme_static_path}/img/img_load_error.svg`};document.querySelector(`#search_url button#copy_url`)?.style.setProperty(`display`,`block`),n.selectImage=e=>{document.getElementById(`results`)?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,n.scrollPageToSelected?.(),e&&a(e)},n.closeDetail=()=>{document.getElementById(`results`)?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),n.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let e=this.getAttribute(`data-btn-text-collapsed`),t=this.getAttribute(`data-btn-text-not-collapsed`),n=this.getAttribute(`data-target`);if(!(n&&e&&t))return;let i=document.querySelector(n);r(i);let a=this.classList.contains(`collapsed`),o=a?t:e,s=a?e:t;this.innerHTML=this.innerHTML.replace(s,o),this.classList.toggle(`collapsed`),i.classList.toggle(`invisible`)}),t(`click`,`.media-loader`,function(){let e=this.getAttribute(`data-target`);if(!e)return;let t=document.querySelector(`${e} > iframe`);if(r(t),!t.getAttribute(`src`)){let e=t.getAttribute(`data-src`);e&&t.setAttribute(`src`,e)}}),t(`click`,`#copy_url`,async function(){let e=this.parentElement?.querySelector(`pre`);if(r(e),window.isSecureContext)await navigator.clipboard.writeText(e.innerText);else{let t=window.getSelection();if(t){let n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),document.execCommand(`copy`)}}this.dataset.copiedText&&(this.innerText=this.dataset.copiedText)}),t(`click`,`.result-detail-close`,e=>{e.preventDefault(),n.closeDetail?.()}),t(`click`,`.result-detail-previous`,e=>{e.preventDefault(),n.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,e=>{e.preventDefault(),n.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&n.closeDetail?.()});var s=document.querySelectorAll(`.swipe-horizontal`);for(let e of s)t(`swiped-left`,e,()=>{n.selectNext?.(!1)}),t(`swiped-right`,e,()=>{n.selectPrevious?.(!1)});window.addEventListener(`scroll`,()=>{let e=document.getElementById(`backToTop`),t=document.getElementById(`results`);if(e&&t){let e=(document.documentElement.scrollTop||document.body.scrollTop)>=100;t.classList.toggle(`scrolling`,e)}},!0);
|
||||
//# sourceMappingURL=od7pNHfk.min.js.map
|
||||
@@ -61,7 +61,7 @@
|
||||
]
|
||||
},
|
||||
"src/js/main/results.ts": {
|
||||
"file": "chunk/od7pNHfk.min.js",
|
||||
"file": "chunk/BVVCO4vX.min.js",
|
||||
"name": "results",
|
||||
"src": "src/js/main/results.ts",
|
||||
"isDynamicEntry": true,
|
||||
@@ -81,7 +81,7 @@
|
||||
]
|
||||
},
|
||||
"src/js/plugin/Calculator.ts": {
|
||||
"file": "chunk/BhYx0_Gb.min.js",
|
||||
"file": "chunk/CdVmyKrK.min.js",
|
||||
"name": "calculator",
|
||||
"src": "src/js/plugin/Calculator.ts",
|
||||
"isDynamicEntry": true,
|
||||
@@ -103,7 +103,7 @@
|
||||
]
|
||||
},
|
||||
"src/js/plugin/MapView.ts": {
|
||||
"file": "chunk/Dms-nhjX.min.js",
|
||||
"file": "chunk/CFmKEewG.min.js",
|
||||
"name": "mapview",
|
||||
"src": "src/js/plugin/MapView.ts",
|
||||
"isDynamicEntry": true,
|
||||
|
||||
4
searx/static/themes/simple/sxng-core.min.js
vendored
4
searx/static/themes/simple/sxng-core.min.js
vendored
@@ -1,3 +1,3 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/Dms-nhjX.min.js","./chunk/BuurKv-k.min.js","./sxng-mapview.min.css","./chunk/D3mcqWOe.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/BhYx0_Gb.min.js","./chunk/CX4Qzp80.min.js","./chunk/5Ako-qGW.min.js","./chunk/Bs-FMxfE.min.js","./chunk/od7pNHfk.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
|
||||
var e={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},t={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},n=()=>{let t=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return t&&t in e?t:e.unknown},r=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},i=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},a=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},o=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?a(`DOMContentLoaded`,document,e,{once:!0}):e()},s=n(),c=r(),l=(e,t)=>{u(t)&&e()},u=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(s)}},d=`modulepreload`,f=function(e,t){return new URL(e,t).href},p={},m=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=f(t,n),t=s(t),t in p)return;p[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:d,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};o(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),a(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),a(`click`,`.searxng_init_map`,async function(t){t.preventDefault(),this.classList.remove(`searxng_init_map`),l(()=>m(async()=>{let{default:e}=await import(`./chunk/Dms-nhjX.min.js`);return{default:e}},__vite__mapDeps([0,1,2]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[e.results]})}),c.plugins?.includes(`infiniteScroll`)&&l(()=>m(async()=>{let{default:e}=await import(`./chunk/D3mcqWOe.min.js`);return{default:e}},__vite__mapDeps([3,1,4,5]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[e.results]}),c.plugins?.includes(`calculator`)&&l(()=>m(async()=>{let{default:e}=await import(`./chunk/BhYx0_Gb.min.js`);return{default:e}},__vite__mapDeps([6,1,5,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[e.results]})}),o(()=>{m(()=>import(`./chunk/CX4Qzp80.min.js`),__vite__mapDeps([7,4]),import.meta.url),m(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,5,4]),import.meta.url),c.autocomplete&&m(()=>import(`./chunk/Bs-FMxfE.min.js`),__vite__mapDeps([9,4]),import.meta.url)},{on:[s===e.index]}),o(()=>{m(()=>import(`./chunk/CX4Qzp80.min.js`),__vite__mapDeps([7,4]),import.meta.url),m(()=>import(`./chunk/od7pNHfk.min.js`),__vite__mapDeps([10,4]),import.meta.url),m(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,5,4]),import.meta.url),c.autocomplete&&m(()=>import(`./chunk/Bs-FMxfE.min.js`),__vite__mapDeps([9,4]),import.meta.url)},{on:[s===e.results]}),o(()=>{m(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([11,4]),import.meta.url)},{on:[s===e.preferences]});export{c as i,a as n,t as r,i as t};
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/CFmKEewG.min.js","./chunk/BuurKv-k.min.js","./sxng-mapview.min.css","./chunk/D3mcqWOe.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/CdVmyKrK.min.js","./chunk/CX4Qzp80.min.js","./chunk/5Ako-qGW.min.js","./chunk/Bs-FMxfE.min.js","./chunk/BVVCO4vX.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
|
||||
var e={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},t={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},n=()=>{let t=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return t&&t in e?t:e.unknown},r=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},i=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},a=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},o=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?a(`DOMContentLoaded`,document,e,{once:!0}):e()},s=n(),c=r(),l=(e,t)=>{u(t)&&e()},u=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(s)}},d=`modulepreload`,f=function(e,t){return new URL(e,t).href},p={},m=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=f(t,n),t=s(t),t in p)return;p[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:d,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};o(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),a(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),a(`click`,`.searxng_init_map`,async function(t){t.preventDefault(),this.classList.remove(`searxng_init_map`),l(()=>m(async()=>{let{default:e}=await import(`./chunk/CFmKEewG.min.js`);return{default:e}},__vite__mapDeps([0,1,2]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[e.results]})}),c.plugins?.includes(`infiniteScroll`)&&l(()=>m(async()=>{let{default:e}=await import(`./chunk/D3mcqWOe.min.js`);return{default:e}},__vite__mapDeps([3,1,4,5]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[e.results]}),c.plugins?.includes(`calculator`)&&l(()=>m(async()=>{let{default:e}=await import(`./chunk/CdVmyKrK.min.js`);return{default:e}},__vite__mapDeps([6,1,5,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[e.results]})}),o(()=>{m(()=>import(`./chunk/CX4Qzp80.min.js`),__vite__mapDeps([7,4]),import.meta.url),m(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,5,4]),import.meta.url),c.autocomplete&&m(()=>import(`./chunk/Bs-FMxfE.min.js`),__vite__mapDeps([9,4]),import.meta.url)},{on:[s===e.index]}),o(()=>{m(()=>import(`./chunk/CX4Qzp80.min.js`),__vite__mapDeps([7,4]),import.meta.url),m(()=>import(`./chunk/BVVCO4vX.min.js`),__vite__mapDeps([10,4]),import.meta.url),m(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,5,4]),import.meta.url),c.autocomplete&&m(()=>import(`./chunk/Bs-FMxfE.min.js`),__vite__mapDeps([9,4]),import.meta.url)},{on:[s===e.results]}),o(()=>{m(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([11,4]),import.meta.url)},{on:[s===e.preferences]});export{c as i,a as n,t as r,i as t};
|
||||
//# sourceMappingURL=sxng-core.min.js.map
|
||||
@@ -735,13 +735,13 @@ def js_obj_str_to_json_str(js_obj_str: str) -> str:
|
||||
if in_string == "'":
|
||||
p = p.replace('"', r'\"')
|
||||
parts[i] = p
|
||||
# deal with the sequence blackslash then quote
|
||||
# since js_obj_str splits on quote, we detect this case:
|
||||
# * the previous part ends with a black slash
|
||||
# * the current part is a single quote
|
||||
# when detected the blackslash is removed on the previous part
|
||||
# drop a trailing \ that was escaping the quote
|
||||
# leave it if it has been escaped twice as a literal i.e. two \ and ' in a row
|
||||
if blackslash_just_before and p[:1] == "'":
|
||||
parts[i - 1] = parts[i - 1][:-1]
|
||||
prev = parts[i - 1]
|
||||
num_backslashes = len(prev) - len(prev.rstrip("\\"))
|
||||
if num_backslashes % 2 == 1:
|
||||
parts[i - 1] = prev[:-1]
|
||||
|
||||
elif in_string is None and p in ('"', "'", "`"):
|
||||
# we are not in string but p is string delimiter
|
||||
|
||||
Reference in New Issue
Block a user