14 Commits

Author SHA1 Message Date
Bnyro
cce0957f54 [feat] engines: add support for iseek.com (general) 2026-06-22 09:51:57 +02:00
Bnyro
9375c0a6b6 [feat] engines: add netherlands startpagina (general, videos, images, news) 2026-06-22 09:50:19 +02:00
Bnyro
a702741e4e [feat] engines: add giphy (images/videos) 2026-06-22 09:49:47 +02:00
Bnyro
aeced67249 [feat] engines: add findfiles.net file search engine
FindFiles.net is a specialized file search engine designed to help you search
files online with precision. Unlike traditional search engines that mainly index
web pages, FindFiles focuses on finding real files on the internet - including
PDFs, documents, archives, videos, datasets, and more. [1]

[1] https://findfiles.net
2026-06-22 09:44:27 +02:00
Bnyro
199e03de1d [feat] engines: add kozmonavt.su (general) 2026-06-22 09:42:55 +02:00
Bnyro
9cd2439e5e [feat] engines: add kukei.eu (general) 2026-06-22 09:42:45 +02:00
Bnyro
9f4d8bca02 [feat] engines: add xonaly.com (general) 2026-06-22 09:41:29 +02:00
Bnyro
de76a4a39b [feat] engines: add cl0q.com (foss domain search) 2026-06-22 09:41:18 +02:00
Bnyro
a85a5e2794 [feat] engines: add unobtanium.rocks (personal websites search) 2026-06-22 09:41:07 +02:00
Bnyro
92abd98a55 [feat] engines: add tusksearch (web, news, videos, images) (#6267)
The code that reads the value of variable `x` from `embed.js`, decodes
it to ASCII and based on that sets `window["tuskheader"]` and `window["tuskkey"]`
is attached below. The only real way to figure out what this is doing is
by stepping through it with the debugger, otherwise it's almost hopeless.

```js
function fe() {
  const B = pe => pe.map(_e => String.fromCharCode(_e)).join(''),
  ae = window,
  o = ae.x;
  if (o?.length) {
    const pe = o.length / 2;
    for (let _e = 0; _e < pe; _e++) ae[B(o[_e])] = B(o[pe + _e]);
    ae.x = void 0
  }
}
```

Minimal script for testing the engine:

```py
import random
from json import loads
import requests

resp = requests.get("https://api.tusksearch.com/revcontent/embed.js")
data = loads(resp.text[6:])

def _decode(text: list[int]) -> str:
    return "".join([chr(x) for x in text])

header = _decode(data[3])
value = _decode(data[4])

resp = requests.get(
    "https://api.tusksearch.com/Search/Web?q=test&p=1&l=center&nextArgs=&prevArgs=",
    # "https://api.tusksearch.com/Search/Image?q=test&p=1&l=center",
    headers={
        header: value,
        'x-lon': str(random.random() * 90),
        'x-lat': str(random.random() * 90),
    },
)
print(resp.text)
```
2026-06-22 09:40:32 +02:00
Bnyro
93e867c6b1 [feat] engine categories: add blogs category
Category for searching personal blogs and websites.
Useful if searching for interesting articles on a topic
rather than the mainstream Wikipedia etc. results.
2026-06-22 09:39:40 +02:00
dependabot[bot]
75c1b1dade [upd] web-client (simple): Bump less (#6289)
Bumps the minor group in /client/simple with 1 update: [less](https://github.com/less/less.js).


Updates `less` from 4.6.4 to 4.6.6
- [Release notes](https://github.com/less/less.js/releases)
- [Changelog](https://github.com/less/less.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/less/less.js/commits/v4.6.6)

---
updated-dependencies:
- dependency-name: less
  dependency-version: 4.6.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:03:15 +02:00
Bnyro
097ab64c70 [del] aol: remove engine (eol) (#6299) 2026-06-22 07:32:23 +02:00
dependabot[bot]
0e9f513efc [upd] pypi: Bump the minor group with 5 updates (#6291)
Bumps the minor group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [certifi](https://github.com/certifi/python-certifi) | `2026.5.20` | `2026.6.17` |
| [pylint](https://github.com/pylint-dev/pylint) | `4.0.5` | `4.0.6` |
| [selenium](https://github.com/SeleniumHQ/Selenium) | `4.44.0` | `4.45.0` |
| [sphinxcontrib-programoutput](https://github.com/OpenNTI/sphinxcontrib-programoutput) | `0.19` | `0.20` |
| [basedpyright](https://github.com/detachhead/basedpyright) | `1.39.7` | `1.39.8` |
2026-06-22 07:30:41 +02:00
13 changed files with 812 additions and 284 deletions

View File

@@ -20,7 +20,7 @@
"browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1",
"less": "^4.6.4",
"less": "^4.6.6",
"mathjs": "^15.2.0",
"sharp": "~0.35.1",
"sort-package-json": "^4.0.0",
@@ -2890,9 +2890,9 @@
"license": "Apache-2.0"
},
"node_modules/less": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/less/-/less-4.6.4.tgz",
"integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==",
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/less/-/less-4.6.6.tgz",
"integrity": "sha512-ooPSwQGQ2sVe8Dh1jVsbKKsRR2gd8lFK72BDkeSzjnD1T5aIHL65hCMfO0GVmtriKgDKrQv6xp9UrihUsWuAzA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2909,7 +2909,7 @@
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"make-dir": "^5.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"source-map": "~0.6.0"
@@ -3191,18 +3191,17 @@
"license": "MIT"
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-5.1.0.tgz",
"integrity": "sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mathjs": {
@@ -3491,17 +3490,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
@@ -3861,17 +3849,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/sharp": {
"version": "0.35.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.1.tgz",

View File

@@ -34,7 +34,7 @@
"browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1",
"less": "^4.6.4",
"less": "^4.6.6",
"mathjs": "^15.2.0",
"sharp": "~0.35.1",
"sort-package-json": "^4.0.0",

View File

@@ -1,8 +0,0 @@
.. _aol engine:
===
AOL
===
.. automodule:: searx.engines.aol
:members:

View File

@@ -2,16 +2,16 @@ mock==5.2.0
nose2[coverage_plugin]==0.16.0
cov-core==1.15.0
black==25.9.0
pylint==4.0.5
pylint==4.0.6
splinter==0.21.0
selenium==4.44.0
selenium==4.45.0
Sphinx==8.2.3;python_version <= "3.11"
Sphinx==9.1.0; python_version > "3.11"
sphinx-issues==6.0.0
sphinx-jinja==2.0.2
sphinx-tabs==3.5.0
furo==2025.12.19
sphinxcontrib-programoutput==0.19
sphinxcontrib-programoutput==0.20
sphinx-autobuild==2025.8.25
sphinx-notfound-page==1.1.0
myst-parser==5.0.0
@@ -24,5 +24,5 @@ docutils>=0.21.2;python_version <= "3.11"
docutils>=0.22.4; python_version > "3.11"
parameterized==0.9.0
granian[reload]==2.7.6
basedpyright==1.39.7
basedpyright==1.39.8
types-lxml==2026.2.16

View File

@@ -1,4 +1,4 @@
certifi==2026.5.20
certifi==2026.6.17
babel==2.18.0
flask-babel==4.0.0
flask==3.1.3

View File

@@ -1,210 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""AOL supports WEB, image, and video search. Internally, it uses the Bing
index.
AOL doesn't seem to support setting the language via request parameters, instead
the results are based on the URL. For example, there is
- `search.aol.com <https://search.aol.com>`_ for English results
- `suche.aol.de <https://suche.aol.de>`_ for German results
However, AOL offers its services only in a few regions:
- en-US: search.aol.com
- de-DE: suche.aol.de
- fr-FR: recherche.aol.fr
- en-GB: search.aol.co.uk
- en-CA: search.aol.ca
In order to still offer sufficient support for language and region, the `search
keywords`_ known from Bing, ``language`` and ``loc`` (region), are added to the
search term (AOL is basically just a proxy for Bing).
.. _search keywords:
https://support.microsoft.com/en-us/topic/advanced-search-keywords-ea595928-5d63-4a0b-9c6b-0b769865e78a
"""
from urllib.parse import urlencode, unquote_plus
import typing as t
from lxml import html
from dateutil import parser
from searx.result_types import EngineResults
from searx.utils import eval_xpath_list, eval_xpath, extract_text
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.aol.com",
"wikidata_id": "Q27585",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
categories = ["general"]
search_type = "search" # supported: search, image, video
paging = True
safesearch = True
time_range_support = True
results_per_page = 10
base_url = "https://search.aol.com"
time_range_map = {"day": "1d", "week": "1w", "month": "1m", "year": "1y"}
safesearch_map = {0: "p", 1: "r", 2: "i"}
enable_http2 = False
def init(_):
if search_type not in ("search", "image", "video"):
raise ValueError(f"unsupported search type {search_type}")
def request(query: str, params: "OnlineParams") -> None:
language, region = (params["searxng_locale"].split("-") + [None])[:2]
if language and language != "all":
query = f"{query} language:{language}"
if region:
query = f"{query} loc:{region}"
args: dict[str, str | int | None] = {
"q": query,
"b": params["pageno"] * results_per_page + 1, # page is 1-indexed
"pz": results_per_page,
}
if params["time_range"]:
args["fr2"] = "time"
args["age"] = params["time_range"]
else:
args["fr2"] = "sb-top-search"
params["cookies"]["sB"] = f"vm={safesearch_map[params['safesearch']]}"
params["url"] = f"{base_url}/aol/{search_type}?{urlencode(args)}"
logger.debug(params)
def _deobfuscate_url(obfuscated_url: str) -> str | None:
# URL looks like "https://search.aol.com/click/_ylt=AwjFSDjd;_ylu=JfsdjDFd/RV=2/RE=1774058166/RO=10/RU=https%3a%2f%2fen.wikipedia.org%2fwiki%2fTree/RK=0/RS=BP2CqeMLjscg4n8cTmuddlEQA2I-" # pylint: disable=line-too-long
if not obfuscated_url:
return None
for part in obfuscated_url.split("/"):
if part.startswith("RU="):
return unquote_plus(part[3:])
# pattern for de-obfuscating URL not found, fall back to Yahoo's tracking link
return obfuscated_url
def _general_results(doc: html.HtmlElement) -> EngineResults:
res = EngineResults()
for result in eval_xpath_list(doc, "//div[@id='web']//ol/li[not(contains(@class, 'first'))]"):
obfuscated_url = extract_text(eval_xpath(result, ".//h3/a/@href"))
if not obfuscated_url:
continue
url = _deobfuscate_url(obfuscated_url)
if not url:
continue
res.add(
res.types.MainResult(
url=url,
title=extract_text(eval_xpath(result, ".//h3/a")) or "",
content=extract_text(eval_xpath(result, ".//div[contains(@class, 'compText')]")) or "",
thumbnail=extract_text(eval_xpath(result, ".//a[contains(@class, 'thm')]/img/@data-src")) or "",
)
)
return res
def _video_results(doc: html.HtmlElement) -> EngineResults:
res = EngineResults()
for result in eval_xpath_list(doc, "//div[contains(@class, 'results')]//ol/li"):
obfuscated_url = extract_text(eval_xpath(result, ".//a/@href"))
if not obfuscated_url:
continue
url = _deobfuscate_url(obfuscated_url)
if not url:
continue
published_date_raw = extract_text(eval_xpath(result, ".//div[contains(@class, 'v-age')]"))
try:
published_date = parser.parse(published_date_raw or "")
except parser.ParserError:
published_date = None
res.add(
res.types.LegacyResult(
{
"template": "videos.html",
"url": url,
"title": extract_text(eval_xpath(result, ".//h3")),
"content": extract_text(eval_xpath(result, ".//div[contains(@class, 'compText')]")),
"thumbnail": extract_text(eval_xpath(result, ".//img[contains(@class, 'thm')]/@src")),
"length": extract_text(eval_xpath(result, ".//span[contains(@class, 'v-time')]")),
"publishedDate": published_date,
}
)
)
return res
def _image_results(doc: html.HtmlElement) -> EngineResults:
res = EngineResults()
for result in eval_xpath_list(doc, "//section[@id='results']//ul/li"):
obfuscated_url = extract_text(eval_xpath(result, "./a/@href"))
if not obfuscated_url:
continue
url = _deobfuscate_url(obfuscated_url)
if not url:
continue
res.add(
res.types.LegacyResult(
{
"template": "images.html",
# results don't have an extra URL, only the image source
"url": url,
"title": extract_text(eval_xpath(result, ".//a/@aria-label")),
"thumbnail_src": extract_text(eval_xpath(result, ".//img/@src")),
"img_src": url,
}
)
)
return res
def response(resp: "SXNG_Response") -> EngineResults:
doc = html.fromstring(resp.text)
match search_type:
case "search":
results = _general_results(doc)
case "image":
results = _image_results(doc)
case "video":
results = _video_results(doc)
case _:
raise ValueError("unsupported search type")
for suggestion in eval_xpath_list(doc, ".//ol[contains(@class, 'searchRightBottom')]//table//a"):
results.add(results.types.LegacyResult({"suggestion": extract_text(suggestion)}))
return results

118
searx/engines/findfiles.py Normal file
View File

@@ -0,0 +1,118 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FindFiles.net_ is a Germany-based file search engine.
FindFiles.net_ is a specialized file search engine designed to help you search
files online with precision. Unlike traditional search engines that mainly index
web pages, FindFiles focuses on finding real files on the internet - including
PDFs, documents, archives, videos, datasets, and more.
.. _FindFiles.net: https://findfiles.net
"""
from os.path import basename
from urllib.parse import urlencode
import typing as t
from lxml import html
from searx.result_types import EngineResults
from searx.utils import extract_text, eval_xpath, eval_xpath_list
if t.TYPE_CHECKING:
from extended_types import SXNG_Response
from search.processors import OnlineParams
about = {
"website": "https://findfiles.net",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
base_url = "https://findfiles.net"
categories = ["files"]
paging = True
safeserach = True
safesearch_map = {
0: "contentguard.off",
1: "contentguard.moderate",
2: "contentguard.strict",
}
FindFilesCategory = t.Literal[
"all",
"document",
"text",
"image",
"audio",
"video",
]
FINDFILES_CATEGORIES = t.get_args(FindFilesCategory)
findfiles_categ: FindFilesCategory = "all"
"""Category to search in."""
def setup(_: dict[str, t.Any]) -> bool:
if findfiles_categ not in FINDFILES_CATEGORIES:
raise ValueError("invalid category: %s" % findfiles_categ)
return True
def request(query: str, params: "OnlineParams") -> None:
args = {
"query": query,
"contentguard": safesearch_map[params["safesearch"]],
"page": params["pageno"],
}
# the language in the path doesn't change anything about the results, it
# only changes the UI
params["url"] = f"{base_url}/en/serp/{findfiles_categ}/?{urlencode(args)}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
dom = html.fromstring(resp.text)
if findfiles_categ == "image":
for result in eval_xpath_list(
dom, "//div[contains(@class, 'image-mosaic')]/div[contains(@class, 'image-item')]"
):
res.add(
res.types.Image(
url=extract_text(eval_xpath(result, ".//div[contains(@class, 'caption')]/a/@href")) or "",
title=extract_text(eval_xpath(result, ".//div[contains(@class, 'caption')]/a")) or "",
thumbnail_src=extract_text(eval_xpath(result, ".//img/@src")) or "",
)
)
elif findfiles_categ == "video":
for result in eval_xpath_list(
dom, "//div[contains(@class, 'video-mosaic')]/div[contains(@class, 'video-item')]"
):
video_src = extract_text(eval_xpath(result, ".//video/@src")) or ""
res.add(
res.types.LegacyResult(
template="videos.html",
url=video_src,
title=extract_text(eval_xpath(result, ".//div[contains(@class, 'caption')]/span")) or "",
iframe_src=video_src or "",
)
)
else:
for result in eval_xpath_list(dom, "//ol/li[contains(@class, 'result-item')]/article"):
filename = basename(extract_text(eval_xpath(result, ".//h3")) or "")
res.add(
res.types.File(
url=extract_text(eval_xpath(result, ".//h3/a/@href")) or "",
title=filename,
content=" ".join(extract_text(el) or "" for el in eval_xpath_list(result, "./div/span")),
filename=filename,
size=extract_text(eval_xpath(result, "(.//span[@id])[1]")) or "",
embedded=extract_text(eval_xpath(result, ".//audio/@src")) or "",
)
)
return res

127
searx/engines/giphy.py Normal file
View File

@@ -0,0 +1,127 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Giphy (images)"""
import random
from urllib.parse import urlencode
import re
import typing as t
from lxml import html
from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException
from searx.network import get
from searx.result_types import EngineResults
from searx.result_types.image import ImageRef
from searx.utils import eval_xpath_list, humanize_bytes
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://giphy.com",
"wikidata_id": "Q17054335",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://giphy.com"
api_url = "https://api.giphy.com"
categories = ["images"]
paging = True
page_size = 15
GiphyCategs = t.Literal["gifs", "stickers", "clips"]
giphy_categ: GiphyCategs = "gifs"
"""Giphy category to search in."""
CACHE: EngineCache
"""Cache for storing the extracted api key."""
_GIPHY_API_KEY_RE = re.compile(r"[Aa]piKey\s*:\s*\"(\w+)\"")
def setup(engine_settings: dict[str, str]) -> bool:
if giphy_categ not in t.get_args(GiphyCategs):
raise ValueError("invalid category: %s" % giphy_categ)
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return True
def _get_api_key() -> str:
"""
Extract the Giphy API key from the JavaScript code. There are different API keys
(e.g. for mobile, desktop, ...), so we just pick a random one of these.
"""
cached = CACHE.get("api_key")
if cached:
return cached
homepage_resp = get(base_url)
homepage_doc = html.fromstring(homepage_resp.text)
for script_src in eval_xpath_list(homepage_doc, "//script[contains(@src, 'layout')]/@src"):
script_resp = get(base_url + script_src)
api_keys = _GIPHY_API_KEY_RE.findall(script_resp.text)
if api_keys:
api_key = random.choice(api_keys)
CACHE.set("api_key", api_key, expire=60 * 60 * 6) # 6 hours
return api_key
raise SearxEngineAPIException("failed to extract api keys")
def request(query: str, params: "OnlineParams") -> None:
args = {
"q": query,
"api_key": _get_api_key(),
"limit": page_size,
"offset": (params["pageno"] - 1) * page_size,
"type": giphy_categ,
}
params["url"] = f"{api_url}/v1/{giphy_categ}/search?{urlencode(args)}"
def response(resp: "SXNG_Response"):
res = EngineResults()
result: dict[str, t.Any]
for result in resp.json()["data"]:
img = result['images']['original']
formats = [
ImageRef(url=img["mp4"], subtype="mp4"), # type: ignore
ImageRef(url=img["webp"], subtype="webp"), # type: ignore
]
thumb = (
result["images"].get("downsized")
or result["images"].get("downsized_medium")
or result["images"].get("downsized_small")
or result["images"].get("downsized_large")
)
res.add(
res.types.Image(
title=result["title"],
content=", ".join(result.get("tags", [])),
url=result["url"],
thumbnail_src=thumb.get("url") or img["url"],
img_src=img["url"],
resolution=f"{img['width']}x{img['height']}",
img_format="GIF",
formats=formats,
author=result["username"],
filesize=humanize_bytes(int(img["size"])),
source=result.get("source_tld") or "",
)
)
return res

88
searx/engines/iseek.py Normal file
View File

@@ -0,0 +1,88 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""iseek_ is a search engine by the AI company Vantage Labs LLC,
that focuses on medical and educational applicances.
Although it's an AI company, it doesn't include any AI stuff in its results.
.. _iseek : https://www.iseek.ai/
"""
import base64
from hashlib import sha256
import typing as t
from urllib.parse import urlencode
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
from searx.extended_types import SXNG_Response
about = {
"website": 'https://www.iseek.com',
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
categories = ["general"]
paging = True
base_url = "https://api.iseek.com"
page_size = 10
def _get_new_token(query: str, pageno: int) -> str:
"""Create a new ``qToken``. This reduced the time for fetching subsequent pages
from 4 seconds to 200ms when testing."""
# The website uses a random value as qToken for the first page. For our use case,
# it's easier if the qToken can be deterministically re-calculated based on the search query,
# so that we can the same result when calling _get_new_token for the second, third, ... page
#
# var qToken = Math.ceil(Math.random() * parseInt("ZZZZ", 36)).toString(36);
# while (qToken.length < 4) qToken = '0' + qToken;
# qToken = qToken + "_" + pageno
query_hash = sha256(query.encode()).digest()
hash_start = base64.b64encode(query_hash).decode()[0:4]
return f"{hash_start}_{pageno}"
def request(query: str, params: "OnlineParams"):
offset = (params["pageno"] - 1) * page_size
# always seems to find 20 results max
if offset >= 20:
params["url"] = None
return
args = {
"q": query,
"key": "core-web",
"num": str(page_size),
"off": offset,
"rSort": "__metasearch_score_d:desc",
# it supports many more fields, but none of them are really relevant
"names": "title_t,content_txt,url_s",
"qNames": "title_t",
"qToken": _get_new_token(query, params["pageno"]),
}
params["url"] = f"{base_url}/search?{urlencode(args)}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
for group in resp.json()["data"]:
group: dict[str, t.Any]
for result in group["doclist"]["docs"]:
result: dict[str, str]
res.add(
res.types.MainResult(
url=result["url_s"],
title=result["title_t"],
content="".join(result["content_txt"]),
)
)
return res

View File

@@ -44,7 +44,7 @@ about = {
base_url = "https://api2.marginalia-search.com"
safesearch = True
categories = ["general"]
categories = ["general", "blogs"]
paging = True
results_per_page = 20
api_key = None

View File

@@ -0,0 +1,107 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Startpagina is a Netherlands search engine by `Kompas`_. It takes all its
results from Google.
.. _Kompas: https://www.kompaspublishing.nl/
"""
import typing as t
from urllib.parse import urlencode
from dateutil import parser
from searx.utils import format_duration
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://startpagina.nl",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
language = "ne"
paging = True
safesearch = True
categories = ["general"]
startpagina_categ = "web"
"""Category to search in. Can be either "web", "images", "videos" or "news"."""
page_size = 10
api_url = "https://search.kompas.services"
def init(_):
if startpagina_categ not in ("web", "images", "videos", "news"):
raise ValueError("invalid search type: %s" % startpagina_categ)
def request(query: str, params: "OnlineParams") -> None:
args = {"q": query, "page_size": page_size, "page": params["pageno"]}
params["url"] = f"{api_url}/api/v2/search/{startpagina_categ}/?{urlencode(args)}"
def response(resp: "SXNG_Response"):
res = EngineResults()
json_resp = resp.json()
for result in json_resp["results"]:
if startpagina_categ == "web":
res.add(
res.types.MainResult(
url=result["original_url"],
title=result["title"],
content=result["description"],
)
)
elif startpagina_categ == "news":
publishedDate = None
try:
publishedDate = parser.parse(result["date"])
except parser.ParserError:
pass
res.add(
res.types.MainResult(
url=result["original_url"],
title=result["title"],
content=result["description"],
thumbnail=result["image"]["thumbnail_url"],
publishedDate=publishedDate,
)
)
elif startpagina_categ == "videos":
res.add(
res.types.LegacyResult(
template="videos.html",
url=result["original_url"],
title=result["title"],
content=result["description"],
thumbnail=result["video"]["thumbnail_url"],
length=format_duration(result["video"]["duration"]),
)
)
elif startpagina_categ == "images":
res.add(
res.types.Image(
url=result["original_url"],
title=result["title"],
content=result["description"],
thumbnail_src=result["image"]["thumbnail_url"],
resolution=f"{result['image']['width']}x{result['image']['height']}",
)
)
for related in json_resp["related_searches"]:
res.add(res.types.LegacyResult(suggestion=related["query"]))
return res

162
searx/engines/tusksearch.py Normal file
View File

@@ -0,0 +1,162 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Tusksearch_ is an American search engine that claims to fight censorship.
Its search results are (at least partially) from Brave.
.. _Tusksearch: https://tusksearch.com/about
"""
from json import loads
import random
import typing as t
from urllib.parse import urlencode
from dateutil import parser
from searx.exceptions import SearxEngineAPIException
from searx.network import get
from searx.utils import html_to_text
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://tusksearch.com",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
paging = True
categories = ["general"]
tusk_categ = "web"
"""Category to search in. Can be either "web", "images", "videos" or "news"."""
api_url = "https://api.tusksearch.com"
def init(_):
if tusk_categ not in ("web", "images", "videos", "news"):
raise ValueError("invalid search type: %s" % tusk_categ)
def _obtain_x_sid() -> tuple[str, str]:
"""
The session ID ("sid") is encoded as a byte array in ``embed.js``.
It is only valid for exactly one request, so we can't cache it.
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")
if not resp.ok:
raise SearxEngineAPIException("failed to obtain request x-sid token")
# data is prefixed by 'var x='
data_array = loads(resp.text[6:])
def _byte_array_to_ascii(text: list[int]) -> str:
"""
Converts a byte array (e.g. [81, 101, 97, 114, 88, 78, 71]) to the ASCII
string representation (e.g. "SearXNG").
"""
return "".join([chr(x) for x in text])
x_sid_header = _byte_array_to_ascii(data_array[3])
x_sid_value = _byte_array_to_ascii(data_array[4])
return x_sid_header, x_sid_value
def request(query: str, params: "OnlineParams") -> None:
# images don't support pagination, news and videos only support two pages
if tusk_categ == "images" and params["pageno"] > 1 or tusk_categ in ("news", "videos") and params["pageno"] > 2:
params["url"] = None
return
args = {
"q": query,
"p": params["pageno"],
"l": "center", # political direction: "left", "center" or "right"
}
if tusk_categ == "images":
params["url"] = f"{api_url}/Search/Image?{urlencode(args)}"
else:
# web response also contains news and videos
params["url"] = f"{api_url}/Search/Web?{urlencode(args)}"
x_sid_header, x_sid_value = _obtain_x_sid()
params["headers"] = {
x_sid_header: x_sid_value,
# required - we send a random longitude and latitude instead of the actual user location
'x-lon': str(random.random() * 90),
'x-lat': str(random.random() * 90),
}
def response(resp: "SXNG_Response"):
res = EngineResults()
json_resp = resp.json()["results"]
if tusk_categ == "web":
for result in (json_resp.get("web") or {}).get("results", []):
res.add(
res.types.MainResult(
url=result["url"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
thumbnail=(result["thumbnail"] or {}).get("src") or "",
)
)
elif tusk_categ == "news":
for result in (json_resp.get("news") or {}).get("results", []):
publishedDate = None
try:
publishedDate = parser.parse(result["age"])
except parser.ParserError:
pass
res.add(
res.types.MainResult(
url=result["url"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
thumbnail=result["thumbnail"]["src"],
publishedDate=publishedDate,
)
)
elif tusk_categ == "videos":
for result in (json_resp.get("videos") or {}).get("results", []):
publishedDate = None
try:
publishedDate = parser.parse(result["age"])
except parser.ParserError:
pass
res.add(
res.types.LegacyResult(
template="videos.html",
url=result["url"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
thumbnail=result["thumbnail"]["src"],
publishedDate=publishedDate,
length=result["video"].get("duration"),
)
)
elif tusk_categ == "images":
for result in json_resp:
res.add(
res.types.Image(
url=result["url"],
title=html_to_text(result["title"]),
img_src=result["properties"]["url"],
thumbnail_src=result["thumbnail"]["src"],
)
)
return res

View File

@@ -444,27 +444,6 @@ engines:
shortcut: conda
disabled: true
- name: aol
engine: aol
search_type: search
categories: [general]
shortcut: aol
disabled: true
- name: aol images
engine: aol
search_type: image
categories: [images]
shortcut: aoli
disabled: true
- name: aol videos
engine: aol
search_type: video
categories: [videos]
shortcut: aolv
disabled: true
- name: arch linux wiki
engine: archlinux
shortcut: al
@@ -688,6 +667,26 @@ engines:
disabled: true
inactive: true
- name: cl0q
engine: json_engine
shortcut: cl
categories: general
paging: true
first_page_num: 0
page_size: 20
search_url: https://cl0q.com/search?q={query}&limit=20&offset={pageno}
results_query: results
url_query: domain
url_prefix: https://
title_query: title
content_query: description
disabled: true
inactive: true
about:
website: https://cl0q.com
description: "Open source network for searching domains"
results: JSON
- name: cloudflareai
engine: cloudflareai
shortcut: cfai
@@ -978,6 +977,34 @@ engines:
shortcut: fd
disabled: true
- name: findfiles
engine: findfiles
findfiles_categ: all
categories: files
shortcut: fif
disabled: true
- name: findfiles images
engine: findfiles
findfiles_categ: image
categories: images
shortcut: fifi
disabled: true
- name: findfiles videos
engine: findfiles
findfiles_categ: video
categories: videos
shortcut: fifv
disabled: true
- name: findfiles music
engine: findfiles
findfiles_categ: audio
categories: music
shortcut: fifm
disabled: true
- name: findthatmeme
engine: findthatmeme
shortcut: ftm
@@ -1114,6 +1141,11 @@ engines:
search_type: text
timeout: 10
- name: giphy
engine: giphy
shortcut: gif
disabled: true
- name: gitlab
engine: gitlab
base_url: https://gitlab.com
@@ -1295,6 +1327,13 @@ engines:
require_api_key: false
results: JSON
- name: iseek
engine: iseek
shortcut: isk
timeout: 4
disabled: true
inactive: true
- name: il post
engine: il_post
shortcut: pst
@@ -1383,6 +1422,23 @@ engines:
# api_key: "" # required
# kagi_categ: videos
- name: kozmonavt
engine: xpath
paging: true
# pageno query param key is U+1D5D1 (looks like an "x")
search_url: https://kozmonavt.su/s?q={query}&%F0%9D%97%91={pageno}
shortcut: koz
disabled: true
# inactive: true
results_xpath: //div[contains(@class, 'list')]/section
url_xpath: concat('https://', substring-after(.//a/@href, '?q='))
title_xpath: .//a
content_xpath: .//div[contains(@class, 'snip')]
about:
website: https://kozmonavt.su
description: "Web site directory with its own index"
results: HTML
- name: jisho
engine: jisho
shortcut: js
@@ -1400,6 +1456,22 @@ engines:
shortcut: kc
timeout: 4.0
- name: kukei
engine: xpath
categories: [general, blogs]
search_url: https://kukei.eu/?q={query}
shortcut: kuk
disabled: true
inactive: true
results_xpath: //ul/li[contains(@class, "result-item-first-level")]
url_xpath: .//a/@href
title_xpath: .//h3
content_xpath: .//p
about:
website: https://kukei.eu
description: "Curated search for the small web"
results: HTML
- name: lemmy communities
engine: lemmy
lemmy_type: Communities
@@ -2033,7 +2105,7 @@ engines:
- name: rawweb
engine: json_engine
shortcut: rw
categories: general
categories: [general, blogs]
paging: true
search_url: 'https://api.rawweb.org/api/search?keyword={query}&page={pageno}&lang=*'
results_query: data
@@ -2088,7 +2160,7 @@ engines:
- name: searchmysite
engine: xpath
shortcut: sms
categories: general
categories: [general, blogs]
paging: true
search_url: https://searchmysite.net/search/?q={query}&page={pageno}
results_xpath: //div[contains(@class,'search-result')]
@@ -2404,6 +2476,35 @@ engines:
- 5000
inactive: true
- name: tusksearch
engine: tusksearch
shortcut: tu
tusk_categ: web
categories: general
disabled: true
- name: tusksearch images
engine: tusksearch
shortcut: tui
paging: false
tusk_categ: images
categories: images
disabled: true
- name: tusksearch videos
engine: tusksearch
shortcut: tuv
tusk_categ: videos
categories: videos
disabled: true
- name: tusksearch news
engine: tusksearch
shortcut: tun
tusk_categ: news
categories: news
disabled: true
# tmp suspended - too slow, too many errors
# - name: urbandictionary
# engine : xpath
@@ -2417,6 +2518,24 @@ engines:
engine: unsplash
shortcut: us
- name: unobtanium
engine: xpath
shortcut: uno
categories: [general, blogs]
paging: true
first_page_num: 0
search_url: https://unobtanium.rocks/search?search={query}&page={pageno}
results_xpath: //ul[contains(@class, 'search-results')]/li
url_xpath: .//a/@href
title_xpath: .//a/b
content_xpath: .//p[2]
disabled: true
inactive: true
about:
website: https://unobtanium.rocks
description: "Personal websites focused search engine."
results: HTML
- name: yandex
engine: yandex
categories: general
@@ -2476,7 +2595,7 @@ engines:
url_query: URL
title_query: Title
content_query: Snippet
categories: [general, web]
categories: [general, blogs]
shortcut: wib
disabled: true
about:
@@ -2901,6 +3020,38 @@ engines:
disabled: true
inactive: true
- name: startpagina
engine: startpagina
shortcut: spnl
startpagina_categ: web
categories: general
disabled: true
inactive: true
- name: startpagina images
engine: startpagina
shortcut: spnli
startpagina_categ: images
categories: images
disabled: true
inactive: true
- name: startpagina videos
engine: startpagina
shortcut: spnlv
startpagina_categ: videos
categories: videos
disabled: true
inactive: true
- name: startpagina news
engine: startpagina
shortcut: spnln
startpagina_categ: news
categories: news
disabled: true
inactive: true
- name: swisscows
engine: swisscows
categories: general
@@ -3023,6 +3174,22 @@ engines:
shortcut: wttr
timeout: 9.0
- name: xonaly
engine: xpath
search_url: https://xonaly.com/?query={query}
categories: general
shortcut: xo
disabled: true
inactive: true
results_xpath: //div[contains(@class, 'results-block')]/ul/li
url_xpath: ./div[contains(@class, 'result-title')]/a/@href
title_xpath: ./div[contains(@class, 'result-title')]/a
content_xpath: ./p[contains(@class, 'excerpt')]
about:
website: https://xonaly.com
description: "Canadian search engine with its own index"
results: HTML
- name: zapmeta
engine: xpath
shortcut: zpm