23 Commits

Author SHA1 Message Date
dependabot[bot]
61d660276f [upd] web-client (simple): Bump the minor group (#6692)
Bumps the minor group in /client/simple with 2 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@biomejs/biome` from 2.5.11 to 2.5.12
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.12/packages/@biomejs/biome)

Updates `@types/node` from 26.4.1 to 26.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 26.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-11 16:57:14 +02:00
Markus Heiser
6a27c21008 [fix] engine: pinterest - fix empty titles and complete modernization (#6694)
* [fix] engine: pinterest - fix empty titles and complete modernization

The titles from the fields ``title`` and ``grid_title`` are mostly empty or have
short strings without meaningful content.  Various fields for the title are now
being queried, which have more informative value.

As part of the bug fix, the engine was completely revised and modernized.

Related:

- https://github.com/searxng/searxng/pull/6690#issuecomment-5631113121

Co-authored-by: @vojkovic
2026-09-11 16:24:58 +02:00
Brock Vojkovic
ffe96f8a6f [fix] engines: bing first word results (#6671)
Fixes the bing web engine, it was just using the first word of the query for the search and return random junk other times. see: vojkovic#10

Swapped to use bing's setlang and cc params. I found us, cn, ru return complete garbage 100% of the time. I reckon that if you don't have an ip address from there it will just return garbage, so those three are skipped. Also removed accept language override because it didn't change anything anymore.


- Closes: https://github.com/searxng/searxng/issues/4964
- Related: https://github.com/vojkovic/searxng/issues/10
2026-09-11 08:41:13 +02:00
Markus Heiser
931fd9787b [fix] engine: core.ac.uk - don't split a string into a tag list (#6689) 2026-09-10 14:45:43 +02:00
vojkovic
42e1d61296 [fix] engines: startpage anubis solver 2026-09-10 19:12:21 +08:00
vojkovic
765a9999df [fix] engines: duckduckgo web bypass botdetection 2026-09-10 18:56:12 +08:00
rdurnik
ba055b3e09 [feat] engines: add europepmc (science, scientific publications) 2026-09-10 11:27:28 +02:00
Markus Heiser
3fdc6d753a [del] remove searx_engine - no use case (#6680)
There is no meaningful use-case for the engine, moreover the implementation
is 10 years old (formerly searx) and no longer fits the current data model of
SearXNG.

Closes: - https://github.com/searxng/searxng/issues/6676

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-09-08 09:29:41 +02:00
Ted Lanham
3e454637fb [fix] braveapi: set JSON Accept header (#6666) 2026-09-07 13:18:07 +02:00
dependabot[bot]
c7f3080aac [upd] web-client (simple): Bump the minor group in /client/simple with 3 updates (#6645)
* [upd] web-client (simple): Bump the minor group

Bumps the minor group in /client/simple with 3 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [sharp](https://github.com/lovell/sharp).


Updates `@biomejs/biome` from 2.5.10 to 2.5.11
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.11/packages/@biomejs/biome)

Updates `@types/node` from 26.3.0 to 26.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `sharp` from 0.35.3 to 0.35.4
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.3...v0.35.4)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 26.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: sharp
  dependency-version: 0.35.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* update lock

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ivan Gabaldon <igabaldon@inetol.net>
2026-09-05 22:57:09 +02:00
dependabot[bot]
072311b5e0 [upd] pypi: Bump the minor group with 3 updates (#6647)
Bumps the minor group with 3 updates: [typer](https://github.com/fastapi/typer), [pylint](https://github.com/pylint-dev/pylint) and [selenium](https://github.com/SeleniumHQ/Selenium).


Updates `typer` from 0.27.1 to 0.27.2
- [Release notes](https://github.com/fastapi/typer/releases)
- [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md)
- [Commits](https://github.com/fastapi/typer/compare/0.27.1...0.27.2)

Updates `pylint` from 4.0.7 to 4.0.8
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v4.0.7...v4.0.8)

Updates `selenium` from 4.47.0 to 4.48.0
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.47.0...selenium-4.48.0)

---
updated-dependencies:
- dependency-name: typer
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: pylint
  dependency-version: 4.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: selenium
  dependency-version: 4.48.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-05 22:48:19 +02:00
Bnyro
c06e9f0889 [del] cara engine: remove because it requires signup for searching 2026-09-05 22:44:32 +02:00
Bnyro
4781754dc4 [mod] engines: remove uses of enable_http2 = False
This has only been needed because we were flagged by HTTP2 fingerprinting.
Now, since we use curl cffi, we can bypass the fingerpinting, so we can
use HTTP2 just fine without getting blocked.
2026-09-05 19:51:58 +02:00
Bnyro
28b61729c7 [fix] resulthunter: detect blocked requests instead of returning 0 results 2026-09-05 19:47:23 +02:00
Bnyro
14a9f84c6c [fix] chatnoir: returns empty responses 2026-09-05 19:46:48 +02:00
vojkovic
8b01679e8f [fix] engines: update brave images/videos parser and news xpath 2026-09-06 01:45:40 +08:00
vojkovic
eaf1fcb349 [fix] engines: resulthunter search_source arg 2026-09-06 01:43:11 +08:00
vojkovic
e20e370353 [fix] engines: tusksearch referer header 2026-09-06 01:38:09 +08:00
vojkovic
3605a2d58b [fix] engines: dogpile request api token 2026-09-06 01:22:09 +08:00
Bnyro
aef258321c [feat] engines: add searchrockit.com (general, news, images) 2026-09-05 19:16:29 +02:00
Bnyro
a303e9c0ca [fix] neosearch: requests blocked due to missing xsrf token 2026-09-05 19:14:16 +02:00
Bnyro
ccffbfc164 [fix] searchzee: bypass botblocking
requires https://github.com/searxng/searxng/pull/6620
2026-09-05 19:11:49 +02:00
Ivan Gabaldon
242dc6e398 [mod] ci: disable armv7 container test (#6661) 2026-09-05 18:34:25 +02:00
46 changed files with 1048 additions and 820 deletions

View File

@@ -105,8 +105,9 @@ jobs:
arch: amd64 arch: amd64
- runner: ubuntu-26.04-arm - runner: ubuntu-26.04-arm
arch: arm64 arch: arm64
- runner: ubuntu-26.04-arm # FIXME: https://github.com/searxng/searxng/pull/6655#issuecomment-5550293085
arch: armv7 # - runner: ubuntu-26.04-arm
# arch: armv7
steps: steps:
- name: Login to GHCR - name: Login to GHCR

File diff suppressed because it is too large Load Diff

View File

@@ -29,14 +29,14 @@
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.10", "@biomejs/biome": "2.5.12",
"@types/node": "^26.3.0", "@types/node": "^26.5.0",
"browserslist": "^4.28.8", "browserslist": "^4.28.8",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.1",
"less": "^4.9.0", "less": "^4.9.0",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.3", "sharp": "~0.35.4",
"sort-package-json": "^4.0.0", "sort-package-json": "^4.0.0",
"stylelint": "^17.14.1", "stylelint": "^17.14.1",
"stylelint-config-standard-less": "^4.1.0", "stylelint-config-standard-less": "^4.1.0",

View File

@@ -1,8 +0,0 @@
.. _cara engine:
===========
Cara Images
===========
.. automodule:: searx.engines.cara
:members:

View File

@@ -0,0 +1,8 @@
.. _europepmc engine:
==========
Europe PMC
==========
.. automodule:: searx.engines.europepmc
:members:

View File

@@ -2,9 +2,9 @@ mock==5.2.0
nose2[coverage_plugin]==0.16.0 nose2[coverage_plugin]==0.16.0
cov-core==1.15.0 cov-core==1.15.0
black==25.9.0 black==25.9.0
pylint==4.0.7 pylint==4.0.8
splinter==0.21.0 splinter==0.21.0
selenium==4.47.0 selenium==4.48.0
Sphinx==8.2.3;python_version <= "3.11" Sphinx==8.2.3;python_version <= "3.11"
Sphinx==9.1.0; python_version > "3.11" Sphinx==9.1.0; python_version > "3.11"
sphinx-issues==6.0.0 sphinx-issues==6.0.0

View File

@@ -11,7 +11,7 @@ curl_cffi==0.16.1
valkey==6.1.1 valkey==6.1.1
markdown-it-py==4.2.0 markdown-it-py==4.2.0
msgspec==0.21.1 msgspec==0.21.1
typer==0.27.1 typer==0.27.2
isodate==0.7.2 isodate==0.7.2
whitenoise==6.12.0 whitenoise==6.12.0
typing-extensions==4.16.0 typing-extensions==4.16.0

View File

@@ -72,43 +72,21 @@ def get_locale_params(engine_region: str | None) -> dict[str, str] | None:
return {"mkt": engine_region} 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"): def request(query: str, params: "OnlineParams"):
"""Assemble a Bing-Web request.""" """Assemble a Bing-Web request."""
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
query_params: dict[str, str | int] = { query_params: dict[str, str | int] = {
"q": query, "q": query,
"adlt": _safesearch_map.get(params.get("safesearch", 0), "off"), "adlt": _safesearch_map.get(params.get("safesearch", 0), "off"),
} }
locale_params = get_locale_params(engine_region) if engine_region and engine_region != "clear":
if locale_params: lang, _, cc = engine_region.partition("-")
query_params.update(locale_params) 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)}" params["url"] = f"{base_url}/search?{urlencode(query_params)}"

View File

@@ -9,7 +9,6 @@ from lxml import html
from searx.engines.bing import ( # pylint: disable=unused-import from searx.engines.bing import ( # pylint: disable=unused-import
fetch_traits, fetch_traits,
get_locale_params, get_locale_params,
override_accept_language,
) )
# about # about
@@ -44,8 +43,6 @@ def request(query, params):
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
# build URL query # build URL query
# - example: https://www.bing.com/images/async?q=foo&async=1&first=1&count=35 # - example: https://www.bing.com/images/async?q=foo&async=1&first=1&count=35
query_params = { query_params = {

View File

@@ -12,10 +12,7 @@ from urllib.parse import urlencode
from lxml import html from lxml import html
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.engines.bing import ( from searx.engines.bing import get_locale_params
get_locale_params,
override_accept_language,
)
from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text
# about # about
@@ -54,8 +51,6 @@ def request(query, params):
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
# build URL query # build URL query
# - example: https://www.bing.com/news/infinitescrollajax?q=london&first=1 # - example: https://www.bing.com/news/infinitescrollajax?q=london&first=1
page = int(params.get("pageno", 1)) - 1 page = int(params.get("pageno", 1)) - 1

View File

@@ -9,7 +9,6 @@ from lxml import html
from searx.engines.bing import ( # pylint: disable=unused-import from searx.engines.bing import ( # pylint: disable=unused-import
fetch_traits, fetch_traits,
get_locale_params, get_locale_params,
override_accept_language,
) )
from searx.engines.bing_images import time_map from searx.engines.bing_images import time_map
from searx.utils import eval_xpath, eval_xpath_getindex 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) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
# build URL query # build URL query
# - example: https://www.bing.com/videos/asyncv2?q=foo&async=content&first=1&count=35 # - example: https://www.bing.com/videos/asyncv2?q=foo&async=content&first=1&count=35
query_params = { query_params = {

View File

@@ -248,13 +248,13 @@ def extract_json_data(text: str) -> dict[str, t.Any]:
# node_ids: [0, 19], # node_ids: [0, 19],
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}] # data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
text = text[text.index("<script") : text.index("</script")] # form: null,
if not text: # error: null
raise ValueError("can't find JS/JSON data in the given text") # });
start = text.index("data: [{") start = text.index("data: [{")
end = text.rindex("}}]") newline = text.index("\n", start)
js_obj_str = text[start:end] end = text.rindex("}}]", start, newline)
js_obj_str = "{" + js_obj_str + "}}]}" js_obj_str = "{" + text[start:end] + "}}]}"
# js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for &nbsp; # js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for &nbsp;
# js_obj_str = js_obj_str.replace(r"\u003C", "<").replace(r"\u003c", "<") # fix broken HTML tags in strings # 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) json_str = js_obj_str_to_json_str(js_obj_str)
@@ -354,14 +354,14 @@ def _parse_news(resp: SXNG_Response) -> EngineResults:
res = EngineResults() res = EngineResults()
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, "//div[contains(@class, 'results')]//div[@data-type='news']"): for result in eval_xpath_list(dom, "//div[@data-type='news']"):
url = eval_xpath_getindex(result, ".//a[contains(@class, 'result-header')]/@href", 0, default=None) url = eval_xpath_getindex(result, ".//a/@href", 0, default=None)
if url is None: if url is None:
continue continue
title = eval_xpath_list(result, ".//span[contains(@class, 'snippet-title')]") title = eval_xpath_list(result, ".//div[contains(@class, 'title')]")
content = eval_xpath_list(result, ".//p[contains(@class, 'desc')]") content = eval_xpath_list(result, ".//div[contains(@class, 'description')]")
thumbnail = eval_xpath_getindex(result, ".//div[contains(@class, 'image-wrapper')]//img/@src", 0, default="") thumbnail = eval_xpath_getindex(result, ".//a[contains(@class, 'thumbnail')]//img/@src", 0, default="")
item = res.types.LegacyResult( item = res.types.LegacyResult(
template="default.html", template="default.html",

View File

@@ -91,6 +91,7 @@ def request(query: str, params: "OnlineParams") -> None:
params["url"] = f"{base_url}?{urlencode(search_args)}" params["url"] = f"{base_url}?{urlencode(search_args)}"
params["headers"]["X-Subscription-Token"] = api_key params["headers"]["X-Subscription-Token"] = api_key
params["headers"]["Accept"] = "application/json"
def _extract_published_date(published_date_raw: str): def _extract_published_date(published_date_raw: str):

View File

@@ -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

View File

@@ -84,6 +84,7 @@ def request(query: str, params: "OnlineParams"):
params["url"] = f"{base_url}/api/v1/_search" params["url"] = f"{base_url}/api/v1/_search"
params["method"] = "POST" params["method"] = "POST"
params["impersonate"] = "none"
json_data = { json_data = {
"query": query, "query": query,

View File

@@ -141,12 +141,13 @@ def response(resp: "SXNG_Response") -> EngineResults:
if name: if name:
authors.add(name) authors.add(name)
tag = result.get("fieldOfStudy")
res.add( res.add(
res.types.Paper( res.types.Paper(
title=result.get("title"), title=result.get("title"),
url=url, url=url,
content=result.get("fullText", "") or "", content=result.get("fullText", "") or "",
tags=result.get("fieldOfStudy", []), tags=[tag] if tag else [],
publishedDate=published_date, publishedDate=published_date,
type=result.get("documentType", "") or "", type=result.get("documentType", "") or "",
authors=authors, authors=authors,

View File

@@ -8,6 +8,9 @@ import typing as t
from datetime import datetime, timezone from datetime import datetime, timezone
import html 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.utils import format_duration, html_to_text, humanize_number
from searx.result_types import EngineResults from searx.result_types import EngineResults
@@ -35,15 +38,36 @@ dogpile_categ = "search"
base_url = "https://www.dogpile.com" base_url = "https://www.dogpile.com"
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"} 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: def setup(_: dict[str, t.Any]) -> bool | None:
if dogpile_categ not in ("search", "images", "videos", "news"): if dogpile_categ not in ("search", "images", "videos", "news"):
raise ValueError("invalid search type: %s" % dogpile_categ) 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"): def request(query: str, params: "OnlineParams"):
params["url"] = f"{base_url}/api/{dogpile_categ}" params["url"] = f"{base_url}/api/{dogpile_categ}"
params["headers"]["Origin"] = base_url 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["method"] = "POST"
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]} params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}

View File

@@ -14,11 +14,12 @@ can't build it ourselves and must scrape it from the HTML pages.
""" """
import typing as t import typing as t
import re
from urllib.parse import quote_plus from urllib.parse import quote_plus, urljoin
from lxml import html 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.result_types import EngineResults
from searx.enginelib import EngineCache from searx.enginelib import EngineCache
from searx.network import get from searx.network import get
@@ -38,7 +39,6 @@ about = {
# engine dependent config # engine dependent config
categories = ["general"] categories = ["general"]
paging = True paging = True
_HTTP_User_Agent: str = gen_useragent()
base_url = "https://duckduckgo.com" base_url = "https://duckduckgo.com"
@@ -73,6 +73,8 @@ def _fetch_first_page_link(
resp = get( resp = get(
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web", url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
headers=headers, headers=headers,
impersonate="firefox",
default_headers=False,
timeout=2, timeout=2,
) )
@@ -96,6 +98,43 @@ def _cache_key(query: str, pageno: int) -> str:
return f"nextpage_url|{query}|{pageno}" 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: def request(query: str, params: "OnlineParams") -> None:
if len(query) >= 500: if len(query) >= 500:
@@ -103,25 +142,15 @@ def request(query: str, params: "OnlineParams") -> None:
params["url"] = None params["url"] = None
return return
headers = params["headers"] # firefox TLS only
params["impersonate"] = "firefox"
# The vqd value is generated from the query and the UA header. To be able params["default_headers"] = False
# 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"
api_url = "" api_url = ""
if params["pageno"] > 1: if params["pageno"] > 1:
api_url = CACHE.get(_cache_key(query, params["pageno"])) api_url = CACHE.get(_cache_key(query, params["pageno"]))
else: else:
api_url = _fetch_first_page_link(query, headers) api_url = _fetch_first_page_link(query, params["headers"])
if not api_url: if not api_url:
params["url"] = None 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&") 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 # TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
def response(resp: "SXNG_Response"): def response(resp: "SXNG_Response"):
res = EngineResults() 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: if "u" not in result:
continue 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"])) res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"]))
) )
# link to next page if results:
next_page_path = res_json["results"][-1].get("n") next_page_path = results[-1].get("n")
if next_page_path: if next_page_path:
CACHE.set( CACHE.set(
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1), _cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
base_url + next_page_path, base_url + next_page_path,
expire=60 * 60, expire=60 * 60,
) )
return res return res

150
searx/engines/europepmc.py Normal file
View 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

View File

@@ -37,17 +37,13 @@ about = {
} }
categories = [] categories = []
safeseach = True safesearch = True
base_url = "https://luxxle.com" base_url = "https://luxxle.com"
luxxle_categ = "search" luxxle_categ = "search"
"""Supported categories: "search", "news", "images", "videos".""" """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"} safe_search_map = {0: "Off", 1: "Moderate", 2: "Strict"}

View File

@@ -6,10 +6,14 @@
from json import loads from json import loads
import typing as t 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.extended_types import SXNG_Response
from searx.network import get
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.utils import eval_xpath, extract_text
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
@@ -25,18 +29,33 @@ about = {
"results": "JSON", "results": "JSON",
} }
paging = False
enable_http3 = True
base_url = "https://neosearch.org" base_url = "https://neosearch.org"
categories = ["general"] 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"): 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"} args = {"q": query, "generate": "auto"}
countrycode = params["searxng_locale"].split("-")[-1].upper() countrycode = params["searxng_locale"].split("-")[-1].upper()
if countrycode in traits.custom["countrycodes"]: if countrycode in traits.custom["countrycodes"]:
args["loc"] = countrycode args["loc"] = countrycode
params["url"] = f"{base_url}/search?{urlencode(args)}" params["json"] = args
def response(resp: "SXNG_Response") -> EngineResults: def response(resp: "SXNG_Response") -> EngineResults:
@@ -67,7 +86,6 @@ def response(resp: "SXNG_Response") -> EngineResults:
def fetch_traits(engine_traits: "EngineTraits") -> None: def fetch_traits(engine_traits: "EngineTraits") -> None:
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from searx.network import get
from searx.utils import extr, js_obj_str_to_python from searx.utils import extr, js_obj_str_to_python
from babel.core import get_global from babel.core import get_global

View File

@@ -8,7 +8,7 @@ from urllib.parse import urlencode
from lxml import html from lxml import html
from searx.result_types import EngineResults 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.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException
from searx.network import get from searx.network import get
@@ -44,8 +44,6 @@ SECRET_KEY_DB_KEY = "secret-key"
CACHE: EngineCache CACHE: EngineCache
"""Cache to store the secret API key for the engine.""" """Cache to store the secret API key for the engine."""
enable_http2 = False
def setup(engine_settings: dict[str, t.Any]) -> bool: def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
@@ -56,13 +54,7 @@ def setup(engine_settings: dict[str, t.Any]) -> bool:
def _get_secret_key(): def _get_secret_key():
resp = get( resp = get(
base_url, base_url,
headers={ headers={"Referer": base_url},
# circumvents Cloudflare bot protections
"User-Agent": gen_useragent(),
"Referer": base_url,
"Sec-GPC": "1",
"Connection": "keep-alive",
},
) )
if resp.status_code != 200: if resp.status_code != 200:
@@ -105,8 +97,6 @@ def request(query, params):
params["headers"]["secret-key"] = secret_key params["headers"]["secret-key"] = secret_key
return params
def response(resp): def response(resp):
res = EngineResults() res = EngineResults()

View File

@@ -2,72 +2,85 @@
"""Pinterest (images)""" """Pinterest (images)"""
from json import dumps from json import dumps
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 = { about = {
"website": 'https://www.pinterest.com/', "website": "https://www.pinterest.com/",
"wikidata_id": 'Q255381', "wikidata_id": "Q255381",
"official_api_documentation": 'https://developers.pinterest.com/docs/api/v5/', "official_api_documentation": "https://developers.pinterest.com/docs/api/v5/",
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": "JSON",
} }
categories = ['images'] categories = ["images"]
paging = True paging = True
base_url = 'https://www.pinterest.com' base_url = "https://www.pinterest.com"
def request(query, params): def request(query: str, params: "OnlineParams") -> None:
args = { args = {
'options': { "options": {
'query': query, "query": query,
'bookmarks': [params['engine_data'].get('bookmark', '')], "bookmarks": [params["engine_data"].get("bookmark", "")],
}, },
'context': {}, "context": {},
} }
params['url'] = f"{base_url}/resource/BaseSearchResource/get/?data={dumps(args)}" params["url"] = f"{base_url}/resource/BaseSearchResource/get/?data={dumps(args)}"
params['headers'] = { params["headers"] = {
'X-Pinterest-AppState': 'active', "X-Requested-With": "XMLHttpRequest",
'X-Pinterest-Source-Url': '/ideas/', "X-Pinterest-AppState": "active",
'X-Pinterest-PWS-Handler': 'www/ideas.js', "X-Pinterest-Source-Url": "/ideas/",
"X-Pinterest-PWS-Handler": "www/ideas.js",
} }
return params
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_resp: dict[str, t.Any] = resp.json() # type: ignore
def response(resp): res.add(
results = []
json_resp = resp.json()
results.append(
{ {
'engine_data': json_resp['resource_response']['bookmark'], "engine_data": json_resp["resource_response"]["bookmark"],
# it's called bookmark by pinterest, but it's rather a nextpage # it's called bookmark by pinterest, but it's rather a nextpage
# parameter to get the next results # parameter to get the next results
'key': 'bookmark', "key": "bookmark",
} }
) )
for result in json_resp['resource_response']['data']['results']: for result in json_resp["resource_response"]["data"]["results"]:
if result['type'] == 'story': if result["type"] == "story":
continue continue
main_image = result['images']['orig'] main_image = result["images"]["orig"]
results.append(
{ title = result.get("title") or result.get("grid_title") or ""
'template': 'images.html', if len(title) < 5:
'url': result.get('link') or f"{base_url}/pin/{result['id']}/", visual_annotation = result.get("pin_join", {}).get("visual_annotation")
'title': result.get('title') or result.get('grid_title'), if visual_annotation:
'content': (result.get('rich_summary') or {}).get('display_description') or "", title = visual_annotation[0]
'img_src': main_image['url'], else:
'thumbnail_src': result['images']['236x']['url'], title = result.get("name") or result.get("auto_alt_text") or ""
'source': (result.get('rich_summary') or {}).get('site_name'),
'resolution': f"{main_image['width']}x{main_image['height']}", res.add(
'author': f"{result['pinner'].get('full_name')} ({result['pinner']['username']})", res.types.Image(
} url=result.get("link") or f"{base_url}/pin/{result['id']}/",
title=title,
content=(result.get("rich_summary") or {}).get("display_description") or "",
img_src=main_image["url"],
thumbnail_src=result["images"]["236x"]["url"],
source=(result.get("rich_summary") or {}).get("site_name") or "",
resolution=f"{main_image['width']}x{main_image['height']}",
author=f"{result['pinner'].get('full_name')} ({result['pinner']['username']})",
)
) )
return results return res

View File

@@ -27,9 +27,6 @@ time_range_support = True
safesearch_map = {0: 'off', 1: '1', 2: '1'} safesearch_map = {0: 'off', 1: '1', 2: '1'}
time_range_map = {'day': '1d', 'week': '1w', 'month': '1m', 'year': '1y'} time_range_map = {'day': '1d', 'week': '1w', 'month': '1m', 'year': '1y'}
# using http2 returns forbidden errors
enable_http2 = False
def request(query, params): def request(query, params):
args = { args = {
@@ -50,8 +47,6 @@ def request(query, params):
# prevent automatic redirects to first page on pagination # prevent automatic redirects to first page on pagination
params['allow_redirects'] = False params['allow_redirects'] = False
return params
def _image_result(result): def _image_result(result):
return { return {

View File

@@ -10,6 +10,7 @@ from urllib.parse import urlencode
from lxml import html from lxml import html
from searx import locales from searx import locales
from searx.exceptions import SearxEngineResponseException
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.utils import eval_xpath_list, eval_xpath, extract_text from searx.utils import eval_xpath_list, eval_xpath, extract_text
@@ -52,6 +53,7 @@ def request(query: str, params: "OnlineParams") -> None:
"q": query, "q": query,
"search_type": resulthunter_categ, "search_type": resulthunter_categ,
"offset": params["pageno"] - 1, "offset": params["pageno"] - 1,
"search_source": "other",
} }
# uses Brave's engine traits # uses Brave's engine traits
@@ -111,6 +113,11 @@ def _image_results(doc: "ElementBase") -> EngineResults:
def response(resp: "SXNG_Response") -> EngineResults: def response(resp: "SXNG_Response") -> EngineResults:
doc = html.fromstring(resp.text) 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: match resulthunter_categ:
case "web": case "web":
return _general_results(doc) return _general_results(doc)

View 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

View File

@@ -4,13 +4,11 @@ independent search infrastructure."""
import typing as t import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
import uuid
from searx.exceptions import SearxEngineAPIException
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
from searx.network import get
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.utils import extr, html_to_text from searx.utils import html_to_text
from searx.enginelib import EngineCache
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams from searx.search.processors import OnlineParams
@@ -34,43 +32,19 @@ SearchzeeCategType = t.Literal["web", "news"]
searchzee_categ: SearchzeeCategType = None # type: ignore[reportAssignmentType] searchzee_categ: SearchzeeCategType = None # type: ignore[reportAssignmentType]
CACHE: EngineCache
"""Cache for storing the scraped API Token."""
base_url = "https://searchzee.com" base_url = "https://searchzee.com"
# only supports for news # only supports for news
time_range_map = {"day": "pd", "week": "pw", "month": "pm", "year": "py"} 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): if searchzee_categ not in t.get_args(SearchzeeCategType):
raise ValueError("invalid category: %s" % searchzee_categ) 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"): 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} args = {"q": query, "type": searchzee_categ, "offset": params["pageno"] - 1}
if params["time_range"]: if params["time_range"]:

View File

@@ -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

View File

@@ -1,5 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # 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: .. _startpage regions:
@@ -84,6 +87,7 @@ Startpage's category (for Web-search, News, Videos, ..) is set by
""" """
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
import hashlib
import re import re
import typing as t import typing as t
from collections import OrderedDict from collections import OrderedDict
@@ -99,7 +103,7 @@ from searx.enginelib import EngineCache
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.exceptions import SearxEngineCaptchaException from searx.exceptions import SearxEngineCaptchaException
from searx.locales import region_tag 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 ( from searx.utils import (
eval_xpath, eval_xpath,
extr, extr,
@@ -176,6 +180,45 @@ def setup(_: dict[str, t.Any]) -> bool | None:
sc_code_cache_sec = 3600 sc_code_cache_sec = 3600
"""Time in seconds the sc-code is cached in memory :py:obj:`get_sc_code`.""" """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): def get_sc_code(params):
"""Get an actual ``sc`` argument from Startpage's search form (HTML page). """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) logger.debug("get_sc_code: request headers: %s", headers)
resp = get(get_sc_url, headers=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) # ?? 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 # ?? https://www.startpage.com/sp/cdn/images/filter-chevron.svg
# ?? ping-back URL: https://www.startpage.com/sp/pb?sc=TLsB0oITjZ8F21 # ?? 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 Additionally the arguments form Startpage's search form needs to be set in
HTML POST data / compare ``<input>`` elements: :py:obj:`search_form_xpath`. HTML POST data / compare ``<input>`` elements: :py:obj:`search_form_xpath`.
""" """
engine_region = traits.get_region(params["searxng_locale"], "en-US") engine_region = traits.get_region(params["searxng_locale"], "en_US")
engine_language = traits.get_language(params["searxng_locale"], "en") engine_language = traits.get_language(params["searxng_locale"], "english")
params["headers"]["Origin"] = base_url params["headers"]["Origin"] = base_url
params["headers"]["Referer"] = base_url + "/" params["headers"]["Referer"] = base_url + "/"
@@ -262,9 +308,9 @@ def request(query, params):
args["language"] = engine_language args["language"] = engine_language
args["lui"] = engine_language args["lui"] = engine_language
args["segment"] = "startpage.udog"
if params["pageno"] > 1: if params["pageno"] > 1:
args["page"] = params["pageno"] args["page"] = params["pageno"]
args["segment"] = "startpage.udog"
# Build cookie # Build cookie
lang_homepage = "en" lang_homepage = "en"
@@ -289,6 +335,8 @@ def request(query, params):
cookie["search_results_region"] = engine_region cookie["search_results_region"] = engine_region
params["cookies"]["preferences"] = "N1N".join(["%sEEE%s" % x for x in cookie.items()]) 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("cookie preferences: %s", params["cookies"]["preferences"])
logger.debug("data: %s", args) logger.debug("data: %s", args)
@@ -400,6 +448,18 @@ def _get_image_result(result) -> dict[str, t.Any] | None:
def response(resp): 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() categ = startpage_categ.capitalize()
results_raw = "{" + extr(resp.text, f"React.createElement(UIStartpage.AppSerp{categ}, {{", "}})") + "}}" results_raw = "{" + extr(resp.text, f"React.createElement(UIStartpage.AppSerp{categ}, {{", "}})") + "}}"

View File

@@ -21,8 +21,6 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
} }
# otherwise all requests get blocked, probably HTTP2 fingerprinting
enable_http2 = False
base_url = "https://stocksnap.io" base_url = "https://stocksnap.io"
cdn_url = "https://cdn.stocksnap.io" cdn_url = "https://cdn.stocksnap.io"

View File

@@ -13,7 +13,7 @@ from dateutil import parser
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.network import get 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 from searx.result_types import EngineResults
if t.TYPE_CHECKING: 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 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). 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: if not resp.ok:
raise SearxEngineAPIException("failed to obtain request x-sid token") 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 # required - we send a random longitude and latitude instead of the actual user location
"x-lon": str(round(random.random() * 90, 4)), "x-lon": str(round(random.random() * 90, 4)),
"x-lat": str(round(random.random() * 90, 4)), "x-lat": str(round(random.random() * 90, 4)),
"Referer": "https://tusksearch.com/",
} }
) )

View File

@@ -17,12 +17,10 @@ about = {
categories = ['images', 'icons'] categories = ['images', 'icons']
base_url = "https://uxwing.com" base_url = "https://uxwing.com"
enable_http2 = False
def request(query, params): def request(query, params):
params['url'] = f"{base_url}/?s={quote_plus(query)}" params['url'] = f"{base_url}/?s={quote_plus(query)}"
return params
def response(resp): def response(resp):

View File

@@ -30,8 +30,6 @@ web_base_url = "https://yep.com"
safesearch = True safesearch = True
safesearch_map = {0: "off", 1: "moderate", 2: "strict"} safesearch_map = {0: "off", 1: "moderate", 2: "strict"}
enable_http2 = False
results_per_page = 20 results_per_page = 20
_IMPORT_RE = re.compile(r"import\"(.*?)\";") _IMPORT_RE = re.compile(r"import\"(.*?)\";")
@@ -50,9 +48,6 @@ def request(query: str, params: "OnlineParams") -> None:
{ {
"Referer": f"{web_base_url}/", "Referer": f"{web_base_url}/",
"Origin": web_base_url, "Origin": web_base_url,
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
} }
) )

View File

@@ -88,6 +88,9 @@ class HTTPParams(t.TypedDict):
impersonate: t.NotRequired[str] impersonate: t.NotRequired[str]
"""curl_cffi impersonate target. Default: ``chrome``.""" """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]] curl_options: t.NotRequired[dict[int, t.Any]]
"""Any extra libcurl options for the request.""" """Any extra libcurl options for the request."""
@@ -169,8 +172,8 @@ class OnlineProcessor(EngineProcessor):
"cookies": params["cookies"], "cookies": params["cookies"],
"auth": params["auth"], "auth": params["auth"],
} }
for key in ("curl_options", "impersonate"): for key in ("curl_options", "impersonate", "default_headers"):
if params.get(key): if params.get(key) is not None:
request_args[key] = params[key] request_args[key] = params[key]
verify = params.get("verify") verify = params.get("verify")

View File

@@ -634,11 +634,6 @@ engines:
shortcut: cos shortcut: cos
disabled: true disabled: true
- name: cara
engine: cara
shortcut: ca
disabled: true
# - name: chatnoir # - name: chatnoir
# engine: chatnoir # engine: chatnoir
# shortcut: cha # shortcut: cha
@@ -797,6 +792,10 @@ engines:
require_api_key: false require_api_key: false
results: JSON results: JSON
- name: europepmc
engine: europepmc
shortcut: epmc
- name: erowid - name: erowid
engine: xpath engine: xpath
paging: true paging: true
@@ -2171,6 +2170,32 @@ engines:
about: about:
website: https://searchmysite.net 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 - name: selfhst icons
engine: selfhst engine: selfhst
shortcut: si shortcut: si
@@ -2275,14 +2300,6 @@ engines:
shortcut: pwv shortcut: pwv
disabled: true disabled: true
# - name: searx
# engine: searx_engine
# shortcut: se
# instance_urls :
# - http://127.0.0.1:8888/
# - ...
# disabled: true
- name: seekninja - name: seekninja
engine: seekninja engine: seekninja
shortcut: sen shortcut: sen
@@ -2326,18 +2343,21 @@ engines:
shortcut: sp shortcut: sp
startpage_categ: web startpage_categ: web
categories: [general, web] categories: [general, web]
inactive: true # uses a Proof Of Work captcha https://github.com/searxng/searxng/pull/6669
- name: startpage news - name: startpage news
engine: startpage engine: startpage
startpage_categ: news startpage_categ: news
categories: [news, web] categories: [news, web]
shortcut: spn shortcut: spn
inactive: true # uses a Proof Of Work captcha https://github.com/searxng/searxng/pull/6669
- name: startpage images - name: startpage images
engine: startpage engine: startpage
startpage_categ: images startpage_categ: images
categories: [images, web] categories: [images, web]
shortcut: spi shortcut: spi
inactive: true # uses a Proof Of Work captcha https://github.com/searxng/searxng/pull/6669
- name: steam - name: steam
engine: steam engine: steam

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -61,7 +61,7 @@
] ]
}, },
"src/js/main/results.ts": { "src/js/main/results.ts": {
"file": "chunk/od7pNHfk.min.js", "file": "chunk/BVVCO4vX.min.js",
"name": "results", "name": "results",
"src": "src/js/main/results.ts", "src": "src/js/main/results.ts",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -81,7 +81,7 @@
] ]
}, },
"src/js/plugin/Calculator.ts": { "src/js/plugin/Calculator.ts": {
"file": "chunk/BhYx0_Gb.min.js", "file": "chunk/CdVmyKrK.min.js",
"name": "calculator", "name": "calculator",
"src": "src/js/plugin/Calculator.ts", "src": "src/js/plugin/Calculator.ts",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -103,7 +103,7 @@
] ]
}, },
"src/js/plugin/MapView.ts": { "src/js/plugin/MapView.ts": {
"file": "chunk/Dms-nhjX.min.js", "file": "chunk/CFmKEewG.min.js",
"name": "mapview", "name": "mapview",
"src": "src/js/plugin/MapView.ts", "src": "src/js/plugin/MapView.ts",
"isDynamicEntry": true, "isDynamicEntry": true,

View File

@@ -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]); 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/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}; 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 //# sourceMappingURL=sxng-core.min.js.map

View File

@@ -735,13 +735,13 @@ def js_obj_str_to_json_str(js_obj_str: str) -> str:
if in_string == "'": if in_string == "'":
p = p.replace('"', r'\"') p = p.replace('"', r'\"')
parts[i] = p parts[i] = p
# deal with the sequence blackslash then quote # drop a trailing \ that was escaping the quote
# since js_obj_str splits on quote, we detect this case: # leave it if it has been escaped twice as a literal i.e. two \ and ' in a row
# * 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
if blackslash_just_before and p[:1] == "'": 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 ('"', "'", "`"): elif in_string is None and p in ('"', "'", "`"):
# we are not in string but p is string delimiter # we are not in string but p is string delimiter