39 Commits

Author SHA1 Message Date
searxng-bot
c6707dc0b6 [data] update searx.data - update_engine_traits.py 2026-06-29 00:44:13 +00:00
Bnyro
8605230eb8 [fix] resulthunter: images in search results are not clickable 2026-06-28 22:31:01 +02:00
Bnyro
a54722e692 [feat] tiger: add support for related search terms / suggestions 2026-06-28 22:30:48 +02:00
Bnyro
7e9a6c2861 [fix] tiger: captcha paths have changed 2026-06-28 22:30:48 +02:00
dependabot[bot]
357662d86d [upd] github-actions: Bump docker/scout-action from 1.21.0 to 1.22.0 (#6317)
Bumps [docker/scout-action](https://github.com/docker/scout-action) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/docker/scout-action/releases)
- [Commits](cd72f264be...7520205ff6)

---
updated-dependencies:
- dependency-name: docker/scout-action
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:19:29 +02:00
dependabot[bot]
bfa76e4cd9 [upd] github-actions: Bump actions/cache from 5.0.5 to 6.0.0 (#6316)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.0.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](27d5ce7f10...2c8a9bd745)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:19:14 +02:00
dependabot[bot]
a9d49a3349 [upd] github-actions: Bump actions/setup-python from 6.2.0 to 6.3.0 (#6315)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](a309ff8b42...ece7cb06ca)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:18:54 +02:00
github-actions[bot]
f8ffbf36f9 [l10n] update translations from Weblate (#6320)
6de4d96dd - 2026-06-23 - code_gremlin <code_gremlin@noreply.codeberg.org>
2026-06-26 10:24:36 +02:00
Markus Heiser
e3126b89e6 [mod] update_engine_descriptions.py - revision of the script (#6309)
* [mod] update_engine_descriptions.py - revision of the script (#6309)

The script was a bit outdated, type hints were missing, and the strings were
converted to double quotation marks

The `descriptions.items()` are sorted to avoid assigning the references to
seen_descriptions randomly (should reduce unnecessary diffs in the future).

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>

* [data] update searx.data - update_engine_descriptions.py (#6309)

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>

---------

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-24 12:10:28 +02:00
Markus Heiser
f9f3d089ec [fix] engine anaconda - add missing about configuration (#6308)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-24 10:45:40 +02:00
Hu Butui
e3713717f2 [fix] engines: remove terminated chinaso media engines
Signed-off-by: Hu Butui <hot123tea123@gmail.com>
2026-06-23 18:20:04 +02:00
Bnyro
952896d29e [feat] image results: automatically guess mimetype based on path 2026-06-22 12:46:22 +02:00
Bnyro
4cc32b2457 [fix] kozmonavt: remove pagination and set to inactive by default
Pagination requires a different nextpage query parameter each
day as it seems, so it's not possible to implement this in the Xpath
engine.
2026-06-22 10:06:09 +02:00
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
Bnyro
fd42d4fda1 [fix] chatnoir: don't re-use/cache session keys
They're invalidated very quickly, so even caching them for
60 seconds results in a lot of unauthorized access errors.
2026-06-20 21:52:14 +02:00
dependabot[bot]
5c38d2feab [upd] web-client (simple): Bump @types/node in /client/simple (#6290)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.3 to 26.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 16:58:47 +02:00
dependabot[bot]
38b678c493 [upd] github-actions: Bump actions/checkout from 6.0.3 to 7.0.0 (#6288)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](df4cb1c069...9c091bb21b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 16:58:27 +02:00
github-actions[bot]
fe1848673f [l10n] update translations from Weblate (#6293)
0f1c1d570 - 2026-06-18 - lugged9922 <lugged9922@noreply.codeberg.org>
81d208307 - 2026-06-18 - Raithlin <raithlin@noreply.codeberg.org>
bf09069e8 - 2026-06-17 - return42 <return42@noreply.codeberg.org>
c010ba929 - 2026-06-17 - return42 <return42@noreply.codeberg.org>
f92ba4e98 - 2026-06-17 - M Alif fadlan <maliffadlan@gmail.com>
442e504e2 - 2026-06-17 - return42 <return42@noreply.codeberg.org>
e2ffb2275 - 2026-06-17 - return42 <return42@noreply.codeberg.org>
cc26d0794 - 2026-06-17 - return42 <return42@noreply.codeberg.org>
9639f4e84 - 2026-06-17 - return42 <return42@noreply.codeberg.org>
63059d4e7 - 2026-06-15 - AndersNordh <andersnordh@noreply.codeberg.org>
460c5260f - 2026-06-15 - kratos <makesocialfoss32@keemail.me>
b212184d9 - 2026-06-16 - ghose <ghose@noreply.codeberg.org>
c9ac8e6d7 - 2026-06-15 - AndersNordh <andersnordh@noreply.codeberg.org>
cc1f5ab59 - 2026-06-15 - Fjuro <fjuro@noreply.codeberg.org>
84f985a9f - 2026-06-14 - Outbreak2096 <outbreak2096@noreply.codeberg.org>
bdb7e25bc - 2026-06-13 - SomeTr <sometr@noreply.codeberg.org>
c3eac4c37 - 2026-06-14 - Stephan-P <stephan-p@noreply.codeberg.org>
d94ab494b - 2026-06-13 - Priit Jõerüüt <jrtcdbrg@noreply.codeberg.org>
3387bab27 - 2026-06-13 - gallegonovato <gallegonovato@noreply.codeberg.org>
2026-06-19 15:11:48 +02:00
Bnyro
8b10095e8a [fix] settings.yml: explicitely set category for xpath engines (ayo, gabanza, zapmeta, abcnyheter) (#6282) 2026-06-19 09:10:27 +02:00
Jayant Sharma
b5ef7ec8f3 [fix] calculator: move math.parse inside try-catch (#6278) (#6280)
* [fix] calculator: move math.parse inside try-catch (#6278)

* build static

---------

Co-authored-by: Ivan Gabaldon <igabaldon@inetol.net>
2026-06-18 17:36:47 +02:00
Bnyro
bd73cc09ea [feat] engines: add support for search.ch/web (Swiss) 2026-06-18 14:02:52 +02:00
Butui Hu
4dfdc822cf [fix] engines: chinaso: handle empty upstream results gracefully (#6266)
Signed-off-by: Hu Butui <hot123tea123@gmail.com>
2026-06-17 19:36:22 +02:00
Ivan Gabaldon
502c820a25 [fix] container: setup minimal (#6268)
Start minimal, use defaults, and extend later on. The templates are no longer
checked for changes, which was confusing and annoying after a while.

See: https://github.com/searxng/searxng/issues/6261#issuecomment-4716008282
2026-06-16 15:32:47 +02:00
Markus Heiser
4fb49b4498 [chore] add DeprecationWarning for obsolete engine.about.language property (#6265)
The old property should still be supported for a transitional period; the
reasons for this can be seen from the discussion in [1] / the further procedure
is also discussed there.

[1] https://github.com/searxng/searxng/issues/6261

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-16 10:31:21 +02:00
Markus Heiser
cf1410af8d [fix] set language_support for engines with languages in traits (#6258)
In the past, the engine option ``language_support`` was not consistently
maintained; with this patch, a ValueError is now thrown if an engine has
languages in its traits but language_support is not set to True.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-15 10:52:00 +02:00
Markus Heiser
6c9dcd4242 [chore] complete and normalize the attributes of engine objects (#6258)
Drop outdated engine attributes: supported_languages, language_aliases

Complete, normalize and document the type definitions for the engine-module and
engine-class.

For the ``engine.about`` section of the configuration, a type check is performed
based on structure ``searx.enginelib.EngineAbout``.

The property ``engine.about.language`` no longer exists; existing values have
been migrated to ``engine.language``.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-15 10:52:00 +02:00
216 changed files with 5744 additions and 5290 deletions

View File

@@ -1,5 +1,6 @@
* *
!container/*.template.*
!container/entrypoint.sh !container/entrypoint.sh
!searx/** !searx/**
!requirements*.txt !requirements*.txt

View File

@@ -73,18 +73,18 @@ jobs:
# yamllint enable rule:line-length # yamllint enable rule:line-length
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -96,7 +96,7 @@ jobs:
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
- name: Setup cache container - name: Setup cache container
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}" key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -141,7 +141,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
@@ -175,7 +175,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"

View File

@@ -41,17 +41,17 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -32,18 +32,18 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -34,17 +34,17 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -62,12 +62,12 @@ jobs:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
@@ -77,13 +77,13 @@ jobs:
node-version-file: "./.nvmrc" node-version-file: "./.nvmrc"
- name: Setup cache Node.js - name: Setup cache Node.js
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}" key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
path: "./client/simple/node_modules/" path: "./client/simple/node_modules/"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -35,18 +35,18 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -83,18 +83,18 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -24,12 +24,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Sync GHCS from Docker Scout - name: Sync GHCS from Docker Scout
uses: docker/scout-action@cd72f264beff1cd72735de31148b9d3244a0234a # v1.21.0 uses: docker/scout-action@7520205ff60037fdc436b40b6a1d1e55a839ec2d # v1.22.0
with: with:
organization: "searxng" organization: "searxng"
dockerhub-user: "${{ secrets.DOCKER_USER }}" dockerhub-user: "${{ secrets.DOCKER_USER }}"

View File

@@ -16,11 +16,11 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.0", "@biomejs/biome": "2.5.0",
"@types/node": "^25.9.3", "@types/node": "^26.0.0",
"browserslist": "^4.28.2", "browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.1",
"less": "^4.6.4", "less": "^4.6.6",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.1", "sharp": "~0.35.1",
"sort-package-json": "^4.0.0", "sort-package-json": "^4.0.0",
@@ -1570,13 +1570,13 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.9.3", "version": "26.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
"integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": ">=7.24.0 <7.24.7" "undici-types": "~8.3.0"
} }
}, },
"node_modules/@types/pluralize": { "node_modules/@types/pluralize": {
@@ -2890,9 +2890,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/less": { "node_modules/less": {
"version": "4.6.4", "version": "4.6.6",
"resolved": "https://registry.npmjs.org/less/-/less-4.6.4.tgz", "resolved": "https://registry.npmjs.org/less/-/less-4.6.6.tgz",
"integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==", "integrity": "sha512-ooPSwQGQ2sVe8Dh1jVsbKKsRR2gd8lFK72BDkeSzjnD1T5aIHL65hCMfO0GVmtriKgDKrQv6xp9UrihUsWuAzA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -2909,7 +2909,7 @@
"errno": "^0.1.1", "errno": "^0.1.1",
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"image-size": "~0.5.0", "image-size": "~0.5.0",
"make-dir": "^2.1.0", "make-dir": "^5.1.0",
"mime": "^1.4.1", "mime": "^1.4.1",
"needle": "^3.1.0", "needle": "^3.1.0",
"source-map": "~0.6.0" "source-map": "~0.6.0"
@@ -3191,18 +3191,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "2.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-5.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "integrity": "sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": { "engines": {
"node": ">=6" "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/mathjs": { "node_modules/mathjs": {
@@ -3491,17 +3490,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/pluralize": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
@@ -3861,17 +3849,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/sharp": {
"version": "0.35.1", "version": "0.35.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.1.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.1.tgz",
@@ -4515,9 +4492,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.24.6", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },

View File

@@ -30,11 +30,11 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.0", "@biomejs/biome": "2.5.0",
"@types/node": "^25.9.3", "@types/node": "^26.0.0",
"browserslist": "^4.28.2", "browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.1",
"less": "^4.6.4", "less": "^4.6.6",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.1", "sharp": "~0.35.1",
"sort-package-json": "^4.0.0", "sort-package-json": "^4.0.0",

View File

@@ -77,9 +77,9 @@ export default class Calculator extends Plugin {
protected async run(): Promise<string | undefined> { protected async run(): Promise<string | undefined> {
const searchInput = getElement<HTMLInputElement>("q"); const searchInput = getElement<HTMLInputElement>("q");
const node = Calculator.math.parse(searchInput.value);
try { try {
const node = Calculator.math.parse(searchInput.value);
return `${node.toString()} = ${node.evaluate()}`; return `${node.toString()} = ${node.evaluate()}`;
} catch { } catch {
// not a compatible math expression // not a compatible math expression

View File

@@ -21,8 +21,6 @@ RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/ COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
ARG TIMESTAMP_SETTINGS="0"
RUN set -eux -o pipefail; \ RUN set -eux -o pipefail; \
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \ python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \
find ./searx/static/ -type f \ find ./searx/static/ -type f \
@@ -30,5 +28,4 @@ RUN set -eux -o pipefail; \
-exec gzip -9 -k {} + \ -exec gzip -9 -k {} + \
-exec brotli -9 -k {} + \ -exec brotli -9 -k {} + \
-exec gzip --test {}.gz + \ -exec gzip --test {}.gz + \
-exec brotli --test {}.br +; \ -exec brotli --test {}.br +
touch -c --date="@$TIMESTAMP_SETTINGS" ./searx/settings.yml

View File

@@ -77,43 +77,23 @@ volume_handler() {
setup_ownership "$target" "directory" setup_ownership "$target" "directory"
} }
# Handle configuration file updates setup() {
config_handler() { local template_settings="/usr/local/searxng/settings.template.yml"
local target="$1" local target_settings="$__SEARXNG_CONFIG_PATH/settings.yml"
local template="$2"
local new_template_target="$target.new"
# Create/Update the configuration file if [ ! -f "$target_settings" ]; then
if [ -f "$target" ]; then
setup_ownership "$target" "file"
if [ "$template" -nt "$target" ]; then
cp -pfT "$template" "$new_template_target"
cat <<EOF
...
... INFORMATION
... Update available for "$target"
... It is recommended to update the configuration file to ensure proper functionality
...
... New version placed at "$new_template_target"
... Please review and merge changes
...
EOF
fi
else
cat <<EOF cat <<EOF
... ...
... INFORMATION ... INFORMATION
... "$target" does not exist, creating from template... ... "$target_settings" does not exist, creating from template...
... ...
EOF EOF
cp -pfT "$template" "$target" cp -pfT "$template_settings" "$target_settings"
sed -i "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" "$target" sed -i "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" "$target_settings"
fi fi
check_file "$target" check_file "$target_settings"
} }
cat <<EOF cat <<EOF
@@ -124,8 +104,7 @@ EOF
volume_handler "$__SEARXNG_CONFIG_PATH" volume_handler "$__SEARXNG_CONFIG_PATH"
volume_handler "$__SEARXNG_DATA_PATH" volume_handler "$__SEARXNG_DATA_PATH"
# Check for files setup
config_handler "$__SEARXNG_SETTINGS_PATH" "/usr/local/searxng/searx/settings.yml"
# root only features # root only features
if [ "$(id -u)" -eq 0 ]; then if [ "$(id -u)" -eq 0 ]; then

View File

@@ -0,0 +1,8 @@
# Read the documentation before extending the defaults:
# https://docs.searxng.org/admin/settings/
use_default_settings: true
server:
secret_key: "ultrasecretkey"
image_proxy: true

View File

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

View File

@@ -58,8 +58,8 @@ Configured Engines
{% for mod in engines %} {% for mod in engines %}
* - `{{mod.name}} <{{mod.about and mod.about.website}}>`_ * - `{{mod.name}} <{{mod.about and mod.about.website}}>`_
{%- if mod.about and mod.about.language %} {%- if mod.language %}
({{mod.about.language | upper}}) ({{mod.language | upper}})
{%- endif %} {%- endif %}
- ``!{{mod.shortcut}}`` - ``!{{mod.shortcut}}``
- {%- if 'searx.engines.' + mod.__name__ in documented_modules %} - {%- if 'searx.engines.' + mod.__name__ in documented_modules %}

View File

@@ -2,16 +2,16 @@ 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.5 pylint==4.0.6
splinter==0.21.0 splinter==0.21.0
selenium==4.44.0 selenium==4.45.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
sphinx-jinja==2.0.2 sphinx-jinja==2.0.2
sphinx-tabs==3.5.0 sphinx-tabs==3.5.0
furo==2025.12.19 furo==2025.12.19
sphinxcontrib-programoutput==0.19 sphinxcontrib-programoutput==0.20
sphinx-autobuild==2025.8.25 sphinx-autobuild==2025.8.25
sphinx-notfound-page==1.1.0 sphinx-notfound-page==1.1.0
myst-parser==5.0.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" docutils>=0.22.4; python_version > "3.11"
parameterized==0.9.0 parameterized==0.9.0
granian[reload]==2.7.6 granian[reload]==2.7.6
basedpyright==1.39.7 basedpyright==1.39.8
types-lxml==2026.2.16 types-lxml==2026.2.16

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -7165,6 +7165,7 @@
"IR", "IR",
"IS", "IS",
"IT", "IT",
"JE",
"JM", "JM",
"JO", "JO",
"JP", "JP",
@@ -7194,6 +7195,7 @@
"MC", "MC",
"MD", "MD",
"ME", "ME",
"MF",
"MG", "MG",
"MH", "MH",
"MK", "MK",
@@ -7260,6 +7262,7 @@
"SS", "SS",
"ST", "ST",
"SV", "SV",
"SX",
"SY", "SY",
"SZ", "SZ",
"TC", "TC",
@@ -7407,6 +7410,7 @@
"th": "thai", "th": "thai",
"ti": "tigrinya", "ti": "tigrinya",
"tk": "turkmen", "tk": "turkmen",
"to": "tonga",
"tr": "turkish", "tr": "turkish",
"tt": "tatar", "tt": "tatar",
"uk": "ukrainian", "uk": "ukrainian",
@@ -7687,460 +7691,22 @@
"all_locale": null, "all_locale": null,
"custom": {}, "custom": {},
"data_type": "traits_v1", "data_type": "traits_v1",
"languages": { "languages": {},
"af": "afrikaans", "regions": {}
"am": "amharic",
"ar": "arabic",
"az": "azerbaijani",
"be": "belarusian",
"bg": "bulgarian",
"bn": "bengali",
"bs": "bosnian",
"ca": "catalan",
"cs": "czech",
"cy": "welsh",
"da": "dansk",
"de": "deutsch",
"el": "greek",
"en": "english",
"eo": "esperanto",
"es": "espanol",
"et": "estonian",
"eu": "basque",
"fa": "persian",
"fi": "suomi",
"fo": "faroese",
"fr": "francais",
"fy": "frisian",
"ga": "irish",
"gd": "gaelic",
"gl": "galician",
"gu": "gujarati",
"he": "hebrew",
"hi": "hindi",
"hr": "croatian",
"hu": "hungarian",
"ia": "interlingua",
"id": "indonesian",
"is": "icelandic",
"it": "italiano",
"ja": "nihongo",
"jv": "javanese",
"ka": "georgian",
"kn": "kannada",
"ko": "hangul",
"la": "latin",
"lt": "lithuanian",
"lv": "latvian",
"mai": "bihari",
"mk": "macedonian",
"ml": "malayalam",
"mr": "marathi",
"ms": "malay",
"mt": "maltese",
"nb": "norsk",
"ne": "nepali",
"nl": "nederlands",
"oc": "occitan",
"pa": "punjabi",
"pl": "polski",
"pt": "portugues",
"pt_BR": "brazilian",
"ro": "romanian",
"ru": "russian",
"si": "sinhalese",
"sk": "slovak",
"sl": "slovenian",
"sq": "albanian",
"sr": "serbian",
"su": "sudanese",
"sv": "svenska",
"sw": "swahili",
"ta": "tamil",
"te": "telugu",
"th": "thai",
"ti": "tigrinya",
"tl": "tagalog",
"tr": "turkce",
"uk": "ukrainian",
"ur": "urdu",
"uz": "uzbek",
"vi": "vietnamese",
"xh": "xhosa",
"zh": "jiantizhongwen",
"zh_Hant": "fantizhengwen",
"zu": "zulu"
},
"regions": {
"ar-EG": "ar_EG",
"bg-BG": "bg_BG",
"ca-ES": "ca_ES",
"cs-CZ": "cs_CZ",
"da-DK": "da_DK",
"de-AT": "de_AT",
"de-CH": "de_CH",
"de-DE": "de_DE",
"el-GR": "el_GR",
"en-AU": "en_AU",
"en-CA": "en_CA",
"en-GB": "en-GB_GB",
"en-ID": "en_ID",
"en-IE": "en_IE",
"en-IN": "en_IN",
"en-MY": "en_MY",
"en-NZ": "en_NZ",
"en-PH": "en_PH",
"en-SG": "en_SG",
"en-US": "en_US",
"en-ZA": "en_ZA",
"es-AR": "es_AR",
"es-CL": "es_CL",
"es-CO": "es_CO",
"es-ES": "es_ES",
"es-MX": "es_MX",
"es-PE": "es_PE",
"es-US": "es_US",
"es-UY": "es_UY",
"es-VE": "es_VE",
"et-EE": "et_EE",
"fi-FI": "fi_FI",
"fil-PH": "fil_PH",
"fr-BE": "fr_BE",
"fr-CA": "fr_CA",
"fr-CH": "fr_CH",
"fr-FR": "fr_FR",
"hi-IN": "hi_IN",
"hu-HU": "hu_HU",
"id-ID": "id_ID",
"it-CH": "it_CH",
"it-IT": "it_IT",
"ja-JP": "ja_JP",
"ko-KR": "ko_KR",
"ms-MY": "ms_MY",
"ms-SG": "ms_SG",
"nb-NO": "no_NO",
"nl-BE": "nl_BE",
"nl-NL": "nl_NL",
"pl-PL": "pl_PL",
"pt-BR": "pt-BR_BR",
"pt-PT": "pt_PT",
"ro-RO": "ro_RO",
"ru-BY": "ru_BY",
"ru-RU": "ru_RU",
"sv-SE": "sv_SE",
"tr-TR": "tr_TR",
"uk-UA": "uk_UA",
"vi-VN": "vi_VN",
"zh-CN": "zh-CN_CN",
"zh-HK": "zh-TW_HK",
"zh-TW": "zh-TW_TW"
}
}, },
"startpage images": { "startpage images": {
"all_locale": null, "all_locale": null,
"custom": {}, "custom": {},
"data_type": "traits_v1", "data_type": "traits_v1",
"languages": { "languages": {},
"af": "afrikaans", "regions": {}
"am": "amharic",
"ar": "arabic",
"az": "azerbaijani",
"be": "belarusian",
"bg": "bulgarian",
"bn": "bengali",
"bs": "bosnian",
"ca": "catalan",
"cs": "czech",
"cy": "welsh",
"da": "dansk",
"de": "deutsch",
"el": "greek",
"en": "english",
"eo": "esperanto",
"es": "espanol",
"et": "estonian",
"eu": "basque",
"fa": "persian",
"fi": "suomi",
"fo": "faroese",
"fr": "francais",
"fy": "frisian",
"ga": "irish",
"gd": "gaelic",
"gl": "galician",
"gu": "gujarati",
"he": "hebrew",
"hi": "hindi",
"hr": "croatian",
"hu": "hungarian",
"ia": "interlingua",
"id": "indonesian",
"is": "icelandic",
"it": "italiano",
"ja": "nihongo",
"jv": "javanese",
"ka": "georgian",
"kn": "kannada",
"ko": "hangul",
"la": "latin",
"lt": "lithuanian",
"lv": "latvian",
"mai": "bihari",
"mk": "macedonian",
"ml": "malayalam",
"mr": "marathi",
"ms": "malay",
"mt": "maltese",
"nb": "norsk",
"ne": "nepali",
"nl": "nederlands",
"oc": "occitan",
"pa": "punjabi",
"pl": "polski",
"pt": "portugues",
"pt_BR": "brazilian",
"ro": "romanian",
"ru": "russian",
"si": "sinhalese",
"sk": "slovak",
"sl": "slovenian",
"sq": "albanian",
"sr": "serbian",
"su": "sudanese",
"sv": "svenska",
"sw": "swahili",
"ta": "tamil",
"te": "telugu",
"th": "thai",
"ti": "tigrinya",
"tl": "tagalog",
"tr": "turkce",
"uk": "ukrainian",
"ur": "urdu",
"uz": "uzbek",
"vi": "vietnamese",
"xh": "xhosa",
"zh": "jiantizhongwen",
"zh_Hant": "fantizhengwen",
"zu": "zulu"
},
"regions": {
"ar-EG": "ar_EG",
"bg-BG": "bg_BG",
"ca-ES": "ca_ES",
"cs-CZ": "cs_CZ",
"da-DK": "da_DK",
"de-AT": "de_AT",
"de-CH": "de_CH",
"de-DE": "de_DE",
"el-GR": "el_GR",
"en-AU": "en_AU",
"en-CA": "en_CA",
"en-GB": "en-GB_GB",
"en-ID": "en_ID",
"en-IE": "en_IE",
"en-IN": "en_IN",
"en-MY": "en_MY",
"en-NZ": "en_NZ",
"en-PH": "en_PH",
"en-SG": "en_SG",
"en-US": "en_US",
"en-ZA": "en_ZA",
"es-AR": "es_AR",
"es-CL": "es_CL",
"es-CO": "es_CO",
"es-ES": "es_ES",
"es-MX": "es_MX",
"es-PE": "es_PE",
"es-US": "es_US",
"es-UY": "es_UY",
"es-VE": "es_VE",
"et-EE": "et_EE",
"fi-FI": "fi_FI",
"fil-PH": "fil_PH",
"fr-BE": "fr_BE",
"fr-CA": "fr_CA",
"fr-CH": "fr_CH",
"fr-FR": "fr_FR",
"hi-IN": "hi_IN",
"hu-HU": "hu_HU",
"id-ID": "id_ID",
"it-CH": "it_CH",
"it-IT": "it_IT",
"ja-JP": "ja_JP",
"ko-KR": "ko_KR",
"ms-MY": "ms_MY",
"ms-SG": "ms_SG",
"nb-NO": "no_NO",
"nl-BE": "nl_BE",
"nl-NL": "nl_NL",
"pl-PL": "pl_PL",
"pt-BR": "pt-BR_BR",
"pt-PT": "pt_PT",
"ro-RO": "ro_RO",
"ru-BY": "ru_BY",
"ru-RU": "ru_RU",
"sv-SE": "sv_SE",
"tr-TR": "tr_TR",
"uk-UA": "uk_UA",
"vi-VN": "vi_VN",
"zh-CN": "zh-CN_CN",
"zh-HK": "zh-TW_HK",
"zh-TW": "zh-TW_TW"
}
}, },
"startpage news": { "startpage news": {
"all_locale": null, "all_locale": null,
"custom": {}, "custom": {},
"data_type": "traits_v1", "data_type": "traits_v1",
"languages": { "languages": {},
"af": "afrikaans", "regions": {}
"am": "amharic",
"ar": "arabic",
"az": "azerbaijani",
"be": "belarusian",
"bg": "bulgarian",
"bn": "bengali",
"bs": "bosnian",
"ca": "catalan",
"cs": "czech",
"cy": "welsh",
"da": "dansk",
"de": "deutsch",
"el": "greek",
"en": "english",
"eo": "esperanto",
"es": "espanol",
"et": "estonian",
"eu": "basque",
"fa": "persian",
"fi": "suomi",
"fo": "faroese",
"fr": "francais",
"fy": "frisian",
"ga": "irish",
"gd": "gaelic",
"gl": "galician",
"gu": "gujarati",
"he": "hebrew",
"hi": "hindi",
"hr": "croatian",
"hu": "hungarian",
"ia": "interlingua",
"id": "indonesian",
"is": "icelandic",
"it": "italiano",
"ja": "nihongo",
"jv": "javanese",
"ka": "georgian",
"kn": "kannada",
"ko": "hangul",
"la": "latin",
"lt": "lithuanian",
"lv": "latvian",
"mai": "bihari",
"mk": "macedonian",
"ml": "malayalam",
"mr": "marathi",
"ms": "malay",
"mt": "maltese",
"nb": "norsk",
"ne": "nepali",
"nl": "nederlands",
"oc": "occitan",
"pa": "punjabi",
"pl": "polski",
"pt": "portugues",
"pt_BR": "brazilian",
"ro": "romanian",
"ru": "russian",
"si": "sinhalese",
"sk": "slovak",
"sl": "slovenian",
"sq": "albanian",
"sr": "serbian",
"su": "sudanese",
"sv": "svenska",
"sw": "swahili",
"ta": "tamil",
"te": "telugu",
"th": "thai",
"ti": "tigrinya",
"tl": "tagalog",
"tr": "turkce",
"uk": "ukrainian",
"ur": "urdu",
"uz": "uzbek",
"vi": "vietnamese",
"xh": "xhosa",
"zh": "jiantizhongwen",
"zh_Hant": "fantizhengwen",
"zu": "zulu"
},
"regions": {
"ar-EG": "ar_EG",
"bg-BG": "bg_BG",
"ca-ES": "ca_ES",
"cs-CZ": "cs_CZ",
"da-DK": "da_DK",
"de-AT": "de_AT",
"de-CH": "de_CH",
"de-DE": "de_DE",
"el-GR": "el_GR",
"en-AU": "en_AU",
"en-CA": "en_CA",
"en-GB": "en-GB_GB",
"en-ID": "en_ID",
"en-IE": "en_IE",
"en-IN": "en_IN",
"en-MY": "en_MY",
"en-NZ": "en_NZ",
"en-PH": "en_PH",
"en-SG": "en_SG",
"en-US": "en_US",
"en-ZA": "en_ZA",
"es-AR": "es_AR",
"es-CL": "es_CL",
"es-CO": "es_CO",
"es-ES": "es_ES",
"es-MX": "es_MX",
"es-PE": "es_PE",
"es-US": "es_US",
"es-UY": "es_UY",
"es-VE": "es_VE",
"et-EE": "et_EE",
"fi-FI": "fi_FI",
"fil-PH": "fil_PH",
"fr-BE": "fr_BE",
"fr-CA": "fr_CA",
"fr-CH": "fr_CH",
"fr-FR": "fr_FR",
"hi-IN": "hi_IN",
"hu-HU": "hu_HU",
"id-ID": "id_ID",
"it-CH": "it_CH",
"it-IT": "it_IT",
"ja-JP": "ja_JP",
"ko-KR": "ko_KR",
"ms-MY": "ms_MY",
"ms-SG": "ms_SG",
"nb-NO": "no_NO",
"nl-BE": "nl_BE",
"nl-NL": "nl_NL",
"pl-PL": "pl_PL",
"pt-BR": "pt-BR_BR",
"pt-PT": "pt_PT",
"ro-RO": "ro_RO",
"ru-BY": "ru_BY",
"ru-RU": "ru_RU",
"sv-SE": "sv_SE",
"tr-TR": "tr_TR",
"uk-UA": "uk_UA",
"vi-VN": "vi_VN",
"zh-CN": "zh-CN_CN",
"zh-HK": "zh-TW_HK",
"zh-TW": "zh-TW_TW"
}
}, },
"wikidata": { "wikidata": {
"all_locale": null, "all_locale": null,
@@ -8394,6 +7960,7 @@
"inh", "inh",
"io", "io",
"is", "is",
"isv",
"it", "it",
"iu", "iu",
"ja", "ja",
@@ -8442,6 +8009,7 @@
"ltg", "ltg",
"lv", "lv",
"mad", "mad",
"mag",
"mai", "mai",
"map-bms", "map-bms",
"mdf", "mdf",
@@ -9585,4 +9153,4 @@
}, },
"regions": {} "regions": {}
} }
} }

View File

@@ -41,7 +41,7 @@ if t.TYPE_CHECKING:
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.search.processors import OfflineParamTypes, OnlineParamTypes from searx.search.processors import OfflineParamTypes, OnlineParamTypes, ProcessorType
ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache( ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache(
ExpireCacheCfg( ExpireCacheCfg(
@@ -180,7 +180,7 @@ class EngineCache:
return ENGINES_CACHE.secret_hash(name=name) return ENGINES_CACHE.secret_hash(name=name)
class EngineAbout(msgspec.Struct): class EngineAbout(msgspec.Struct, kw_only=True):
"""Additional fields describing the engine. """Additional fields describing the engine.
.. code:: yaml .. code:: yaml
@@ -207,19 +207,22 @@ class EngineAbout(msgspec.Struct):
use_official_api: bool = False use_official_api: bool = False
"""SearXNG engine makes use of the official API or not""" """SearXNG engine makes use of the official API or not"""
require_api_key: bool = False require_api_key: bool = False
"""API requires a key or not.""" """API requires a key or not."""
results: str = "" results: str = ""
"""Data format of the source (online-engines: of the response).""" """Data format of the source (online-engines: of the response)."""
language: str = "" description: str = ""
"""If the engine supports only one language, this language is specified """Brief description of the engine and where it gets its data from.
here (``en``, ``de``, ``"no"`` or ..); otherwise, the value remains empty.
For the YAML configuration: think of the `YAML-Norway problem This value should only be set as long as no description of the data source
<https://ruuda.nl/2023/the-yaml-document-from-hell#the-norway-problem>`_ is available via a :py:obj:`EngineAbout.wikidata_id`."""
"""
language: str = ""
"""Deprecated! Migrate your setting from `engine.about.language` to
`engine.language`"""
class Engine(abc.ABC): # pylint: disable=too-few-public-methods class Engine(abc.ABC): # pylint: disable=too-few-public-methods
@@ -227,6 +230,8 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
Further documentation see :ref:`general engine configuration`. Further documentation see :ref:`general engine configuration`.
The defaults are taken from :py:obj:`searx.engines.ENGINE_DEFAULT_ARGS`.
.. hint:: .. hint::
This class is currently never initialized and only used for type hinting. This class is currently never initialized and only used for type hinting.
@@ -234,49 +239,27 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
logger: logging.Logger logger: logging.Logger
# Common options in the engine module # Common options of the engine module
engine_type: str engine_type: "ProcessorType" = "online"
"""Type of the engine (:ref:`searx.search.processors`)""" """Type of the engine (:ref:`searx.search.processors`)"""
paging: bool paging: bool = False
"""Engine supports multiple pages.""" """Engine supports multiple pages."""
max_page: int = 0 max_page: int = 0
"""If the engine supports paging, then this is the value for the last page """If the engine supports paging, then this is the value for the last page
that is still supported. ``0`` means unlimited numbers of pages.""" that is still supported. ``0`` means unlimited numbers of pages."""
time_range_support: bool time_range_support: bool = False
"""Engine supports search time range.""" """Engine supports search time range."""
safesearch: bool safesearch: bool = False
"""Engine supports SafeSearch""" """Engine supports SafeSearch"""
language_support: bool language_support: bool = False
"""Engine supports languages (locales) search.""" """Engine supports languages (locales) search."""
language: str
"""For an engine, when there is ``language: ...`` in the YAML settings the engine
does support only this one language:
.. code:: yaml
- name: google french
engine: google
language: fr
"""
region: str
"""For an engine, when there is ``region: ...`` in the YAML settings the engine
does support only this one region::
.. code:: yaml
- name: google belgium
engine: google
region: fr-BE
"""
fetch_traits: "Callable[[EngineTraits, bool], None]" fetch_traits: "Callable[[EngineTraits, bool], None]"
"""Function to to fetch engine's traits from origin.""" """Function to to fetch engine's traits from origin."""
@@ -285,9 +268,6 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
# settings.yml # settings.yml
categories: list[str]
"""Specifies to which :ref:`engine categories` the engine should be added."""
name: str name: str
"""Name that will be used across SearXNG to define this engine. In settings, on """Name that will be used across SearXNG to define this engine. In settings, on
the result page ..""" the result page .."""
@@ -297,6 +277,43 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
this search engine (file name from :origin:`searx/engines` without this search engine (file name from :origin:`searx/engines` without
``.py``).""" ``.py``)."""
categories: list[str] = ["general"]
"""Specifies to which :ref:`engine categories` the engine should be added."""
language: str = ""
"""If the engine supports only one language, this language is specified here
(``en``, ``de``, ``"no"`` or ..); otherwise, the value remains empty. For
the YAML configuration: think of the `YAML-Norway problem
<https://ruuda.nl/2023/the-yaml-document-from-hell#the-norway-problem>`_
.. code:: yaml
- name: google norway
engine: google
language: "no"
Depending on ``language_support``, this value has similar but also slightly
different meanings.
- When ``language_support`` is **true**, the map of
:py:obj:`traits.EngineTraits.languages` is reduced to the selected
language
- When ``language_support`` is **false**, then the implementation of the
engine only supports this one ``language``
"""
region: str = ""
"""For an engine, when there is ``region: ...`` in the YAML settings the engine
does support only this one region::
.. code:: yaml
- name: google belgium
engine: google
region: fr-BE
"""
enable_http: bool enable_http: bool
"""Enable HTTP (by default only HTTPS is enabled).""" """Enable HTTP (by default only HTTPS is enabled)."""
@@ -309,6 +326,31 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
display_error_messages: bool display_error_messages: bool
"""Display error messages on the web UI.""" """Display error messages on the web UI."""
disabled: bool = False
"""To disable by default the engine, but not deleting it. It will allow the
user to manually activate it in the settings."""
inactive: bool = False
"""Remove the engine from the settings (*disabled & removed*)."""
about: EngineAbout = EngineAbout()
"""Additional fields describing the engine."""
using_tor_proxy: bool = False
"""Using tor proxy (``true``) or not (``false``) for this engine."""
send_accept_language_header: bool = True
"""When this option is activated (default), the language (locale) that is
selected by the user is used to build and send a ``Accept-Language`` header
in the request to the origin search engine."""
tokens: list[str] = []
"""A list of secret tokens to make this engine *private*, more details see
:ref:`private engines`."""
weight: float = 1.0
"""Weighting of the results of this engine (:ref:`weight <settings engines>`)."""
proxies: dict[str, dict[str, str]] proxies: dict[str, dict[str, str]]
"""Set proxies for a specific engine (YAML): """Set proxies for a specific engine (YAML):
@@ -319,31 +361,6 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
https: socks5://proxy:port https: socks5://proxy:port
""" """
disabled: bool
"""To disable by default the engine, but not deleting it. It will allow the
user to manually activate it in the settings."""
inactive: bool
"""Remove the engine from the settings (*disabled & removed*)."""
about: EngineAbout
"""Additional fields describing the engine."""
using_tor_proxy: bool
"""Using tor proxy (``true``) or not (``false``) for this engine."""
send_accept_language_header: bool
"""When this option is activated (default), the language (locale) that is
selected by the user is used to build and send a ``Accept-Language`` header
in the request to the origin search engine."""
tokens: list[str]
"""A list of secret tokens to make this engine *private*, more details see
:ref:`private engines`."""
weight: int
"""Weighting of the results of this engine (:ref:`weight <settings engines>`)."""
def setup(self, engine_settings: dict[str, t.Any]) -> bool: # pylint: disable=unused-argument def setup(self, engine_settings: dict[str, t.Any]) -> bool: # pylint: disable=unused-argument
"""Dynamic setup of the engine settings. """Dynamic setup of the engine settings.

View File

@@ -142,11 +142,11 @@ class EngineTraits:
""" """
if self.data_type == "traits_v1": if self.data_type == "traits_v1":
self._set_traits_v1(engine) self._set_traits_v1(engine) # pyright: ignore[reportArgumentType]
else: else:
raise TypeError("engine traits of type %s is unknown" % self.data_type) raise TypeError("engine traits of type %s is unknown" % self.data_type)
def _set_traits_v1(self, engine: "Engine | types.ModuleType") -> None: def _set_traits_v1(self, engine: "Engine") -> None:
# For an engine, when there is `language: ...` in the YAML settings the engine # For an engine, when there is `language: ...` in the YAML settings the engine
# does support only this one language (region):: # does support only this one language (region)::
# #
@@ -159,22 +159,25 @@ class EngineTraits:
_msg = "settings.yml - engine: '%s' / %s: '%s' not supported" _msg = "settings.yml - engine: '%s' / %s: '%s' not supported"
languages = traits.languages if engine.language:
if hasattr(engine, "language"): if engine.language_support:
if engine.language not in languages: if not len(traits.languages) > 1:
raise ValueError(_msg % (engine.name, "language", engine.language)) raise ValueError(
traits.languages = {engine.language: languages[engine.language]} f"engine {engine.name}: activated language_support with just one or less languages"
)
if engine.language not in traits.languages:
raise ValueError(_msg % (engine.name, "language", engine.language))
traits.languages = {engine.language: traits.languages[engine.language]}
regions = traits.regions if engine.region:
if hasattr(engine, "region"): if engine.region not in traits.regions:
if engine.region not in regions:
raise ValueError(_msg % (engine.name, "region", engine.region)) raise ValueError(_msg % (engine.name, "region", engine.region))
traits.regions = {engine.region: regions[engine.region]} traits.regions = {engine.region: traits.regions[engine.region]}
engine.language_support = bool(traits.languages or traits.regions) engine.language_support = bool(traits.languages or traits.regions)
# set the copied & modified traits in engine's namespace # set the copied & modified traits in engine's namespace
engine.traits = traits # pyright: ignore[reportAttributeAccessIssue] engine.traits = traits
class EngineTraitsMap(dict[str, EngineTraits]): class EngineTraitsMap(dict[str, EngineTraits]):

View File

@@ -22,8 +22,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "zh",
} }
language = "zh"
# Engine Configuration # Engine Configuration
categories = ["general"] categories = ["general"]

View File

@@ -5,19 +5,19 @@ intended monkey patching of the engine modules.
.. attention:: .. attention::
Monkey-patching modules is a practice from the past that shouldn't be Monkey-patching modules is a practice from the past that shouldn't be
expanded upon. In the long run, there should be an engine class that can be expanded upon. In the long run, engines should be instances of
inherited. However, as long as this class doesn't exist, and as long as all :py:obj:`searx.enginelib.Engine`. However, as long as long as all engine
engine modules aren't converted to an engine class, these builtin types will modules aren't converted to this class, these builtin types will still be
still be needed. needed.
""" """
import logging import logging
from searx.enginelib import traits as _traits from searx.enginelib import traits as _traits
logger: logging.Logger logger: logging.Logger
supported_languages: str
language_aliases: str
language_support: bool language_support: bool
language: str
region: str
traits: _traits.EngineTraits traits: _traits.EngineTraits
# from searx.engines.ENGINE_DEFAULT_ARGS # from searx.engines.ENGINE_DEFAULT_ARGS

View File

@@ -14,40 +14,48 @@ import sys
import copy import copy
import os import os
from os.path import realpath, dirname from os.path import realpath, dirname
import warnings
import types import types
import inspect import inspect
import msgspec
from searx import logger, settings from searx import logger, settings
from searx.utils import load_module from searx.utils import load_module
from searx.data import ENGINE_TRAITS
if t.TYPE_CHECKING: from searx.enginelib import Engine, EngineAbout
from searx.enginelib import Engine
logger = logger.getChild('engines') logger = logger.getChild('engines')
ENGINE_DIR = dirname(realpath(__file__)) ENGINE_DIR = dirname(realpath(__file__))
# Defaults for the namespace of an engine module, see load_engine() # Defaults for the namespace of an engine module, see load_engine()
ENGINE_DEFAULT_ARGS: dict[str, int | str | list[t.Any] | dict[str, t.Any] | bool] = { ENGINE_DEFAULT_ARGS: dict[str, t.Any] = {
# Common options in the engine module # Common options in the engine module
"engine_type": "online", "engine_type": "online",
"paging": False, "paging": False,
"max_page": 0,
"time_range_support": False, "time_range_support": False,
"safesearch": False, "safesearch": False,
"language_support": False,
# settings.yml # settings.yml
"categories": ["general"], "categories": ["general"],
"language": "",
"region": "",
"enable_http": False, "enable_http": False,
"shortcut": "-", "shortcut": "-",
"timeout": settings["outgoing"]["request_timeout"], "timeout": settings["outgoing"]["request_timeout"],
"display_error_messages": True, "display_error_messages": True,
"disabled": False, "disabled": False,
"inactive": False, "inactive": False,
"about": {}, "about": EngineAbout(),
"using_tor_proxy": False, "using_tor_proxy": False,
"send_accept_language_header": True, "send_accept_language_header": True,
"tokens": [], "tokens": [],
"max_page": 0, "weight": 1.0,
} }
"""Default values that are set in an engine of type *module*, please compare
with the class :py:obj:`searx.enginelib.Engine`."""
# set automatically when an engine does not have any tab category # set automatically when an engine does not have any tab category
DEFAULT_CATEGORY = 'other' DEFAULT_CATEGORY = 'other'
@@ -177,14 +185,41 @@ def set_loggers(engine: "Engine|types.ModuleType", engine_name: str):
def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]): def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]):
# pylint: disable=too-many-branches
# set engine attributes from engine_data # set engine attributes from engine_data
kvargs: dict[str, t.Any]
if isinstance(engine.about, EngineAbout):
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
else:
kvargs = {**engine.about, **engine_data.get("about", {})}
try:
engine.about = EngineAbout(**kvargs)
except TypeError as exc:
raise TypeError(
f"engine '{engine_data['name']}' ({engine_data['engine']}) - in the about section --> {exc}"
) from exc
# warn about deprecated engine settings
if engine.about.language:
if hasattr(engine, "language") and not engine.language:
engine.language = engine.about.language
warnings.warn(
f"engine '{engine_data['name']}' ({engine_data['engine']})"
f" - migrate engine.about.language to engine.language!",
DeprecationWarning,
2,
)
for param_name, param_value in engine_data.items(): for param_name, param_value in engine_data.items():
if param_name == "about":
continue
if param_name == 'categories': if param_name == 'categories':
if isinstance(param_value, str): if isinstance(param_value, str):
param_value = list(map(str.strip, param_value.split(','))) param_value = list(map(str.strip, param_value.split(',')))
engine.categories = param_value # type: ignore engine.categories = param_value # type: ignore
elif hasattr(engine, 'about') and param_name == 'about':
engine.about = {**engine.about, **engine_data['about']} # type: ignore
else: else:
setattr(engine, param_name, param_value) setattr(engine, param_name, param_value)
@@ -193,6 +228,9 @@ def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: d
if not hasattr(engine, arg_name): if not hasattr(engine, arg_name):
setattr(engine, arg_name, copy.deepcopy(arg_value)) setattr(engine, arg_name, copy.deepcopy(arg_value))
if ENGINE_TRAITS.get(engine.name, {}).get("languages") and not engine.language_support:
raise ValueError(f"engine '{engine.name}' ({engine_data['engine']}) language_support should be set to True")
def update_attributes_for_tor(engine: "Engine | types.ModuleType"): def update_attributes_for_tor(engine: "Engine | types.ModuleType"):
if using_tor_proxy(engine) and hasattr(engine, 'onion_url'): if using_tor_proxy(engine) and hasattr(engine, 'onion_url'):

View File

@@ -16,12 +16,12 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "zh",
} }
# Engine Configuration # Engine Configuration
categories = ["videos"] categories = ["videos"]
paging = True paging = True
language = "zh"
# Base URL # Base URL
base_url = "https://www.acfun.cn" base_url = "https://www.acfun.cn"

View File

@@ -64,6 +64,7 @@ about: dict[str, t.Any] = {
# engine dependent config # engine dependent config
categories = ["files", "books"] categories = ["files", "books"]
paging: bool = True paging: bool = True
language_support = True
# search-url # search-url
base_url: list[str] | str = [] base_url: list[str] | str = []

View File

@@ -42,8 +42,8 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'HTML', 'results': 'HTML',
'language': 'it',
} }
language = "it"
def request(query, params): def request(query, params):

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

View File

@@ -35,6 +35,7 @@ about = {
categories = ["it", "software wikis"] categories = ["it", "software wikis"]
paging = True paging = True
main_wiki = "wiki.archlinux.org" main_wiki = "wiki.archlinux.org"
language_support = True
def request(query, params): def request(query, params):

View File

@@ -54,8 +54,8 @@ about = {
"use_official_api": True, "use_official_api": True,
"require_api_key": True, "require_api_key": True,
"results": "JSON", "results": "JSON",
"language": "en",
} }
language = "en"
CACHE: EngineCache CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire`` """Persistent (SQLite) key/value cache that deletes its values after ``expire``

View File

@@ -23,8 +23,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
"language": "zh",
} }
language = "zh"
paging = True paging = True
categories = [] categories = []

View File

@@ -34,6 +34,7 @@ about = {
categories = ["general", "social media"] categories = ["general", "social media"]
paging = True paging = True
time_range_support = True time_range_support = True
language_support = True
base_url = "https://boardreader.com" base_url = "https://boardreader.com"
time_range_map = {"day": "1", "week": "7", "month": "30", "year": "365"} time_range_map = {"day": "1", "week": "7", "month": "30", "year": "365"}

View File

@@ -13,8 +13,8 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'JSON', 'results': 'JSON',
'language': 'de',
} }
language = "de"
paging = True paging = True
categories = ['general'] categories = ['general']

View File

@@ -14,7 +14,6 @@ from searx.extended_types import SXNG_Response
from searx.network import get, post from searx.network import get, post
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.utils import 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
@@ -42,21 +41,7 @@ search_index = "cw22"
<https://www.chatnoir.eu/docs/api-general>`_ for a full list.""" <https://www.chatnoir.eu/docs/api-general>`_ for a full list."""
CACHE: EngineCache
"""Cache to store session info (i.e. api key, csrf token, session id)."""
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return True
def _obtain_api_key() -> tuple[str, str, str]: def _obtain_api_key() -> tuple[str, str, str]:
cached_session = CACHE.get("session")
if cached_session:
return tuple(cached_session.split("|"))
home_resp = get(base_url) home_resp = get(base_url)
if not home_resp.ok: if not home_resp.ok:
raise SearxEngineAPIException("failed to obtain api key") raise SearxEngineAPIException("failed to obtain api key")
@@ -76,10 +61,6 @@ def _obtain_api_key() -> tuple[str, str, str]:
session_id = token_resp.cookies["sessionid"] session_id = token_resp.cookies["sessionid"]
scraped_api_key = token_resp.json()["token"]["token"] scraped_api_key = token_resp.json()["token"]["token"]
# session keys seem to become rate-limited very fast, so only remembering
# for 1 minute here
CACHE.set("session", f"{csrf_token}|{session_id}|{scraped_api_key}", expire=60)
return csrf_token, session_id, scraped_api_key return csrf_token, session_id, scraped_api_key

View File

@@ -10,8 +10,8 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'JSON', 'results': 'JSON',
'language': 'de',
} }
language = "de"
paging = True paging = True
categories = [] categories = []

View File

@@ -19,11 +19,9 @@ Configuration
The engine has the following additional settings: The engine has the following additional settings:
- :py:obj:`chinaso_category` (:py:obj:`ChinasoCategoryType`)
- :py:obj:`chinaso_news_source` (:py:obj:`ChinasoNewsSourceType`) - :py:obj:`chinaso_news_source` (:py:obj:`ChinasoNewsSourceType`)
In the example below, all three ChinaSO engines are using the :ref:`network In the example below, ChinaSO is configured for news search.
<engine network>` from the ``chinaso news`` engine.
.. code:: yaml .. code:: yaml
@@ -31,23 +29,8 @@ In the example below, all three ChinaSO engines are using the :ref:`network
engine: chinaso engine: chinaso
shortcut: chinaso shortcut: chinaso
categories: [news] categories: [news]
chinaso_category: news
chinaso_news_source: all chinaso_news_source: all
- name: chinaso images
engine: chinaso
network: chinaso news
shortcut: chinasoi
categories: [images]
chinaso_category: images
- name: chinaso videos
engine: chinaso
network: chinaso news
shortcut: chinasov
categories: [videos]
chinaso_category: videos
Implementations Implementations
=============== ===============
@@ -70,26 +53,13 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
"language": "zh",
} }
paging = True paging = True
time_range_support = True time_range_support = True
results_per_page = 10 results_per_page = 10
categories = [] categories = []
language = "zh"
ChinasoCategoryType = t.Literal['news', 'videos', 'images']
"""ChinaSo supports news, videos, images search.
- ``news``: search for news
- ``videos``: search for videos
- ``images``: search for images
In the category ``news`` you can additionally filter by option
:py:obj:`chinaso_news_source`.
"""
chinaso_category = 'news'
"""Configure ChinaSo category (:py:obj:`ChinasoCategoryType`)."""
ChinasoNewsSourceType = t.Literal['CENTRAL', 'LOCAL', 'BUSINESS', 'EPAPER', 'all'] ChinasoNewsSourceType = t.Literal['CENTRAL', 'LOCAL', 'BUSINESS', 'EPAPER', 'all']
"""Filtering ChinaSo-News results by source: """Filtering ChinaSo-News results by source:
@@ -109,39 +79,24 @@ base_url = "https://www.chinaso.com"
def init(_): def init(_):
if chinaso_category not in ('news', 'videos', 'images'): if chinaso_news_source not in t.get_args(ChinasoNewsSourceType):
raise ValueError(f"Unsupported category: {chinaso_category}")
if chinaso_category == 'news' and chinaso_news_source not in t.get_args(ChinasoNewsSourceType):
raise ValueError(f"Unsupported news source: {chinaso_news_source}") raise ValueError(f"Unsupported news source: {chinaso_news_source}")
def request(query, params): def request(query, params):
query_params = {"q": query} query_params = {'q': query, 'pn': params["pageno"], 'ps': results_per_page}
if time_range_dict.get(params['time_range']): if time_range_dict.get(params['time_range']):
query_params["stime"] = time_range_dict[params['time_range']] query_params["stime"] = time_range_dict[params['time_range']]
query_params["etime"] = 'now' query_params["etime"] = 'now'
category_config = {
'news': {'endpoint': '/v5/general/v1/web/search', 'params': {'pn': params["pageno"], 'ps': results_per_page}},
'images': {
'endpoint': '/v5/general/v1/search/image',
'params': {'start_index': (params["pageno"] - 1) * results_per_page, 'rn': results_per_page},
},
'videos': {
'endpoint': '/v5/general/v1/search/video',
'params': {'start_index': (params["pageno"] - 1) * results_per_page, 'rn': results_per_page},
},
}
if chinaso_news_source != 'all': if chinaso_news_source != 'all':
if chinaso_news_source == 'EPAPER': if chinaso_news_source == 'EPAPER':
category_config['news']['params']["type"] = 'EPAPER' query_params["type"] = 'EPAPER'
else: else:
category_config['news']['params']["cate"] = chinaso_news_source query_params["cate"] = chinaso_news_source
query_params.update(category_config[chinaso_category]['params']) params["url"] = f"{base_url}/v5/general/v1/web/search?{urlencode(query_params)}"
params["url"] = f"{base_url}{category_config[chinaso_category]['endpoint']}?{urlencode(query_params)}"
cookie = { cookie = {
"uid": base64.b64encode(secrets.token_bytes(16)).decode("utf-8"), "uid": base64.b64encode(secrets.token_bytes(16)).decode("utf-8"),
} }
@@ -156,12 +111,13 @@ def response(resp):
except Exception as e: except Exception as e:
raise SearxEngineAPIException(f"Invalid response: {e}") from e raise SearxEngineAPIException(f"Invalid response: {e}") from e
parsers = {'news': parse_news, 'images': parse_images, 'videos': parse_videos} # Upstream returns {'status': 0, 'msg': 'empty result', 'data': {}} when there
# are no results; this is a valid empty result rather than an API error.
if not isinstance(data, dict) or "data" not in data:
raise SearxEngineAPIException("Invalid response")
if not data["data"]:
return []
return parsers[chinaso_category](data)
def parse_news(data):
results = [] results = []
if not data.get("data", {}).get("data"): if not data.get("data", {}).get("data"):
raise SearxEngineAPIException("Invalid response") raise SearxEngineAPIException("Invalid response")
@@ -183,47 +139,3 @@ def parse_news(data):
} }
) )
return results return results
def parse_images(data):
results = []
if not data.get("data", {}).get("arrRes"):
raise SearxEngineAPIException("Invalid response")
for entry in data["data"]["arrRes"]:
results.append(
{
'url': entry["web_url"],
'title': html_to_text(entry["title"]),
'content': html_to_text(entry.get("ImageInfo", "")),
'template': 'images.html',
'img_src': entry["url"].replace("http://", "https://"),
'thumbnail_src': entry["largeimage"].replace("http://", "https://"),
}
)
return results
def parse_videos(data):
results = []
if not data.get("data", {}).get("arrRes"):
raise SearxEngineAPIException("Invalid response")
for entry in data["data"]["arrRes"]:
published_date = None
if entry.get("VideoPubDate"):
try:
published_date = datetime.fromtimestamp(int(entry["VideoPubDate"]))
except (ValueError, TypeError):
pass
results.append(
{
'url': entry["url"],
'title': html_to_text(entry["raw_title"]),
'template': 'videos.html',
'publishedDate': published_date,
'thumbnail': entry["image_src"].replace("http://", "https://"),
}
)
return results

View File

@@ -40,6 +40,7 @@ categories = ["videos"]
paging = True paging = True
page_size = 10 page_size = 10
language_support = True
time_range_support = True time_range_support = True
time_delta_dict = { time_delta_dict = {
"day": timedelta(days=1), "day": timedelta(days=1),

View File

@@ -24,7 +24,7 @@ import typing as t
import json import json
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineCache from searx.enginelib import EngineCache, EngineAbout
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.search.processors import RequestParams from searx.search.processors import RequestParams
@@ -35,13 +35,11 @@ categories = ["general"]
disabled = True disabled = True
timeout = 2.0 timeout = 2.0
about = { language = "en"
"wikidata_id": None, about = EngineAbout(
"official_api_documentation": None, results="JSON",
"use_official_api": False, description="Demo offline engine Engine with results in the English language.",
"require_api_key": False, )
"results": "JSON",
}
# if there is a need for globals, use a leading underline # if there is a need for globals, use a leading underline
_my_offline_engine: str = "" _my_offline_engine: str = ""

View File

@@ -25,6 +25,7 @@ import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineAbout
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -43,14 +44,14 @@ page_size = 20
search_api = "https://api.artic.edu/api/v1/artworks/search" search_api = "https://api.artic.edu/api/v1/artworks/search"
image_api = "https://www.artic.edu/iiif/2/" image_api = "https://www.artic.edu/iiif/2/"
about = { about = EngineAbout(
"website": "https://www.artic.edu", website="https://www.artic.edu",
"wikidata_id": "Q239303", wikidata_id="Q239303",
"official_api_documentation": "http://api.artic.edu/docs/", official_api_documentation="http://api.artic.edu/docs/",
"use_official_api": True, use_official_api=True,
"require_api_key": False, require_api_key=False,
"results": "JSON", results="JSON",
} )
# if there is a need for globals, use a leading underline # if there is a need for globals, use a leading underline

View File

@@ -11,8 +11,8 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'HTML', 'results': 'HTML',
'language': 'de',
} }
language = "de"
categories = [] categories = []
paging = True paging = True

View File

@@ -203,6 +203,7 @@ about: dict[str, str | bool] = {
categories: list[str] = ["general", "web"] categories: list[str] = ["general", "web"]
paging: bool = True paging: bool = True
time_range_support: bool = True time_range_support: bool = True
language_support = True
safesearch: bool = True safesearch: bool = True
"""DDG-lite: user can't select but the results are filtered.""" """DDG-lite: user can't select but the results are filtered."""

View File

@@ -28,6 +28,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON (site requires js to get images)", "results": "JSON (site requires js to get images)",
} }
language_support = True
# engine dependent config # engine dependent config
categories = [] categories = []

View File

@@ -26,6 +26,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
} }
language_support = True
# engine dependent config # engine dependent config
categories = ["weather"] categories = ["weather"]

View File

@@ -14,8 +14,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": 'HTML', "results": 'HTML',
"language": 'de',
} }
language = "de"
categories = ['dictionaries'] categories = ['dictionaries']
paging = True paging = True

View File

@@ -55,7 +55,7 @@ about = {
'official_api_documentation': 'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html', 'official_api_documentation': 'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html',
'use_official_api': True, 'use_official_api': True,
'require_api_key': False, 'require_api_key': False,
'format': 'JSON', "results": "JSON",
} }
base_url = 'http://localhost:9200' base_url = 'http://localhost:9200'

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

View File

@@ -27,8 +27,8 @@ about = {
'official_api_documentation': None, 'official_api_documentation': None,
'require_api_key': False, 'require_api_key': False,
'results': 'HTML', 'results': 'HTML',
'language': 'de',
} }
language = "de"
paging = True paging = True
categories = ['shopping'] categories = ['shopping']

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

View File

@@ -57,6 +57,7 @@ max_page = 50
.. _Google max 50 pages: https://github.com/searxng/searxng/issues/2982 .. _Google max 50 pages: https://github.com/searxng/searxng/issues/2982
""" """
time_range_support = True time_range_support = True
language_support = True
safesearch = True safesearch = True
time_range_dict = {"day": "d", "week": "w", "month": "m", "year": "y"} time_range_dict = {"day": "d", "week": "w", "month": "m", "year": "y"}

View File

@@ -43,6 +43,7 @@ max_page = 50
""" """
time_range_support = True time_range_support = True
language_support = True
safesearch = True safesearch = True
filter_mapping = {0: 'images', 1: 'active', 2: 'active'} filter_mapping = {0: 'images', 1: 'active', 2: 'active'}

View File

@@ -66,6 +66,7 @@ about = {
categories = ["news"] categories = ["news"]
paging = False paging = False
time_range_support = False time_range_support = False
language_support = True
# Google-News results are always *SafeSearch*. Option 'safesearch' is set to # Google-News results are always *SafeSearch*. Option 'safesearch' is set to
# False here. # False here.

View File

@@ -34,8 +34,8 @@ about = {
"use_official_api": True, "use_official_api": True,
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
"language": "it",
} }
language = "it"
def request(query, params): def request(query, params):

View File

@@ -16,8 +16,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": 'HTML', "results": 'HTML',
"language": 'fr',
} }
language = "fr"
# engine dependent config # engine dependent config
categories = ['videos'] categories = ['videos']

View File

@@ -14,9 +14,9 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
"language": "zh",
} }
language = "zh"
paging = True paging = True
time_range_support = True time_range_support = True
categories = ["videos"] categories = ["videos"]

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

@@ -13,8 +13,8 @@ about = {
"use_official_api": True, "use_official_api": True,
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": 'JSON',
"language": 'ja',
} }
language = "ja"
categories = ['dictionaries'] categories = ['dictionaries']
paging = False paging = False
@@ -110,8 +110,8 @@ def get_infobox(alt_forms, result_url, definitions):
# definitions # definitions
infobox_content.append( infobox_content.append(
''' '''
<small><a href="https://www.edrdg.org/wiki/index.php/JMdict-EDICT_Dictionary_Project">JMdict</a> <small><a href="https://www.edrdg.org/wiki/index.php/JMdict-EDICT_Dictionary_Project">JMdict</a>
and <a href="https://www.edrdg.org/enamdict/enamdict_doc.html">JMnedict</a> and <a href="https://www.edrdg.org/enamdict/enamdict_doc.html">JMnedict</a>
by <a href="https://www.edrdg.org/edrdg/licence.html">EDRDG</a>, CC BY-SA 3.0.</small> by <a href="https://www.edrdg.org/edrdg/licence.html">EDRDG</a>, CC BY-SA 3.0.</small>
<ul> <ul>
''' '''

View File

@@ -79,6 +79,9 @@ from json import loads
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.utils import to_string, html_to_text from searx.utils import to_string, html_to_text
from searx.network import raise_for_httperror from searx.network import raise_for_httperror
from searx.enginelib import EngineAbout
about = EngineAbout()
search_url = None search_url = None
""" """

View File

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

View File

@@ -11,9 +11,9 @@ about = {
"use_official_api": True, "use_official_api": True,
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": 'JSON',
"language": "de",
} }
language = "de"
categories = ['videos'] categories = ['videos']
paging = True paging = True
time_range_support = False time_range_support = False

View File

@@ -20,6 +20,7 @@ about = {
} }
paging = True # paging is only supported for general search paging = True # paging is only supported for general search
safesearch = True safesearch = True
language_support = True
time_range_support = True # time range search is supported for general and news time_range_support = True # time range search is supported for general and news
max_page = 10 max_page = 10

View File

@@ -35,8 +35,9 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'JSON', 'results': 'JSON',
'language': 'de',
} }
language = "de"
paging = True paging = True
categories = ["movies"] categories = ["movies"]

View File

@@ -26,8 +26,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "ko",
} }
language = "ko"
categories = [] categories = []
paging = True paging = True

View File

@@ -13,8 +13,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "ja",
} }
language = "ja"
categories = ["videos"] categories = ["videos"]
paging = True paging = True

View File

@@ -26,6 +26,7 @@ about = {
# Engine configuration # Engine configuration
paging = True paging = True
time_range_support = True time_range_support = True
language_support = True
results_per_page = 20 results_per_page = 20
categories = ["videos"] categories = ["videos"]

View File

@@ -25,6 +25,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
} }
language_support = True
# engine dependent config # engine dependent config
categories = ["videos"] categories = ["videos"]

View File

@@ -28,7 +28,7 @@ search_string = 'api/?{query}&limit={limit}'
result_base_url = 'https://openstreetmap.org/{osm_type}/{osm_id}' result_base_url = 'https://openstreetmap.org/{osm_type}/{osm_id}'
# list of supported languages # list of supported languages
supported_languages = ['de', 'en', 'fr', 'it'] photon_supported_languages = ["de", "en", "fr", "it"]
# do search-request # do search-request
@@ -37,7 +37,7 @@ def request(query, params):
if params['language'] != 'all': if params['language'] != 'all':
language = params['language'].split('_')[0] language = params['language'].split('_')[0]
if language in supported_languages: if language in photon_supported_languages:
params['url'] = params['url'] + "&lang=" + language params['url'] = params['url'] + "&lang=" + language
# using SearXNG User-Agent # using SearXNG User-Agent

View File

@@ -77,7 +77,7 @@ from searx.utils import gen_useragent, html_to_text, parse_duration_string
about = { about = {
"website": "https://presearch.io", "website": "https://presearch.io",
"wikidiata_id": "Q7240905", "wikidata_id": "Q7240905",
"official_api_documentation": "https://docs.presearch.io/nodes/api", "official_api_documentation": "https://docs.presearch.io/nodes/api",
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,

View File

@@ -16,8 +16,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "zh",
} }
language = "zh"
# Engine Configuration # Engine Configuration
categories = [] categories = []

View File

@@ -26,6 +26,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
} }
language_support = True
paging = True paging = True
categories = ["music", "radio"] categories = ["music", "radio"]

View File

@@ -101,7 +101,7 @@ def _image_results(doc: "ElementBase") -> EngineResults:
res.types.Image( res.types.Image(
url=extract_text(eval_xpath(result, "./@href")) or "", url=extract_text(eval_xpath(result, "./@href")) or "",
title=extract_text(eval_xpath(result, "./img/@alt")) or "", title=extract_text(eval_xpath(result, "./img/@alt")) or "",
thumbnail_src=extract_text(eval_xpath(result, "./img/@src")) or "", img_src=extract_text(eval_xpath(result, "./img/@src")) or "",
), ),
) )
) )

View File

@@ -13,8 +13,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": 'JSON',
'language': 'fr',
} }
language = "fr"
categories = ['movies'] categories = ['movies']
paging = True paging = True

View File

@@ -25,6 +25,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": 'JSON',
} }
language_support = True
# engine dependent config # engine dependent config
categories = ['videos'] categories = ['videos']

View File

@@ -19,8 +19,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "cz",
} }
language = "cz"
categories = ['general', 'web'] categories = ['general', 'web']
base_url = 'https://search.seznam.cz/' base_url = 'https://search.seznam.cz/'

View File

@@ -16,8 +16,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "zh",
} }
language = "zh"
# Engine Configuration # Engine Configuration
categories = ["general"] categories = ["general"]

View File

@@ -11,8 +11,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
"language": "zh",
} }
language = "zh"
categories = ["videos"] categories = ["videos"]
paging = True paging = True

View File

@@ -14,8 +14,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "zh",
} }
language = "zh"
# Engine Configuration # Engine Configuration
categories = ["news"] categories = ["news"]

View File

@@ -131,6 +131,7 @@ max_page = 18
"""Tested 18 pages maximum (argument ``page``), to be save max is set to 20.""" """Tested 18 pages maximum (argument ``page``), to be save max is set to 20."""
time_range_support = True time_range_support = True
language_support = True
safesearch = True safesearch = True
time_range_dict = {"day": "d", "week": "w", "month": "m", "year": "y"} time_range_dict = {"day": "d", "week": "w", "month": "m", "year": "y"}

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

View File

@@ -27,8 +27,9 @@ about = {
'use_official_api': True, 'use_official_api': True,
'require_api_key': False, 'require_api_key': False,
'results': 'JSON', 'results': 'JSON',
'language': 'de',
} }
language = "de"
categories = ['general', 'news'] categories = ['general', 'news']
paging = True paging = True

View File

@@ -73,7 +73,7 @@ def _obtain_session_code() -> str:
if cached_session: if cached_session:
return cached_session return cached_session
results_page = get(f"{base_url}/_internCode.aspx") results_page = get(f"{base_url}/checkCode.aspx")
doc = html.fromstring(results_page.text) doc = html.fromstring(results_page.text)
extra_data: dict[str, str] = {} extra_data: dict[str, str] = {}
@@ -107,7 +107,7 @@ def _obtain_session_code() -> str:
} }
challenge_response = post( challenge_response = post(
f"{base_url}/_internCode.aspx", f"{base_url}/checkCode.aspx",
cookies=results_page.cookies, cookies=results_page.cookies,
data=data, data=data,
) )
@@ -125,7 +125,9 @@ def request(query: str, params: "OnlineParams"):
code = _obtain_session_code() code = _obtain_session_code()
args = {"w": query, "page": params["pageno"]} args = {"w": query, "page": params["pageno"]}
params["url"] = f"{base_url}/{tiger_category}?{urlencode(args)}" params["url"] = f"{base_url}/{tiger_category}?{urlencode(args)}"
params["cookies"]["Tiger.ch"] = f"Code={code}" # Setting Checked=1 shows related search terms / suggestions
# Language and country could be set with Lng= and Land= in the future
params["cookies"]["Tiger.ch"] = f"Tiger.ch=&Code={code}&Checked=1"
def response(resp: "SXNG_Response") -> EngineResults: def response(resp: "SXNG_Response") -> EngineResults:
@@ -144,6 +146,9 @@ def response(resp: "SXNG_Response") -> EngineResults:
content=extract_text(eval_xpath(result, ".//*[contains(@class, 'webbodynopic')]")) or "", content=extract_text(eval_xpath(result, ".//*[contains(@class, 'webbodynopic')]")) or "",
) )
) )
for suggestion in eval_xpath_list(doc, "//a[contains(@class, 'linkAnders')]"):
res.add(res.types.LegacyResult(suggestion=extract_text(suggestion)))
elif tiger_category == "News": elif tiger_category == "News":
for result in eval_xpath_list(doc, "//div[@id='panNews']/div"): for result in eval_xpath_list(doc, "//div[@id='panNews']/div"):
publishedDate = None publishedDate = None

View File

@@ -16,20 +16,17 @@ from lxml import html
from searx.utils import eval_xpath_list, eval_xpath, extract_text, get_embeded_stream_url, ElementType from searx.utils import eval_xpath_list, eval_xpath, extract_text, get_embeded_stream_url, ElementType
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineAbout
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams from searx.search.processors import OnlineParams
about = { about = EngineAbout(
"website": "https://www.t-online.de", website="https://www.t-online.de",
"wikidata_id": "Q590940", wikidata_id="Q590940",
"official_api_documentation": None, results="HTML",
"use_official_api": False, )
"require_api_key": False,
"results": "HTML",
"language": "de",
}
paging = True paging = True
time_range_support = True time_range_support = True
@@ -44,6 +41,8 @@ time_range_map = {"day": "d", "week": "w", "month": "m", "year": "y"}
# use "tonline" to only search for results from t-online news articles # use "tonline" to only search for results from t-online news articles
tonline_channel_map = {"images": "flickr", "videos": "yt"} tonline_channel_map = {"images": "flickr", "videos": "yt"}
language = "de"
def init(_): def init(_):
if tonline_categ not in ("web", "images", "videos", "news"): if tonline_categ not in ("web", "images", "videos", "news"):

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

@@ -40,6 +40,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": 'JSON',
} }
language_support = True
display_type = ["infobox"] display_type = ["infobox"]
"""A list of display types composed from ``infobox`` and ``list``. The latter """A list of display types composed from ``infobox`` and ``list``. The latter

View File

@@ -72,6 +72,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
} }
language_support = True
display_type = ["infobox"] display_type = ["infobox"]
"""A list of display types composed from ``infobox`` and ``list``. The latter """A list of display types composed from ``infobox`` and ``list``. The latter

View File

@@ -76,6 +76,9 @@ from lxml import html
from searx.utils import extract_text, extract_url, eval_xpath, eval_xpath_list from searx.utils import extract_text, extract_url, eval_xpath, eval_xpath_list
from searx.network import raise_for_httperror from searx.network import raise_for_httperror
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineAbout
about = EngineAbout()
search_url = None search_url = None
""" """
@@ -289,7 +292,7 @@ def response(resp) -> EngineResults: # pylint: disable=too-many-branches
if results_xpath: if results_xpath:
for result in eval_xpath_list(dom, results_xpath): for result in eval_xpath_list(dom, results_xpath):
url = extract_url(eval_xpath_list(result, url_xpath, min_len=1), search_url) url = extract_url(eval_xpath(result, url_xpath), search_url)
title = extract_text(eval_xpath_list(result, title_xpath, min_len=1)) title = extract_text(eval_xpath_list(result, title_xpath, min_len=1))
content = extract_text(eval_xpath_list(result, content_xpath)) content = extract_text(eval_xpath_list(result, content_xpath))
tmp_result = {'url': url, 'title': title, 'content': content} tmp_result = {'url': url, 'title': title, 'content': content}

View File

@@ -22,6 +22,7 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
} }
language_support = True
base_url = "https://api.yep.com" base_url = "https://api.yep.com"
web_base_url = "https://yep.com" web_base_url = "https://yep.com"

View File

@@ -61,6 +61,7 @@ about: dict[str, t.Any] = {
categories: list[str] = ["files", "books"] categories: list[str] = ["files", "books"]
paging: bool = True paging: bool = True
language_support = True
base_url: str = "https://zlibrary-global.se" base_url: str = "https://zlibrary-global.se"
zlib_year_from: str = "" zlib_year_from: str = ""

View File

@@ -14,6 +14,7 @@ template.
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
__all__ = ["Image", "ImageRef"] __all__ = ["Image", "ImageRef"]
import mimetypes
import types import types
import typing as t import typing as t
from collections.abc import Callable from collections.abc import Callable
@@ -71,7 +72,7 @@ class Image(MainResult, kw_only=True):
"""The resolution of the image (e.g. ``1920 x 1080`` pixel)""" """The resolution of the image (e.g. ``1920 x 1080`` pixel)"""
img_format: str = "" img_format: str = ""
"""The format of the image (e.g. ``png``).""" """The format of the image :py:obj:`.MainResult.img_src` (e.g. ``png``)."""
source: str = "" source: str = ""
"""Source of the image.""" """Source of the image."""
@@ -83,6 +84,19 @@ class Image(MainResult, kw_only=True):
formats: list[ImageRef] = [] formats: list[ImageRef] = []
"""List of links to alternative image formats.""" """List of links to alternative image formats."""
def __post_init__(self):
super().__post_init__()
if not self.img_format:
# automatically guess the image format based on the path of the image
mimetype = mimetypes.guess_type(self.img_src)[0]
if mimetype:
subtype = mimetype.split("/")[-1]
if subtype in MIMESUB:
self.img_format = MIMESUB[subtype]
else:
self.img_format = subtype.upper()
def filter_urls(self, filter_func: "Callable[[Result | LegacyResult, str, str], str | bool ]"): def filter_urls(self, filter_func: "Callable[[Result | LegacyResult, str, str], str | bool ]"):
for _ref in self.formats[:]: for _ref in self.formats[:]:

View File

@@ -11,6 +11,7 @@ __all__ = [
"PROCESSORS", "PROCESSORS",
"ParamTypes", "ParamTypes",
"RequestParams", "RequestParams",
"ProcessorType",
] ]
import typing as t import typing as t
@@ -27,6 +28,14 @@ from .online_url_search import OnlineUrlSearchProcessor, OnlineUrlSearchParams
logger = logger.getChild("search.processors") logger = logger.getChild("search.processors")
ProcessorType = t.Literal[
"offline",
"online",
"online_currency",
"online_dictionary",
"online_url_search",
]
OnlineParamTypes: t.TypeAlias = OnlineParams | OnlineDictParams | OnlineCurrenciesParams | OnlineUrlSearchParams OnlineParamTypes: t.TypeAlias = OnlineParams | OnlineDictParams | OnlineCurrenciesParams | OnlineUrlSearchParams
OfflineParamTypes: t.TypeAlias = RequestParams OfflineParamTypes: t.TypeAlias = RequestParams
ParamTypes: t.TypeAlias = OfflineParamTypes | OnlineParamTypes ParamTypes: t.TypeAlias = OfflineParamTypes | OnlineParamTypes

View File

@@ -322,6 +322,7 @@ engines:
- name: abcnyheter - name: abcnyheter
engine: xpath engine: xpath
categories: general
paging: true paging: true
search_url: https://startsiden.abcnyheter.no/sok/?q={query}&page={pageno} search_url: https://startsiden.abcnyheter.no/sok/?q={query}&page={pageno}
shortcut: abc shortcut: abc
@@ -330,12 +331,12 @@ engines:
url_xpath: ./a/@href url_xpath: ./a/@href
title_xpath: ./a/h3 title_xpath: ./a/h3
content_xpath: ./div content_xpath: ./div
language: "no"
about: about:
website: https://abcnyheter.no website: https://abcnyheter.no
use_official_api: false use_official_api: false
require_api_key: false require_api_key: false
results: HTML results: HTML
language: "no"
- name: acfun - name: acfun
engine: acfun engine: acfun
@@ -442,27 +443,12 @@ engines:
timeout: 6.0 timeout: 6.0
shortcut: conda shortcut: conda
disabled: true disabled: true
about:
- name: aol website: https://anaconda.org/search
engine: aol wikidata_id: Q18209310
search_type: search official_api_documentation: https://api.anaconda.org/search
categories: [general] use_official_api: false
shortcut: aol results: HTML
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 - name: arch linux wiki
engine: archlinux engine: archlinux
@@ -493,6 +479,7 @@ engines:
- name: ayo - name: ayo
engine: xpath engine: xpath
categories: general
shortcut: ayo shortcut: ayo
search_url: https://search.ayo.de/search?q={query} search_url: https://search.ayo.de/search?q={query}
results_xpath: //div[contains(@class, 'search-result')]/div results_xpath: //div[contains(@class, 'search-result')]/div
@@ -663,28 +650,29 @@ engines:
engine: chinaso engine: chinaso
shortcut: chinaso shortcut: chinaso
categories: [news] categories: [news]
chinaso_category: news
chinaso_news_source: all chinaso_news_source: all
disabled: true disabled: true
inactive: true inactive: true
- name: chinaso images - name: cl0q
engine: chinaso engine: json_engine
network: chinaso news shortcut: cl
shortcut: chinasoi categories: general
categories: [images] paging: true
chinaso_category: images first_page_num: 0
disabled: true page_size: 20
inactive: true search_url: https://cl0q.com/search?q={query}&limit=20&offset={pageno}
results_query: results
- name: chinaso videos url_query: domain
engine: chinaso url_prefix: https://
network: chinaso news title_query: title
shortcut: chinasov content_query: description
categories: [videos]
chinaso_category: videos
disabled: true disabled: true
inactive: true inactive: true
about:
website: https://cl0q.com
description: "Open source network for searching domains"
results: JSON
- name: cloudflareai - name: cloudflareai
engine: cloudflareai engine: cloudflareai
@@ -976,6 +964,34 @@ engines:
shortcut: fd shortcut: fd
disabled: true 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 - name: findthatmeme
engine: findthatmeme engine: findthatmeme
shortcut: ftm shortcut: ftm
@@ -1079,6 +1095,7 @@ engines:
- name: gabanza - name: gabanza
engine: xpath engine: xpath
categories: general
search_url: https://www.gabanza.com/search?query={query} search_url: https://www.gabanza.com/search?query={query}
shortcut: gab shortcut: gab
timeout: 4 timeout: 4
@@ -1111,6 +1128,11 @@ engines:
search_type: text search_type: text
timeout: 10 timeout: 10
- name: giphy
engine: giphy
shortcut: gif
disabled: true
- name: gitlab - name: gitlab
engine: gitlab engine: gitlab
base_url: https://gitlab.com base_url: https://gitlab.com
@@ -1292,6 +1314,13 @@ engines:
require_api_key: false require_api_key: false
results: JSON results: JSON
- name: iseek
engine: iseek
shortcut: isk
timeout: 4
disabled: true
inactive: true
- name: il post - name: il post
engine: il_post engine: il_post
shortcut: pst shortcut: pst
@@ -1380,6 +1409,21 @@ engines:
# api_key: "" # required # api_key: "" # required
# kagi_categ: videos # kagi_categ: videos
- name: kozmonavt
engine: xpath
search_url: https://kozmonavt.su/s?q={query}
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 - name: jisho
engine: jisho engine: jisho
shortcut: js shortcut: js
@@ -1397,6 +1441,22 @@ engines:
shortcut: kc shortcut: kc
timeout: 4.0 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 - name: lemmy communities
engine: lemmy engine: lemmy
lemmy_type: Communities lemmy_type: Communities
@@ -2030,7 +2090,7 @@ engines:
- name: rawweb - name: rawweb
engine: json_engine engine: json_engine
shortcut: rw shortcut: rw
categories: general categories: [general, blogs]
paging: true paging: true
search_url: 'https://api.rawweb.org/api/search?keyword={query}&page={pageno}&lang=*' search_url: 'https://api.rawweb.org/api/search?keyword={query}&page={pageno}&lang=*'
results_query: data results_query: data
@@ -2085,7 +2145,7 @@ engines:
- name: searchmysite - name: searchmysite
engine: xpath engine: xpath
shortcut: sms shortcut: sms
categories: general categories: [general, blogs]
paging: true paging: true
search_url: https://searchmysite.net/search/?q={query}&page={pageno} search_url: https://searchmysite.net/search/?q={query}&page={pageno}
results_xpath: //div[contains(@class,'search-result')] results_xpath: //div[contains(@class,'search-result')]
@@ -2401,6 +2461,35 @@ engines:
- 5000 - 5000
inactive: true 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 # tmp suspended - too slow, too many errors
# - name: urbandictionary # - name: urbandictionary
# engine : xpath # engine : xpath
@@ -2414,6 +2503,24 @@ engines:
engine: unsplash engine: unsplash
shortcut: us 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 - name: yandex
engine: yandex engine: yandex
categories: general categories: general
@@ -2473,7 +2580,7 @@ engines:
url_query: URL url_query: URL
title_query: Title title_query: Title
content_query: Snippet content_query: Snippet
categories: [general, web] categories: [general, blogs]
shortcut: wib shortcut: wib
disabled: true disabled: true
about: about:
@@ -2808,13 +2915,13 @@ engines:
shortcut: rel shortcut: rel
categories: general categories: general
disabled: true disabled: true
language: de
about: about:
website: https://reloado.com website: https://reloado.com
official_api_documentation: official_api_documentation:
use_official_api: false use_official_api: false
require_api_key: false require_api_key: false
results: HTML results: HTML
language: de
- name: repology - name: repology
engine: repology engine: repology
@@ -2836,6 +2943,25 @@ engines:
shortcut: rehi shortcut: rehi
disabled: true disabled: true
- name: searchch
engine: xpath
shortcut: sch
paging: true
search_url: https://search.ch/web/api/loadmore.html?path=/web/&q={query}&page={pageno}
results_xpath: //div[contains(@class, 'www-feed-web-result')]
# the URL looks like "//search.ch/web/r/redirect?...result!uffe8997e781e241d/https://example.com"
url_xpath: concat('https://', substring-after(.//a[contains(@class, 'sl-gus-result-url')]/@href, '://'))
title_xpath: .//a[contains(@class, 'sl-gus-result-title')]
content_xpath: .//div[contains(@class, 'sl-gus-result-body')]
language: "ch"
disabled: true
about:
website: https://search.ch/web
description: "Swiss search engine with its own Swiss index"
use_official_api: false
require_api_key: false
results: HTML
- name: searchzee - name: searchzee
engine: json_engine engine: json_engine
search_url: https://searchzee.com/api/search?q={query}&type=web&offset={pageno} search_url: https://searchzee.com/api/search?q={query}&type=web&offset={pageno}
@@ -2879,6 +3005,38 @@ engines:
disabled: true disabled: true
inactive: 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 - name: swisscows
engine: swisscows engine: swisscows
categories: general categories: general
@@ -2927,13 +3085,13 @@ engines:
content_xpath: //div[@class="synonyms-list-group"] content_xpath: //div[@class="synonyms-list-group"]
title_xpath: //div[@class="upper-synonyms"]/a title_xpath: //div[@class="upper-synonyms"]/a
no_result_for_http_status: [404] no_result_for_http_status: [404]
language: de
about: about:
website: https://www.woxikon.de/ website: https://www.woxikon.de/
wikidata_id: # No Wikidata ID wikidata_id: # No Wikidata ID
use_official_api: false use_official_api: false
require_api_key: false require_api_key: false
results: HTML results: HTML
language: de
- name: tootfinder - name: tootfinder
engine: tootfinder engine: tootfinder
@@ -2988,22 +3146,39 @@ engines:
content_xpath: //li/div[@class="searchresult"] content_xpath: //li/div[@class="searchresult"]
categories: general categories: general
disabled: true disabled: true
language: fr
about: about:
website: https://wikimini.org/ website: https://wikimini.org/
wikidata_id: Q3568032 wikidata_id: Q3568032
use_official_api: false use_official_api: false
require_api_key: false require_api_key: false
results: HTML results: HTML
language: fr
- name: wttr.in - name: wttr.in
engine: wttr engine: wttr
shortcut: wttr shortcut: wttr
timeout: 9.0 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 - name: zapmeta
engine: xpath engine: xpath
shortcut: zpm shortcut: zpm
categories: general
search_url: https://www.zapmeta.com/search?q={query}&pg={pageno} search_url: https://www.zapmeta.com/search?q={query}&pg={pageno}
results_xpath: //article[contains(@class, "organic-results-item")] results_xpath: //article[contains(@class, "organic-results-item")]
url_xpath: ./h2/a/@href url_xpath: ./h2/a/@href

File diff suppressed because one or more lines are too long

View File

@@ -77,7 +77,7 @@
] ]
}, },
"src/js/plugin/Calculator.ts": { "src/js/plugin/Calculator.ts": {
"file": "chunk/DDL5uWMz.min.js", "file": "chunk/CTP0QVHn.min.js",
"name": "calculator", "name": "calculator",
"src": "src/js/plugin/Calculator.ts", "src": "src/js/plugin/Calculator.ts",
"isDynamicEntry": true, "isDynamicEntry": true,

View File

@@ -1,3 +1,3 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/U6YV4Y8e.min.js","./sxng-mapview.min.css","./chunk/DpvWr1cn.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/DDL5uWMz.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/DvCYLbJr.min.js","./chunk/B8prKeWj.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/U6YV4Y8e.min.js","./sxng-mapview.min.css","./chunk/DpvWr1cn.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/CTP0QVHn.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/DvCYLbJr.min.js","./chunk/B8prKeWj.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{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),{}}},a=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},o=(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)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=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}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.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)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/U6YV4Y8e.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DpvWr1cn.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DDL5uWMz.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/B8prKeWj.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t}; var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{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),{}}},a=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},o=(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)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=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}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.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)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/U6YV4Y8e.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DpvWr1cn.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/CTP0QVHn.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/B8prKeWj.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t};
//# sourceMappingURL=sxng-core.min.js.map //# sourceMappingURL=sxng-core.min.js.map

View File

@@ -9,7 +9,6 @@
''' '''
sxng_locales = ( sxng_locales = (
('af', 'Afrikaans', '', 'Afrikaans', '\U0001f310'),
('ar', 'العربية', '', 'Arabic', '\U0001f310'), ('ar', 'العربية', '', 'Arabic', '\U0001f310'),
('ar-SA', 'العربية', 'المملكة العربية السعودية', 'Arabic', '\U0001f1f8\U0001f1e6'), ('ar-SA', 'العربية', 'المملكة العربية السعودية', 'Arabic', '\U0001f1f8\U0001f1e6'),
('bg', 'Български', '', 'Bulgarian', '\U0001f310'), ('bg', 'Български', '', 'Bulgarian', '\U0001f310'),
@@ -46,7 +45,6 @@ sxng_locales = (
('es-ES', 'Español', 'España', 'Spanish', '\U0001f1ea\U0001f1f8'), ('es-ES', 'Español', 'España', 'Spanish', '\U0001f1ea\U0001f1f8'),
('es-MX', 'Español', 'México', 'Spanish', '\U0001f1f2\U0001f1fd'), ('es-MX', 'Español', 'México', 'Spanish', '\U0001f1f2\U0001f1fd'),
('es-PE', 'Español', 'Perú', 'Spanish', '\U0001f1f5\U0001f1ea'), ('es-PE', 'Español', 'Perú', 'Spanish', '\U0001f1f5\U0001f1ea'),
('es-VE', 'Español', 'Venezuela', 'Spanish', '\U0001f1fb\U0001f1ea'),
('et', 'Eesti', '', 'Estonian', '\U0001f310'), ('et', 'Eesti', '', 'Estonian', '\U0001f310'),
('et-EE', 'Eesti', 'Eesti', 'Estonian', '\U0001f1ea\U0001f1ea'), ('et-EE', 'Eesti', 'Eesti', 'Estonian', '\U0001f1ea\U0001f1ea'),
('fi', 'Suomi', '', 'Finnish', '\U0001f310'), ('fi', 'Suomi', '', 'Finnish', '\U0001f310'),
@@ -58,7 +56,6 @@ sxng_locales = (
('fr-CA', 'Français', 'Canada', 'French', '\U0001f1e8\U0001f1e6'), ('fr-CA', 'Français', 'Canada', 'French', '\U0001f1e8\U0001f1e6'),
('fr-CH', 'Français', 'Suisse', 'French', '\U0001f1e8\U0001f1ed'), ('fr-CH', 'Français', 'Suisse', 'French', '\U0001f1e8\U0001f1ed'),
('fr-FR', 'Français', 'France', 'French', '\U0001f1eb\U0001f1f7'), ('fr-FR', 'Français', 'France', 'French', '\U0001f1eb\U0001f1f7'),
('gl', 'Galego', '', 'Galician', '\U0001f310'),
('hi', 'हिन्दी', '', 'Hindi', '\U0001f310'), ('hi', 'हिन्दी', '', 'Hindi', '\U0001f310'),
('hi-IN', 'हिन्दी', 'भारत', 'Hindi', '\U0001f1ee\U0001f1f3'), ('hi-IN', 'हिन्दी', 'भारत', 'Hindi', '\U0001f1ee\U0001f1f3'),
('hr', 'Hrvatski', '', 'Croatian', '\U0001f310'), ('hr', 'Hrvatski', '', 'Croatian', '\U0001f310'),
@@ -74,6 +71,8 @@ sxng_locales = (
('ja-JP', '日本語', '日本', 'Japanese', '\U0001f1ef\U0001f1f5'), ('ja-JP', '日本語', '日本', 'Japanese', '\U0001f1ef\U0001f1f5'),
('ko', '한국어', '', 'Korean', '\U0001f310'), ('ko', '한국어', '', 'Korean', '\U0001f310'),
('ko-KR', '한국어', '대한민국', 'Korean', '\U0001f1f0\U0001f1f7'), ('ko-KR', '한국어', '대한민국', 'Korean', '\U0001f1f0\U0001f1f7'),
('mi', 'Māori', '', 'Māori', '\U0001f310'),
('mi-NZ', 'Māori', 'Aotearoa', 'Māori', '\U0001f1f3\U0001f1ff'),
('nb', 'Norsk Bokmål', '', 'Norwegian Bokmål', '\U0001f310'), ('nb', 'Norsk Bokmål', '', 'Norwegian Bokmål', '\U0001f310'),
('nb-NO', 'Norsk Bokmål', 'Norge', 'Norwegian Bokmål', '\U0001f1f3\U0001f1f4'), ('nb-NO', 'Norsk Bokmål', 'Norge', 'Norwegian Bokmål', '\U0001f1f3\U0001f1f4'),
('nl', 'Nederlands', '', 'Dutch', '\U0001f310'), ('nl', 'Nederlands', '', 'Dutch', '\U0001f310'),
@@ -90,8 +89,6 @@ sxng_locales = (
('ro-RO', 'Română', 'România', 'Romanian', '\U0001f1f7\U0001f1f4'), ('ro-RO', 'Română', 'România', 'Romanian', '\U0001f1f7\U0001f1f4'),
('ru', 'Русский', '', 'Russian', '\U0001f310'), ('ru', 'Русский', '', 'Russian', '\U0001f310'),
('ru-RU', 'Русский', 'Россия', 'Russian', '\U0001f1f7\U0001f1fa'), ('ru-RU', 'Русский', 'Россия', 'Russian', '\U0001f1f7\U0001f1fa'),
('sk', 'Slovenčina', '', 'Slovak', '\U0001f310'),
('sq', 'Shqip', '', 'Albanian', '\U0001f310'),
('sv', 'Svenska', '', 'Swedish', '\U0001f310'), ('sv', 'Svenska', '', 'Swedish', '\U0001f310'),
('sv-FI', 'Svenska', 'Finland', 'Swedish', '\U0001f1eb\U0001f1ee'), ('sv-FI', 'Svenska', 'Finland', 'Swedish', '\U0001f1eb\U0001f1ee'),
('sv-SE', 'Svenska', 'Sverige', 'Swedish', '\U0001f1f8\U0001f1ea'), ('sv-SE', 'Svenska', 'Sverige', 'Swedish', '\U0001f1f8\U0001f1ea'),
@@ -99,7 +96,6 @@ sxng_locales = (
('th-TH', 'ไทย', 'ไทย', 'Thai', '\U0001f1f9\U0001f1ed'), ('th-TH', 'ไทย', 'ไทย', 'Thai', '\U0001f1f9\U0001f1ed'),
('tr', 'Türkçe', '', 'Turkish', '\U0001f310'), ('tr', 'Türkçe', '', 'Turkish', '\U0001f310'),
('tr-TR', 'Türkçe', 'Türkiye', 'Turkish', '\U0001f1f9\U0001f1f7'), ('tr-TR', 'Türkçe', 'Türkiye', 'Turkish', '\U0001f1f9\U0001f1f7'),
('uk', 'Українська', '', 'Ukrainian', '\U0001f310'),
('vi', 'Tiếng Việt', '', 'Vietnamese', '\U0001f310'), ('vi', 'Tiếng Việt', '', 'Vietnamese', '\U0001f310'),
('vi-VN', 'Tiếng Việt', 'Việt Nam', 'Vietnamese', '\U0001f1fb\U0001f1f3'), ('vi-VN', 'Tiếng Việt', 'Việt Nam', 'Vietnamese', '\U0001f1fb\U0001f1f3'),
('zh', '中文', '', 'Chinese', '\U0001f310'), ('zh', '中文', '', 'Chinese', '\U0001f310'),

View File

@@ -60,8 +60,8 @@
{%- endif -%} {%- endif -%}
<label for="{{ engine_id }}"> <label for="{{ engine_id }}">
{{- ' ' -}}{{- search_engine.name -}} {{- ' ' -}}{{- search_engine.name -}}
{%- if search_engine.about and search_engine.about.language -%} {%- if search_engine.language -%}
{{- ' ' -}}({{search_engine.about.language | upper}}) {{- ' ' -}}({{search_engine.language | upper}})
{%- endif -%} {%- endif -%}
</label> </label>
{{- engine_about(search_engine) -}} {{- engine_about(search_engine) -}}

View File

@@ -17,22 +17,23 @@
# APoniatowski <aponiatowski@users.noreply.translate.codeberg.org>, 2025. # APoniatowski <aponiatowski@users.noreply.translate.codeberg.org>, 2025.
# French <french@noreply.codeberg.org>, 2025. # French <french@noreply.codeberg.org>, 2025.
# return42 <return42@noreply.codeberg.org>, 2025, 2026. # return42 <return42@noreply.codeberg.org>, 2025, 2026.
# Raithlin <raithlin@noreply.codeberg.org>, 2025. # Raithlin <raithlin@noreply.codeberg.org>, 2025, 2026.
# grumpyoldtechie <grumpyoldtechie@noreply.codeberg.org>, 2025, 2026. # grumpyoldtechie <grumpyoldtechie@noreply.codeberg.org>, 2025, 2026.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-05-25 10:44+0000\n" "POT-Creation-Date: 2026-06-13 11:31+0000\n"
"PO-Revision-Date: 2026-05-25 10:44+0000\n" "PO-Revision-Date: 2026-06-19 05:08+0000\n"
"Last-Translator: grumpyoldtechie <grumpyoldtechie@noreply.codeberg.org>\n" "Last-Translator: Raithlin <raithlin@noreply.codeberg.org>\n"
"Language-Team: Afrikaans <https://translate.codeberg.org/projects/searxng/"
"searxng/af/>\n"
"Language: af\n" "Language: af\n"
"Language-Team: Afrikaans "
"<https://translate.codeberg.org/projects/searxng/searxng/af/>\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.6.1\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING'] #. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -76,7 +77,7 @@ msgid "videos"
msgstr "videos" msgstr "videos"
#. CATEGORY_NAMES['RADIO'] #. CATEGORY_NAMES['RADIO']
#: searx/engines/radio_browser.py:146 searx/searxng.msg #: searx/engines/radio_browser.py:159 searx/searxng.msg
msgid "radio" msgid "radio"
msgstr "radio" msgstr "radio"
@@ -550,8 +551,8 @@ msgstr "geantwoord"
msgid "No item found" msgid "No item found"
msgstr "Geen item gevind" msgstr "Geen item gevind"
#: searx/engines/qwant.py:291 #: searx/engines/qwant.py:258
#: searx/templates/simple/result_templates/images.html:23 searx/webapp.py:328 #: searx/templates/simple/result_templates/images.html:62 searx/webapp.py:328
msgid "Source" msgid "Source"
msgstr "Bron" msgstr "Bron"
@@ -559,15 +560,15 @@ msgstr "Bron"
msgid "Error loading the next page" msgid "Error loading the next page"
msgstr "Fout met die laai van die volgende bladsy" msgstr "Fout met die laai van die volgende bladsy"
#: searx/webapp.py:479 searx/webapp.py:877 #: searx/webapp.py:478 searx/webapp.py:876
msgid "Invalid settings, please edit your preferences" msgid "Invalid settings, please edit your preferences"
msgstr "Ongeldige opstellings, redigeer asb jou voorkeure" msgstr "Ongeldige opstellings, redigeer asb jou voorkeure"
#: searx/webapp.py:495 #: searx/webapp.py:494
msgid "Invalid settings" msgid "Invalid settings"
msgstr "Ongeldige opstellings" msgstr "Ongeldige opstellings"
#: searx/webapp.py:571 searx/webapp.py:661 #: searx/webapp.py:570 searx/webapp.py:660
msgid "search error" msgid "search error"
msgstr "soekfout" msgstr "soekfout"
@@ -649,7 +650,7 @@ msgstr "Bereken {func} van die opsies"
#: searx/engines/boardreader.py:107 #: searx/engines/boardreader.py:107
#, python-brace-format #, python-brace-format
msgid "Posted by {author}" msgid "Posted by {author}"
msgstr "" msgstr "Geplaas deur {author}"
#: searx/engines/openstreetmap.py:155 #: searx/engines/openstreetmap.py:155
msgid "Show route in map .." msgid "Show route in map .."
@@ -664,19 +665,19 @@ msgstr "{title} (UITGEDIEN)"
msgid "This entry has been superseded by" msgid "This entry has been superseded by"
msgstr "Hierdie inskrywing was vervang deur" msgstr "Hierdie inskrywing was vervang deur"
#: searx/engines/qwant.py:293 #: searx/engines/qwant.py:260
msgid "Channel" msgid "Channel"
msgstr "Kanaal" msgstr "Kanaal"
#: searx/engines/radio_browser.py:148 #: searx/engines/radio_browser.py:161
msgid "bitrate" msgid "bitrate"
msgstr "bitsnelheid" msgstr "bitsnelheid"
#: searx/engines/radio_browser.py:149 #: searx/engines/radio_browser.py:162
msgid "votes" msgid "votes"
msgstr "stemme" msgstr "stemme"
#: searx/engines/radio_browser.py:150 #: searx/engines/radio_browser.py:163
msgid "clicks" msgid "clicks"
msgstr "klikke" msgstr "klikke"
@@ -734,11 +735,11 @@ msgstr "Filter uit onion resultate wat op Ahmia se swartlys is."
#: searx/plugins/calculator.py:25 #: searx/plugins/calculator.py:25
msgid "Calculator" msgid "Calculator"
msgstr "" msgstr "Sakrekenaar"
#: searx/plugins/calculator.py:26 #: searx/plugins/calculator.py:26
msgid "Parses and solves mathematical expressions." msgid "Parses and solves mathematical expressions."
msgstr "" msgstr "Ontleed en los wiskundige uitdrukkings op."
#: searx/plugins/hash_plugin.py:33 #: searx/plugins/hash_plugin.py:33
msgid "Hash plugin" msgid "Hash plugin"
@@ -934,7 +935,7 @@ msgstr "sienings"
#: searx/templates/simple/macros.html:43 #: searx/templates/simple/macros.html:43
#: searx/templates/simple/result_templates/file.html:18 #: searx/templates/simple/result_templates/file.html:18
#: searx/templates/simple/result_templates/images.html:19 #: searx/templates/simple/result_templates/images.html:52
#: searx/templates/simple/result_templates/paper.html:14 #: searx/templates/simple/result_templates/paper.html:14
msgid "Author" msgid "Author"
msgstr "Outeur" msgstr "Outeur"
@@ -1625,7 +1626,7 @@ msgid "This site did not provide any description."
msgstr "Hierdie webwerf het geen beskrywing verskaf nie." msgstr "Hierdie webwerf het geen beskrywing verskaf nie."
#: searx/templates/simple/result_templates/file.html:30 #: searx/templates/simple/result_templates/file.html:30
#: searx/templates/simple/result_templates/images.html:22 #: searx/templates/simple/result_templates/images.html:61
#: searx/templates/simple/result_templates/torrent.html:18 #: searx/templates/simple/result_templates/torrent.html:18
msgid "Filesize" msgid "Filesize"
msgstr "Lêergrootte" msgstr "Lêergrootte"
@@ -1643,21 +1644,25 @@ msgstr "Tik"
msgid "Download" msgid "Download"
msgstr "Laai af" msgstr "Laai af"
#: searx/templates/simple/result_templates/images.html:20 #: searx/templates/simple/result_templates/images.html:53
msgid "Resolution" msgid "Resolution"
msgstr "Resolusie" msgstr "Resolusie"
#: searx/templates/simple/result_templates/images.html:21 #: searx/templates/simple/result_templates/images.html:55
msgid "Format" msgid "Image formats"
msgstr "Formaat" msgstr "Beeldformate"
#: searx/templates/simple/result_templates/images.html:24 #: searx/templates/simple/result_templates/images.html:56
msgid "original format"
msgstr "oorspronklike formaat"
#: searx/templates/simple/result_templates/images.html:63
msgid "Engine" msgid "Engine"
msgstr "Enjin" msgstr "Enjin"
#: searx/templates/simple/result_templates/images.html:25 #: searx/templates/simple/result_templates/images.html:64
msgid "View source" msgid "View source"
msgstr "Sien Bron" msgstr "Bekyk bron"
#: searx/templates/simple/result_templates/map.html:12 #: searx/templates/simple/result_templates/map.html:12
msgid "address" msgid "address"
@@ -2159,3 +2164,5 @@ msgstr "versteek video"
#~ msgid "Number of results" #~ msgid "Number of results"
#~ msgstr "Aantal resultate" #~ msgstr "Aantal resultate"
#~ msgid "Format"
#~ msgstr "Formaat"

View File

@@ -30,20 +30,19 @@
# mustafa-phd <mustafa-phd@noreply.codeberg.org>, 2026. # mustafa-phd <mustafa-phd@noreply.codeberg.org>, 2026.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-05-25 10:44+0000\n" "POT-Creation-Date: 2026-06-13 11:31+0000\n"
"PO-Revision-Date: 2026-05-27 12:07+0000\n" "PO-Revision-Date: 2026-05-27 12:07+0000\n"
"Last-Translator: mustafa-phd <mustafa-phd@noreply.codeberg.org>\n" "Last-Translator: mustafa-phd <mustafa-phd@noreply.codeberg.org>\n"
"Language-Team: Arabic <https://translate.codeberg.org/projects/searxng/"
"searxng/ar/>\n"
"Language: ar\n" "Language: ar\n"
"Language-Team: Arabic "
"<https://translate.codeberg.org/projects/searxng/searxng/ar/>\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : "
"n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 2026.5\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING'] #. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -87,7 +86,7 @@ msgid "videos"
msgstr "ڤيديوهات" msgstr "ڤيديوهات"
#. CATEGORY_NAMES['RADIO'] #. CATEGORY_NAMES['RADIO']
#: searx/engines/radio_browser.py:146 searx/searxng.msg #: searx/engines/radio_browser.py:159 searx/searxng.msg
msgid "radio" msgid "radio"
msgstr "إذاعة" msgstr "إذاعة"
@@ -561,8 +560,8 @@ msgstr "تمت الإجابة"
msgid "No item found" msgid "No item found"
msgstr "لم يتم العثور على عنصر" msgstr "لم يتم العثور على عنصر"
#: searx/engines/qwant.py:291 #: searx/engines/qwant.py:258
#: searx/templates/simple/result_templates/images.html:23 searx/webapp.py:328 #: searx/templates/simple/result_templates/images.html:62 searx/webapp.py:328
msgid "Source" msgid "Source"
msgstr "مصدر" msgstr "مصدر"
@@ -570,15 +569,15 @@ msgstr "مصدر"
msgid "Error loading the next page" msgid "Error loading the next page"
msgstr "خطأ في تحميل الصفحة التالية" msgstr "خطأ في تحميل الصفحة التالية"
#: searx/webapp.py:479 searx/webapp.py:877 #: searx/webapp.py:478 searx/webapp.py:876
msgid "Invalid settings, please edit your preferences" msgid "Invalid settings, please edit your preferences"
msgstr "إعدادات غير صالحة، يرجى تعديل تفضيلاتك" msgstr "إعدادات غير صالحة، يرجى تعديل تفضيلاتك"
#: searx/webapp.py:495 #: searx/webapp.py:494
msgid "Invalid settings" msgid "Invalid settings"
msgstr "إعدادات غير صالحة" msgstr "إعدادات غير صالحة"
#: searx/webapp.py:571 searx/webapp.py:661 #: searx/webapp.py:570 searx/webapp.py:660
msgid "search error" msgid "search error"
msgstr "خطأ في البحث" msgstr "خطأ في البحث"
@@ -675,19 +674,19 @@ msgstr "{title} (قديم)"
msgid "This entry has been superseded by" msgid "This entry has been superseded by"
msgstr "تم استبدال هذا الإدخال بـ" msgstr "تم استبدال هذا الإدخال بـ"
#: searx/engines/qwant.py:293 #: searx/engines/qwant.py:260
msgid "Channel" msgid "Channel"
msgstr "القناة" msgstr "القناة"
#: searx/engines/radio_browser.py:148 #: searx/engines/radio_browser.py:161
msgid "bitrate" msgid "bitrate"
msgstr "معدل البت" msgstr "معدل البت"
#: searx/engines/radio_browser.py:149 #: searx/engines/radio_browser.py:162
msgid "votes" msgid "votes"
msgstr "تصويتات" msgstr "تصويتات"
#: searx/engines/radio_browser.py:150 #: searx/engines/radio_browser.py:163
msgid "clicks" msgid "clicks"
msgstr "نقرات" msgstr "نقرات"
@@ -942,7 +941,7 @@ msgstr "المشاهدات"
#: searx/templates/simple/macros.html:43 #: searx/templates/simple/macros.html:43
#: searx/templates/simple/result_templates/file.html:18 #: searx/templates/simple/result_templates/file.html:18
#: searx/templates/simple/result_templates/images.html:19 #: searx/templates/simple/result_templates/images.html:52
#: searx/templates/simple/result_templates/paper.html:14 #: searx/templates/simple/result_templates/paper.html:14
msgid "Author" msgid "Author"
msgstr "الكاتب" msgstr "الكاتب"
@@ -993,8 +992,8 @@ msgid ""
"This is a preview of the settings used by the 'Search URL' you used to " "This is a preview of the settings used by the 'Search URL' you used to "
"get here." "get here."
msgstr "" msgstr ""
"هذه معاينة للإعدادات المستخدمة في 'عنوان URL للبحث' الذي استخدمته للوصول إلى " "هذه معاينة للإعدادات المستخدمة في 'عنوان URL للبحث' الذي استخدمته للوصول "
"هنا." "إلى هنا."
#: searx/templates/simple/preferences.html:158 #: searx/templates/simple/preferences.html:158
msgid "Press save to copy these preferences to your browser." msgid "Press save to copy these preferences to your browser."
@@ -1630,7 +1629,7 @@ msgid "This site did not provide any description."
msgstr "هذا الموقع لم يقدم أي وصف." msgstr "هذا الموقع لم يقدم أي وصف."
#: searx/templates/simple/result_templates/file.html:30 #: searx/templates/simple/result_templates/file.html:30
#: searx/templates/simple/result_templates/images.html:22 #: searx/templates/simple/result_templates/images.html:61
#: searx/templates/simple/result_templates/torrent.html:18 #: searx/templates/simple/result_templates/torrent.html:18
msgid "Filesize" msgid "Filesize"
msgstr "حجم الملف" msgstr "حجم الملف"
@@ -1648,19 +1647,23 @@ msgstr "نوع"
msgid "Download" msgid "Download"
msgstr "تنزيل" msgstr "تنزيل"
#: searx/templates/simple/result_templates/images.html:20 #: searx/templates/simple/result_templates/images.html:53
msgid "Resolution" msgid "Resolution"
msgstr "الدقة" msgstr "الدقة"
#: searx/templates/simple/result_templates/images.html:21 #: searx/templates/simple/result_templates/images.html:55
msgid "Format" msgid "Image formats"
msgstr "صيغة" msgstr ""
#: searx/templates/simple/result_templates/images.html:24 #: searx/templates/simple/result_templates/images.html:56
msgid "original format"
msgstr ""
#: searx/templates/simple/result_templates/images.html:63
msgid "Engine" msgid "Engine"
msgstr "محرك" msgstr "محرك"
#: searx/templates/simple/result_templates/images.html:25 #: searx/templates/simple/result_templates/images.html:64
msgid "View source" msgid "View source"
msgstr "عرض المصدر" msgstr "عرض المصدر"
@@ -2396,3 +2399,7 @@ msgstr "إخفاء الفيديو"
#~ msgid "Number of results" #~ msgid "Number of results"
#~ msgstr "حصيلة نتائج البحث" #~ msgstr "حصيلة نتائج البحث"
#~ msgid "Format"
#~ msgstr "صيغة"

Some files were not shown because too many files have changed in this diff Show More