68 Commits

Author SHA1 Message Date
Ivan Gabaldon
401d7f9f51 fmt 2026-09-01 21:46:00 +02:00
dependabot[bot]
aee33412aa [upd] pypi: Bump black from 25.9.0 to 26.5.1
Bumps [black](https://github.com/psf/black) from 25.9.0 to 26.5.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.9.0...26.5.1)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-01 21:45:23 +02:00
Bnyro
79c8ffe0da [update] data: update wikidata data 2026-09-01 13:56:11 +02:00
Bnyro
2b1c88c54c [refactor] wikidata: cache wikidata properties in searxng data 2026-09-01 13:56:11 +02:00
github-actions[bot]
d226b78bc4 [mod] data: update searx.data - update_engine_traits.py (#6594)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-08-29 10:14:39 +02:00
github-actions[bot]
bdbf9774f5 [mod] data: update searx.data - update_firefox_version.py (#6592)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-08-29 10:14:25 +02:00
github-actions[bot]
451c46aa32 [mod] data: update searx.data - update_ahmia_blacklist.py (#6591)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-08-29 10:13:59 +02:00
dependabot[bot]
a30b2d4749 [upd] pypi: Bump the minor group with 2 updates (#6589)
Bumps the minor group with 2 updates: [granian](https://github.com/emmett-framework/granian) and [lxml](https://github.com/lxml/lxml).


Updates `granian` from 2.8.1 to 2.8.2
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.8.1...v2.8.2)

Updates `lxml` from 6.1.1 to 6.1.2
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-6.1.1...lxml-6.1.2)

---
updated-dependencies:
- dependency-name: granian
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: lxml
  dependency-version: 6.1.2
  dependency-type: direct:production
  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-08-28 10:14:10 +02:00
github-actions[bot]
9fea41204f [l10n] update translations from Weblate (#6565)
930f4d95f - 2026-08-21 - return42 <return42@noreply.codeberg.org>
45e05f3a5 - 2026-08-21 - return42 <return42@noreply.codeberg.org>
f97f47265 - 2026-08-21 - return42 <return42@noreply.codeberg.org>
c5de4638c - 2026-08-21 - return42 <return42@noreply.codeberg.org>
02aac306b - 2026-08-21 - return42 <return42@noreply.codeberg.org>
f88e28cd0 - 2026-08-14 - Mooo <mooo@noreply.codeberg.org>
2026-08-22 07:32:09 +02:00
Markus Heiser
777ba8fa48 [mod] data: update searx.data - update_engine_traits.py (#6546)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-08-22 11:00:06 +08:00
vojkovic
a4cb7df053 [fix] google: use Nokia UA (#6546) 2026-08-22 11:00:06 +08:00
dependabot[bot]
bbb3c7d829 [upd] web-client (simple): Bump the minor group (#6558)
Bumps the minor group in /client/simple with 2 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) and [less](https://github.com/less/less.js).


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

Updates `less` from 4.8.1 to 4.9.0
- [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/compare/v4.8.1...v4.9.0)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: less
  dependency-version: 4.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 22:33:59 +02:00
dependabot[bot]
f31ff05db3 [upd] pypi: Bump the minor group with 2 updates (#6559)
Bumps the minor group with 2 updates: [basedpyright](https://github.com/detachhead/basedpyright) and [pygments](https://github.com/pygments/pygments).

Updates `basedpyright` from 1.39.9 to 1.39.10
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.39.9...v1.39.10)

Updates `pygments` from 2.20.0 to 2.21.0
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.20.0...2.21.0)

---
updated-dependencies:
- dependency-name: basedpyright
  dependency-version: 1.39.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: pygments
  dependency-version: 2.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 22:32:36 +02:00
vojkovic
5f4005996e [fix] preferences: cap zlib decompress 2026-08-21 20:55:33 +08:00
Ivan Gabaldon
487d7a96e0 [fix] py: (un)trusted proxies (#6556)
A missing condition allowed an attacker to mask their IP address from the
server. This exploit can only be carried under specific client & server
environments.

Reported-by: Raffaele Forte <raffaele@backbox.org>
Signed-off-by: Ivan Gabaldon <igabaldon@inetol.net>
2026-08-20 18:06:24 +02:00
SVHawk13
8d3dd0cd45 [feat] marginalia: add support for custom filters (#6543) 2026-08-20 14:22:22 +02:00
Brock Vojkovic
5ffd32ca2f [fix] calculator: XSS from innerHTML (#6551)
* [fix] calculator: XSS from innerHTML

* [build] /static
2026-08-19 14:09:02 +02:00
vojkovic
374939b888 [fix] engines: deviantart access denied 2026-08-17 20:06:57 +08:00
dependabot[bot]
b2da6b90f2 [upd] pypi: Bump the minor group across 1 directory with 6 updates (#6540)
* [upd] pypi: Bump the minor group across 1 directory with 6 updates

Bumps the minor group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [granian](https://github.com/emmett-framework/granian) | `2.7.9` | `2.8.1` |
| [pylint](https://github.com/pylint-dev/pylint) | `4.0.6` | `4.0.7` |
| [selenium](https://github.com/SeleniumHQ/Selenium) | `4.46.0` | `4.47.0` |
| [wlc](https://github.com/WeblateOrg/wlc) | `2.1.0` | `2.1.1` |
| [httpx-socks](https://github.com/romis2012/httpx-socks) | `0.10.0` | `0.13.0` |
| [typer](https://github.com/fastapi/typer) | `0.27.0` | `0.27.1` |



Updates `granian` from 2.7.9 to 2.8.1
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.7.9...v2.8.1)

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

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

Updates `wlc` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/WeblateOrg/wlc/releases)
- [Changelog](https://github.com/WeblateOrg/wlc/blob/main/CHANGES.rst)
- [Commits](https://github.com/WeblateOrg/wlc/compare/2.1.0...2.1.1)

Updates `httpx-socks` from 0.10.0 to 0.13.0
- [Release notes](https://github.com/romis2012/httpx-socks/releases)
- [Commits](https://github.com/romis2012/httpx-socks/compare/v0.10.0...v0.13.0)

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

---
updated-dependencies:
- dependency-name: granian
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: httpx-socks
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: pylint
  dependency-version: 4.0.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: selenium
  dependency-version: 4.47.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: typer
  dependency-version: 0.27.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: wlc
  dependency-version: 2.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
...

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

* [fix] network: bump httpx-socks to 0.13.1 for python-socks 3

httpx-socks 0.10.0 calls the default AnyIO Proxy with proxy_ssl.
python-socks 3.0.0 dropped that kwarg, so Tor/SOCKS startup checks
crash. 0.13.1 uses the v2 API (proxy_ssl still accepted) and allows
python-socks<4.

Drop the unused loop= argument; 0.13.1 forwards extra kwargs to
httpcore.AsyncConnectionPool, which rejects loop.

Closes: searxng/searxng#6514
Signed-off-by: Richard Freeman <rich@rich0.org>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Richard Freeman <rich@rich0.org>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Richard Freeman <rich@rich0.org>
2026-08-16 11:10:39 +02:00
dependabot[bot]
36698aff6b [upd] web-client (simple): Bump the minor group across 1 directory with 7 updates (#6522)
Bumps the minor group with 7 updates in the /client/simple directory:

| Package | From | To |
| --- | --- | --- |
| [ionicons](https://github.com/ionic-team/ionicons) | `8.0.13` | `8.1.0` |
| [ol](https://github.com/openlayers/openlayers) | `10.9.0` | `10.10.0` |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.5` | `2.5.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.1` | `26.2.0` |
| [browserslist](https://github.com/browserslist/browserslist) | `4.28.7` | `4.28.8` |
| [less](https://github.com/less/less.js) | `4.8.0` | `4.8.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.5` | `8.2.1` |

Updates `ionicons` from 8.0.13 to 8.1.0
- [Release notes](https://github.com/ionic-team/ionicons/releases)
- [Commits](https://github.com/ionic-team/ionicons/compare/v8.0.13...v8.1.0)

Updates `ol` from 10.9.0 to 10.10.0
- [Release notes](https://github.com/openlayers/openlayers/releases)
- [Commits](https://github.com/openlayers/openlayers/compare/v10.9.0...v10.10.0)

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

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

Updates `browserslist` from 4.28.7 to 4.28.8
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.7...4.28.8)

Updates `less` from 4.8.0 to 4.8.1
- [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/compare/v4.8.0...v4.8.1)

Updates `vite` from 8.1.5 to 8.2.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.2.1/packages/vite)

---
updated-dependencies:
- dependency-name: ionicons
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: ol
  dependency-version: 10.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 26.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: less
  dependency-version: 4.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: vite
  dependency-version: 8.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-16 10:34:47 +02:00
dependabot[bot]
094c33d406 [upd] github-actions: Bump JamesIves/github-pages-deploy-action (#6521)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.8.0 to 4.9.0.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](d92aa235d0...fa24774553)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-version: 4.9.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-08-14 10:18:45 +02:00
ArsenBilov
ef9a188cc8 [feat] engines: add paid yandex search api (#6349) 2026-08-13 18:16:56 +02:00
Bnyro
cdfdaa5a88 [mod] dogpile: set to inactive by default 2026-08-12 16:14:58 +02:00
Bnyro
5638231358 [fix] dogpile: access denied due to missing origin header 2026-08-12 16:13:15 +02:00
vojkovic
1c3bb1e88f [mod] defaults: GET method and ddg autocomplete 2026-08-12 19:05:02 +08:00
Ivan Gabaldon
54613defc7 Revert "[upd] pypi: Bump the minor group across 1 directory with 3 updates (#…" (#6515)
This reverts commit e8e710e42a.
2026-08-12 01:34:08 +02:00
dependabot[bot]
e8e710e42a [upd] pypi: Bump the minor group across 1 directory with 3 updates (#6495)
Bumps the minor group with 3 updates in the / directory: [typer](https://github.com/fastapi/typer), [wlc](https://github.com/WeblateOrg/wlc) and [granian](https://github.com/emmett-framework/granian).


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

Updates `wlc` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/WeblateOrg/wlc/releases)
- [Changelog](https://github.com/WeblateOrg/wlc/blob/main/CHANGES.rst)
- [Commits](https://github.com/WeblateOrg/wlc/compare/2.1.0...2.1.1)

Updates `granian` from 2.7.9 to 2.8.0
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.7.9...v2.8.0)

---
updated-dependencies:
- dependency-name: typer
  dependency-version: 0.27.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: wlc
  dependency-version: 2.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: granian
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 17:45:08 +02:00
dependabot[bot]
7b0c7f0bf8 [upd] github-actions: Bump docker/login-action from 4.5.2 to 4.6.0 (#6493)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.2 to 4.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](371161bbe7...dbcb813823)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.6.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-08-11 17:38:34 +02:00
github-actions[bot]
e033be7c7c [l10n] update translations from Weblate (#6496)
8f9804206 - 2026-08-03 - Aindriú Mac Giolla Eoin <aindriu80@noreply.codeberg.org>
6850566e7 - 2026-08-04 - mvtbh <mvtbh@noreply.codeberg.org>
f97257801 - 2026-08-01 - ShinyDust <shinydust@noreply.codeberg.org>

Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-08-11 17:37:46 +02:00
Bnyro
0a118066d8 [feat] engines: add jina (general) 2026-08-10 14:14:33 +02:00
Bnyro
b023a28bab [fix] engines: only print search traceback in debug mode 2026-08-06 17:43:17 +02:00
Ivan Gabaldon
c63835bd2a [mod] data: update searx.data - update_wikidata_units.py (#6474) 2026-08-04 19:38:59 +02:00
vojkovic
1689cb1b53 [feat] autocomplete: add kagi autocompleter 2026-08-05 00:00:54 +08:00
Bnyro
aa059419ff [fix] autocompleter: suggestions contain HTML escape codes
The results were previously HTML-escaped for some reason.
That doesn't really make much sense because they never got
unescaped anywhere.

For example, `A&B` gets escaped to `A&amp;B` and never unescaped,
so the autocompletion frontend shows `A&amp;B`, which isn't user-friendly
at all.

Also, it doesn't make sense to escape the full JSON instead
of only escaping the suggestion texts individually because that
makes it even more unclear.
2026-08-03 19:17:57 +02:00
vojkovic
0734ee6c71 [build] /static 2026-08-03 19:51:02 +08:00
vojkovic
d81810d2a7 [fix] autocomplete: drop POST method 2026-08-03 19:51:02 +08:00
vojkovic
8892414dc3 [feat] add kagi favicon resolver 2026-08-01 11:09:50 +08:00
dependabot[bot]
6bfd82705a [upd] github-actions: Bump docker/login-action from 4.5.0 to 4.5.2 (#6478)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.0 to 4.5.2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](06fb636fac...371161bbe7)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 17:03:08 +02:00
vojkovic
057a77168d [ci] add ai policy checkbox detection 2026-07-31 06:28:06 +08:00
Ilya Bogin
0be6f87801 [feat] engines: add keenable (general) 2026-07-30 16:11:42 +02:00
Ivan Gabaldon
ef3a6ea9fd [mod] data: update searx.data - update_currencies.py (#6475) 2026-07-30 15:16:02 +02:00
Ivan Gabaldon
f25b75e613 [mod] py: cleanup link_token.py 2026-07-30 14:54:39 +02:00
Bnyro
98e10f9ab4 [mod] swisscows: use alphabet from string module instead of hardcoding alphabet 2026-07-30 14:54:39 +02:00
Bnyro
a449518ed4 [mod] engines: replace [random.choice(...) for ... in range(..)] with random.choices 2026-07-30 14:54:39 +02:00
Bnyro
702f702f9b [del] reddit: remove engine, requires authentication now 2026-07-30 14:53:46 +02:00
github-actions[bot]
afdfd81613 [mod] data: update searx.data - update_firefox_version.py (#6466)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-07-30 14:26:18 +02:00
github-actions[bot]
c81ed99c69 [mod] data: update searx.data - update_engine_traits.py (#6468)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-07-30 14:26:02 +02:00
github-actions[bot]
ecf8497b65 [mod] data: update searx.data - update_ahmia_blacklist.py (#6467)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-07-30 14:25:37 +02:00
Ivan Gabaldon
e28131f8d4 [mod] docs: sidebar remove verbose features (#6465)
Remove all references to `dev.searxng.org` and get rid of some highlights
2026-07-30 14:24:39 +02:00
Léon Tiekötter
81b0ed7b38 chore: remove presearch engine
Removing the presearch engine and configuration because it got shutdown.

ref.: https://news.presearch.io/a-message-from-the-presearch-team-aaa448052b2b
https://x.com/presearchnews/status/2080744472791441807
2026-07-30 14:23:05 +02:00
prodzeus
c01178d031 Updated pronoun in favicons documentation (#6464)
Updated pronoun 'his' to 'their', to be more inclusive.
2026-07-28 21:40:42 +02:00
github-actions[bot]
c73861ab46 [l10n] update translations from Weblate (#6447)
c62975ec7 - 2026-07-22 - alexgabi <alexgabi@noreply.codeberg.org>
a0f3db680 - 2026-07-23 - gallegonovato <gallegonovato@noreply.codeberg.org>
c1271523f - 2026-07-17 - codebergnew2 <codebergnew2@noreply.codeberg.org>

Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-07-28 21:03:43 +02:00
dependabot[bot]
891bc69550 [upd] web-client (simple): Bump the minor group across 1 directory with 6 updates (#6445)
Bumps the minor group with 6 updates in the /client/simple directory:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.3` | `2.5.5` |
| [browserslist](https://github.com/browserslist/browserslist) | `4.28.6` | `4.28.7` |
| [less](https://github.com/less/less.js) | `4.6.7` | `4.8.0` |
| [stylelint](https://github.com/stylelint/stylelint) | `17.14.0` | `17.14.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.4` | `8.1.5` |
| [vite-bundle-analyzer](https://github.com/nonzzz/vite-bundle-analyzer) | `1.3.8` | `1.3.9` |

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

Updates `browserslist` from 4.28.6 to 4.28.7
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.6...4.28.7)

Updates `less` from 4.6.7 to 4.8.0
- [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/compare/v4.6.7...v4.8.0)

Updates `stylelint` from 17.14.0 to 17.14.1
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/17.14.0...17.14.1)

Updates `vite` from 8.1.4 to 8.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite)

Updates `vite-bundle-analyzer` from 1.3.8 to 1.3.9
- [Release notes](https://github.com/nonzzz/vite-bundle-analyzer/releases)
- [Changelog](https://github.com/nonzzz/vite-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nonzzz/vite-bundle-analyzer/compare/v1.3.8...v1.3.9)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: browserslist
  dependency-version: 4.28.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: less
  dependency-version: 4.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: stylelint
  dependency-version: 17.14.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: vite
  dependency-version: 8.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: vite-bundle-analyzer
  dependency-version: 1.3.9
  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-07-28 21:02:52 +02:00
dependabot[bot]
d661b6114d [upd] pypi: Bump certifi from 2026.6.17 to 2026.7.22 in the minor group (#6446)
Bumps the minor group with 1 update: [certifi](https://github.com/certifi/python-certifi).


Updates `certifi` from 2026.6.17 to 2026.7.22
- [Commits](https://github.com/certifi/python-certifi/compare/2026.06.17...2026.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2026.7.22
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 20:52:53 +02:00
Bnyro
8372f5d855 [fix] exaapi: missing shortcut causes engine to crash 2026-07-28 18:33:27 +02:00
kesku
8f8b5d2b8d [feat] engines: add Exa Search API engine 2026-07-28 18:02:42 +02:00
vojkovic
b060c780d0 Revert "[ci] add ai policy checkbox detection"
This reverts commit 6d8b550280.
2026-07-26 16:53:10 +00:00
vojkovic
6d8b550280 [ci] add ai policy checkbox detection 2026-07-26 16:51:14 +00:00
dependabot[bot]
0909dbc9ef [upd] github-actions: Bump actions/setup-python from 6.3.0 to 7.0.0 (#6444)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.3.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](ece7cb06ca...5fda3b95a4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  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-07-25 08:44:50 +02:00
dependabot[bot]
b4e94417b7 [upd] github-actions: Bump actions/checkout from 7.0.0 to 7.0.1 (#6443)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](9c091bb21b...3d3c42e5aa)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 08:44:30 +02:00
dependabot[bot]
4f64d95013 [upd] github-actions: Bump docker/login-action from 4.4.0 to 4.5.0 (#6442)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](af1e73f918...06fb636fac)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.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-07-24 16:15:49 +02:00
Burak Emir Sezen
ef8f6470e0 [fix] container: ignore invalid SEARXNG_PORT values (#6439)
* [fix] container: ignore invalid SEARXNG_PORT values

* add note

---------

Co-authored-by: Ivan Gabaldon <igabaldon@inetol.net>
2026-07-22 22:23:30 +02:00
Kes
6da6eee265 [upd] update to gecko driver v37 (#6433) 2026-07-19 17:04:57 +02:00
github-actions[bot]
277d8469cd [l10n] update translations from Weblate (#6426)
b498ffaf1 - 2026-07-17 - return42 <return42@noreply.codeberg.org>
341dacc88 - 2026-07-17 - impar <impar@noreply.codeberg.org>
2650a73a9 - 2026-07-17 - return42 <return42@noreply.codeberg.org>
37d2751a8 - 2026-07-17 - return42 <return42@noreply.codeberg.org>
2026-07-18 11:44:39 +02:00
dependabot[bot]
81c9c23862 [upd] github-actions: Bump actions/setup-node from 6.4.0 to 7.0.0 (#6423)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](48b55a011b...8207627860)

---
updated-dependencies:
- dependency-name: actions/setup-node
  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-07-17 14:27:38 +02:00
dependabot[bot]
6913fba208 [upd] pypi: Bump the minor group with 2 updates (#6425)
Bumps the minor group with 2 updates: [typer](https://github.com/fastapi/typer) and [selenium](https://github.com/SeleniumHQ/Selenium).


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

Updates `selenium` from 4.45.0 to 4.46.0
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.45.0...selenium-4.46.0)
2026-07-17 14:24:17 +02:00
Bnyro
2daa4d4815 [fix] qwant: can't fetch engine traits because engine.about is no longer a dict 2026-07-17 12:27:46 +02:00
Bnyro
de8f73f434 [fix] cache: maintenance period comment duration 2h instead of 1h 2026-07-17 12:24:47 +02:00
304 changed files with 28129 additions and 5028 deletions

39
.github/scripts/ai_policy.cjs vendored Normal file
View File

@@ -0,0 +1,39 @@
// Closes issues and prs whose authors/agents don't accept the ai policy
// https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
module.exports = async ({ github, context }) => {
const item = context.payload.pull_request || context.payload.issue;
const body = item.body || '';
const kind = context.payload.pull_request ? 'pull request' : 'issue';
// https://github.com/searxng/searxng/pull/6476#discussion_r3683782481
const hasBox = /\[[Xx]\].*AI Policy/.test(body);
const hasRef = /\[AI Policy\]:\s*https:\/\/github\.com\/searxng\/searxng\/.*AI_POLICY/.test(body);
if (hasBox && hasRef) {
return;
}
const { owner, repo } = context.repo;
await github.rest.issues.createComment({
owner,
repo,
issue_number: item.number,
body:
'Hello! Thank you for your contribution.\n\n' +
`Unfortunately your ${kind} was closed as the AI Policy has not been accepted.\n\n` +
`Please open a new ${kind} after confirming your contribution aligns with our AI Policy.`,
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: item.number,
labels: ['invalid:slop'],
});
await github.rest.issues.update({
owner,
repo,
issue_number: item.number,
state: 'closed',
state_reason: 'not_planned',
});
};

38
.github/workflows/ai-policy.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
---
# yamllint disable rule:line-length
name: AI Policy
# Closes any new issues and PRs from people (or agents) who don't accept the AI Policy
# yamllint disable-line rule:truthy
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
check:
name: Check AI Policy
# for issues with an author who has not contributed before
if: >-
github.event.sender.type != 'Bot' &&
contains(fromJSON('["NONE","FIRST_TIMER","FIRST_TIME_CONTRIBUTOR"]'),
github.event.issue.author_association ||
github.event.pull_request.author_association)
runs-on: ubuntu-26.04-arm
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: "false"
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const script = require('./.github/scripts/ai_policy.cjs');
await script({ github, context });

View File

@@ -50,14 +50,14 @@ jobs:
steps: steps:
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}" password: "${{ secrets.GITHUB_TOKEN }}"
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
@@ -65,7 +65,7 @@ jobs:
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}" ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"
@@ -110,7 +110,7 @@ jobs:
steps: steps:
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
@@ -120,7 +120,7 @@ jobs:
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}" ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"
@@ -144,21 +144,21 @@ jobs:
steps: steps:
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: "docker.io" registry: "docker.io"
username: "${{ secrets.DOCKER_USER }}" username: "${{ secrets.DOCKER_USER }}"
password: "${{ secrets.DOCKER_TOKEN }}" password: "${{ secrets.DOCKER_TOKEN }}"
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}" password: "${{ secrets.GITHUB_TOKEN }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}" ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"

View File

@@ -31,7 +31,7 @@ jobs:
- update_external_bangs.py - update_external_bangs.py
- update_firefox_version.py - update_firefox_version.py
- update_engine_traits.py - update_engine_traits.py
- update_wikidata_units.py - update_wikidata.py
- update_engine_descriptions.py - update_engine_descriptions.py
permissions: permissions:
@@ -40,12 +40,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"

View File

@@ -32,12 +32,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
@@ -61,7 +61,7 @@ jobs:
- if: github.ref_name == 'master' - if: github.ref_name == 'master'
name: Release name: Release
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0 uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4.9.0
with: with:
folder: "dist/docs" folder: "dist/docs"
branch: "gh-pages" branch: "gh-pages"

View File

@@ -34,12 +34,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
@@ -62,18 +62,18 @@ jobs:
runs-on: ubuntu-26.04-arm runs-on: ubuntu-26.04-arm
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: "26" node-version: "26"
check-latest: "true" check-latest: "true"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"

View File

@@ -35,12 +35,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
@@ -83,12 +83,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"

View File

@@ -2,7 +2,7 @@
/* /*
this file is generated automatically by searxng_extra/update/update_pygments.py this file is generated automatically by searxng_extra/update/update_pygments.py
using pygments version 2.20.0: using pygments version 2.21.0:
./manage templates.simple.pygments ./manage templates.simple.pygments
*/ */
@@ -114,14 +114,14 @@
.gd { color: #FF4689 } /* Generic.Deleted */ .gd { color: #FF4689 } /* Generic.Deleted */
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */ .ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ .ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.gr { color: #F8F8F2 } /* Generic.Error */ .gr { color: #FF4689 } /* Generic.Error */
.gh { color: #F8F8F2 } /* Generic.Heading */ .gh { color: #F8F8F2 } /* Generic.Heading */
.gi { color: #A6E22E } /* Generic.Inserted */ .gi { color: #A6E22E } /* Generic.Inserted */
.go { color: #66D9EF } /* Generic.Output */ .go { color: #66D9EF } /* Generic.Output */
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */ .gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */ .gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
.gu { color: #959077 } /* Generic.Subheading */ .gu { color: #959077 } /* Generic.Subheading */
.gt { color: #F8F8F2 } /* Generic.Traceback */ .gt { color: #66D9EF } /* Generic.Traceback */
.kc { color: #66D9EF } /* Keyword.Constant */ .kc { color: #66D9EF } /* Keyword.Constant */
.kd { color: #66D9EF } /* Keyword.Declaration */ .kd { color: #66D9EF } /* Keyword.Declaration */
.kn { color: #FF4689 } /* Keyword.Namespace */ .kn { color: #FF4689 } /* Keyword.Namespace */
@@ -132,7 +132,7 @@
.m { color: #AE81FF } /* Literal.Number */ .m { color: #AE81FF } /* Literal.Number */
.s { color: #E6DB74 } /* Literal.String */ .s { color: #E6DB74 } /* Literal.String */
.na { color: #A6E22E } /* Name.Attribute */ .na { color: #A6E22E } /* Name.Attribute */
.nb { color: #F8F8F2 } /* Name.Builtin */ .nb { color: #A6E22E } /* Name.Builtin */
.nc { color: #A6E22E } /* Name.Class */ .nc { color: #A6E22E } /* Name.Class */
.no { color: #66D9EF } /* Name.Constant */ .no { color: #66D9EF } /* Name.Constant */
.nd { color: #A6E22E } /* Name.Decorator */ .nd { color: #A6E22E } /* Name.Decorator */
@@ -166,7 +166,7 @@
.sr { color: #E6DB74 } /* Literal.String.Regex */ .sr { color: #E6DB74 } /* Literal.String.Regex */
.s1 { color: #E6DB74 } /* Literal.String.Single */ .s1 { color: #E6DB74 } /* Literal.String.Single */
.ss { color: #E6DB74 } /* Literal.String.Symbol */ .ss { color: #E6DB74 } /* Literal.String.Symbol */
.bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */ .bp { color: #A6E22E } /* Name.Builtin.Pseudo */
.fm { color: #A6E22E } /* Name.Function.Magic */ .fm { color: #A6E22E } /* Name.Function.Magic */
.vc { color: #F8F8F2 } /* Name.Variable.Class */ .vc { color: #F8F8F2 } /* Name.Variable.Class */
.vg { color: #F8F8F2 } /* Name.Variable.Global */ .vg { color: #F8F8F2 } /* Name.Variable.Global */

File diff suppressed because it is too large Load Diff

View File

@@ -23,27 +23,27 @@
"not dead" "not dead"
], ],
"dependencies": { "dependencies": {
"ionicons": "^8.0.13", "ionicons": "^8.1.0",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"ol": "^10.9.0", "ol": "^10.10.0",
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.3", "@biomejs/biome": "2.5.9",
"@types/node": "^26.1.1", "@types/node": "^26.2.0",
"browserslist": "^4.28.6", "browserslist": "^4.28.8",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.1",
"less": "^4.6.7", "less": "^4.9.0",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.3", "sharp": "~0.35.3",
"sort-package-json": "^4.0.0", "sort-package-json": "^4.0.0",
"stylelint": "^17.14.0", "stylelint": "^17.14.1",
"stylelint-config-standard-less": "^4.1.0", "stylelint-config-standard-less": "^4.1.0",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svgo": "^4.0.2", "svgo": "^4.0.2",
"typescript": "~7.0.2", "typescript": "~7.0.2",
"vite": "^8.1.4", "vite": "^8.2.1",
"vite-bundle-analyzer": "^1.3.8" "vite-bundle-analyzer": "^1.3.9"
} }
} }

View File

@@ -5,13 +5,7 @@ import { assertElement } from "../util/assertElement.ts";
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => { const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
try { try {
let res: Response; const res = await http("GET", `./autocompleter?q=${query}`);
if (settings.method === "GET") {
res = await http("GET", `./autocompleter?q=${query}`);
} else {
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
}
const results = await res.json(); const results = await res.json();

View File

@@ -80,7 +80,12 @@ export default class Calculator extends Plugin {
try { try {
const node = Calculator.math.parse(searchInput.value); const node = Calculator.math.parse(searchInput.value);
return `${node.toString()} = ${node.evaluate()}`; const value = node.evaluate();
if (typeof value !== "number") {
return;
}
return `${node.toString()} = ${value}`;
} catch { } catch {
// not a compatible math expression // not a compatible math expression
return; return;

View File

@@ -23,7 +23,7 @@ export const appendAnswerElement = (element: HTMLElement | string | number): voi
if (!(element instanceof HTMLElement)) { if (!(element instanceof HTMLElement)) {
const span = document.createElement("span"); const span = document.createElement("span");
span.innerHTML = element.toString(); span.textContent = element.toString();
// biome-ignore lint/style/noParameterAssign: TODO // biome-ignore lint/style/noParameterAssign: TODO
element = span; element = span;
} }

View File

@@ -112,6 +112,15 @@ if [ "$(id -u)" -eq 0 ]; then
fi fi
# ENVs aliases # ENVs aliases
export GRANIAN_PORT="${SEARXNG_PORT:-$GRANIAN_PORT}" # https://github.com/searxng/searxng/issues/5934
case "${SEARXNG_PORT:-}" in
'') ;;
*[!0-9]*)
unset SEARXNG_PORT
;;
*)
export GRANIAN_PORT="$SEARXNG_PORT"
;;
esac
exec /usr/local/searxng/.venv/bin/granian searx.webapp:app exec /usr/local/searxng/.venv/bin/granian searx.webapp:app

View File

@@ -29,10 +29,11 @@ By default and without any extensions, SearXNG serves these resolvers:
- ``duckduckgo`` - ``duckduckgo``
- ``allesedv`` - ``allesedv``
- ``google`` - ``google``
- ``kagi``
- ``yandex`` - ``yandex``
With the above setting favicons are displayed, the user has the option to With the above setting favicons are displayed, the user has the option to
deactivate this feature in his settings. If the user is to have the option of deactivate this feature in their settings. If the user is to have the option of
selecting from several *resolvers*, a further setting is required / but this selecting from several *resolvers*, a further setting is required / but this
setting will be discussed :ref:`later <register resolvers>` in this article, setting will be discussed :ref:`later <register resolvers>` in this article,
first we have to setup the favicons cache. first we have to setup the favicons cache.
@@ -208,6 +209,7 @@ choose from, the following configuration could be used:
"duckduckgo" = "searx.favicons.resolvers.duckduckgo" "duckduckgo" = "searx.favicons.resolvers.duckduckgo"
"allesedv" = "searx.favicons.resolvers.allesedv" "allesedv" = "searx.favicons.resolvers.allesedv"
# "google" = "searx.favicons.resolvers.google" # "google" = "searx.favicons.resolvers.google"
# "kagi" = "searx.favicons.resolvers.kagi"
# "yandex" = "searx.favicons.resolvers.yandex" # "yandex" = "searx.favicons.resolvers.yandex"
.. note:: .. note::
@@ -226,6 +228,7 @@ into the *proxy*:
- :py:obj:`searx.favicons.resolvers.duckduckgo` - :py:obj:`searx.favicons.resolvers.duckduckgo`
- :py:obj:`searx.favicons.resolvers.allesedv` - :py:obj:`searx.favicons.resolvers.allesedv`
- :py:obj:`searx.favicons.resolvers.google` - :py:obj:`searx.favicons.resolvers.google`
- :py:obj:`searx.favicons.resolvers.kagi`
- :py:obj:`searx.favicons.resolvers.yandex` - :py:obj:`searx.favicons.resolvers.yandex`

View File

@@ -8,7 +8,7 @@
search: search:
safe_search: 0 safe_search: 0
autocomplete: "" autocomplete: "duckduckgo"
favicon_resolver: "" favicon_resolver: ""
default_lang: "" default_lang: ""
ban_time_on_fail: 5 ban_time_on_fail: 5
@@ -32,7 +32,7 @@
- ``2``: Strict - ``2``: Strict
``autocomplete``: ``autocomplete``:
Existing autocomplete backends, leave blank to turn it off. Existing autocomplete backends, set blank to turn it off.
- ``360search`` - ``360search``
- ``baidu`` - ``baidu``
@@ -41,6 +41,7 @@
- ``dbpedia`` - ``dbpedia``
- ``duckduckgo`` - ``duckduckgo``
- ``google`` - ``google``
- ``kagi``
- ``mwmbl`` - ``mwmbl``
- ``naver`` - ``naver``
- ``privacywall`` - ``privacywall``

View File

@@ -14,7 +14,7 @@
limiter: false limiter: false
public_instance: false public_instance: false
image_proxy: false image_proxy: false
method: "POST" method: "GET"
default_http_headers: default_http_headers:
X-Content-Type-Options : nosniff X-Content-Type-Options : nosniff
X-Download-Options : noopen X-Download-Options : noopen
@@ -58,8 +58,8 @@
``method`` : ``GET`` | ``POST`` ``method`` : ``GET`` | ``POST``
HTTP method. By defaults ``POST`` is used / The ``POST`` method has the HTTP method. By default, ``GET`` is used / The ``POST`` method has the
advantage with some WEB browsers that the history is not easy to read, but advantage with some browsers that the history is not saved, but
there are also various disadvantages that sometimes **severely restrict the there are also various disadvantages that sometimes **severely restrict the
ease of use for the end user** (e.g. back button to jump back to the previous ease of use for the end user** (e.g. back button to jump back to the previous
search page and drag & drop of search term to new tabs do not work as search page and drag & drop of search term to new tabs do not work as

View File

@@ -0,0 +1,8 @@
.. _exaapi engine:
==============
Exa API Engine
==============
.. automodule:: searx.engines.exaapi
:members:

View File

@@ -0,0 +1,8 @@
.. _jina engine:
===========
Jina Engine
===========
.. automodule:: searx.engines.jina
:members:

View File

@@ -1,8 +0,0 @@
.. _engine presearch:
================
Presearch Engine
================
.. automodule:: searx.engines.presearch
:members:

View File

@@ -0,0 +1,8 @@
.. _yandex api engine:
=================
Yandex Search API
=================
.. automodule:: searx.engines.yandex_api
:members:

View File

@@ -80,8 +80,8 @@ same environment, here are a few examples::
# to test one of the update scripts # to test one of the update scripts
(dev.env)$ searxng_extra/update/update_engine_traits.py --help (dev.env)$ searxng_extra/update/update_engine_traits.py --help
# to test the update of the wikidata units # to test the update of the wikidata units and property names
(dev.env)$ searxng_extra/update/update_wikidata_units.py (dev.env)$ searxng_extra/update/update_wikidata.py
.. sidebar:: further read .. sidebar:: further read

View File

@@ -90,10 +90,10 @@ Scripts to update static data in :origin:`searx/data/`
:members: :members:
``update_wikidata_units.py`` ``update_wikidata.py``
============================ ============================
:origin:`[source] <searxng_extra/update/update_wikidata_units.py>` :origin:`[source] <searxng_extra/update/update_wikidata.py>`
.. automodule:: searxng_extra.update.update_wikidata_units .. automodule:: searxng_extra.update.update_wikidata
:members: :members:

View File

@@ -20,15 +20,11 @@ If you don't trust anyone, you can set up your own, see :ref:`installation`.
- :ref:`self hosted <installation>` - :ref:`self hosted <installation>`
- :ref:`no user tracking / no profiling <SearXNG protect privacy>` - :ref:`no user tracking / no profiling <SearXNG protect privacy>`
- script & cookies are optional - javascript & cookies are optional
- secure, encrypted connections
- :ref:`{{engines | length}} search engines <configured engines>` - :ref:`{{engines | length}} search engines <configured engines>`
- `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_ - `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_
- about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_ - about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_
- :ref:`easy integration of search engines <demo online engine>` - :ref:`easy integration of search engines <demo online engine>`
- professional development: `CI <https://github.com/searxng/searxng/actions>`_,
`quality assurance <https://dev.searxng.org/>`_ &
`automated tested UI <https://dev.searxng.org/screenshots.html>`_
.. sidebar:: be a part .. sidebar:: be a part

2
manage
View File

@@ -48,7 +48,7 @@ PATH="${PY_ENV}/bin:${REPO_ROOT}/node_modules/.bin:${GOROOT}/bin:${GOPATH}/bin:$
PYOBJECTS="searx" PYOBJECTS="searx"
PY_SETUP_EXTRAS='[test]' PY_SETUP_EXTRAS='[test]'
GECKODRIVER_VERSION="v0.36.0" GECKODRIVER_VERSION="v0.37.0"
# SPHINXOPTS= # SPHINXOPTS=
BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization") BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization")
BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests") BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests")

View File

@@ -1,10 +1,10 @@
mock==5.2.0 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==26.5.1
pylint==4.0.6 pylint==4.0.7
splinter==0.21.0 splinter==0.21.0
selenium==4.45.0 selenium==4.47.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
@@ -18,11 +18,11 @@ myst-parser==5.0.0
linuxdoc==20260504 linuxdoc==20260504
aiounittest==1.5.0 aiounittest==1.5.0
yamllint==1.38.0 yamllint==1.38.0
wlc==2.1.0 wlc==2.1.1
coloredlogs==15.0.1 coloredlogs==15.0.1
docutils>=0.21.2;python_version <= "3.11" 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.9 granian[reload]==2.8.2
basedpyright==1.39.9 basedpyright==1.39.10
types-lxml==2026.2.16 types-lxml==2026.2.16

View File

@@ -1,2 +1,2 @@
granian==2.7.9 granian==2.8.2
granian[pname]==2.7.9 granian[pname]==2.8.2

View File

@@ -1,19 +1,19 @@
certifi==2026.6.17 certifi==2026.7.22
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
jinja2==3.1.6 jinja2==3.1.6
lxml==6.1.1 lxml==6.1.2
pygments==2.20.0 pygments==2.21.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pyyaml==6.0.3 pyyaml==6.0.3
httpx[http2]==0.28.1 httpx[http2]==0.28.1
httpx-socks[asyncio]==0.10.0 httpx-socks[asyncio]==0.13.1
sniffio==1.3.1 sniffio==1.3.1
valkey==6.1.1 valkey==6.1.1
markdown-it-py==4.2.0 markdown-it-py==4.2.0
msgspec==0.21.1 msgspec==0.21.1
typer==0.26.8 typer==0.27.1
isodate==0.7.2 isodate==0.7.2
whitenoise==6.12.0 whitenoise==6.12.0
typing-extensions==4.16.0 typing-extensions==4.16.0

View File

@@ -1,5 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of the :py:obj:`preference <searx.preference>` settings.""" """Implementation of the :py:obj:`preference <searx.preference>` settings."""
# pylint: disable = too-few-public-methods # pylint: disable = too-few-public-methods
import typing as t import typing as t

View File

@@ -38,7 +38,6 @@ area:
""" """
__all__ = ["AnswererInfo", "Answerer", "AnswerStorage"] __all__ = ["AnswererInfo", "Answerer", "AnswerStorage"]

View File

@@ -13,7 +13,6 @@ from dataclasses import dataclass
from searx.utils import load_module from searx.utils import load_module
from searx.result_types.answer import BaseAnswer from searx.result_types.answer import BaseAnswer
_default = pathlib.Path(__file__).parent _default = pathlib.Path(__file__).parent
log: logging.Logger = logging.getLogger("searx.answerers") log: logging.Logger = logging.getLogger("searx.answerers")

View File

@@ -16,7 +16,7 @@ from . import Answerer, AnswererInfo
def random_characters(): def random_characters():
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
return [random.choice(random_string_letters) for _ in range(random.randint(8, 32))] return random.choices(random_string_letters, k=random.randint(8, 32))
def random_string(): def random_string():

View File

@@ -62,7 +62,7 @@ def bing(query: str, _sxng_locale: str) -> list[str]:
# bing search autocompleter # bing search autocompleter
base_url = "https://www.bing.com/AS/Suggestions?" base_url = "https://www.bing.com/AS/Suggestions?"
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters # cvid has to be a 32 character long string consisting of numbers and uppsercase characters
cvid = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32)) cvid = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32))
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid})) response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}))
results: list[str] = [] results: list[str] = []
@@ -127,18 +127,17 @@ def duckduckgo(query: str, sxng_locale: str) -> list[str]:
def google_complete(query: str, sxng_locale: str) -> list[str]: def google_complete(query: str, sxng_locale: str) -> list[str]:
"""Autocomplete from Google. Supports Google's languages and subdomains """Autocomplete from Google. Supports Google's languages
(:py:obj:`searx.engines.google.get_google_info`) by using the async REST (:py:obj:`searx.engines.google.get_google_info`) by using the async REST
API:: API::
https://{subdomain}/complete/search?{args} https://www.google.com/complete/search?{args}
""" """
data = ENGINE_TRAITS.get("google") or {} data = ENGINE_TRAITS.get("google") or {}
traits = EngineTraits(**data) traits = EngineTraits(**data)
google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, traits) google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, traits)
url = 'https://{subdomain}/complete/search?{args}'
args = urlencode( args = urlencode(
{ {
'q': query, 'q': query,
@@ -148,7 +147,7 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
) )
results: list[str] = [] results: list[str] = []
resp = get(url.format(subdomain=google_info['subdomain'], args=args)) resp = get('https://www.google.com/complete/search?' + args)
if resp and resp.ok: if resp and resp.ok:
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1] json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
data = json.loads(json_txt) data = json.loads(json_txt)
@@ -157,6 +156,24 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
return results return results
def kagi(query: str, sxng_locale: str) -> list[str]:
"""Autocomplete from Kagi."""
args: dict[str, str] = {'q': query}
if '-' in sxng_locale:
args['r'] = sxng_locale.split('-')[1].lower()
resp = get("https://kagisuggest.com/api/autosuggest?" + urlencode(args))
results: list[str] = []
if resp.ok:
data = resp.json()
if len(data) > 1:
results = data[1]
return results
def mwmbl(query: str, _sxng_locale: str) -> list[str]: def mwmbl(query: str, _sxng_locale: str) -> list[str]:
"""Autocomplete from Mwmbl_.""" """Autocomplete from Mwmbl_."""
@@ -380,6 +397,7 @@ backends: dict[str, t.Callable[[str, str], list[str]]] = {
'dbpedia': dbpedia, 'dbpedia': dbpedia,
'duckduckgo': duckduckgo, 'duckduckgo': duckduckgo,
'google': google_complete, 'google': google_complete,
'kagi': kagi,
'mwmbl': mwmbl, 'mwmbl': mwmbl,
'naver': naver, 'naver': naver,
'privacywall': privacywall, 'privacywall': privacywall,

View File

@@ -5,7 +5,6 @@ Implementations used for bot detection.
""" """
__all__ = ["init", "dump_request", "get_network", "too_many_requests", "ProxyFix"] __all__ = ["init", "dump_request", "get_network", "too_many_requests", "ProxyFix"]

View File

@@ -182,7 +182,7 @@ class Config:
if default is UNSET: if default is UNSET:
raise KeyError(name) raise KeyError(name)
return default return default
(modulename, name) = str(fqn).rsplit('.', 1) modulename, name = str(fqn).rsplit('.', 1)
m = __import__(modulename, {}, {}, [name], 0) m = __import__(modulename, {}, {}, [name], 0)
return getattr(m, name) return getattr(m, name)

View File

@@ -13,7 +13,6 @@ Accept_ header ..
""" """
from ipaddress import ( from ipaddress import (
IPv4Network, IPv4Network,
IPv6Network, IPv6Network,

View File

@@ -14,7 +14,6 @@ bot if the Accept-Encoding_ header ..
""" """
from ipaddress import ( from ipaddress import (
IPv4Network, IPv4Network,
IPv6Network, IPv6Network,

View File

@@ -11,7 +11,6 @@ if the Accept-Language_ header is unset.
""" """
from ipaddress import ( from ipaddress import (
IPv4Network, IPv4Network,
IPv6Network, IPv6Network,

View File

@@ -11,7 +11,6 @@ the Connection_ header is set to ``close``.
""" """
from ipaddress import ( from ipaddress import (
IPv4Network, IPv4Network,
IPv6Network, IPv6Network,

View File

@@ -20,6 +20,7 @@ Metadata`_. A request is filtered out in case of:
""" """
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@@ -12,7 +12,6 @@ the User-Agent_ header is unset or matches the regular expression
""" """
import re import re
from ipaddress import ( from ipaddress import (
IPv4Network, IPv4Network,
@@ -25,7 +24,6 @@ import flask
from . import config from . import config
from ._helpers import too_many_requests from ._helpers import too_many_requests
USER_AGENT = ( USER_AGENT = (
r'(' r'('
+ r'unknown' + r'unknown'

View File

@@ -55,7 +55,6 @@ from ._helpers import (
logger, logger,
) )
logger = logger.getChild('ip_limit') logger = logger.getChild('ip_limit')
BURST_WINDOW = 20 BURST_WINDOW = 20

View File

@@ -23,6 +23,7 @@ The ``ip_lists`` method implements :py:obj:`block-list <block_ip>` and
] ]
""" """
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@@ -151,6 +151,6 @@ def get_token() -> str:
if token: if token:
token = token.decode('UTF-8') # type: ignore token = token.decode('UTF-8') # type: ignore
else: else:
token = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16)) token = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16))
valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME) valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
return token return token

View File

@@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of a middleware to determine the real IP of an HTTP request """Implementation of a middleware to determine the real IP of an HTTP request
(:py:obj:`flask.request.remote_addr`) behind a proxy chain.""" (:py:obj:`flask.request.remote_addr`) behind a proxy chain."""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
@@ -63,6 +64,20 @@ class ProxyFix:
proxy_list: list[str] = cfg.get("botdetection.trusted_proxies", default=[]) proxy_list: list[str] = cfg.get("botdetection.trusted_proxies", default=[])
return [ip_network(net, strict=False) for net in proxy_list] return [ip_network(net, strict=False) for net in proxy_list]
def is_trusted_proxy(
self,
addr: IPv4Address | IPv6Address | None,
trusted_proxies: list[IPv4Network | IPv6Network],
) -> bool:
if addr is None:
return False
for net in trusted_proxies:
if addr.version == net.version and addr in net:
return True
return False
def trusted_remote_addr( def trusted_remote_addr(
self, self,
x_forwarded_for: list[IPv4Address | IPv6Address], x_forwarded_for: list[IPv4Address | IPv6Address],
@@ -70,16 +85,8 @@ class ProxyFix:
) -> str: ) -> str:
# always rtl # always rtl
for addr in reversed(x_forwarded_for): for addr in reversed(x_forwarded_for):
trust: bool = False if not self.is_trusted_proxy(addr, trusted_proxies):
logger.debug("client address from X-Forwarded-For: %s", addr)
for net in trusted_proxies:
if addr.version == net.version and addr in net:
logger.debug("trust proxy %s (member of %s)", addr, net)
trust = True
break
# client address
if not trust:
return addr.compressed return addr.compressed
# fallback to first address # fallback to first address
@@ -95,19 +102,21 @@ class ProxyFix:
# in this function! # in this function!
orig_remote_addr: str | None = environ.pop("REMOTE_ADDR") orig_remote_addr: str | None = environ.pop("REMOTE_ADDR")
orig_remote_ip: IPv4Address | IPv6Address | None = None
# Validate the IPs involved in this game and delete all invalid ones # Validate the IPs involved in this game and delete all invalid ones
# from the WSGI environment. # from the WSGI environment.
if orig_remote_addr: if orig_remote_addr:
try: try:
addr = ip_address(orig_remote_addr) orig_remote_ip = ip_address(orig_remote_addr)
if addr.version == 6 and addr.ipv4_mapped: if orig_remote_ip.version == 6 and orig_remote_ip.ipv4_mapped:
addr = addr.ipv4_mapped orig_remote_ip = orig_remote_ip.ipv4_mapped
orig_remote_addr = addr.compressed orig_remote_addr = orig_remote_ip.compressed
except ValueError as exc: except ValueError as exc:
logger.error("REMOTE_ADDR: %s / discard REMOTE_ADDR from WSGI environment", exc) logger.error("REMOTE_ADDR: %s / discard REMOTE_ADDR from WSGI environment", exc)
orig_remote_addr = None orig_remote_addr = None
orig_remote_ip = None
x_real_ip: str | None = environ.get("HTTP_X_REAL_IP") x_real_ip: str | None = environ.get("HTTP_X_REAL_IP")
if x_real_ip: if x_real_ip:
@@ -141,11 +150,13 @@ class ProxyFix:
if not x_forwarded_for and not x_real_ip: if not x_forwarded_for and not x_real_ip:
log_error_only_once("X-Forwarded-For nor X-Real-IP header is set!") log_error_only_once("X-Forwarded-For nor X-Real-IP header is set!")
if x_forwarded_for and not trusted_proxies: if x_forwarded_for or x_real_ip:
log_error_only_once("missing botdetection.trusted_proxies config") if not trusted_proxies:
# without trusted_proxies, this variable is useless for determining log_error_only_once("missing botdetection.trusted_proxies config")
# the real IP
x_forwarded_for = [] if not self.is_trusted_proxy(orig_remote_ip, trusted_proxies):
x_forwarded_for = []
x_real_ip = None
# securing the WSGI environment variables that are adjusted # securing the WSGI environment variables that are adjusted

View File

@@ -1,7 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Providing a Valkey database for the botdetection methods.""" """Providing a Valkey database for the botdetection methods."""
import valkey import valkey
__all__ = ["set_valkey_client", "get_valkey_client"] __all__ = ["set_valkey_client", "get_valkey_client"]

View File

@@ -1,5 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementations needed for a branding of SearXNG.""" """Implementations needed for a branding of SearXNG."""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
# Struct fields aren't discovered in Python 3.14 # Struct fields aren't discovered in Python 3.14

View File

@@ -48,7 +48,7 @@ class ExpireCacheCfg(msgspec.Struct): # pylint: disable=too-few-public-methods
MAXHOLD_TIME: int = 60 * 60 * 24 * 7 # 7 days MAXHOLD_TIME: int = 60 * 60 * 24 * 7 # 7 days
"""Hold time (default in sec.), after which a value is removed from the cache.""" """Hold time (default in sec.), after which a value is removed from the cache."""
MAINTENANCE_PERIOD: int = 60 * 60 # 2h MAINTENANCE_PERIOD: int = 60 * 60 # 1h
"""Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to """Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to
``auto``.""" ``auto``."""
@@ -465,7 +465,7 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
# Check if value is expired. It's possible that it's expired but has not # Check if value is expired. It's possible that it's expired but has not
# yet been automatically deleted by the periodic maintenance # yet been automatically deleted by the periodic maintenance
(value, expire) = row value, expire = row
now = time.time() now = time.time()
if expire < now: if expire < now:
# The record is deleted during the maintenance interval. Deleting # The record is deleted during the maintenance interval. Deleting

View File

@@ -3,7 +3,6 @@
import warnings import warnings
# limiter backward compatibility # limiter backward compatibility
# ------------------------------ # ------------------------------

View File

@@ -4,6 +4,7 @@
make data.all make data.all
""" """
# pylint: disable=invalid-name # pylint: disable=invalid-name
__all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"] __all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"]
@@ -32,6 +33,13 @@ class WikiDataUnitType(t.TypedDict):
to_si_factor: float to_si_factor: float
WikiDataPropertyNameType = str | dict[str, str]
"""Name of a Wikidata property. Can be either the plain name or a dictionary of
language code to property name, e.g. ``{"en": "Date of birth"}``."""
WikiDataPropertiesType = dict[str, WikiDataPropertyNameType]
"""Dictionary from wikidata property ID to property name."""
class LocalesType(t.TypedDict): class LocalesType(t.TypedDict):
"""Data structure of an item in ``locales.json``""" """Data structure of an item in ``locales.json``"""
@@ -41,6 +49,7 @@ class LocalesType(t.TypedDict):
USER_AGENTS: UserAgentType USER_AGENTS: UserAgentType
WIKIDATA_UNITS: dict[str, WikiDataUnitType] WIKIDATA_UNITS: dict[str, WikiDataUnitType]
WIKIDATA_PROPERTIES: WikiDataPropertiesType
TRACKER_PATTERNS: TrackerPatternsDB TRACKER_PATTERNS: TrackerPatternsDB
LOCALES: LocalesType LOCALES: LocalesType
CURRENCIES: CurrenciesDB CURRENCIES: CurrenciesDB
@@ -52,11 +61,12 @@ ENGINE_DESCRIPTIONS: dict[str, dict[str, t.Any]]
ENGINE_TRAITS: dict[str, dict[str, t.Any]] ENGINE_TRAITS: dict[str, dict[str, t.Any]]
lazy_globals = { lazy_globals: dict[str, t.Any] = {
"CURRENCIES": CurrenciesDB(), "CURRENCIES": CurrenciesDB(),
"USER_AGENTS": None, "USER_AGENTS": None,
"EXTERNAL_URLS": None, "EXTERNAL_URLS": None,
"WIKIDATA_UNITS": None, "WIKIDATA_UNITS": None,
"WIKIDATA_PROPERTIES": None,
"EXTERNAL_BANGS": None, "EXTERNAL_BANGS": None,
"OSM_KEYS_TAGS": None, "OSM_KEYS_TAGS": None,
"ENGINE_DESCRIPTIONS": None, "ENGINE_DESCRIPTIONS": None,
@@ -69,6 +79,7 @@ data_json_files = {
"USER_AGENTS": "useragents.json", "USER_AGENTS": "useragents.json",
"EXTERNAL_URLS": "external_urls.json", "EXTERNAL_URLS": "external_urls.json",
"WIKIDATA_UNITS": "wikidata_units.json", "WIKIDATA_UNITS": "wikidata_units.json",
"WIKIDATA_PROPERTIES": "wikidata_properties.json",
"EXTERNAL_BANGS": "external_bangs.json", "EXTERNAL_BANGS": "external_bangs.json",
"OSM_KEYS_TAGS": "osm_keys_tags.json", "OSM_KEYS_TAGS": "osm_keys_tags.json",
"ENGINE_DESCRIPTIONS": "engine_descriptions.json", "ENGINE_DESCRIPTIONS": "engine_descriptions.json",

File diff suppressed because it is too large Load Diff

View File

@@ -288,7 +288,7 @@
"oc": "Kwanza", "oc": "Kwanza",
"pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ", "pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ",
"pl": "Kwanza", "pl": "Kwanza",
"pt": "Kwanza", "pt": "kwanza",
"ru": "ангольская кванза", "ru": "ангольская кванза",
"si": "ක්වන්සා", "si": "ක්වන්සා",
"sr": "анголска кванза", "sr": "анголска кванза",
@@ -334,6 +334,7 @@
"ro": "Peso argentinian", "ro": "Peso argentinian",
"ru": "аргентинское песо", "ru": "аргентинское песо",
"sk": "Argentinské peso", "sk": "Argentinské peso",
"sl": "argentinski peso",
"sr": "аргентински пезос", "sr": "аргентински пезос",
"sv": "Argentinsk peso", "sv": "Argentinsk peso",
"ta": "ஆர்ஜென்டின பீசோ", "ta": "ஆர்ஜென்டின பீசோ",
@@ -2002,7 +2003,7 @@
"eo": "ganaa cedio", "eo": "ganaa cedio",
"es": "cedi", "es": "cedi",
"fi": "Cedi", "fi": "Cedi",
"fr": "Cedi", "fr": "cedi",
"ga": "cedi", "ga": "cedi",
"gl": "Cedi", "gl": "Cedi",
"he": "סדי גאני", "he": "סדי גאני",
@@ -2952,7 +2953,7 @@
"pap": "won nortkoreano", "pap": "won nortkoreano",
"pl": "won północnokoreański", "pl": "won północnokoreański",
"pt": "won norte-coreano", "pt": "won norte-coreano",
"ro": "Won nord-coreean", "ro": "won nord-coreean",
"ru": "вона КНДР", "ru": "вона КНДР",
"sk": "severokorejsky won", "sk": "severokorejsky won",
"sl": "severnokorejski von", "sl": "severnokorejski von",
@@ -3094,6 +3095,7 @@
"ca": "tenge", "ca": "tenge",
"cs": "Tenge", "cs": "Tenge",
"cy": "tenge Casachstan", "cy": "tenge Casachstan",
"da": "Tenge",
"de": "Tenge", "de": "Tenge",
"en": "Kazakhstani tenge", "en": "Kazakhstani tenge",
"eo": "kazaĥa tengo", "eo": "kazaĥa tengo",
@@ -4834,6 +4836,7 @@
"nl": "Seychelse roepie", "nl": "Seychelse roepie",
"pl": "Rupia seszelska", "pl": "Rupia seszelska",
"pt": "rupia das Seicheles", "pt": "rupia das Seicheles",
"ro": "rupie seychelloză",
"ru": "сейшельская рупия", "ru": "сейшельская рупия",
"sk": "Seychelská rupia", "sk": "Seychelská rupia",
"sl": "sejšelska rupija", "sl": "sejšelska rupija",
@@ -5064,6 +5067,7 @@
"nl": "Somalische shilling", "nl": "Somalische shilling",
"pl": "Szyling somalijski", "pl": "Szyling somalijski",
"pt": "xelim somaliano", "pt": "xelim somaliano",
"ro": "șiling somalez",
"ru": "сомалийский шиллинг", "ru": "сомалийский шиллинг",
"sk": "Somálsky šiling", "sk": "Somálsky šiling",
"sl": "somalski šiling", "sl": "somalski šiling",
@@ -5883,6 +5887,7 @@
"ja": "ドン", "ja": "ドン",
"ko": "베트남 동", "ko": "베트남 동",
"lt": "Vietnamo dongas", "lt": "Vietnamo dongas",
"ms": "Dồng Vietnam",
"nl": "Vietnamese dong", "nl": "Vietnamese dong",
"oc": "Dong", "oc": "Dong",
"pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ", "pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ",
@@ -6122,7 +6127,8 @@
"ro": "Gulden caraibian", "ro": "Gulden caraibian",
"ru": "Карибский гульден", "ru": "Карибский гульден",
"sk": "Karibský gulden", "sk": "Karibský gulden",
"sl": "karibski goldinar" "sl": "karibski goldinar",
"sv": "Karibisk gulden"
}, },
"XDR": { "XDR": {
"ar": "حقوق السحب الخاصة", "ar": "حقوق السحب الخاصة",
@@ -6724,6 +6730,8 @@
"antilliaanse gulden": "ANG", "antilliaanse gulden": "ANG",
"antilski gulden": "ANG", "antilski gulden": "ANG",
"aoa": "AOA", "aoa": "AOA",
"apvienotās karalistes ekonomika": "GBP",
"apvienotās karalistes saimniecība": "GBP",
"apvienotās karalistes sterliņu mārciņa": "GBP", "apvienotās karalistes sterliņu mārciņa": "GBP",
"ar": "MGA", "ar": "MGA",
"arab accounting dinar": "XAD", "arab accounting dinar": "XAD",
@@ -6836,6 +6844,7 @@
"avustralya doları": "AUD", "avustralya doları": "AUD",
"awg": "AWG", "awg": "AWG",
"az arany mint befektetés": "XAU", "az arany mint befektetés": "XAU",
"az egyesült királyság gazdasága": "GBP",
"azerbaidžanin manat": "AZN", "azerbaidžanin manat": "AZN",
"azerbaidžano manatas": "AZN", "azerbaidžano manatas": "AZN",
"azerbaidžānas manats": "AZN", "azerbaidžānas manats": "AZN",
@@ -7019,6 +7028,7 @@
"bir etíope": "ETB", "bir etíope": "ETB",
"biras": "ETB", "biras": "ETB",
"birleşik arap emirlikleri dirhemi": "AED", "birleşik arap emirlikleri dirhemi": "AED",
"birleşik krallık ekonomisi": "GBP",
"birma kjato": "MMK", "birma kjato": "MMK",
"birr": "ETB", "birr": "ETB",
"birr da etiópia": "ETB", "birr da etiópia": "ETB",
@@ -7106,15 +7116,19 @@
"brit font": "GBP", "brit font": "GBP",
"brita pundo": "GBP", "brita pundo": "GBP",
"britaj pundoj": "GBP", "britaj pundoj": "GBP",
"britannian talous": "GBP",
"britanska funta": "GBP", "britanska funta": "GBP",
"britanski funt": "GBP", "britanski funt": "GBP",
"britische wirtschaft": "GBP",
"britisches pfund": "GBP", "britisches pfund": "GBP",
"british economy": "GBP",
"british pound": "GBP", "british pound": "GBP",
"britisk pund": "GBP", "britisk pund": "GBP",
"britiske pund": "GBP", "britiske pund": "GBP",
"brits pond": "GBP", "brits pond": "GBP",
"britse pond": "GBP", "britse pond": "GBP",
"britská libra": "GBP", "britská libra": "GBP",
"brittisk ekonomi": "GBP",
"brittiska pund": "GBP", "brittiska pund": "GBP",
"brittiskt pund": "GBP", "brittiskt pund": "GBP",
"brunei doları": "BND", "brunei doları": "BND",
@@ -7198,6 +7212,7 @@
"cedi du ghana": "GHS", "cedi du ghana": "GHS",
"cedi ghana": "GHS", "cedi ghana": "GHS",
"cedi ghanese": "GHS", "cedi ghanese": "GHS",
"cedi ghanéen": "GHS",
"centr afrika franko": "XAF", "centr afrika franko": "XAF",
"central african cfa franc": "XAF", "central african cfa franc": "XAF",
"centralafrikansk cfa franc": "XAF", "centralafrikansk cfa franc": "XAF",
@@ -7300,7 +7315,6 @@
"colón costa ricense": "CRC", "colón costa ricense": "CRC",
"colón costa riquenho": "CRC", "colón costa riquenho": "CRC",
"colón costa riquense": "CRC", "colón costa riquense": "CRC",
"colón costa riqueny": "CRC",
"colón costaricain": "CRC", "colón costaricain": "CRC",
"colón costaricano": "CRC", "colón costaricano": "CRC",
"colón costaricien": "CRC", "colón costaricien": "CRC",
@@ -8413,6 +8427,7 @@
"dólares canadenses": "CAD", "dólares canadenses": "CAD",
"dólares estadounidenses": "USD", "dólares estadounidenses": "USD",
"dólares neozelandeses": "NZD", "dólares neozelandeses": "NZD",
"dồng vietnam": "VND",
"dram": "AMD", "dram": "AMD",
"dram armean": "AMD", "dram armean": "AMD",
"dram armenia": "AMD", "dram armenia": "AMD",
@@ -8436,6 +8451,7 @@
"droits de tirage speciaux": "XDR", "droits de tirage speciaux": "XDR",
"droits de tirage spéciaux": "XDR", "droits de tirage spéciaux": "XDR",
"dschibuti franc": "DJF", "dschibuti franc": "DJF",
"dvn": "VND",
"dzd": "DZD", "dzd": "DZD",
"dzsibuti frank": "DJF", "dzsibuti frank": "DJF",
"džibučio frankas": "DJF", "džibučio frankas": "DJF",
@@ -8449,6 +8465,21 @@
"eastern caribbean currency union": "XCD", "eastern caribbean currency union": "XCD",
"eastern caribbean dollar": "XCD", "eastern caribbean dollar": "XCD",
"ec$": "XCD", "ec$": "XCD",
"economi'r deyrnas unedig": "GBP",
"economia": "GBP",
"economia del regne unit": "GBP",
"economia del regno unito": "GBP",
"economia del reialme unit": "GBP",
"economia del reino unido": "GBP",
"economia do reino unido": "GBP",
"economia regatului unit": "GBP",
"economie du royaume uni": "GBP",
"economie van het verenigd koninkrijk": "GBP",
"economía del reino unido": "GBP",
"economía do reino unido": "GBP",
"economy": "GBP",
"economy of the uk": "GBP",
"economy of the united kingdom": "GBP",
"egipatska funta": "EGP", "egipatska funta": "EGP",
"egipta pundo": "EGP", "egipta pundo": "EGP",
"egipto svaras": "EGP", "egipto svaras": "EGP",
@@ -8468,6 +8499,12 @@
"einr": "INR", "einr": "INR",
"eiro": "EUR", "eiro": "EUR",
"ekialdeko karibeko dolar": "XCD", "ekialdeko karibeko dolar": "XCD",
"ekonomi britania raya": "GBP",
"ekonomi united kingdom": "GBP",
"ekonomie van die verenigde koninkryk": "GBP",
"ekonomika spojeného království": "GBP",
"ekonomika v spojenom kráľovstve": "GBP",
"ekonomio de britujo": "GBP",
"el peso": "GTQ", "el peso": "GTQ",
"emalangeni": "SZL", "emalangeni": "SZL",
"emas sebagai pelaburan": "XAU", "emas sebagai pelaburan": "XAU",
@@ -8499,6 +8536,7 @@
"ermenistan dramı": "AMD", "ermenistan dramı": "AMD",
"ern": "ERN", "ern": "ERN",
"erreal brasildar": "BRL", "erreal brasildar": "BRL",
"erresuma batuko ekonomia": "GBP",
"errublo": "RUB", "errublo": "RUB",
"errublo errusiar": "RUB", "errublo errusiar": "RUB",
"errupia indiar": "INR", "errupia indiar": "INR",
@@ -8569,6 +8607,8 @@
"eyrir": "ISK", "eyrir": "ISK",
"e£": "EGP", "e£": "EGP",
"èuro": "EUR", "èuro": "EUR",
"économie britannique": "GBP",
"économie du royaume uni": "GBP",
"észak ír font": "GBP", "észak ír font": "GBP",
"észak koreai von": "KPW", "észak koreai von": "KPW",
"e₹": "INR", "e₹": "INR",
@@ -8702,6 +8742,9 @@
"forintti": "HUF", "forintti": "HUF",
"forinți": "HUF", "forinți": "HUF",
"fòrint": "HUF", "fòrint": "HUF",
"förenade konungariket storbritannien och irlands ekonomi": "GBP",
"förenade konungariket storbritannien och nordirlands ekonomi": "GBP",
"förenade kungarikets ekonomi": "GBP",
"franak cfp": "XPF", "franak cfp": "XPF",
"franc": [ "franc": [
"XPF", "XPF",
@@ -8954,6 +8997,9 @@
"gold als kapitalanlage": "XAU", "gold als kapitalanlage": "XAU",
"gold as an investment": "XAU", "gold as an investment": "XAU",
"gold as currency": "XAU", "gold as currency": "XAU",
"gospodarka wielkiej brytanii": "GBP",
"gospodarstvo ujedinjenog kraljevstva": "GBP",
"gospodarstvo združenega kraljestva": "GBP",
"gourde": "HTG", "gourde": "HTG",
"gourde haiti": "HTG", "gourde haiti": "HTG",
"gourde haitiano": "HTG", "gourde haitiano": "HTG",
@@ -9373,6 +9419,7 @@
"juaņs": "CNY", "juaņs": "CNY",
"juhokoréjsky won": "KRW", "juhokoréjsky won": "KRW",
"juhosudánska libra": "SSP", "juhosudánska libra": "SSP",
"jungtinės karalystės ekonomika": "GBP",
"jungtinių arabų emyratų dirhamas": "AED", "jungtinių arabų emyratų dirhamas": "AED",
"jungtinių valstijų doleris": "USD", "jungtinių valstijų doleris": "USD",
"južnoafrički rand": "ZAR", "južnoafrički rand": "ZAR",
@@ -9437,6 +9484,7 @@
"karibi forint": "XCG", "karibi forint": "XCG",
"karibia guldeno": "XCG", "karibia guldeno": "XCG",
"karibischer gulden": "XCG", "karibischer gulden": "XCG",
"karibisk gulden": "XCG",
"karibski goldinar": "XCG", "karibski goldinar": "XCG",
"karibský gulden": "XCG", "karibský gulden": "XCG",
"karipski gulden": "XCG", "karipski gulden": "XCG",
@@ -9497,6 +9545,9 @@
"kina papua nugini": "PGK", "kina papua nugini": "PGK",
"kina papuana": "PGK", "kina papuana": "PGK",
"kina papuásia": "PGK", "kina papuásia": "PGK",
"kinh tế anh": "GBP",
"kinh tế vương quốc anh": "GBP",
"kinh tế vương quốc liên hiệp anh và bắc ireland": "GBP",
"kip": "LAK", "kip": "LAK",
"kip laos": "LAK", "kip laos": "LAK",
"kip laosiano": "LAK", "kip laosiano": "LAK",
@@ -11141,6 +11192,7 @@
"põhja korea won": "KPW", "põhja korea won": "KPW",
"põhja makedoonia denaar": "MKD", "põhja makedoonia denaar": "MKD",
"prata como investimento": "XAG", "prata como investimento": "XAG",
"produits agricole de l'angleterre": "GBP",
"pula": "BWP", "pula": "BWP",
"pula botswana": "BWP", "pula botswana": "BWP",
"pula botswanais": "BWP", "pula botswanais": "BWP",
@@ -11191,6 +11243,7 @@
"qatarisk rial": "QAR", "qatarisk rial": "QAR",
"qäpik": "AZN", "qäpik": "AZN",
"qindarka": "ALL", "qindarka": "ALL",
"quanza": "AOA",
"quetzal": "GTQ", "quetzal": "GTQ",
"quetzal guatemala": "GTQ", "quetzal guatemala": "GTQ",
"quetzal guatemalteco": "GTQ", "quetzal guatemalteco": "GTQ",
@@ -11516,6 +11569,7 @@
"rupia del pakistan": "PKR", "rupia del pakistan": "PKR",
"rupia dell'india": "INR", "rupia dell'india": "INR",
"rupia delle seychelles": "SCR", "rupia delle seychelles": "SCR",
"rupia din seychelles": "SCR",
"rupia do nepal": "NPR", "rupia do nepal": "NPR",
"rupia do paquistão": "PKR", "rupia do paquistão": "PKR",
"rupia do seri lanca": "LKR", "rupia do seri lanca": "LKR",
@@ -11571,6 +11625,7 @@
], ],
"rupie indiană": "INR", "rupie indiană": "INR",
"rupie indiane": "INR", "rupie indiane": "INR",
"rupie seychelloză": "SCR",
"rupies índies": "INR", "rupies índies": "INR",
"rupija": [ "rupija": [
"NPR", "NPR",
@@ -12000,6 +12055,10 @@
"sterliņu mārciņa": "GBP", "sterliņu mārciņa": "GBP",
"stērliņu mārciņa": "GBP", "stērliņu mārciņa": "GBP",
"stn": "STN", "stn": "STN",
"storbritannien och irlands ekonomi": "GBP",
"storbritannien och nordirlands ekonomi": "GBP",
"storbritanniens ekonomi": "GBP",
"storbritanniens økonomi": "GBP",
"stredoafrický frank": "XAF", "stredoafrický frank": "XAF",
"středoafrický frank": "XAF", "středoafrický frank": "XAF",
"sucre": "XSU", "sucre": "XSU",
@@ -12049,6 +12108,7 @@
"suriye lirası": "SYP", "suriye lirası": "SYP",
"suudi arabistan riyali": "SAR", "suudi arabistan riyali": "SAR",
"suudi riyali": "SAR", "suudi riyali": "SAR",
"suurbritannia majandus": "GBP",
"suurbritannia nael": "GBP", "suurbritannia nael": "GBP",
"suurbritannia naelsterling": "GBP", "suurbritannia naelsterling": "GBP",
"suvereni bolivar": "VES", "suvereni bolivar": "VES",
@@ -12155,6 +12215,7 @@
"švicarski frank": "CHF", "švicarski frank": "CHF",
"švýcarský frank": "CHF", "švýcarský frank": "CHF",
"șekel nou": "ILS", "șekel nou": "ILS",
"șiling somalez": "SOS",
"şekel": "ILS", "şekel": "ILS",
"şili pesosu": "CLP", "şili pesosu": "CLP",
"s₣": "CHF", "s₣": "CHF",
@@ -12497,6 +12558,8 @@
"uguiya": "MRU", "uguiya": "MRU",
"ugx": "UGX", "ugx": "UGX",
"ui": "UYI", "ui": "UYI",
"uk economy": "GBP",
"uk's economy": "GBP",
"ukl": "GBP", "ukl": "GBP",
"ukraina grivna": "UAH", "ukraina grivna": "UAH",
"ukraina hrivno": "UAH", "ukraina hrivno": "UAH",
@@ -12537,6 +12600,8 @@
"unidades de inversion": "MXV", "unidades de inversion": "MXV",
"unidades de inversión": "MXV", "unidades de inversión": "MXV",
"united arab emirates dirham": "AED", "united arab emirates dirham": "AED",
"united kingdom economy": "GBP",
"united kingdom's economy": "GBP",
"united states dollar": [ "united states dollar": [
"USN", "USN",
"USD" "USD"
@@ -12638,6 +12703,7 @@
"venemaa rubla": "RUB", "venemaa rubla": "RUB",
"venezuelai bolívar": "VES", "venezuelai bolívar": "VES",
"venezuelan digital bolívar": "VED", "venezuelan digital bolívar": "VED",
"verenigd koninkrijk economie": "GBP",
"verenigde arabiese emirate dirham": "AED", "verenigde arabiese emirate dirham": "AED",
"verenigde arabische emiraten dirham": "AED", "verenigde arabische emiraten dirham": "AED",
"ves": "VES", "ves": "VES",
@@ -12669,6 +12735,12 @@
"wir euro": "CHE", "wir euro": "CHE",
"wir franc": "CHW", "wir franc": "CHW",
"wir franken": "CHW", "wir franken": "CHW",
"wirtschaft": "GBP",
"wirtschaft des vereinigten königreichs": "GBP",
"wirtschaft im vereinigten königreich": "GBP",
"wirtschaft in dem vereinigten königreich": "GBP",
"wirtschaft vom vereinigten königreich": "GBP",
"wirtschaft von dem vereinigten königreich": "GBP",
"wit russische roebel": "BYN", "wit russische roebel": "BYN",
"won": "KRW", "won": "KRW",
"won bắc triều tiên": "KPW", "won bắc triều tiên": "KPW",
@@ -12762,6 +12834,7 @@
"yeşil burun adaları eskudosu": "CVE", "yeşil burun adaları eskudosu": "CVE",
"yên nhật": "JPY", "yên nhật": "JPY",
"yhdistyneen kuningaskunnan punta": "GBP", "yhdistyneen kuningaskunnan punta": "GBP",
"yhdistyneen kuningaskunnan talous": "GBP",
"yhdistyneiden arabiemiraattien dirhami": "AED", "yhdistyneiden arabiemiraattien dirhami": "AED",
"yhdysvaltain dollari": "USD", "yhdysvaltain dollari": "USD",
"ytl": "TRY", "ytl": "TRY",
@@ -13511,6 +13584,8 @@
"египетский фунт": "EGP", "египетский фунт": "EGP",
"единая система региональных взаиморасчётов": "XSU", "единая система региональных взаиморасчётов": "XSU",
"единая система региональных взаиморасчетов": "XSU", "единая система региональных взаиморасчетов": "XSU",
"економіка великобританії": "GBP",
"економіка великої британії": "GBP",
"енглеска фунта": "GBP", "енглеска фунта": "GBP",
"еритрейська накфа": "ERN", "еритрейська накфа": "ERN",
"еритрејска накфа": "ERN", "еритрејска накфа": "ERN",
@@ -13567,6 +13642,8 @@
"израелски шекел": "ILS", "израелски шекел": "ILS",
"израильский новый шекель": "ILS", "израильский новый шекель": "ILS",
"източнокарибски долар": "XCD", "източнокарибски долар": "XCD",
"икономика на великобритания": "GBP",
"икономика на обединеното кралство": "GBP",
"индийска рупия": "INR", "индийска рупия": "INR",
"индийская рупия": "INR", "индийская рупия": "INR",
"индијска рупија": "INR", "индијска рупија": "INR",
@@ -13980,6 +14057,7 @@
"PLZ", "PLZ",
"PLN" "PLN"
], ],
"привреда уједињеног краљевства": "GBP",
"пула": "BWP", "пула": "BWP",
"південно африканський ранд": "ZAR", "південно африканський ранд": "ZAR",
"південнокорейська вона": "KRW", "південнокорейська вона": "KRW",
@@ -14067,6 +14145,7 @@
"севернокорејски вон": "KPW", "севернокорејски вон": "KPW",
"северо корейская вона": "KPW", "северо корейская вона": "KPW",
"северокорейская вона": "KPW", "северокорейская вона": "KPW",
"седі": "GHS",
"сейшел рупиясе": "SCR", "сейшел рупиясе": "SCR",
"сейшелска рупия": "SCR", "сейшелска рупия": "SCR",
"сейшельская рупия": "SCR", "сейшельская рупия": "SCR",
@@ -14119,6 +14198,8 @@
"старый румынский лей": "RON", "старый румынский лей": "RON",
"стерлинг фунты": "GBP", "стерлинг фунты": "GBP",
"стерлиң фунты": "GBP", "стерлиң фунты": "GBP",
"стопанство на великобритания": "GBP",
"стопанство на обединеното кралство": "GBP",
"суверен боливар": "VES", "суверен боливар": "VES",
"суверенний болівар": "VES", "суверенний болівар": "VES",
"суверенный боливар": "VES", "суверенный боливар": "VES",
@@ -14369,6 +14450,7 @@
"шриланкийска рупия": "LKR", "шриланкийска рупия": "LKR",
"шриланчанска рупија": "LKR", "шриланчанска рупија": "LKR",
"щатски долар": "USD", "щатски долар": "USD",
"экономика великобритании": "GBP",
"эритрейская накфа": "ERN", "эритрейская накфа": "ERN",
"эритрея накфасы": "ERN", "эритрея накфасы": "ERN",
"эсватини лилангение": "SZL", "эсватини лилангение": "SZL",
@@ -14518,6 +14600,8 @@
"יואן סיני": "CNY", "יואן סיני": "CNY",
"ין יפני": "JPY", "ין יפני": "JPY",
"כארתולי לארי": "GEL", "כארתולי לארי": "GEL",
"כלכלת בריטניה": "GBP",
"כלכלת הממלכה המאוחדת": "GBP",
"כתר דני": "DKK", "כתר דני": "DKK",
"כתר נורבגי": "NOK", "כתר נורבגי": "NOK",
"כתר נורווגי": "NOK", "כתר נורווגי": "NOK",
@@ -14665,6 +14749,7 @@
"استثمار البلاتين": "XPT", "استثمار البلاتين": "XPT",
"استثمار الذهب": "XAU", "استثمار الذهب": "XAU",
"استثمار الفضة": "XAG", "استثمار الفضة": "XAG",
"اقتصاد المملكة المتحدة": "GBP",
"الاستثمار في الذهب": "XAU", "الاستثمار في الذهب": "XAU",
"الأوقية الموريتانية": "MRU", "الأوقية الموريتانية": "MRU",
"البات": "THB", "البات": "THB",
@@ -14718,6 +14803,7 @@
"أوقية": "MRU", "أوقية": "MRU",
"أوقية موريتانية": "MRU", "أوقية موريتانية": "MRU",
"أوقيه موريتانيه": "MRU", "أوقيه موريتانيه": "MRU",
"إقتصاد بريطانى": "GBP",
"إيسكودو جزر الرأس الأخضر": "CVE", "إيسكودو جزر الرأس الأخضر": "CVE",
"بات": "THB", "بات": "THB",
"بات تايلاندي": "THB", "بات تايلاندي": "THB",
@@ -15100,6 +15186,7 @@
"মালদ্বীপীয় রুফিয়াহ": "MVR", "মালদ্বীপীয় রুফিয়াহ": "MVR",
"মিয়ানমার ক্যত": "MMK", "মিয়ানমার ক্যত": "MMK",
"মিশরীয় পাউন্ড": "EGP", "মিশরীয় পাউন্ড": "EGP",
"যুক্তরাজ্যের অর্থনীতি": "GBP",
"রুশ রুবল": "RUB", "রুশ রুবল": "RUB",
"রেনমিনবি": "CNY", "রেনমিনবি": "CNY",
"রেন্মিন্বি": "CNY", "রেন্মিন্বি": "CNY",
@@ -15731,6 +15818,7 @@
"엔": "JPY", "엔": "JPY",
"엔화": "JPY", "엔화": "JPY",
"영국 파운드": "GBP", "영국 파운드": "GBP",
"영국의 경제": "GBP",
"예멘 리알": "YER", "예멘 리알": "YER",
"예멘 리얄": "YER", "예멘 리얄": "YER",
"예멘리얄": "YER", "예멘리얄": "YER",
@@ -15938,9 +16026,11 @@
"イエメン・リアル": "YER", "イエメン・リアル": "YER",
"イエメン・リヤル": "YER", "イエメン・リヤル": "YER",
"イエメン・リヤール": "YER", "イエメン・リヤール": "YER",
"イギリスの経済": "GBP",
"イギリスの通貨": "GBP", "イギリスの通貨": "GBP",
"イギリスポンド": "GBP", "イギリスポンド": "GBP",
"イギリス・ポンド": "GBP", "イギリス・ポンド": "GBP",
"イギリス経済": "GBP",
"イラクの通貨": "IQD", "イラクの通貨": "IQD",
"イラク・ディナール": "IQD", "イラク・ディナール": "IQD",
"イランの通貨": "IRR", "イランの通貨": "IRR",
@@ -16242,6 +16332,7 @@
"英ポンド": "GBP", "英ポンド": "GBP",
"西アフリカcfaフラン": "XOF", "西アフリカcfaフラン": "XOF",
"豪ドル": "AUD", "豪ドル": "AUD",
"財政・経済政策": "GBP",
"越南銅": "VND", "越南銅": "VND",
"金投資": "XAU", "金投資": "XAU",
"韓国ウォン": "KRW", "韓国ウォン": "KRW",

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Simple implementation to store TrackerPatterns data in a SQL database.""" """Simple implementation to store TrackerPatterns data in a SQL database."""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
import typing as t import typing as t

View File

@@ -5,7 +5,7 @@
], ],
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}", "ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
"versions": [ "versions": [
"152.0", "154.0",
"151.0" "153.0"
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3474,11 +3474,6 @@
"symbol": "mm⁻²", "symbol": "mm⁻²",
"to_si_factor": 1e-06 "to_si_factor": 1e-06
}, },
"Q136039973": {
"si_name": "Q6137407",
"symbol": "FPS",
"to_si_factor": 1.0
},
"Q1361854": { "Q1361854": {
"si_name": "Q11570", "si_name": "Q11570",
"symbol": "dwt", "symbol": "dwt",
@@ -3521,7 +3516,7 @@
}, },
"Q1377741": { "Q1377741": {
"si_name": "Q25250", "si_name": "Q25250",
"symbol": "V_P", "symbol": "V<sub>P</sub>",
"to_si_factor": 1.0429e+27 "to_si_factor": 1.0429e+27
}, },
"Q1386162": { "Q1386162": {
@@ -3694,6 +3689,11 @@
"symbol": "apc", "symbol": "apc",
"to_si_factor": 0.0308568 "to_si_factor": 0.0308568
}, },
"Q16068": {
"si_name": null,
"symbol": "DM",
"to_si_factor": null
},
"Q160857": { "Q160857": {
"si_name": "Q25236", "si_name": "Q25236",
"symbol": "hp", "symbol": "hp",
@@ -3872,11 +3872,11 @@
"Q180892": { "Q180892": {
"si_name": "Q11570", "si_name": "Q11570",
"symbol": "M☉", "symbol": "M☉",
"to_si_factor": 1.9884e+30 "to_si_factor": 1.988416e+30
}, },
"Q1811": { "Q1811": {
"si_name": "Q11573", "si_name": "Q11573",
"symbol": "AU", "symbol": "au",
"to_si_factor": 149597870700.0 "to_si_factor": 149597870700.0
}, },
"Q1815100": { "Q1815100": {
@@ -4454,6 +4454,11 @@
"symbol": "ng", "symbol": "ng",
"to_si_factor": 1e-12 "to_si_factor": 1e-12
}, },
"Q2285395": {
"si_name": null,
"symbol": "dBW",
"to_si_factor": null
},
"Q22934083": { "Q22934083": {
"si_name": "Q25406", "si_name": "Q25406",
"symbol": "nC", "symbol": "nC",
@@ -5244,6 +5249,11 @@
"symbol": "μA", "symbol": "μA",
"to_si_factor": 1e-06 "to_si_factor": 1e-06
}, },
"Q31274648": {
"si_name": "Q6137407",
"symbol": "FPS",
"to_si_factor": 1.0
},
"Q3186734": { "Q3186734": {
"si_name": "Q3186734", "si_name": "Q3186734",
"symbol": "J/(m³ K)", "symbol": "J/(m³ K)",
@@ -6316,7 +6326,7 @@
}, },
"Q536785": { "Q536785": {
"si_name": "Q844211", "si_name": "Q844211",
"symbol": "ρ_P", "symbol": "ρ<sub>P</sub>",
"to_si_factor": 5.155e+96 "to_si_factor": 5.155e+96
}, },
"Q53679433": { "Q53679433": {
@@ -6971,7 +6981,7 @@
}, },
"Q685662": { "Q685662": {
"si_name": "Q44395", "si_name": "Q44395",
"symbol": "p_P", "symbol": "p<sub>P</sub>",
"to_si_factor": 4.633e+113 "to_si_factor": 4.633e+113
}, },
"Q686163": { "Q686163": {

View File

@@ -47,7 +47,7 @@ ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache(
ExpireCacheCfg( ExpireCacheCfg(
name="ENGINES_CACHE", name="ENGINES_CACHE",
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
MAINTENANCE_PERIOD=60 * 60, # 2h MAINTENANCE_PERIOD=60 * 60, # 1h
MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB
) )
) )

View File

@@ -82,7 +82,7 @@ fragment SXNG_query on Query {
def setup(_) -> bool: def setup(_) -> bool:
global SXNG_query # pylint: disable=global-statement global SXNG_query # pylint: disable=global-statement
rand_str: str = "".join(random.choice(string.ascii_letters) for _ in range(5)) rand_str: str = "".join(random.choices(string.ascii_letters, k=5))
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str) SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
return True return True

View File

@@ -25,6 +25,7 @@ To use this engine, add an entry similar to the following to your engine list in
https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
""" """
import typing as t import typing as t
from searx.enginelib import EngineCache from searx.enginelib import EngineCache

View File

@@ -1,5 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""BASE (Scholar publications)""" """BASE (Scholar publications)"""
from datetime import datetime from datetime import datetime
import re import re

View File

@@ -32,7 +32,7 @@ base_url = "https://api.bilibili.com/x/web-interface/search/type"
cookie = { cookie = {
"innersign": "0", "innersign": "0",
"buvid3": "".join(random.choice(string.hexdigits) for _ in range(16)) + "infoc", "buvid3": "".join(random.choices(string.hexdigits, k=16)) + "infoc",
"i-wanna-go-back": "-1", "i-wanna-go-back": "-1",
"b_ut": "7", "b_ut": "7",
"FEED_LIVE_VERSION": "V8", "FEED_LIVE_VERSION": "V8",

View File

@@ -83,7 +83,6 @@ from threading import Thread
from searx import logger from searx import logger
from searx.result_types import EngineResults from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
paging = True paging = True
command = [] command = []

View File

@@ -1,11 +1,18 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Deviantart (Images)""" """Deviantart (Images)"""
import typing as t
import urllib.parse import urllib.parse
from lxml import html from lxml import html
from searx.result_types import EngineResults
from searx.utils import extract_text, eval_xpath, eval_xpath_list from searx.utils import extract_text, eval_xpath, eval_xpath_list
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
# about # about
about = { about = {
"website": 'https://www.deviantart.com/', "website": 'https://www.deviantart.com/',
@@ -23,63 +30,62 @@ paging = True
# search-url # search-url
base_url = 'https://www.deviantart.com' base_url = 'https://www.deviantart.com'
results_xpath = '//div[@class="V_S0t_"]/div/div/a' results_xpath = '//div[@data-testid="content_row"]//a[.//*[@data-testid="thumb"]]'
url_xpath = './@href' img_src_xpath = './/img/@srcset'
thumbnail_src_xpath = './div/img/@src' thumbnail_src_xpath = './/img/@src'
img_src_xpath = './div/img/@srcset' author_xpath = './/*[@property="schema:name"]/@content'
title_xpath = './@aria-label' cursor_xpath = '//a[contains(@href, "cursor=") and contains(., "Next")]/@href'
premium_xpath = '../div/div/div/text()'
premium_keytext = 'Watch the artist to view this deviation'
cursor_xpath = '(//a[@class="vQ2brP"]/@href)[last()]'
def request(query, params): def request(query: str, params: "OnlineParams"):
# https://www.deviantart.com/search?q=foo # https://www.deviantart.com/search?q=foo
nextpage_url = params['engine_data'].get('nextpage') args = {'q': query}
# don't use nextpage when user selected to jump back to page 1 if params['pageno'] > 1:
if params['pageno'] > 1 and nextpage_url is not None: cursor = params['engine_data'].get('cursor')
params['url'] = nextpage_url if cursor:
else: args['cursor'] = cursor
params['url'] = f"{base_url}/search?{urllib.parse.urlencode({'q': query})}"
return params params['url'] = f"{base_url}/search?{urllib.parse.urlencode(args)}"
def response(resp): def response(resp: "SXNG_Response") -> EngineResults:
results = [] res = EngineResults()
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, results_xpath): for result in eval_xpath_list(dom, results_xpath):
# skip images that are blurred thumbnail_src = extract_text(eval_xpath(result, thumbnail_src_xpath))
_text = extract_text(eval_xpath(result, premium_xpath))
if _text and premium_keytext in _text:
continue
img_src = extract_text(eval_xpath(result, img_src_xpath)) img_src = extract_text(eval_xpath(result, img_src_xpath))
# mature locked thumbs have blur transform (blur_15, blur_30 etc..)
if ',blur_' in f'{thumbnail_src}{img_src}':
continue
if img_src: if img_src:
img_src = img_src.split(' ')[0] img_src = img_src.split(' ')[0]
parsed_url = urllib.parse.urlparse(img_src) parsed_url = urllib.parse.urlparse(img_src)
img_src = parsed_url._replace(path=parsed_url.path.split('/v1')[0]).geturl() img_src = parsed_url._replace(path=parsed_url.path.split('/v1')[0]).geturl()
results.append( author = extract_text(eval_xpath(result, author_xpath))
{
'template': 'images.html', res.add(
'url': extract_text(eval_xpath(result, url_xpath)), res.types.Image(
'img_src': img_src, template='images.html',
'thumbnail_src': extract_text(eval_xpath(result, thumbnail_src_xpath)), url=result.get('href'),
'title': extract_text(eval_xpath(result, title_xpath)), img_src=img_src or "",
} thumbnail_src=thumbnail_src or "",
title=result.get('aria-label'),
author=author or "",
)
) )
nextpage_url = extract_text(eval_xpath(dom, cursor_xpath)) nextpage_url = extract_text(eval_xpath(dom, cursor_xpath))
if nextpage_url: cursor = urllib.parse.parse_qs(urllib.parse.urlparse(nextpage_url or '').query).get('cursor', [None])[0]
results.append( if cursor:
{ res.add(
'engine_data': nextpage_url.replace("http://", "https://"), res.types.LegacyResult(
'key': 'nextpage', engine_data=cursor,
} key='cursor',
)
) )
return results return res

View File

@@ -1,5 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Docker Hub (IT)""" """Docker Hub (IT)"""
# pylint: disable=use-dict-literal # pylint: disable=use-dict-literal
from urllib.parse import urlencode from urllib.parse import urlencode

View File

@@ -43,9 +43,10 @@ def init(_):
def request(query: str, params: "OnlineParams"): def request(query: str, params: "OnlineParams"):
params["url"] = f"{base_url}/api/{dogpile_categ}" params["url"] = f"{base_url}/api/{dogpile_categ}"
params["headers"]["Origin"] = base_url
params["method"] = "POST" params["method"] = "POST"
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]} params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
return params
def response(resp: "SXNG_Response"): def response(resp: "SXNG_Response"):

View File

@@ -164,6 +164,7 @@ Terms / phrases that you keep coming across:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language
""" """
# pylint: disable=global-statement # pylint: disable=global-statement
import json import json

View File

@@ -12,6 +12,7 @@ least we could not find out how language support should work. It seems that
most of the features are based on English terms. most of the features are based on English terms.
""" """
import typing as t import typing as t
from urllib.parse import urlencode, urlparse, urljoin from urllib.parse import urlencode, urlparse, urljoin

View File

@@ -17,7 +17,6 @@ from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
from searx import weather from searx import weather
about = { about = {
"website": 'https://duckduckgo.com/', "website": 'https://duckduckgo.com/',
"wikidata_id": 'Q12805', "wikidata_id": 'Q12805',

View File

@@ -2,7 +2,6 @@
# pylint: disable=invalid-name # pylint: disable=invalid-name
"""Dummy Offline""" """Dummy Offline"""
# about # about
about = { about = {
"wikidata_id": None, "wikidata_id": None,

168
searx/engines/exaapi.py Normal file
View File

@@ -0,0 +1,168 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Engine to search using the official `Exa Search API`_. Exa is a search engine for AI agents.
.. _Exa Search API: https://exa.ai/docs/reference/search
Configuration
=============
The engine has the following mandatory setting:
- :py:obj:`api_key`
You can obtain an API key from the `API Key section <https://dashboard.exa.ai/api-keys>`_ in the Exa dashboard.
Optional settings are:
- :py:obj:`results_per_page`
- :py:obj:`search_type`
- :py:obj:`content_mode`
- :py:obj:`content_max_characters`
.. code:: yaml
- name: exaapi
engine: exaapi
shortcut: exa
api_key: "..."
results_per_page: 10
search_type: auto
content_mode: highlights
inactive: false
The API supports SafeSearch and region-aware results.
"""
import typing as t
from dateutil import parser
from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults
from searx.utils import html_to_text
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
SearchType = t.Literal["fast", "auto", "instant", "deep", "deep-lite", "deep-reasoning"]
ContentMode = t.Literal["highlights", "text"]
about = {
"website": "https://exa.ai",
"wikidata_id": None,
"official_api_documentation": "https://exa.ai/docs/reference/search",
"use_official_api": True,
"require_api_key": True,
"results": "JSON",
}
api_key: str = ""
"""API key for Exa Search API (required)."""
categories = ["general", "web"]
safesearch = True
base_url = "https://api.exa.ai/search"
results_per_page: int = 10
"""Maximum number of results per request. Value must be between 1 and 100, default is 10."""
search_type: SearchType = "auto"
"""Search type. Default is auto, see documentation for more information."""
content_mode: ContentMode = "highlights"
"""Content to request from the API: ``highlights`` (excerpts) or ``text`` (page text)."""
content_max_characters: int = 500
"""Maximum characters for the requested content."""
def init(_):
if not api_key:
raise SearxEngineAPIException("No API key provided")
if not 1 <= results_per_page <= 100:
raise ValueError("results_per_page must be between 1 and 100")
if search_type not in t.get_args(SearchType):
raise ValueError(f"Unsupported search type: {search_type}")
if content_mode not in t.get_args(ContentMode):
raise ValueError(f"Unsupported content mode: {content_mode}")
if content_max_characters < 1:
raise ValueError("content_max_characters must be at least 1")
def _contents_payload() -> dict[str, t.Any]:
if content_mode == "text":
return {"text": {"maxCharacters": content_max_characters, "stripLinks": True}}
return {"highlights": {"maxCharacters": content_max_characters}}
def _extract_content(result: dict[str, t.Any]) -> str:
if content_mode == "text":
return html_to_text(result.get("text") or "")
return html_to_text(" ".join(result.get("highlights") or []))
def request(query: str, params: "OnlineParams") -> None:
"""Create the API request."""
body: dict[str, t.Any] = {
"query": query,
"type": search_type,
"numResults": results_per_page,
"contents": _contents_payload(),
}
# Apply SafeSearch if enabled
if params["safesearch"]:
body["moderation"] = True
# Apply region-aware results if specified
locale_parts = params["searxng_locale"].split("-")
region = locale_parts[-1]
if len(locale_parts) > 1:
body["userLocation"] = region.upper()
params["url"] = base_url
params["method"] = "POST"
params["headers"]["x-api-key"] = api_key
params["json"] = body
def _extract_published_date(value: str | None):
"""Extract and parse the published date from the API response.
Args:
value: Raw date string from the API
Returns:
Parsed datetime object or None if parsing fails
"""
if not value:
return None
try:
return parser.parse(value)
except (parser.ParserError, TypeError, OverflowError):
return None
def response(resp: "SXNG_Response") -> EngineResults:
"""Process the API response and return results."""
res = EngineResults()
for result in resp.json().get("results", []):
url = result.get("url")
if not url:
continue
res.add(
res.types.MainResult(
url=url,
title=html_to_text(result.get("title") or url),
content=_extract_content(result),
thumbnail=result.get("image") or "",
publishedDate=_extract_published_date(result.get("publishedDate")),
author=result.get("author") or "",
)
)
return res

View File

@@ -65,7 +65,6 @@ code lines are just relabeled (starting from 1) and appended (a disjoint set of
code blocks in a single file might be returned from the API). code blocks in a single file might be returned from the API).
""" """
import typing as t import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode

View File

@@ -9,12 +9,15 @@ engines:
- :ref:`google scholar engine` - :ref:`google scholar engine`
- :ref:`google autocomplete` - :ref:`google autocomplete`
This implementation uses Nokia user agents to request an XML layout from Google.
The normal web version requires executing JavaScript to load the results and
therefore is currently not used here. See `Google discussion`_ for more
information on that topic.
.. _Google discussion: https://github.com/searxng/searxng/issues/6359
""" """
import random import random
import re
import string
import time
import typing as t import typing as t
from urllib.parse import unquote, urlencode from urllib.parse import unquote, urlencode
@@ -44,16 +47,16 @@ about = {
"official_api_documentation": "https://developers.google.com/custom-search/", "official_api_documentation": "https://developers.google.com/custom-search/",
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "XML",
} }
# engine dependent config # engine dependent config
categories = ["general", "web"] categories = ["general", "web"]
paging = True paging = True
max_page = 50 max_page = 50
"""`Google max 50 pages`_ """Google supports up to 50 pages of results, see the `Google max_page discussion`_.
.. _Google max 50 pages: https://github.com/searxng/searxng/issues/2982 .. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
""" """
time_range_support = True time_range_support = True
language_support = True language_support = True
@@ -64,38 +67,23 @@ time_range_dict = {"day": "d", "week": "w", "month": "m", "year": "y"}
# Filter results. 0: None, 1: Moderate, 2: Strict # Filter results. 0: None, 1: Moderate, 2: Strict
filter_mapping = {0: "off", 1: "medium", 2: "high"} filter_mapping = {0: "off", 1: "medium", 2: "high"}
# https://github.com/searxng/searxng/issues/6359
nokia_useragents = (
"Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0",
"Nokia7610/2.0 (7.0642.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0",
"Nokia6230/2.0 (05.50) Profile/MIDP-2.0 Configuration/CLDC-1.1",
"Nokia6230i/2.0 (03.80) Profile/MIDP-2.0 Configuration/CLDC-1.1",
"Nokia6280/2.0 (03.60) Profile/MIDP-2.0 Configuration/CLDC-1.1",
"NokiaN72/2.0617.1.0.3 Series60/2.8 Profile/MIDP-2.0 Configuration/CLDC-1.1",
)
# specific xpath variables # specific xpath variables
# ------------------------ # ------------------------
# Suggestions are links placed in a *card-section*, we extract only the text # Suggestions are links placed in a *card-section*, we extract only the text
# from the links not the links itself. # from the links not the links itself.
suggestion_xpath = '//div[contains(@class, "gGQDvd iIWm4b")]//a' suggestion_xpath = '//table[contains(@class, "HExoMb")]//a[contains(@class, "ZWRArf")]'
_arcid_range = string.ascii_letters + string.digits + "_-"
_arcid_random: tuple[str, int] | None = None
def ui_async(start: int) -> str:
"""Format of the response from UI's async request.
- ``arc_id:<...>,use_ac:true,_fmt:prog``
The arc_id is random generated every hour.
"""
global _arcid_random # pylint: disable=global-statement
use_ac = "use_ac:true"
# _fmt:html returns a HTTP 500 when user search for celebrities like
# '!google natasha allegri' or '!google chris evans'
_fmt = "_fmt:prog"
# create a new random arc_id every hour
if not _arcid_random or (int(time.time()) - _arcid_random[1]) > 3600:
_arcid_random = ("".join(random.choices(_arcid_range, k=23)), int(time.time()))
arc_id = f"arc_id:srp_{_arcid_random[0]}_1{start:02}"
return ",".join([arc_id, use_ac, _fmt])
def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[str, t.Any]: def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[str, t.Any]:
@@ -127,19 +115,11 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
A instance of :py:obj:`babel.core.Locale` build from the A instance of :py:obj:`babel.core.Locale` build from the
``searxng_locale`` value. ``searxng_locale`` value.
subdomain:
Google subdomain :py:obj:`google_domains` that fits to the country
code.
params: params:
Py-Dictionary with additional request arguments (can be passed to Py-Dictionary with additional request arguments (can be passed to
:py:func:`urllib.parse.urlencode`). :py:func:`urllib.parse.urlencode`).
- ``hl`` parameter: specifies the interface language of user interface. - ``hl`` parameter: specifies the interface language of user interface.
- ``lr`` parameter: restricts search results to documents written in
a particular language.
- ``cr`` parameter: restricts search results to documents
originating in a particular country.
- ``ie`` parameter: sets the character encoding scheme that should - ``ie`` parameter: sets the character encoding scheme that should
be used to interpret the query string ('utf8'). be used to interpret the query string ('utf8').
- ``oe`` parameter: sets the character encoding scheme that should - ``oe`` parameter: sets the character encoding scheme that should
@@ -156,7 +136,6 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
ret_val: dict[str, t.Any] = { ret_val: dict[str, t.Any] = {
"language": None, "language": None,
"country": None, "country": None,
"subdomain": None,
"params": {}, "params": {},
"headers": {}, "headers": {},
"cookies": {}, "cookies": {},
@@ -169,7 +148,7 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
except babel.core.UnknownLocaleError: except babel.core.UnknownLocaleError:
locale = None locale = None
eng_lang = eng_traits.get_language(sxng_locale, "lang_en") eng_lang = eng_traits.get_language(sxng_locale) or "lang_en"
lang_code = eng_lang.split("_")[-1] # lang_zh-TW --> zh-TW / lang_en --> en lang_code = eng_lang.split("_")[-1] # lang_zh-TW --> zh-TW / lang_en --> en
country = eng_traits.get_region(sxng_locale, eng_traits.all_locale) country = eng_traits.get_region(sxng_locale, eng_traits.all_locale)
@@ -184,7 +163,6 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
ret_val["language"] = eng_lang ret_val["language"] = eng_lang
ret_val["country"] = country ret_val["country"] = country
ret_val["locale"] = locale ret_val["locale"] = locale
ret_val["subdomain"] = eng_traits.custom["supported_domains"].get(country.upper(), "www.google.com")
# hl parameter: # hl parameter:
# The hl parameter specifies the interface language (host language) of # The hl parameter specifies the interface language (host language) of
@@ -223,9 +201,11 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
# specify a region (country) only if a region is given in the selected # specify a region (country) only if a region is given in the selected
# locale --> https://github.com/searxng/searxng/issues/2672 # locale --> https://github.com/searxng/searxng/issues/2672
ret_val["params"]["cr"] = ""
if len(sxng_locale.split("-")) > 1: if country is not None:
ret_val["params"]["cr"] = "country" + country ret_val["params"]["cr"] = ""
if len(sxng_locale.split("-")) > 1:
ret_val["params"]["cr"] = "country" + country
# gl parameter: (mandatory by Google News) # gl parameter: (mandatory by Google News)
# The gl parameter value is a two-letter country code. For WebSearch # The gl parameter value is a two-letter country code. For WebSearch
@@ -300,88 +280,77 @@ def detect_google_sorry(resp: "SXNG_Response"):
raise SearxEngineCaptchaException() raise SearxEngineCaptchaException()
def request(query: str, params: "OnlineParams") -> None: def unwrap_google_url(raw_url: str) -> str:
"""Google search request""" # remove redirector from url
# pylint: disable=line-too-long if raw_url.startswith("/url?q="):
start = (params["pageno"] - 1) * 10 return unquote(raw_url[7:].split("&sa=U")[0])
google_info = get_google_info(params, traits) return raw_url
# https://www.google.de/search?q=corona&hl=de&lr=lang_de&start=0&tbs=qdr%3Ad&safe=medium
query_url = (
"https://"
+ google_info["subdomain"]
+ "/search"
+ "?"
+ urlencode(
{
"q": query,
**google_info["params"],
"filter": "0",
"start": start,
# 'vet': '12ahUKEwik3ZbIzfn7AhXMX_EDHbUDBh0QxK8CegQIARAC..i',
# 'ved': '2ahUKEwik3ZbIzfn7AhXMX_EDHbUDBh0Q_skCegQIARAG',
# 'cs' : 1,
# 'sa': 'N',
# 'yv': 3,
# 'prmd': 'vin',
# 'ei': 'GASaY6TxOcy_xc8PtYeY6AE',
# 'sa': 'N',
# 'sstk': 'AcOHfVkD7sWCSAheZi-0tx_09XDO55gTWY0JNq3_V26cNN-c8lfD45aZYPI8s_Bqp8s57AHz5pxchDtAGCA_cikAWSjy9kw3kgg'
# formally known as use_mobile_ui
# "asearch": "arc",
# "async": str_async,
}
)
)
if params["time_range"] in time_range_dict:
query_url += "&" + urlencode({"tbs": "qdr:" + time_range_dict[params["time_range"]]})
if params["safesearch"]:
query_url += "&" + urlencode({"safe": filter_mapping[params["safesearch"]]})
params["url"] = query_url
params["cookies"] = google_info["cookies"]
params["headers"].update(google_info["headers"])
# regex match to get image map that is found inside the returned javascript: def wml_dom(resp: "SXNG_Response"):
# (function(){var s='...';var i=['...'] ...}
RE_DATA_IMAGE = re.compile(r"(data:image[^']*?)'[^']*?'((?:dimg|pimg|tsuid)[^']*)")
def parse_url_images(text: str):
data_image_map = {}
for image_url, img_id in RE_DATA_IMAGE.findall(text):
data_image_map[img_id] = image_url.encode('utf-8').decode("unicode-escape")
logger.debug("data:image objects --> %s", list(data_image_map.keys()))
return data_image_map
def response(resp: "SXNG_Response"):
"""Get response from google's search request"""
# pylint: disable=too-many-branches, too-many-statements
detect_google_sorry(resp) detect_google_sorry(resp)
data_image_map = parse_url_images(resp.text) text = resp.text
if text.lstrip().startswith("<?xml"):
text = text.split("?>", 1)[-1]
return html.fromstring(text)
def google_request(
query: str,
params: "OnlineParams",
extra_args: dict[str, t.Any] | None = None,
*,
eng_traits: EngineTraits | None = None,
use_time_range: bool = True,
use_safesearch: bool = True,
safesearch_map: dict[int, str] | None = None,
use_locales: bool = True,
) -> None:
google_info = get_google_info(params, eng_traits or traits)
if not use_locales:
google_info["params"].pop("lr")
google_info["params"].pop("cr")
start = (params["pageno"] - 1) * 10
args: dict[str, t.Any] = {
"q": query,
"sca_esv": "1",
**google_info["params"],
**(extra_args or {}),
}
if start:
args["start"] = start
if use_time_range and params["time_range"] in time_range_dict:
args["tbs"] = "qdr:" + time_range_dict[params["time_range"]]
if use_safesearch and params["safesearch"]:
args["safe"] = (safesearch_map or filter_mapping)[params["safesearch"]]
params["url"] = f"https://www.google.com/wml/search?{urlencode(args)}"
params["headers"]["User-Agent"] = random.choice(nokia_useragents)
def request(query: str, params: "OnlineParams") -> None:
google_request(query, params)
def response(resp: "SXNG_Response") -> EngineResults:
results = EngineResults() results = EngineResults()
dom = wml_dom(resp)
# convert the text to dom
dom = html.fromstring(resp.text)
# parse results # parse results
for result in eval_xpath_list(dom, '//a[@data-ved and not(@class)]'): for result in eval_xpath_list(dom, '//div[contains(@class, "zMzFAb")]'):
# pylint: disable=too-many-nested-blocks
try: try:
title_tag = eval_xpath_getindex(result, './/div[@style]', 0, default=None) title_tag = eval_xpath_getindex(
result, './/a[contains(@class, "fuLhoc")]//span[contains(@class, "CVA68e")]', 0, default=None
)
if title_tag is None: if title_tag is None:
# this not one of the common google results *section* # this not one of the common google results *section*
logger.debug("ignoring item from the result_xpath list: missing title") logger.debug("ignoring item from the result_xpath list: missing title")
continue continue
title = extract_text(title_tag) title = extract_text(title_tag)
raw_url = result.get("href") raw_url = eval_xpath_getindex(result, './/a[contains(@class, "fuLhoc")]/@href', 0, default=None)
if raw_url is None: if raw_url is None:
logger.debug( logger.debug(
'ignoring item from the result_xpath list: missing url of title "%s"', 'ignoring item from the result_xpath list: missing url of title "%s"',
@@ -389,30 +358,19 @@ def response(resp: "SXNG_Response"):
) )
continue continue
if raw_url.startswith('/url?q='): url = unwrap_google_url(raw_url)
url = unquote(raw_url[7:].split("&sa=U")[0]) # remove the google redirector content = extract_text(
else: eval_xpath(result, './/div[contains(@class, "taTFJ")]//span[contains(@class, "FrIlee")]')
url = raw_url )
thumbnail = eval_xpath_getindex(result, './/img[contains(@src, "encrypted-tbn")]/@src', 0, default=None)
content_nodes = eval_xpath(result, '../..//div[contains(@class, "ilUpNd H66NU aSRlid")]') results.add(
for item in content_nodes: results.types.MainResult(
for script in item.xpath(".//script"): url=url,
script.getparent().remove(script) title=title or "",
content=content or "",
content = extract_text(content_nodes[0]) thumbnail=thumbnail or "",
)
# Images that are NOT the favicon )
xpath_image = eval_xpath_getindex(result, './/img', index=0, default=None)
thumbnail = None
if xpath_image is not None:
thumbnail = xpath_image.get("src")
if thumbnail.startswith("data:image"):
img_id = xpath_image.get("id")
if img_id:
thumbnail = data_image_map.get(img_id)
results.append({"url": url, "title": title, "content": content or '', "thumbnail": thumbnail})
except Exception as e: # pylint: disable=broad-except except Exception as e: # pylint: disable=broad-except
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
@@ -420,10 +378,8 @@ def response(resp: "SXNG_Response"):
# parse suggestion # parse suggestion
for suggestion in eval_xpath_list(dom, suggestion_xpath): for suggestion in eval_xpath_list(dom, suggestion_xpath):
# append suggestion results.add(results.types.LegacyResult(suggestion=extract_text(suggestion)))
results.append({"suggestion": extract_text(suggestion)})
# return results
return results return results
@@ -456,14 +412,12 @@ skip_countries = [
] ]
def fetch_traits(engine_traits: EngineTraits, add_domains: bool = True): def fetch_traits(engine_traits: EngineTraits):
"""Fetch languages from Google.""" """Fetch languages from Google."""
# pylint: disable=import-outside-toplevel, too-many-branches # pylint: disable=import-outside-toplevel, too-many-branches
from searx.network import get # see https://github.com/searxng/searxng/issues/762 from searx.network import get # see https://github.com/searxng/searxng/issues/762
engine_traits.custom["supported_domains"] = {}
resp = get("https://www.google.com/preferences", timeout=5) resp = get("https://www.google.com/preferences", timeout=5)
if not resp.ok: if not resp.ok:
raise RuntimeError("Response from Google preferences is not OK.") raise RuntimeError("Response from Google preferences is not OK.")
@@ -514,22 +468,3 @@ def fetch_traits(engine_traits: EngineTraits, add_domains: bool = True):
# alias regions # alias regions
engine_traits.regions["zh-CN"] = "HK" engine_traits.regions["zh-CN"] = "HK"
# supported domains
if add_domains:
resp = get("https://www.google.com/supported_domains", timeout=5)
if not resp.ok:
raise RuntimeError("Response from Google supported domains is not OK.")
for domain in resp.text.split():
domain = domain.strip()
if not domain or domain in [
".google.com",
]:
continue
region = domain.split(".")[-1].upper()
engine_traits.custom["supported_domains"][region] = "www" + domain
if region == "HK":
# There is no google.cn, we use .com.hk for zh-CN
engine_traits.custom["supported_domains"]["CN"] = "www" + domain

View File

@@ -95,12 +95,11 @@ def request(query: str, params: "OnlineParams") -> None:
token = _cse_token() token = _cse_token()
google_info = get_google_info(params, traits) google_info = get_google_info(params, traits)
info: dict[str, str] = google_info["params"]
args = { args = {
"rsz": "filtered_cse", "rsz": "filtered_cse",
"num": str(page_size), "num": str(page_size),
"hl": info["hl"], "hl": google_info["params"]["hl"],
"cselibv": token["cselibv"], "cselibv": token["cselibv"],
"cx": CX, "cx": CX,
"q": query, "q": query,
@@ -114,10 +113,6 @@ def request(query: str, params: "OnlineParams") -> None:
start_date, end_date = _get_start_and_end_date_str(params["time_range"]) start_date, end_date = _get_start_and_end_date_str(params["time_range"])
args["sort"] = f"date:r:{start_date}:{end_date}" args["sort"] = f"date:r:{start_date}:{end_date}"
if info.get("lr"):
args["lr"] = info["lr"]
if info.get("cr"):
args["cr"] = info["cr"]
if google_info["country"] not in (None, "ZZ"): if google_info["country"] not in (None, "ZZ"):
args["gl"] = google_info["country"] args["gl"] = google_info["country"]
if token["exp"]: if token["exp"]:

View File

@@ -1,122 +1,75 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""This is the implementation of the Google Images engine using the internal """Google Images: see :py:obj:`searx.engines.google`."""
Google API used by the Google Go Android app.
This internal API offer results in import typing as t
from urllib.parse import parse_qs, unquote, urlparse
- JSON (``_fmt:json``)
- Protobuf_ (``_fmt:pb``)
- Protobuf_ compressed? (``_fmt:pc``)
- HTML (``_fmt:html``)
- Protobuf_ encoded in JSON (``_fmt:jspb``).
.. _Protobuf: https://en.wikipedia.org/wiki/Protocol_Buffers
"""
from urllib.parse import urlencode
from json import loads
from searx.engines.google import fetch_traits # pylint: disable=unused-import from searx.engines.google import fetch_traits # pylint: disable=unused-import
from searx.engines.google import ( from searx.engines.google import google_request, wml_dom
get_google_info, from searx.result_types import EngineResults
time_range_dict, from searx.utils import eval_xpath_list
detect_google_sorry,
) if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
# about # about
about = { about = {
"website": 'https://images.google.com', "website": "https://images.google.com",
"wikidata_id": 'Q521550', "wikidata_id": "Q521550",
"official_api_documentation": 'https://developers.google.com/custom-search', "official_api_documentation": "https://developers.google.com/custom-search",
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": "XML",
} }
# engine dependent config # engine dependent config
categories = ['images', 'web'] categories = ["images", "web"]
paging = True paging = True
max_page = 50 max_page = 50
"""`Google max 50 pages`_ """Google supports up to 50 pages of results, see the `Google max_page discussion`_.
.. _Google max 50 pages: https://github.com/searxng/searxng/issues/2982 .. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
""" """
time_range_support = True time_range_support = True
language_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"}
def request(query, params): def request(query: str, params: "OnlineParams") -> None:
"""Google-Image search request""" google_request(
query,
google_info = get_google_info(params, traits) params,
{"tbm": "isch"},
query_url = ( eng_traits=traits,
'https://' safesearch_map=filter_mapping,
+ google_info['subdomain'] use_locales=False,
+ '/search'
+ '?'
+ urlencode({'q': query, 'tbm': "isch", **google_info['params'], 'asearch': 'isch'})
# don't urlencode this because wildly different AND bad results
# pagination uses Zero-based numbering
+ f'&async=_fmt:json,p:1,ijn:{params["pageno"] - 1}'
) )
if params['time_range'] in time_range_dict:
query_url += '&' + urlencode({'tbs': 'qdr:' + time_range_dict[params['time_range']]})
if params['safesearch']:
query_url += '&' + urlencode({'safe': filter_mapping[params['safesearch']]})
params['url'] = query_url
params['cookies'] = google_info['cookies']
params['headers'].update(google_info['headers'])
# this ua will allow getting ~50 results instead of 10. #1641
params['headers']['User-Agent'] = (
'NSTN/3.60.474802233.release Dalvik/2.1.0 (Linux; U; Android 12;' f' {google_info.get("country", "US")}) gzip'
)
return params def response(resp: "SXNG_Response") -> EngineResults:
results = EngineResults()
dom = wml_dom(resp)
for link in eval_xpath_list(dom, '//a[contains(@href, "/imgres?")]'):
def response(resp): qs = parse_qs(urlparse(link.get("href", "")).query)
"""Get response from google's search request""" img_src = qs.get("imgurl", [""])[0]
results = [] url = qs.get("imgrefurl", [""])[0]
if not img_src or not url:
detect_google_sorry(resp) continue
width, height = qs.get("w", [""])[0], qs.get("h", [""])[0]
json_start = resp.text.find('{"ischj":') tbnid = qs.get("tbnid", [""])[0]
json_data = loads(resp.text[json_start:]) results.add(
results.types.Image(
for item in json_data["ischj"].get("metadata", []): url=url,
result_item = { title=unquote(urlparse(img_src).path.rsplit("/", 1)[-1]) or urlparse(url).netloc,
'url': item["result"]["referrer_url"], img_src=img_src,
'title': item["result"]["page_title"], thumbnail_src=f"https://encrypted-tbn0.gstatic.com/images?q=tbn:{tbnid}",
'content': item["text_in_grid"]["snippet"], resolution=f"{width} x {height}" if width and height else "",
'source': item["result"]["site_title"], )
'resolution': f'{item["original_image"]["width"]} x {item["original_image"]["height"]}', )
'img_src': item["original_image"]["url"],
'thumbnail_src': item["thumbnail"]["url"],
'template': 'images.html',
}
author = item["result"].get('iptc', {}).get('creator')
if author:
result_item['author'] = ', '.join(author)
copyright_notice = item["result"].get('iptc', {}).get('copyright_notice')
if copyright_notice:
result_item['source'] += ' | ' + copyright_notice
freshness_date = item["result"].get("freshness_date")
if freshness_date:
result_item['source'] += ' | ' + freshness_date
file_size = item.get('gsa', {}).get('file_size')
if file_size:
result_item['source'] += ' (%s)' % file_size
results.append(result_item)
return results return results

View File

@@ -1,324 +1,91 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""This is the implementation of the Google News engine. """Google News: see :py:obj:`searx.engines.google`."""
Google News has a different region handling compared to Google WEB.
- the ``ceid`` argument has to be set (:py:obj:`ceid_list`)
- the hl_ argument has to be set correctly (and different to Google WEB)
- the gl_ argument is mandatory
If one of this argument is not set correctly, the request is redirected to
CONSENT dialog::
https://consent.google.com/m?continue=
The google news API ignores some parameters from the common :ref:`google API`:
- num_ : the number of search results is ignored / there is no paging all
results for a query term are in the first response.
- save_ : is ignored / Google-News results are always *SafeSearch*
.. _hl: https://developers.google.com/custom-search/docs/xml_results#hlsp
.. _gl: https://developers.google.com/custom-search/docs/xml_results#glsp
.. _num: https://developers.google.com/custom-search/docs/xml_results#numsp
.. _save: https://developers.google.com/custom-search/docs/xml_results#safesp
"""
import typing as t import typing as t
import json from searx.engines.google import fetch_traits # pylint: disable=unused-import
import base64 from searx.engines.google import google_request, unwrap_google_url, wml_dom
from urllib.parse import urlencode from searx.result_types import EngineResults
from lxml import html
import babel
from searx import locales
from searx.utils import ( from searx.utils import (
eval_xpath,
eval_xpath_list,
eval_xpath_getindex, eval_xpath_getindex,
eval_xpath_list,
extract_text, extract_text,
) )
from searx.engines.google import fetch_traits as _fetch_traits # pylint: disable=unused-import
from searx.engines.google import (
get_google_info,
detect_google_sorry,
)
from searx.enginelib.traits import EngineTraits
from searx.result_types import EngineResults
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
about = { about = {
"website": "https://news.google.com", "website": "https://www.google.com",
"wikidata_id": "Q12020", "wikidata_id": "Q12020",
"official_api_documentation": "https://developers.google.com/custom-search", "official_api_documentation": "https://developers.google.com/custom-search",
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "XML",
} }
# engine dependent config # engine dependent config
categories = ["news"] categories = ["news"]
paging = False paging = True
max_page = 50
"""Google supports up to 50 pages of results, see the `Google max_page discussion`_.
.. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
"""
time_range_support = False time_range_support = False
language_support = True language_support = True
safesearch = False
# Google-News results are always *SafeSearch*. Option 'safesearch' is set to
# False here.
#
# safesearch : results are identical for safesearch=0 and safesearch=2
safesearch = True
base_url: str = "https://news.google.com"
def request(query: str, params: "OnlineParams") -> None: def request(query: str, params: "OnlineParams") -> None:
"""Google-News search request""" google_request(
query,
sxng_locale = params.get("searxng_locale", "en-US") params,
ceid: str = locales.get_engine_locale( {"tbm": "nws"},
sxng_locale, traits.custom["ceid"], default="US:en" eng_traits=traits,
) # pyright: ignore[reportAssignmentType] use_time_range=False,
google_info = get_google_info(params, traits) use_safesearch=False,
google_info["subdomain"] = "news.google.com" # google news has only one domain use_locales=False,
ceid_region, ceid_lang = ceid.split(":")
ceid_lang, ceid_suffix = (
ceid_lang.split(":")
+ [
"",
]
)[:2]
google_info["params"]["hl"] = ceid_lang
if ceid_suffix and ceid_suffix not in ["Hans", "Hant"]:
if ceid_region.lower() == ceid_lang:
google_info["params"]["hl"] = ceid_lang + "-" + ceid_region
else:
google_info["params"]["hl"] = ceid_lang + "-" + ceid_suffix
elif ceid_region.lower() != ceid_lang:
if ceid_region in ["AT", "BE", "CH", "IL", "SA", "IN", "BD", "PT"]:
google_info["params"]["hl"] = ceid_lang
else:
google_info["params"]["hl"] = ceid_lang + "-" + ceid_region
google_info["params"]["lr"] = "lang_" + ceid_lang.split("-")[0]
google_info["params"]["gl"] = ceid_region
query_url = (
"https://"
+ google_info["subdomain"]
+ "/search?"
+ urlencode(
{"q": query, **google_info["params"]},
)
# ceid includes a ':' character which must not be urlencoded
+ ("&ceid=%s" % ceid)
) )
params["url"] = query_url
params["cookies"] = google_info["cookies"] def _span_text(link, css_class: str):
params["headers"].update(google_info["headers"]) return extract_text(
eval_xpath_getindex(link, f'.//span[contains(@class, "{css_class}")]', 0, default=None),
allow_none=True,
)
def response(resp: "SXNG_Response") -> EngineResults: def response(resp: "SXNG_Response") -> EngineResults:
"""Get response from google's search request""" results = EngineResults()
seen = set()
res = EngineResults() for link in eval_xpath_list(wml_dom(resp), '//a[contains(@href, "/url?q=")]'):
href = link.get("href")
detect_google_sorry(resp) if not href:
# convert the text to dom
dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, "//div[@jslog and @data-n-tid and @jsdata]"):
url: str = eval_xpath_getindex(result, "./a[@target='_blank']/@href", 0, default=0)
if not url:
continue
if url.startswith("./"):
url = base_url + url[1:]
# The real URL is often encoded in the "jslog" attribute
jslog: str | None = eval_xpath_getindex(result, "./a[@target='_blank']/@jslog", 0, default=None)
# Try to extract the real URL from jslog
real_url: str | None = None
if jslog:
# jslog format is usually: "95014; 5:<base64>; track:click,vis". We
# want the second part (index 1) after splitting by ";"
parts: list[str] = jslog.split(";")
if len(parts) > 1:
b64_data: str = parts[1].split(":")[-1].strip()
# Pad base64 if necessary
b64_data += "=" * (-len(b64_data) % 4)
decoded_data: list[str | None] = json.loads(base64.b64decode(b64_data).decode("utf-8"))
# The URL is typically the last element in the decoded array
if (
isinstance(decoded_data, list)
and isinstance(decoded_data[-1], str)
and decoded_data[-1].startswith("http")
):
real_url = decoded_data[-1]
if real_url:
url = real_url
else:
logger.error(f"no real-url found: {url}")
continue continue
title = extract_text(eval_xpath(result, "./h4")) or "" url = unwrap_google_url(href)
if url in seen or "google.com/search" in url:
continue
# The pub_date is mostly a string like 'yesterday', not a real timezone title = _span_text(link, "M3vVJe") or _span_text(link, "fuLhoc")
# date or time. Therefore we can't use publishedDate and place the if not title:
# *pub* sting into the content. continue
pub_date = extract_text(eval_xpath(result, ".//time")) source = _span_text(link, "dXDvrc")
pub_origin = extract_text(eval_xpath(result, ".//div[contains(@class, 'vr1PYe')]")) pub_date = _span_text(link, "YVIcad")
content = " / ".join([x for x in [pub_origin, pub_date] if x]) thumbnail = eval_xpath_getindex(link, './/img[contains(@src, "encrypted-tbn")]/@src', 0, default=None)
thumbnail: str = eval_xpath_getindex(result, ".//figure/img/@src", 0, default="") seen.add(url)
if thumbnail and thumbnail.startswith("/"): results.add(
thumbnail = base_url + thumbnail results.types.MainResult(
res.add(
res.types.MainResult(
url=url, url=url,
title=title, title=title,
content=content, content=" / ".join(x for x in [source, pub_date] if x),
thumbnail=thumbnail, thumbnail=thumbnail or "",
) )
) )
return res return results
ceid_list = [
"AE:ar",
"AR:es-419",
"AT:de",
"AU:en",
"BD:bn",
"BE:fr",
"BE:nl",
"BG:bg",
"BR:pt-419",
"BW:en",
"CA:en",
"CA:fr",
"CH:de",
"CH:fr",
"CL:es-419",
"CN:zh-Hans",
"CO:es-419",
"CU:es-419",
"CZ:cs",
"DE:de",
"EE:et",
"EG:ar",
"ES:ca",
"ES:es",
"ET:en",
"FI:fi",
"FR:fr",
"GB:en",
"GH:en",
"GR:el",
"HK:zh-Hant",
"HU:hu",
"ID:en",
"ID:id",
"IE:en",
"IL:en",
"IL:he",
"IN:bn",
"IN:en",
"IN:gu",
"IN:hi",
"IN:ml",
"IN:mr",
"IN:pa",
"IN:ta",
"IN:te",
"IT:it",
"JP:ja",
"KE:en",
"KR:ko",
"LB:ar",
"LT:lt",
"LV:en",
"LV:lv",
"MA:fr",
"MY:en",
"MY:ms",
"NA:en",
"NG:en",
"NL:nl",
"NO:no",
"NZ:en",
"PH:en",
"PK:en",
"PL:pl",
"RO:ro",
"RS:sr",
"RU:ru",
"SA:ar",
"SE:sv",
"SG:en",
"SI:sl",
"SK:sk",
"SN:fr",
"TH:th",
"TR:tr",
"TZ:en",
"UA:ru",
"UA:uk",
"UG:en",
"US:en",
"VN:vi",
"ZA:en",
"ZW:en",
]
"""List of region/language combinations supported by Google News. Values of the
``ceid`` argument of the Google News REST API."""
_skip_values = [
"ET:en", # english (ethiopia)
"ID:en", # english (indonesia)
"LV:en", # english (latvia)
]
_ceid_locale_map = {"NO:no": "nb-NO"}
def fetch_traits(engine_traits: EngineTraits):
_fetch_traits(engine_traits, add_domains=False)
engine_traits.custom["ceid"] = {}
for ceid in ceid_list:
if ceid in _skip_values:
continue
region, lang = ceid.split(":")
x = lang.split("-")
if len(x) > 1:
if x[1] not in ["Hant", "Hans"]:
lang = x[0]
sxng_locale = _ceid_locale_map.get(ceid, lang + "-" + region)
try:
locale = babel.Locale.parse(sxng_locale, sep="-")
except babel.UnknownLocaleError:
print("ERROR: %s -> %s is unknown by babel" % (ceid, sxng_locale))
continue
engine_traits.custom["ceid"][locales.region_tag(locale)] = ceid

View File

@@ -77,8 +77,6 @@ def request(query: str, params: "OnlineParams") -> None:
"""Google-Scholar search request""" """Google-Scholar search request"""
google_info = get_google_info(params, traits) google_info = get_google_info(params, traits)
# subdomain is: scholar.google.xy
google_info["subdomain"] = google_info["subdomain"].replace("www.", "scholar.")
args = { args = {
"q": query, "q": query,
@@ -89,7 +87,7 @@ def request(query: str, params: "OnlineParams") -> None:
} }
args.update(time_range_args(params)) args.update(time_range_args(params))
params["url"] = "https://" + google_info["subdomain"] + "/scholar?" + urlencode(args) params["url"] = "https://scholar.google.com/scholar?" + urlencode(args)
params["cookies"] = google_info["cookies"] params["cookies"] = google_info["cookies"]
params["headers"].update(google_info["headers"]) params["headers"].update(google_info["headers"])

View File

@@ -1,185 +1,87 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""This is the implementation of the Google Videos engine. """Google Videos: see :py:obj:`searx.engines.google`."""
.. admonition:: Content-Security-Policy (CSP) import typing as t
This engine needs to allow images from the `data URLs`_ (prefixed with the
``data:`` scheme)::
Header set Content-Security-Policy "img-src 'self' data: ;"
.. _data URLs:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
"""
import re
from urllib.parse import urlencode, urlparse, parse_qs, unquote
from lxml import html
from searx.utils import (
eval_xpath_list,
eval_xpath_getindex,
extract_text,
)
from searx.engines.google import fetch_traits # pylint: disable=unused-import from searx.engines.google import fetch_traits # pylint: disable=unused-import
from searx.engines.google import ( from searx.engines.google import google_request, unwrap_google_url, wml_dom
get_google_info, from searx.result_types import EngineResults
time_range_dict, from searx.utils import (
filter_mapping, eval_xpath_getindex,
suggestion_xpath, eval_xpath_list,
detect_google_sorry, extract_text,
ui_async, get_embeded_stream_url,
parse_duration_string,
) )
from searx.utils import get_embeded_stream_url
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
# about # about
about = { about = {
"website": 'https://www.google.com', "website": "https://www.google.com",
"wikidata_id": 'Q219885', "wikidata_id": "Q219885",
"official_api_documentation": 'https://developers.google.com/custom-search', "official_api_documentation": "https://developers.google.com/custom-search",
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": 'HTML', "results": "XML",
} }
# engine dependent config # engine dependent config
categories = ['videos', 'web'] categories = ["videos", "web"]
paging = True paging = True
max_page = 50 max_page = 50
"""Google supports up to 50 pages of results, see the `Google max_page discussion`_.
.. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
"""
language_support = True language_support = True
time_range_support = True time_range_support = True
safesearch = True safesearch = True
# =26;[3,"dimg_ZNMiZPCqE4apxc8P3a2tuAQ_137"]a87;data:image/jpeg;base64,/9j/4AAQSkZJRgABA def request(query: str, params: "OnlineParams") -> None:
# ...6T+9Nl4cnD+gr9OK8I56/tX3l86nWYw//2Q==26; google_request(
RE_DATA_IMAGE = re.compile(r'"(dimg_[^"]*)"[^;]*;(data:image[^;]*;[^;]*);?') query,
params,
{"tbm": "vid"},
def parse_data_images(text: str): eng_traits=traits,
data_image_map = {} use_locales=False,
for img_id, data_image in RE_DATA_IMAGE.findall(text):
end_pos = data_image.rfind("=")
if end_pos > 0:
data_image = data_image[: end_pos + 1]
data_image_map[img_id] = data_image
logger.debug("data:image objects --> %s", list(data_image_map.keys()))
return data_image_map
def request(query, params):
"""Google-Video search request"""
google_info = get_google_info(params, traits)
start = (params['pageno'] - 1) * 10
query_url = (
'https://'
+ google_info['subdomain']
+ '/search'
+ "?"
+ urlencode(
{
'q': query,
'tbm': "vid",
'start': start,
**google_info['params'],
'asearch': 'arc',
'async': ui_async(start),
}
)
) )
if params['time_range'] in time_range_dict:
query_url += '&' + urlencode({'tbs': 'qdr:' + time_range_dict[params['time_range']]})
if 'safesearch' in params:
query_url += '&' + urlencode({'safe': filter_mapping[params['safesearch']]})
params['url'] = query_url
params['cookies'] = google_info['cookies'] def response(resp: "SXNG_Response") -> EngineResults:
params['headers'].update(google_info['headers']) results = EngineResults()
return params
for result in eval_xpath_list(wml_dom(resp), '//div[contains(@class, "zMzFAb")]'):
def response(resp):
"""Get response from google's search request"""
results = []
detect_google_sorry(resp)
data_image_map = parse_data_images(resp.text)
# convert the text to dom
dom = html.fromstring(resp.text)
result_divs = eval_xpath_list(dom, '//div[contains(@class, "MjjYud")]')
# parse results
for result in result_divs:
title = extract_text( title = extract_text(
eval_xpath_getindex(result, './/h3[contains(@class, "LC20lb")] | .//div[@role="heading"]', 0, default=None), eval_xpath_getindex(result, './/span[contains(@class, "CVA68e")]', 0, default=None),
allow_none=True, allow_none=True,
) )
url = eval_xpath_getindex( raw_url = eval_xpath_getindex(result, './/a[contains(@class, "fuLhoc")]/@href', 0, default=None)
result, './/a[@jsname="UWckNb"]/@href | .//a[contains(@href, "/url?q=")]/@href', 0, default=None if not title or not raw_url:
) continue
if url and url.startswith('/url?q='):
url = unquote(url[7:].split('&sa=U')[0])
content = extract_text( url = unwrap_google_url(raw_url)
eval_xpath_getindex(result, './/div[contains(@class, "ITZIwc")]', 0, default=None), allow_none=True thumbnail = eval_xpath_getindex(result, './/img[contains(@class, "SygO9d")]/@src', 0, default="")
) if "/default.jpg" in thumbnail:
pub_info = extract_text( thumbnail = thumbnail.split("?")[0].replace("/default.jpg", "/hqdefault.jpg")
eval_xpath_getindex( length = None
result, './/div[contains(@class, "gqF9jc")] | .//div[contains(@class, "WRu9Cd")]', 0, default=None for span in eval_xpath_list(result, './/span[contains(@class, "YVIcad")]'):
), length = parse_duration_string(extract_text(span) or "")
allow_none=True, if length:
) break
# Broader XPath to find any <img> element
thumbnail = eval_xpath_getindex(result, './/img/@src', 0, default=None)
duration = extract_text(
eval_xpath_getindex(result, './/span[contains(@class, "k1U36b")]', 0, default=None), allow_none=True
)
video_id = eval_xpath_getindex(result, './/div[@jscontroller="rTuANe"]/@data-vid', 0, default=None)
# Fallback for video_id from URL if not found via XPath results.add(
if not video_id and url and 'youtube.com' in url: results.types.MainResult(
parsed_url = urlparse(url) url=url,
video_id = parse_qs(parsed_url.query).get('v', [None])[0] title=title,
thumbnail=thumbnail,
# Handle thumbnail length=length,
if thumbnail and thumbnail.startswith('data:image'): iframe_src=get_embeded_stream_url(url) or "",
img_id = eval_xpath_getindex(result, './/img/@id', 0, default=None) template="videos.html",
if img_id and img_id in data_image_map:
thumbnail = data_image_map[img_id]
else:
thumbnail = None
if not thumbnail and video_id:
thumbnail = f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
# Handle video embed URL
embed_url = None
if video_id:
embed_url = get_embeded_stream_url(f"https://www.youtube.com/watch?v={video_id}")
elif url:
embed_url = get_embeded_stream_url(url)
# Only append results with valid title and url
if title and url:
results.append(
{
'url': url,
'title': title,
'content': content or '',
'author': pub_info,
'thumbnail': thumbnail,
'length': duration,
'iframe_src': embed_url,
'template': 'videos.html',
}
) )
)
# parse suggestion
for suggestion in eval_xpath_list(dom, suggestion_xpath):
results.append({'suggestion': extract_text(suggestion)})
return results return results

View File

@@ -4,7 +4,6 @@
from urllib.parse import urlencode from urllib.parse import urlencode
from dateutil import parser from dateutil import parser
about = { about = {
# pylint: disable=line-too-long # pylint: disable=line-too-long
"website": "https://hex.pm/", "website": "https://hex.pm/",

89
searx/engines/jina.py Normal file
View File

@@ -0,0 +1,89 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Jina is a search AI and part of Elastic, the company behind ElasticSearch.
The engine requires an API key, you can get one from the
`API dashboard <https://jina.ai/api-dashboard/>`_ without signup.
.. code:: yaml
- name: jina
engine: jina
shortcut: ji
api_key: "jina_..."
jina_engine: reader
inactive: false
By default, Jina's own index is used. You can change that by setting a different :py:obj:`jina_engine`.
"""
import typing as t
from urllib.parse import urlencode
from dateutil import parser
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://jina.ai",
"wikidata_id": None,
"official_api_documentation": "https://s.jina.ai/docs",
"use_official_api": True,
"require_api_key": True,
"results": "JSON",
}
categories = ["general"]
paging = True
jina_engine = "reader"
"""Search mode. Currently supported values are 'reader', 'google' and 'bing'."""
base_url = "https://s.jina.ai"
api_key: str | None = None
def setup(_):
if not api_key:
raise ValueError("missing api key")
def request(query: str, params: "OnlineParams"):
# setting 'no-content' pushes the response time down to a third
args = {"q": query, "page": params["pageno"], "engine": jina_engine, "respondWith": "no-content"}
params["url"] = f"{base_url}/?{urlencode(args)}"
params["headers"].update(
{
"Accept": "application/json",
"Authorization": f"Bearer {api_key}",
}
)
def response(resp: "SXNG_Response"):
res = EngineResults()
json_resp: dict[str, t.Any] = resp.json()
result: dict[str, str]
for result in json_resp["data"]:
published_date = None
if result.get("date"):
try:
published_date = parser.parse(result["date"])
except parser.ParserError:
pass
res.add(
res.types.MainResult(
url=result["url"],
title=result["title"],
content=result["description"],
publishedDate=published_date,
)
)
return res

View File

@@ -108,14 +108,12 @@ def get_infobox(alt_forms, result_url, definitions):
infobox_content.append(f'<p><i>Other forms:</i> {", ".join(alt_forms[1:])}</p>') infobox_content.append(f'<p><i>Other forms:</i> {", ".join(alt_forms[1:])}</p>')
# 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>
''' ''')
)
for pos, engdef, extra in definitions: for pos, engdef, extra in definitions:
if pos == 'Wikipedia definition': if pos == 'Wikipedia definition':
infobox_content.append('</ul><small>Wikipedia, CC BY-SA 3.0.</small><ul>') infobox_content.append('</ul><small>Wikipedia, CC BY-SA 3.0.</small><ul>')

66
searx/engines/keenable.py Normal file
View File

@@ -0,0 +1,66 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Keenable is a fast web search with keyless mode support"""
import typing as t
from datetime import datetime
from searx.extended_types import SXNG_Response
from searx.result_types import EngineResults
from searx.utils import searxng_useragent
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
about = {
"website": "https://keenable.ai",
"official_api_documentation": "https://docs.keenable.ai",
"use_official_api": True,
"require_api_key": False,
"results": "JSON",
}
api_key = ""
""" Optional API Key. You can create a key at `the official website
<https://keenable.ai/signup>'_ if you need higher rate limits."""
categories = ["general"]
base_url = "https://api.keenable.ai"
keenable_mode = "pro"
def request(query: str, params: "OnlineParams"):
if api_key:
params["url"] = f"{base_url}/v1/search"
params["headers"]["X-API-KEY"] = api_key
else:
params["url"] = f"{base_url}/v1/search/public"
params["method"] = "POST"
params["headers"]["X-Keenable-Title"] = searxng_useragent()
params["json"] = {"query": query, "mode": keenable_mode}
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
results: list[dict[str, str]] = resp.json()["results"] # type: ignore[reportAny]
for result in results:
published = None
pub = result.get("published_at")
if pub:
try:
published = datetime.fromisoformat(pub.rstrip("Z"))
except ValueError:
pass
res.add(
res.types.MainResult(
url=result["url"],
title=result["title"],
content=result["description"] or result["snippet"],
publishedDate=published,
)
)
return res

View File

@@ -6,6 +6,27 @@ Lofgren .
.. _Marginalia Search: .. _Marginalia Search:
https://about.marginalia-search.com/ https://about.marginalia-search.com/
.. _marginalia filters:
Marginalia Filters
=================
Custom filters enable server-side customization of Marginalia search results.
Filter definitions are written in XML and scoped to an API key. Filters can
not be used with the public API key ``public``. The
`Marginalia Filter Editor`_ can be used to create custom filters with a GUI.
Alternatively, filters can be written manually in XML. To associate a filter
definition with an API key, upload the XML data to the ``/filter/<NAME>`` API
endpoint, where ``<NAME>`` is the name for the newly created filter. For more
information, see the `Marginalia filters announcement blogpost`_ and the
official `Marginalia API documentation`_.
.. _Marginalia Filter Editor: https://marginalia-search.com/filters
.. _Marginalia filters announcement blogpost: https://www.marginalia.nu/log/a_127_index_filtering/
.. _Marginalia API documentation: https://about.marginalia-search.com/article/api/
Configuration Configuration
============= =============
@@ -13,6 +34,10 @@ The engine has the following required settings:
- :py:obj:`api_key` - :py:obj:`api_key`
The engine has the following optional settings:
- :py:obj:`filter_name`
You can configure a Marginalia engine by: You can configure a Marginalia engine by:
.. code:: yaml .. code:: yaml
@@ -21,6 +46,7 @@ You can configure a Marginalia engine by:
engine: marginalia engine: marginalia
shortcut: mar shortcut: mar
api_key: ... api_key: ...
filter_name: ...
Implementations Implementations
=============== ===============
@@ -29,6 +55,8 @@ Implementations
import typing as t import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.network import get
from searx.utils import searxng_useragent from searx.utils import searxng_useragent
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -54,6 +82,8 @@ api_key = None
https://about.marginalia-search.com/article/api/ https://about.marginalia-search.com/article/api/
""" """
filter_name: str | None = None
"""The name of the custom filter to apply to each search."""
class ApiSearchResult(t.TypedDict): class ApiSearchResult(t.TypedDict):
@@ -83,6 +113,25 @@ class ApiSearchResults(t.TypedDict):
results: list[ApiSearchResult] results: list[ApiSearchResult]
def _marginalia_headers() -> dict[str, t.Any]:
return {
"User-Agent": searxng_useragent(),
"API-Key": api_key,
}
def _get_filter_names() -> list[str]:
resp = get(f"{base_url}/filter", headers=_marginalia_headers())
if resp.ok:
filter_names = resp.json()
else:
filter_names = []
if not isinstance(filter_names, list):
raise TypeError("marginalia api returned invalid filter list format")
return filter_names
def request(query: str, params: dict[str, t.Any]): def request(query: str, params: dict[str, t.Any]):
query_params = { query_params = {
@@ -91,10 +140,11 @@ def request(query: str, params: dict[str, t.Any]):
"nsfw": min(params["safesearch"], 1), "nsfw": min(params["safesearch"], 1),
"query": query, "query": query,
} }
if filter_name:
query_params["filter"] = filter_name
params["url"] = f"{base_url}/search?{urlencode(query_params)}" params["url"] = f"{base_url}/search?{urlencode(query_params)}"
params["headers"]["User-Agent"] = searxng_useragent() params["headers"].update(_marginalia_headers())
params["headers"]["API-Key"] = api_key
def response(resp: SXNG_Response): def response(resp: SXNG_Response):
@@ -114,14 +164,18 @@ def response(resp: SXNG_Response):
return res return res
def init(engine_settings: dict[str, t.Any]): def init(_: dict[str, t.Any]):
_api_key = engine_settings.get("api_key") if not api_key:
if not _api_key:
logger.error("missing api_key: see https://about.marginalia-search.com/article/api") logger.error("missing api_key: see https://about.marginalia-search.com/article/api")
return False return False
if _api_key == "public": if api_key == "public":
logger.error("invalid api_key (%s): see https://about.marginalia-search.com/article/api", api_key) logger.error("invalid api_key (%s): see https://about.marginalia-search.com/article/api", api_key)
elif filter_name:
filter_names: list[str] = _get_filter_names()
if filter_name not in filter_names:
logger.error(f"invalid value for filter_name: '{filter_name}'")
return False
return True return True

View File

@@ -49,7 +49,6 @@ except ImportError:
from searx.result_types import EngineResults from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
# mongodb connection variables # mongodb connection variables

View File

@@ -4,7 +4,6 @@
from urllib.parse import urlencode from urllib.parse import urlencode
from dateutil import parser from dateutil import parser
about = { about = {
"website": "https://npms.io/", "website": "https://npms.io/",
"wikidata_id": "Q7067518", "wikidata_id": "Q7067518",

View File

@@ -9,7 +9,6 @@ from datetime import datetime
from searx.result_types import EngineResults, WeatherAnswer from searx.result_types import EngineResults, WeatherAnswer
from searx import weather from searx import weather
about = { about = {
"website": "https://open-meteo.com", "website": "https://open-meteo.com",
"wikidata_id": None, "wikidata_id": None,

View File

@@ -10,7 +10,8 @@ from flask_babel import gettext
from searx.data import OSM_KEYS_TAGS, CURRENCIES from searx.data import OSM_KEYS_TAGS, CURRENCIES
from searx.external_urls import get_external_url from searx.external_urls import get_external_url
from searx.engines.wikidata import send_wikidata_query, sparql_string_escape, get_thumbnail from searx.wikidata import send_wikidata_query
from searx.engines.wikidata import sparql_string_escape, get_thumbnail
from searx.result_types import EngineResults from searx.result_types import EngineResults
# about # about
@@ -290,7 +291,8 @@ def get_title_address(result):
'house_number': address_raw.get('house_number'), 'house_number': address_raw.get('house_number'),
'road': address_raw.get('road'), 'road': address_raw.get('road'),
'locality': address_raw.get( 'locality': address_raw.get(
'city', address_raw.get('town', address_raw.get('village')) # noqa 'city',
address_raw.get('town', address_raw.get('village')), # noqa
), # noqa ), # noqa
'postcode': address_raw.get('postcode'), 'postcode': address_raw.get('postcode'),
'country': address_raw.get('country'), 'country': address_raw.get('country'),

View File

@@ -8,7 +8,6 @@ Openverse (formerly known as: Creative Commons search engine) [Images]
from json import loads from json import loads
from urllib.parse import urlencode from urllib.parse import urlencode
about = { about = {
"website": 'https://openverse.org/', "website": 'https://openverse.org/',
"wikidata_id": None, "wikidata_id": None,

View File

@@ -12,7 +12,6 @@ from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException
from searx.network import get from searx.network import get
# about # about
about = { about = {
"website": 'https://www.pexels.com', "website": 'https://www.pexels.com',

View File

@@ -48,7 +48,6 @@ Implementations
""" """
import time import time
import random import random
from urllib.parse import urlencode from urllib.parse import urlencode

View File

@@ -1,305 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Presearch supports the search types listed in :py:obj:`search_type` (general,
images, videos, news).
Configured ``presarch`` engines:
.. code:: yaml
- name: presearch
engine: presearch
search_type: search
categories: [general, web]
- name: presearch images
...
search_type: images
categories: [images, web]
- name: presearch videos
...
search_type: videos
categories: [general, web]
- name: presearch news
...
search_type: news
categories: [news, web]
.. hint::
By default Presearch's video category is intentionally placed into::
categories: [general, web]
Search type ``video``
=====================
The results in the video category are most often links to pages that contain a
video, for instance many links from Preasearch's video category link content
from facebook (aka Meta) or Twitter (aka X). Since these are not real links to
video streams SearXNG can't use the video template for this and if SearXNG can't
use this template, then the user doesn't want to see these hits in the videos
category.
Languages & Regions
===================
In Presearch there are languages for the UI and regions for narrowing down the
search. If we set "auto" for the region in the WEB-UI of Presearch and cookie
``use_local_search_results=false``, then the defaults are set for both (the
language and the region) from the ``Accept-Language`` header.
Since the region is already "auto" by default, we only need to set the
``use_local_search_results`` cookie and send the ``Accept-Language`` header. We
have to set these values in both requests we send to Presearch; in the first
request to get the request-ID from Presearch and in the final request to get the
result list.
The time format returned by Presearch varies depending on the language set.
Multiple different formats can be supported by using ``dateutil`` parser, but
it doesn't support formats such as "N time ago", "vor N time" (German),
"Hace N time" (Spanish). Because of this, the dates are simply joined together
with the rest of other metadata.
Implementations
===============
"""
from urllib.parse import urlencode, urlparse
from searx import locales
from searx.network import get
from searx.utils import gen_useragent, html_to_text, parse_duration_string
about = {
"website": "https://presearch.io",
"wikidata_id": "Q7240905",
"official_api_documentation": "https://docs.presearch.io/nodes/api",
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
paging = True
safesearch = True
time_range_support = True
categories = ["general", "web"] # general, images, videos, news
# HTTP2 requests immediately get blocked by a CAPTCHA
enable_http2 = False
search_type = "search"
"""must be any of ``search``, ``images``, ``videos``, ``news``"""
base_url = "https://presearch.com"
safesearch_map = {0: 'false', 1: 'true', 2: 'true'}
def init(_):
if search_type not in ['search', 'images', 'videos', 'news']:
raise ValueError(f'presearch search_type: {search_type}')
def _get_request_id(query, params):
args = {
"q": query,
"page": params["pageno"],
}
if params["time_range"]:
args["time"] = params["time_range"]
url = f"{base_url}/{search_type}?{urlencode(args)}"
headers = {
'User-Agent': gen_useragent(),
'Cookie': (
f"b=1;"
f" presearch_session=;"
f" use_local_search_results=false;"
f" use_safe_search={safesearch_map[params['safesearch']]}"
),
}
if params['searxng_locale'] != 'all':
l = locales.get_locale(params['searxng_locale'])
# Presearch narrows down the search by region. In SearXNG when the user
# does not set a region (e.g. 'en-CA' / canada) we cannot hand over a region.
# We could possibly use searx.locales.get_official_locales to determine
# in which regions this language is an official one, but then we still
# wouldn't know which region should be given more weight / Presearch
# performs an IP-based geolocation of the user, we don't want that in
# SearXNG ;-)
if l and l.territory:
headers['Accept-Language'] = f"{l.language}-{l.territory},{l.language};" "q=0.9,*;" "q=0.5"
resp = get(url, headers=headers, timeout=5)
for line in resp.text.split("\n"):
if "window.searchId = " in line:
return line.split("= ")[1][:-1].replace('"', ""), resp.cookies
raise RuntimeError("Couldn't find any request id for presearch")
def request(query, params):
request_id, cookies = _get_request_id(query, params)
params["headers"]["Accept"] = "application/json"
params["url"] = f"{base_url}/results?id={request_id}"
params["cookies"] = cookies
return params
def _strip_leading_strings(text):
for x in ['wikipedia', 'google']:
if text.lower().endswith(x):
text = text[: -len(x)]
return text.strip()
def _fix_title(title, url):
"""
Titles from Presearch shows domain + title without spacing, and HTML
This function removes these 2 issues.
Transforming "translate.google.co.in<em>Google</em> Translate" into "Google Translate"
"""
parsed_url = urlparse(url)
domain = parsed_url.netloc
title = html_to_text(title)
# Fixes issue where domain would show up in the title
# translate.google.co.inGoogle Translate -> Google Translate
if (
title.startswith(domain)
and len(title) > len(domain)
and not title.startswith(domain + "/")
and not title.startswith(domain + " ")
):
title = title.removeprefix(domain)
return title
def parse_search_query(json_results):
results = []
if not json_results:
return results
for item in json_results.get('specialSections', {}).get('topStoriesCompact', {}).get('data', []):
result = {
'url': item['link'],
'title': _fix_title(item['title'], item['link']),
'thumbnail': item['image'],
'content': '',
'metadata': item.get('source'),
}
results.append(result)
for item in json_results.get('standardResults', []):
result = {
'url': item['link'],
'title': _fix_title(item['title'], item['link']),
'content': html_to_text(item['description']),
}
results.append(result)
info = json_results.get('infoSection', {}).get('data')
if info:
attributes = []
for item in info.get('about', []):
text = html_to_text(item)
if ':' in text:
# split text into key / value
label, value = text.split(':', 1)
else:
# In other languages (tested with zh-TW) a colon is represented
# by a different symbol --> then we split at the first space.
label, value = text.split(' ', 1)
label = label[:-1]
value = _strip_leading_strings(value)
attributes.append({'label': label, 'value': value})
content = []
for item in [info.get('subtitle'), info.get('description')]:
if not item:
continue
item = _strip_leading_strings(html_to_text(item))
if item:
content.append(item)
results.append(
{
'infobox': info['title'],
'id': info['title'],
'img_src': info.get('image'),
'content': ' | '.join(content),
'attributes': attributes,
}
)
return results
def response(resp):
results = []
json_resp = resp.json()
if search_type == 'search':
results = parse_search_query(json_resp.get('results', {}))
elif search_type == 'images':
for item in json_resp.get('images', []):
results.append(
{
'template': 'images.html',
'title': html_to_text(item['title']),
'url': item.get('link'),
'img_src': item.get('image'),
'thumbnail_src': item.get('thumbnail'),
}
)
elif search_type == 'videos':
# The results in the video category are most often links to pages that contain
# a video and not to a video stream --> SearXNG can't use the video template.
for item in json_resp.get('videos', []):
duration = item.get('duration')
if duration:
duration = parse_duration_string(duration)
results.append(
{
'title': html_to_text(item['title']),
'url': item.get('link'),
'content': item.get('description', ''),
'thumbnail': item.get('image'),
'length': duration,
}
)
elif search_type == 'news':
for item in json_resp.get('news', []):
source = item.get('source')
# Bug on their end, time sometimes returns "</a>"
time = html_to_text(item.get('time')).strip()
metadata = [source]
if time != "":
metadata.append(time)
results.append(
{
'title': html_to_text(item['title']),
'url': item.get('link'),
'content': html_to_text(item.get('description', '')),
'metadata': ' / '.join(metadata),
'thumbnail': item.get('image'),
}
)
return results

View File

@@ -18,7 +18,6 @@ from searx.utils import eval_xpath_list, eval_xpath, extract_text, get_embeded_s
from searx.locales import region_tag from searx.locales import region_tag
from searx.result_types import EngineResults from searx.result_types import EngineResults
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from lxml.etree import ElementBase from lxml.etree import ElementBase
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response

View File

@@ -318,7 +318,7 @@ def fetch_traits(engine_traits: EngineTraits):
from searx.utils import extr from searx.utils import extr
resp = get( resp = get(
about["website"], # pyright: ignore[reportArgumentType] base_url, # pyright: ignore[reportArgumentType]
timeout=5, timeout=5,
) )
if not resp.ok: if not resp.ok:

View File

@@ -35,6 +35,7 @@ Implementations
=============== ===============
""" """
import typing as t import typing as t
from datetime import date, timedelta from datetime import date, timedelta

View File

@@ -1,74 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Reddit"""
import json
from datetime import datetime
from urllib.parse import urlencode, urljoin, urlparse
# about
about = {
"website": 'https://www.reddit.com/',
"wikidata_id": 'Q1136',
"official_api_documentation": 'https://www.reddit.com/dev/api',
"use_official_api": True,
"require_api_key": False,
"results": 'JSON',
}
# engine dependent config
categories = ['social media']
page_size = 25
# search-url
base_url = 'https://www.reddit.com/'
search_url = base_url + 'search.json?{query}'
def request(query, params):
query = urlencode({'q': query, 'limit': page_size})
params['url'] = search_url.format(query=query)
return params
def response(resp):
img_results = []
text_results = []
search_results = json.loads(resp.text)
# return empty array if there are no results
if 'data' not in search_results:
return []
posts = search_results.get('data', {}).get('children', [])
# process results
for post in posts:
data = post['data']
# extract post information
params = {'url': urljoin(base_url, data['permalink']), 'title': data['title']}
# if thumbnail field contains a valid URL, we need to change template
thumbnail = data['thumbnail']
url_info = urlparse(thumbnail)
# netloc & path
if url_info[1] != '' and url_info[2] != '':
params['img_src'] = data['url']
params['thumbnail_src'] = thumbnail
params['template'] = 'images.html'
img_results.append(params)
else:
created = datetime.fromtimestamp(data['created_utc'])
content = data['selftext']
if len(content) > 500:
content = content[:500] + '...'
params['content'] = content
params['publishedDate'] = created
text_results.append(params)
# show images first and text results second
return img_results + text_results

View File

@@ -34,7 +34,6 @@ from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
base_url = 'http://localhost:8983' base_url = 'http://localhost:8983'
collection = '' collection = ''
rows = 10 rows = 10

View File

@@ -117,7 +117,7 @@ def response(resp):
def init(engine_settings): # pylint: disable=unused-argument def init(engine_settings): # pylint: disable=unused-argument
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"]) # type:ignore CACHE = EngineCache(engine_settings["name"]) # type: ignore
def get_client_id() -> str | None: def get_client_id() -> str | None:

View File

@@ -44,6 +44,7 @@ Implementations
=============== ===============
""" """
import typing as t import typing as t
import sqlite3 import sqlite3
import contextlib import contextlib

View File

@@ -82,6 +82,7 @@ Startpage's category (for Web-search, News, Videos, ..) is set by
Supported categories are ``web``, ``news`` and ``images``. Supported categories are ``web``, ``news`` and ``images``.
""" """
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
import re import re

View File

@@ -9,6 +9,7 @@ import codecs
import hashlib import hashlib
import json import json
import random import random
import string
from datetime import datetime from datetime import datetime
from urllib.parse import urlencode from urllib.parse import urlencode
@@ -43,8 +44,8 @@ paging = True
base_url = "https://api.swisscows.com" base_url = "https://api.swisscows.com"
CAESAR_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" CAESAR_ALPHABET = string.ascii_uppercase
NONCE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" NONCE_ALPHABET = string.ascii_letters + string.digits + "-._~"
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"} time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
@@ -92,7 +93,7 @@ def generate_nonce(length: int = 32) -> str:
""" """
Generate a random char sequence with the given length. Generate a random char sequence with the given length.
""" """
return "".join([random.choice(NONCE_ALPHABET) for _ in range(length)]) return "".join(random.choices(NONCE_ALPHABET, k=length))
def caesar_shift_with_switch_case(s: str, offset: int = 13) -> str: def caesar_shift_with_switch_case(s: str, offset: int = 13) -> str:

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