148 Commits

Author SHA1 Message Date
searxng-bot
9072c77aea [l10n] update translations from Weblate
6fd00e66a - 2025-12-18 - dtalens <dtalens@noreply.codeberg.org>
037518f3b - 2025-12-17 - dtalens <dtalens@noreply.codeberg.org>
2025-12-19 08:37:40 +00:00
dependabot[bot]
c32b8100c3 [upd] github-actions: Bump actions/cache from 5.0.0 to 5.0.1
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](a783357455...9255dc7a25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 08:35:15 +00:00
dependabot[bot]
f93257941e [upd] github-actions: Bump github/codeql-action from 4.31.7 to 4.31.9
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.7 to 4.31.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cf1bb45a27...5d4e8d1aca)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 08:34:32 +00:00
Guanzhong Chen
896863802e [fix] engine: brave - BrotliDecoderDecompressStream encoding issue (#5572)
For some reason, I keep getting this error from the brave engine:

    httpx.DecodingError: BrotliDecoderDecompressStream failed while processing the stream

Forcing the server to use either gzip or deflate fixes this issue.

This makes the brave engine work when the server seems to be encoding brotli incorrectly, or at least in a way incompatible with certain installs.

Related:

- https://github.com/searxng/searxng/pull/1787
- https://github.com/searxng/searxng/pull/5536
2025-12-17 09:39:03 +01:00
searxng-bot
920b40253c [l10n] update translations from Weblate
e23dc20f7 - 2025-12-11 - SomeTr <sometr@noreply.codeberg.org>
eb67f948f - 2025-12-11 - artnay <artnay@noreply.codeberg.org>
d4fdfc449 - 2025-12-10 - SomeTr <sometr@noreply.codeberg.org>
0f02ac7cd - 2025-12-10 - IcewindX <icewindx@noreply.codeberg.org>
533ae3947 - 2025-12-11 - return42 <return42@noreply.codeberg.org>
19fe65dc7 - 2025-12-11 - return42 <return42@noreply.codeberg.org>
bca557cea - 2025-12-09 - Hangry-Studios <hangry-studios@noreply.codeberg.org>
e43e9a299 - 2025-12-10 - Priit Jõerüüt <jrtcdbrg@noreply.codeberg.org>
c98083cef - 2025-12-09 - eudemo <eudemo@noreply.codeberg.org>
316225017 - 2025-12-08 - aindriu80 <aindriu80@noreply.codeberg.org>
1b827e5a4 - 2025-12-08 - Aadniz <aadniz@noreply.codeberg.org>
68e760f9b - 2025-12-09 - kratos <makesocialfoss32@keemail.me>
99945ac31 - 2025-12-07 - lucasmz.dev <lucasmz.dev@noreply.codeberg.org>
56602eb75 - 2025-12-07 - Fjuro <fjuro@alius.cz>
df092e811 - 2025-12-06 - c2qd <c2qd@noreply.codeberg.org>
12c25cd85 - 2025-12-06 - Outbreak2096 <outbreak2096@noreply.codeberg.org>
081243428 - 2025-12-05 - SomeTr <sometr@noreply.codeberg.org>
66362c02d - 2025-12-06 - ghose <ghose@noreply.codeberg.org>
2025-12-12 20:23:43 +00:00
dependabot[bot]
07440e3332 [upd] web-client (simple): Bump the minor group
Bumps the minor group in /client/simple with 2 updates: [sort-package-json](https://github.com/keithamus/sort-package-json) and [vite-bundle-analyzer](https://github.com/nonzzz/vite-bundle-analyzer).

Updates `sort-package-json` from 3.5.0 to 3.5.1
- [Release notes](https://github.com/keithamus/sort-package-json/releases)
- [Commits](https://github.com/keithamus/sort-package-json/compare/v3.5.0...v3.5.1)

Updates `vite-bundle-analyzer` from 1.2.3 to 1.3.1
- [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.2.3...v1.3.1)

---
updated-dependencies:
- dependency-name: sort-package-json
  dependency-version: 3.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: vite-bundle-analyzer
  dependency-version: 1.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 20:22:48 +00:00
dependabot[bot]
1827dfc071 [upd] web-client (simple): Bump @types/node in /client/simple
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.1 to 25.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 20:13:11 +00:00
dependabot[bot]
c46aecd4e3 [upd] web-client (simple): Bump vite in /client/simple
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.0-beta.0 to 8.0.0-beta.2.
- [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.0.0-beta.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.0-beta.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:59:31 +00:00
dependabot[bot]
21bf8a6973 [upd] github-actions: Bump peter-evans/create-pull-request
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.9 to 8.0.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](84ae59a2cd...98357b18bf)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:56:18 +00:00
dependabot[bot]
f5475ba782 [upd] github-actions: Bump JamesIves/github-pages-deploy-action
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.7.4 to 4.7.6.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](4a3abc783e...9d877eea73)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-version: 4.7.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:52:52 +00:00
dependabot[bot]
265f15498c [upd] github-actions: Bump github/codeql-action from 4.31.6 to 4.31.7
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.6 to 4.31.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](fe4161a26a...cf1bb45a27)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:51:40 +00:00
dependabot[bot]
666409ec7e [upd] github-actions: Bump actions/cache from 4.3.0 to 5.0.0
Bumps [actions/cache](https://github.com/actions/cache) from 4.3.0 to 5.0.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](0057852bfa...a783357455)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:51:17 +00:00
Viktor
b719d559b6 [feat] marginalia: switch to the new, improved API version 2025-12-09 18:18:37 +01:00
Austin-Olacsi
9d3ec9a2a2 [feat] pixiv engine: add filter for AI generated images 2025-12-07 23:34:16 +01:00
Ivan Gabaldon
74ec225ad1 [fix] themes: clear search input (#5540)
* [fix] themes: clear search input

Closes https://github.com/searxng/searxng/issues/5539

* [fix] themes: rebuild static
2025-12-07 13:26:01 +00:00
Markus Heiser
b5a1a092f1 [debug] partial revert of 5e0e1c6b3 (#5535)
Issue #5490 was caused by a regression of GH action in v6.0.0, updating to
v6.0.1 [2] fixed our workflow and debug messages are no longer needed.

[1] https://github.com/searxng/searxng/issues/5490#issuecomment-3616230451
[2] https://github.com/searxng/searxng/pull/5530

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-12-06 09:15:56 +01:00
dependabot[bot]
ddc6d68114 [upd] pypi: Bump the minor group with 2 updates (#5527)
Bumps the minor group with 2 updates: [pylint](https://github.com/pylint-dev/pylint) and [basedpyright](https://github.com/detachhead/basedpyright).


Updates `pylint` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v4.0.3...v4.0.4)

Updates `basedpyright` from 1.34.0 to 1.35.0
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.34.0...v1.35.0)
2025-12-05 15:39:35 +01:00
github-actions[bot]
32eb84d6d3 [l10n] update translations from Weblate (#5532) 2025-12-05 15:38:35 +01:00
Ivan Gabaldon
da6c635ea2 [upd] themes: update vite 2025-12-05 11:14:26 +00:00
dependabot[bot]
e34c356e64 [upd] web-client (simple): Bump the minor group
Bumps the minor group in /client/simple with 2 updates: [browserslist](https://github.com/browserslist/browserslist) and [mathjs](https://github.com/josdejong/mathjs).

Updates `browserslist` from 4.28.0 to 4.28.1
- [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.0...4.28.1)

Updates `mathjs` from 15.0.0 to 15.1.0
- [Changelog](https://github.com/josdejong/mathjs/blob/develop/HISTORY.md)
- [Commits](https://github.com/josdejong/mathjs/compare/v15.0.0...v15.1.0)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: mathjs
  dependency-version: 15.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 10:42:18 +00:00
dependabot[bot]
7017393647 [upd] github-actions: Bump actions/setup-node from 6.0.0 to 6.1.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](2028fbc5c2...395ad32622)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 10:26:25 +00:00
dependabot[bot]
aa49f5b933 [upd] github-actions: Bump github/codeql-action from 4.31.5 to 4.31.6
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.5 to 4.31.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](fdbfb4d275...fe4161a26a)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 10:25:37 +00:00
dependabot[bot]
3f91ac47e6 [upd] github-actions: Bump actions/checkout from 6.0.0 to 6.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](1af3b93b68...8e8c483db8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 10:14:41 +00:00
Markus Heiser
8c631b92ce [mod] setup.py package_data - use recursive globs for package_data
To test this patch build a python wheel::

    $ make clean py.build

and llok out if you are missing any files in the wheel::

    $ unzip -l dist/searxng-*-py3-none-any.whl

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-12-04 15:04:36 +01:00
leaty
0ebac144f5 [fix] py: sxng wheel build still broken
Directories chunk and img were not included

Signed-off-by: leaty <dev@leaty.net>
2025-12-04 15:04:36 +01:00
Markus Heiser
5e0e1c6b31 [debug] CI - add debug to trace issue #5490 (#5519)
The error only occurs in the CI action, which is why we need to commit the coded
debug.  As soon as the bug is identified and fixed, this commit can be reverted
/ the ``set -x`` can be removed from the code.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-12-04 14:07:22 +01:00
Markus Heiser
3c7545c6ce [fix] plugin unit-converter - remove leftovers (#5517)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-12-04 07:51:48 +01:00
Austin-Olacsi
aba839195b [fix] findthatmeme: hardening the response against KeyErrors (#5516) 2025-12-04 07:13:05 +01:00
Bnyro
1f6ea41272 [fix] mojeek: first search page is rate-limited
Mojeek blocks all requests where the page offset parameter `s`
is set to 0, hence we may not set it for fetching the first results page.
2025-12-03 17:03:27 +01:00
Ivan Gabaldon
5450d22796 [fix] py: sxng wheel build broken (#5510)
Include everything inside `static/themes/` on SearXNG wheel creation.

```
2025-12-03 10:40:43,509 INFO🛞 adding
'searx/static/themes/simple/manifest.json' 2025-12-03 10:40:43,509
INFO🛞 adding 'searx/static/themes/simple/sxng-core.min.js'
2025-12-03 10:40:43,509 INFO🛞 adding
'searx/static/themes/simple/sxng-core.min.js.map' 2025-12-03
10:40:43,510 INFO🛞 adding
'searx/static/themes/simple/sxng-ltr.min.css' 2025-12-03 10:40:43,510
INFO🛞 adding 'searx/static/themes/simple/sxng-rss.min.css'
2025-12-03 10:40:43,511 INFO🛞 adding
'searx/static/themes/simple/sxng-rtl.min.css' 2025-12-03 10:40:43,511
INFO🛞 adding 'searx/static/themes/simple/chunk/13gvpunf.min.js'
2025-12-03 10:40:43,511 INFO🛞 adding
'searx/static/themes/simple/chunk/13gvpunf.min.js.map' 2025-12-03
10:40:43,516 INFO🛞 adding
'searx/static/themes/simple/chunk/BAcZkB_P.min.js' 2025-12-03
10:40:43,548 INFO🛞 adding
'searx/static/themes/simple/chunk/BAcZkB_P.min.js.map' 2025-12-03
10:40:43,551 INFO🛞 adding
'searx/static/themes/simple/chunk/CHkLfdMs.min.js' 2025-12-03
10:40:43,561 INFO🛞 adding
'searx/static/themes/simple/chunk/CHkLfdMs.min.js.map' 2025-12-03
10:40:43,562 INFO🛞 adding
'searx/static/themes/simple/chunk/CyyZ9XJS.min.js' 2025-12-03
10:40:43,562 INFO🛞 adding
'searx/static/themes/simple/chunk/CyyZ9XJS.min.js.map' 2025-12-03
10:40:43,562 INFO🛞 adding
'searx/static/themes/simple/chunk/DBO1tjH7.min.js' 2025-12-03
10:40:43,562 INFO🛞 adding
'searx/static/themes/simple/chunk/DBO1tjH7.min.js.map' 2025-12-03
10:40:43,562 INFO🛞 adding
'searx/static/themes/simple/chunk/Db5v-hxx.min.js' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/Db5v-hxx.min.js.map' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/DxJxX49r.min.js' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/DxJxX49r.min.js.map' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/KPZlR0ib.min.js' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/KPZlR0ib.min.js.map' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/Q2SRo2ED.min.js' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/Q2SRo2ED.min.js.map' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/gZqIRpW1.min.js' 2025-12-03
10:40:43,563 INFO🛞 adding
'searx/static/themes/simple/chunk/gZqIRpW1.min.js.map' 2025-12-03
10:40:43,564 INFO🛞 adding
'searx/static/themes/simple/img/empty_favicon.svg' 2025-12-03
10:40:43,564 INFO🛞 adding
'searx/static/themes/simple/img/favicon.png' 2025-12-03 10:40:43,564
INFO🛞 adding 'searx/static/themes/simple/img/favicon.svg'
2025-12-03 10:40:43,564 INFO🛞 adding
'searx/static/themes/simple/img/img_load_error.svg' 2025-12-03
10:40:43,564 INFO🛞 adding
'searx/static/themes/simple/img/searxng.png' 2025-12-03 10:40:43,564
INFO🛞 adding 'searx/static/themes/simple/img/searxng.svg'
2025-12-03 10:40:43,564 INFO🛞 adding
'searx/static/themes/simple/img/select-dark.svg' 2025-12-03 10:40:43,564
INFO🛞 adding 'searx/static/themes/simple/img/select-light.svg'
```
2025-12-03 11:21:18 +00:00
Bnyro
1174fde1f3 [feat] engines: add lucide icons (#5503) 2025-12-03 09:57:42 +01:00
Ivan Gabaldon
fb089ae297 [mod] client/simple: client plugins (#5406)
* [mod] client/simple: client plugins

Defines a new interface for client side *"plugins"* that coexist with server
side plugin system. Each plugin (e.g., `InfiniteScroll`) extends the base
`ts Plugin`. Client side plugins are independent and lazy‑loaded via `router.ts`
when their `load()` conditions are met. On each navigation request, all
applicable plugins are instanced.

Since these are client side plugins, we can only invoke them once DOM is fully
loaded. E.g. `Calculator` will not render a new `answer` block until fully
loaded and executed.

For some plugins, we might want to handle its availability in `settings.yml`
and toggle in UI, like we do for server side plugins. In that case, we extend
`py Plugin` instancing only the information and then checking client side if
[`settings.plugins`](1ad832b1dc/client/simple/src/js/toolkit.ts (L134))
array has the plugin id.

* [mod] client/simple: rebuild static
2025-12-02 10:18:00 +00:00
Bnyro
ab8224c939 [fix] brave: content description also contains website URL (#5502)
there are other classes like 'site-name-content' we don't want to match,
however only using contains(@class, 'content') would e.g. also match `site-name-content`
thus, we explicitly also require the spaces as class separator
2025-12-01 15:19:06 +01:00
github-actions[bot]
c954e71f87 [data] update searx.data - update_engine_descriptions.py (#5496) 2025-11-29 16:04:34 +01:00
Ivan Gabaldon
cbc04a839a [fix] py: missing module sniffio 2025-11-29 14:56:30 +00:00
searxng-bot
cb4a5abc8c [data] update searx.data - update_currencies.py 2025-11-29 14:54:09 +00:00
searxng-bot
07ff6e3ccc [data] update searx.data - update_wikidata_units.py 2025-11-29 09:00:05 +00:00
searxng-bot
cdaab944b4 [data] update searx.data - update_firefox_version.py 2025-11-29 08:58:56 +00:00
searxng-bot
6ecf32fd4a [data] update searx.data - update_ahmia_blacklist.py 2025-11-29 08:58:26 +00:00
Markus Heiser
20de10df4e Revert "[fix:py3.14] Struct fields aren't discovered in Python 3.14"
This reverts commit 8fdc59a760.
2025-11-28 13:38:37 +01:00
dependabot[bot]
673c29efeb [upd] pypi: Bump the minor group with 2 updates
Bumps the minor group with 2 updates: [msgspec](https://github.com/jcrist/msgspec) and [types-lxml](https://github.com/abelcheung/types-lxml).


Updates `msgspec` from 0.19.0 to 0.20.0
- [Release notes](https://github.com/jcrist/msgspec/releases)
- [Changelog](https://github.com/jcrist/msgspec/blob/main/docs/changelog.md)
- [Commits](https://github.com/jcrist/msgspec/compare/0.19.0...0.20.0)

Updates `types-lxml` from 2025.8.25 to 2025.11.25
- [Release notes](https://github.com/abelcheung/types-lxml/releases)
- [Commits](https://github.com/abelcheung/types-lxml/compare/2025.08.25...2025.11.25)

---
updated-dependencies:
- dependency-name: msgspec
  dependency-version: 0.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: types-lxml
  dependency-version: 2025.11.25
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-28 13:38:37 +01:00
dependabot[bot]
c4abf40e6e [upd] web-client (simple): Bump the minor group
Bumps the minor group in /client/simple with 3 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome), [sort-package-json](https://github.com/keithamus/sort-package-json) and [stylelint](https://github.com/stylelint/stylelint).

Updates `@biomejs/biome` from 2.3.7 to 2.3.8
- [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.3.8/packages/@biomejs/biome)

Updates `sort-package-json` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/keithamus/sort-package-json/releases)
- [Commits](https://github.com/keithamus/sort-package-json/compare/v3.4.0...v3.5.0)

Updates `stylelint` from 16.25.0 to 16.26.0
- [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/16.25.0...16.26.0)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: sort-package-json
  dependency-version: 3.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: stylelint
  dependency-version: 16.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-28 10:13:02 +00:00
dependabot[bot]
39b9922609 [upd] github-actions: Bump actions/setup-python from 6.0.0 to 6.1.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](e797f83bcb...83679a892e)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-28 09:28:12 +00:00
dependabot[bot]
7018e6583b [upd] github-actions: Bump peter-evans/create-pull-request
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.8 to 7.0.9.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](271a8d0340...84ae59a2cd)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 7.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-28 09:27:25 +00:00
dependabot[bot]
b957e587da [upd] github-actions: Bump github/codeql-action from 4.31.4 to 4.31.5
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.4 to 4.31.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e12f017898...fdbfb4d275)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-28 09:25:18 +00:00
Markus Heiser
ebb9ea4571 [fix] brave engines - web, images & videos (#5478)
brave web:
  xpath selectors needed to be justified

brave images & videos:
  The JS code with the JS object was read incorrectly; not always, but quite
  often, it led to exceptions when the Python data structure was created from it.

BTW: A complete review was conducted and corrections or additions were made to
the type definitions.

To test all brave engines in once::

    !br !brimg !brvid !brnews weather

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-25 13:28:47 +01:00
Markus Heiser
54a97e1043 [mod] replace js_variable_to_python by js_obj_str_to_python (#2792) (#5477)
This patch is based on PR #2792 (old PR from 2023)

- js_obj_str_to_python handle more cases
- bring tests from chompjs ..
- comment out tests do not pass

The tests from chompjs give some overview of what is not implemented.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-25 12:51:08 +01:00
Markus Heiser
0ee78c19dd [mod] yandex engines: all egine should use one network
- The three Yandex engines should use the same network context.
- There is no reason to set these engines inactive

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-25 11:50:18 +01:00
Aadniz
bcc7a5eb2e [mod] yandex engine: add supported languages
Add support for Yandex's supported languages; Russian, English, Belarusian,
French, German, Indonesian, Kazakh, Tatar, Turkish and Ukrainian.
2025-11-25 11:50:18 +01:00
Markus Heiser
2313b972a3 [fix] engines: base URL can be a list or a string, but its not None!
The code injection and monkey patching examine the names in the module of the
engine; if a variable there starts without an underscore and has the value None,
then this variable needs to be configured. This outdated concept does not fit
engines that may have multiple URLs. At least not as long as the value of the
base URL (list) is None.

The default is now an empty list instead of None

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-25 06:25:45 +01:00
Markus Heiser
989b49335c [fix] engines initialization - if engine load fails, set to inactive
- if engine load fails, set the engine to inactive
- dont' load a engine, when the config says its inactive

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-25 06:25:45 +01:00
Markus Heiser
3f30831640 [fix] don't raise fatal exception when engine isn't available
When wikidata or any other engine with a init method (is active!)  raises an
exception in it's init method, the engine is never registered.

[1] https://github.com/searxng/searxng/issues/5456#issuecomment-3567790287

Closes: https://github.com/searxng/searxng/issues/5456
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-25 06:25:45 +01:00
Hermógenes Oliveira
5fcee9bc30 [fix] recoll engine: remove HTML markup from result snippets (#5472)
Recoll inserts markup tags in snippets to indicate matching terms in a
search query.  We remove them so that they don't show to users.
2025-11-24 06:54:45 +01:00
Ivan Gabaldon
2f0e52d6eb [upd] ci: docker secret maintenance
I've narrowed the permissions and rotated the token for the deploy account on
DockerHub registry. I replaced the secret ref in GitHub so that it's available
organization wide. No further actions are necessary.
2025-11-23 12:26:40 +00:00
Grant
c0d69cec4e [fix] drop mullvad-leta engine (#5428)
On 2025 November 27th, Mullvad will be shutting down the Leta servers.
For this reason, we also need to remove this engine from SearXNG.

[1] https://mullvad.net/en/blog/shutting-down-our-search-proxy-leta
2025-11-22 10:02:51 +01:00
Austin-Olacsi
c852b9a90a [feat] engine: add grokipedia (#5396) 2025-11-22 09:59:38 +01:00
Ivan Gabaldon
b876d0bed0 [upd] theme/simple: bump rolldown
(no static changes...)
2025-11-21 11:03:04 +00:00
Léon Tiekötter
e245cade25 [fix] engines: typo (#5466)
Fix typo in engine timeout definition: 'timout' -> 'timeout'
2025-11-21 11:20:10 +01:00
dependabot[bot]
7c223b32a7 [upd] web-client (simple): Bump @biomejs/biome
Bumps the minor group in /client/simple with 1 update: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome).

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 10:15:49 +00:00
dependabot[bot]
33a176813d [upd] github-actions: Bump actions/checkout from 5.0.0 to 6.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...1af3b93b68)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 09:38:50 +00:00
dependabot[bot]
20ec01c5f7 [upd] github-actions: Bump github/codeql-action from 4.31.3 to 4.31.4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.3 to 4.31.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](014f16e7ab...e12f017898)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 09:21:20 +00:00
dependabot[bot]
6376601ba1 [upd] pypi: Bump the minor group with 4 updates (#5462)
Bumps the minor group with 4 updates: [granian](https://github.com/emmett-framework/granian), [granian[pname]](https://github.com/emmett-framework/granian), [granian[reload]](https://github.com/emmett-framework/granian) and [basedpyright](https://github.com/detachhead/basedpyright).


Updates `granian` from 2.5.7 to 2.6.0
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.7...v2.6.0)

Updates `granian[pname]` from 2.5.7 to 2.6.0
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.7...v2.6.0)

Updates `granian[reload]` from 2.5.7 to 2.6.0
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.7...v2.6.0)

Updates `basedpyright` from 1.33.0 to 1.34.0
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.33.0...v1.34.0)
2025-11-21 08:31:16 +01:00
Markus Heiser
ca441f419c [fix] engines - set hard timouts in *sub-request* (#5460)
The requests changed here all run outside of the network context timeout,
thereby preventing the engine's timeout from being applied (the engine's timeout
can become longer than it was configured).

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-21 08:16:24 +01:00
searxng-bot
04e66a2bb4 [l10n] update translations from Weblate 2025-11-20 21:22:43 +00:00
Markus Heiser
b299386d3e [fix] minor type hint issues (#5459)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-20 18:35:43 +01:00
Markus Heiser
21a4622f23 [fix] utils.js_variable_to_python - partial revert of 156d1eb8c (#5458)
The JS string, whose encoding will be corrupted if all single quotes (followed
by a comma) are replaced with double quotes. Bug was introduced in PR #4573.

Here is a simple example in which the list get corrupted::

    >>> s = r"""[ 'foo\'', 'bar']"""
    >>> print(s)
    [ 'foo\'', 'bar']
    >>> print(s.replace("',", "\","))
    [ 'foo\'", 'bar']

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-20 18:32:27 +01:00
Bnyro
041f457dfa [fix] presearch engine: blocked by captcha on every request
Presearch responds with a Cloudflare captcha on each request when using HTTP2.
Using HTTP1.1, everything seems to work fine.

- other engines with the same issue: pixabay, uxwing
- closes https://github.com/searxng/searxng/issues/5438
2025-11-20 13:48:13 +01:00
Hermógenes Oliveira
af111e413c [fix] recoll engine: fix media preview
The results from the recoll engine were not displaying the usual
toggle for showing media previews. After the changes described bellow,
the toggle is displayed and works as expected.

In the JSON returned by recoll-webui, the field containing the
mimetype is actually `mtype`, not `mime`.

Furthermore, according to the documentation for the `File` class in
`searx/result_types/file.py`, `embedded` should contain the URL to the
media itself. The embedding of the media into the page for preview is
done in `searx/templates/simple/result_templates/file.html`.
2025-11-20 13:24:17 +01:00
Ivan Gabaldon
431bf5d235 [mod] docs: add acknowledgements section (#5449)
* [mod] docs: add acknowledgements section

Moves all images into a `assets/` folder.

* [fix] docs: normalize brands svg
2025-11-20 10:31:53 +00:00
Edge-Seven
576c8ca99c [fix] client/simple: docs typo in plg.ts (#5450) 2025-11-18 09:44:41 +00:00
dependabot[bot]
45a4b8ad1c [upd] pypi: Bump the minor group with 3 updates (#5443)
Bumps the minor group with 3 updates: [certifi](https://github.com/certifi/python-certifi), [pylint](https://github.com/pylint-dev/pylint) and [basedpyright](https://github.com/detachhead/basedpyright).


Updates `certifi` from 2025.10.5 to 2025.11.12
- [Commits](https://github.com/certifi/python-certifi/compare/2025.10.05...2025.11.12)

Updates `pylint` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v4.0.2...v4.0.3)

Updates `basedpyright` from 1.32.1 to 1.33.0
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.32.1...v1.33.0)
2025-11-15 09:04:50 +01:00
Austin-Olacsi
d14d695966 [fix] drop alexandria.org (#5446) 2025-11-15 07:38:17 +01:00
dependabot[bot]
a2a47337cb [upd] web-client (simple): Bump the minor group (#5444)
Bumps the minor group in /client/simple with 3 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [browserslist](https://github.com/browserslist/browserslist).

Updates `@biomejs/biome` from 2.3.4 to 2.3.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.3.5/packages/@biomejs/biome)

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

Updates `browserslist` from 4.27.0 to 4.28.0
- [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.27.0...4.28.0)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 24.10.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: browserslist
  dependency-version: 4.28.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>
2025-11-14 21:50:02 +00:00
Bnyro
ba98030438 [feat] engines: add devicons engine
- official website: https://devicon.dev/
- the engine contains a lot of icons of popular software frameworks (e.g. pytest),
so they could for example be useful for visualizing a diagram of the tech stack used in an app
2025-11-14 20:26:43 +01:00
dependabot[bot]
1e200a1107 [upd] github-actions: Bump github/codeql-action from 4.31.2 to 4.31.3 (#5445)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.2 to 4.31.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](0499de31b9...014f16e7ab)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.3
  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>
2025-11-14 08:33:14 +00:00
Austin-Olacsi
7a1b959646 [fix] hackernews contains HTML escape codes 2025-11-10 20:37:01 +01:00
dependabot[bot]
b9b46431be [upd] web-client (simple): Bump the minor group in /client/simple with 4 updates (#5423)
* [upd] web-client (simple): Bump the minor group

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

Updates `ol` from 10.6.1 to 10.7.0
- [Release notes](https://github.com/openlayers/openlayers/releases)
- [Commits](https://github.com/openlayers/openlayers/compare/v10.6.1...v10.7.0)

Updates `@biomejs/biome` from 2.3.2 to 2.3.4
- [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.3.4/packages/@biomejs/biome)

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

Updates `sharp` from 0.34.4 to 0.34.5
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.34.4...v0.34.5)

---
updated-dependencies:
- dependency-name: ol
  dependency-version: 10.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 24.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: sharp
  dependency-version: 0.34.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
...

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

* [upd] web-client (simple): rebuild static

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ivan Gabaldon <igabaldon@inetol.net>
2025-11-07 10:48:05 +01:00
dependabot[bot]
3f18c0f40f [upd] pypi: Bump the minor group with 3 updates (#5422)
Bumps the minor group with 3 updates: [granian](https://github.com/emmett-framework/granian), [granian[pname]](https://github.com/emmett-framework/granian) and [granian[reload]](https://github.com/emmett-framework/granian).


Updates `granian` from 2.5.6 to 2.5.7
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.6...v2.5.7)

Updates `granian[pname]` from 2.5.6 to 2.5.7
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.6...v2.5.7)

Updates `granian[reload]` from 2.5.6 to 2.5.7
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.6...v2.5.7)

---
updated-dependencies:
- dependency-name: granian
  dependency-version: 2.5.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: granian[pname]
  dependency-version: 2.5.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: granian[reload]
  dependency-version: 2.5.7
  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>
2025-11-07 10:46:18 +01:00
dependabot[bot]
1cfbd32a1d [upd] github-actions: Bump JamesIves/github-pages-deploy-action (#5425)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.7.3 to 4.7.4.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](6c2d9db40f...4a3abc783e)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-version: 4.7.4
  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>
2025-11-07 10:01:43 +01:00
dependabot[bot]
a15b594003 [upd] github-actions: Bump docker/setup-qemu-action from 3.6.0 to 3.7.0 (#5424)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](29109295f8...c7c5346462)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 3.7.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>
2025-11-07 09:56:18 +01:00
Aadniz
24d27a7a21 [fix] drop goo engine 2025-11-07 08:34:05 +01:00
Ivan Gabaldon
7af922c9df [enh] py: drop deps (#5407)
The difference between decompression with brotli or gzip in HTML files is
negligible for 3 MB of compiled binary package.

Introduced in eaa694fb7d

Closes https://github.com/searxng/searxng/security/code-scanning/276
Closes https://github.com/searxng/searxng/security/dependabot/37
2025-11-06 10:09:10 +01:00
Aadniz
b1918dd121 [fix] yandex engine: capture captcha from header instead of url path (#5417)
Yandex engine will return parsing error instead of informing that a CAPTCHA was found. It is confusing for the admin and the users (#5415).


This patch fixes an issue where the CAPTCHA response from Yandex wouldn't be detected, resulting in `ParserError` when trying to parse the response to DOM.

In this fix, I replaced the url condition and instead is checking if the `x-yandex-captcha` header is set, and is equal to `captcha`.

Alternatively, maybe something like `resp.headers.get('Location', '').startswith("https://yandex.com/showcaptcha")` could be done instead. Lastly, setting `params['allow_redirects'] = True` can also work, but this will waste an extra request. Just let me know.

Closes: https://github.com/searxng/searxng/issues/5415
2025-11-06 07:00:48 +01:00
Bnyro
1be19f8b58 [feat] sourcehut engine: implement as custom module, fix user agent
SourceHut uses a foss bot protection tool called `go-away` (which I can
recommend BTW).  It blocks common crawler user agents, such as the standard
Firefox user agent.  Hence, we're now using our custom SearXNG user agent to
clarify we're not a crawler.

Closes: https://github.com/searxng/searxng/issues/5270
Co-authored-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-05 17:56:13 +01:00
Bnyro
3763b4bff4 [fix] engine ahmia blacklist, arch linux: use proper searxng user agent including version (#5414) 2025-11-05 09:19:42 +01:00
Aadniz
52ffc4c7f4 [fix] qwant engine: order query parameters to prevent 403 forbidden (#5410) 2025-11-03 22:53:50 +01:00
Markus Heiser
0245327fc5 Revert "[fix] !weather crashes - cls.TURN .. (#5309)"
This reverts HOTFIX from commit fc7d8b8b [1]

[1] https://github.com/searxng/searxng/pull/5309

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-01 09:46:47 +01:00
Markus Heiser
b155e66fe5 [fix] msgspec.Struct: alias name t.ClassVar not properly detected
Reported in [1], HOTFIX in [2], this patch here is now the final solution.

Note that if using PEP 563 postponed evaluation of annotations" (e.g. ``from
__future__ import annotations``) only the following spellings will work:

    ClassVar or ClassVar[<type>]
    typing.ClassVar or typing.ClassVar[<type>]

Importing ClassVar or typing under an aliased name (e.g. ``import typing as t``)
will not be properly detected. [3]

[1] https://github.com/searxng/searxng/issues/5304#issuecomment-3394140820
[2] https://github.com/searxng/searxng/pull/5309
[3] https://jcristharif.com/msgspec/structs.html#class-variables

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-11-01 09:46:47 +01:00
dependabot[bot]
5712827703 [upd] web-client (simple): Bump the minor group (#5402)
Bumps the minor group in /client/simple with 2 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).

Updates `@biomejs/biome` from 2.2.7 to 2.3.2
- [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.3.2/packages/@biomejs/biome)

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

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 24.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
2025-11-01 09:45:54 +01:00
dependabot[bot]
7ba53d302d [upd] pypi: Bump the minor group with 3 updates (#5401)
Bumps the minor group with 3 updates: [granian](https://github.com/emmett-framework/granian), [selenium](https://github.com/SeleniumHQ/Selenium) and [granian[reload]](https://github.com/emmett-framework/granian).


Updates `granian` from 2.5.5 to 2.5.6
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.5...v2.5.6)

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

Updates `granian[reload]` from 2.5.5 to 2.5.6
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.5.5...v2.5.6)

---
updated-dependencies:
- dependency-name: granian
  dependency-version: 2.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: selenium
  dependency-version: 4.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: granian[reload]
  dependency-version: 2.5.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
2025-11-01 09:44:12 +01:00
dependabot[bot]
b8e4ebdc0c [upd] github-actions: Bump github/codeql-action from 4.30.9 to 4.31.2 (#5403)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.9 to 4.31.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](16140ae1a1...0499de31b9)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.2
  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>
2025-10-31 11:31:10 +01:00
github-actions[bot]
b37d09557a [l10n] update translations from Weblate (#5404)
0bdbdde2e - 2025-10-26 - 0ko <0ko@noreply.codeberg.org>
7b0abb9aa - 2025-10-27 - artens <artens@noreply.codeberg.org>
882a28944 - 2025-10-27 - langckx <langckx@noreply.codeberg.org>
c2d025563 - 2025-10-25 - Flyingfufu <flyingfufu@noreply.codeberg.org>
2025-10-31 08:27:05 +01:00
Markus Heiser
aa28af772c [fix] ./manage dev.env - nvm is not installed by nvm.env (#5399)
To complete a SearXNG developer environment, nvm needs to be
installed (ensured).  Without this patch::

    $ LANG=C ./manage dev.env
    ...
    ./utils/lib_nvm.sh: line 27: .nvm/nvm.sh: No such file or directory
    ./utils/lib_nvm.sh: line 28: .nvm/bash_completion: No such file or directory
    ...
    (dev.env)$

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-30 17:46:05 +01:00
Markus Heiser
9c2b8f2f93 [data] update searx.data - update_ahmia_blacklist.py
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-30 07:40:20 +01:00
Markus Heiser
c48993452f [fix] update_ahmia_blacklist.py - User-Agent become required
The User-Agent header recently become required to fetch blacklist from URL

  https://ahmia.fi/blacklist/

[1] https://github.com/searxng/searxng/actions/runs/18892940199/job/53924400294

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-30 07:40:20 +01:00
Markus Heiser
6a2196c03d [fix] simple theme: fix *play* icon in the "show media" button (#5395)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-30 07:39:48 +01:00
github-actions[bot]
dce383881d [data] update searx.data - update_firefox_version.py (#5388) 2025-10-29 07:15:11 +01:00
github-actions[bot]
1ebedcbc17 [data] update searx.data - update_wikidata_units.py (#5389) 2025-10-29 07:14:31 +01:00
github-actions[bot]
5d99877d8d [data] update searx.data - update_currencies.py (#5390)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2025-10-29 07:13:53 +01:00
github-actions[bot]
adc1a2a1ea [data] update searx.data - update_engine_descriptions.py (#5391)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2025-10-29 07:13:19 +01:00
Aadniz
43065c5026 [fix] deviantart engine: pagination match change (#5384)
Pagination currently does not work for deviantart, resulting in the same page
being shown when going to the next page in SearXNG.
2025-10-28 06:21:40 +01:00
Aadniz
ea4a55fa57 [fix] qwant engine: set header Accept-Language to bypass bot detection (#5382)
Set HTTP header Accept-Language [1] for the Qwant engine.

Qwant does not seem to work on any SearXNG instance right now, and this is a fix
for this issue.

During testing, it seems like setting the Accept-Language gives more success for
bypassing bot detection (tested with a few ~20 searches).

[1] https://docs.searxng.org/dev/engines/enginelib.html#searx.enginelib.Engine.send_accept_language_header
2025-10-27 08:33:07 +01:00
Aadniz
d514dea5cc [fix] deviantart engine: does not return any results (#5383) 2025-10-27 08:02:01 +01:00
Aadniz
22e1d30017 [fix] startpage engine: properly display CAPTCHA if redirect page is seen (#5380)
Fixes an issue where startpage engine would display parsing error
(`json.decoder.JSONDecodeError`) when returning CAPTCHA redirect page.

The fix simply checks if response header has `Location` set, and if it starts
with `https://www.startpage.com/sp/captcha`, it will raise a CAPTCHA exception
before trying to parse the data.
2025-10-26 11:32:45 +01:00
Aadniz
4ca75a0450 [fix] engine qwant - return forbidden instead of showing parse error (#5377) 2025-10-25 13:43:37 +02:00
Bnyro
50a4c653dc [build] /static 2025-10-25 10:00:28 +02:00
Bnyro
b7f9b489c9 [fix] search bar: cursor jumps to beginning when clicking text field
Apparently, setting padding on an input field and then clicking that
space created by the padding forces the seekbar cursor to jump to the
beginning of the input field instead of the actual text position.

By removing that padding, the search bar input automatically claims that
height for itself and thus clicking on the blank space moves the cursor to
the correct position.

closes https://github.com/searxng/searxng/issues/5371
2025-10-25 10:00:28 +02:00
dependabot[bot]
2cdbbb249a [upd] web-client (simple): Bump the minor group (#5368)
Bumps the minor group in /client/simple with 3 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [browserslist](https://github.com/browserslist/browserslist).

Updates `@biomejs/biome` from 2.2.6 to 2.2.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.2.7/packages/@biomejs/biome)

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

Updates `browserslist` from 4.26.3 to 4.27.0
- [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.26.3...4.27.0)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 24.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: browserslist
  dependency-version: 4.27.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>
2025-10-24 16:34:09 +02:00
Ivan Gabaldon
edfa71cdea [mod] rebuild static 2025-10-24 12:32:43 +02:00
Ivan Gabaldon
8dacbbbb15 [fix] client/simple: insecure ctx clipboard copy
Uses the deprecated [`execCommand()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand)
to copy content to clipboard if accessing the instance through HTTP, this method
isn't going away soon.

Closes https://github.com/searxng/searxng/issues/5359
2025-10-24 12:32:43 +02:00
dependabot[bot]
b770a46e1f [upd] pypi: Bump the minor group across 1 directory with 5 updates (#5372)
Bumps the minor group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [typer-slim](https://github.com/fastapi/typer) | `0.19.2` | `0.20.0` |
| [typing-extensions](https://github.com/python/typing_extensions) | `4.14.1` | `4.15.0` |
| [pylint](https://github.com/pylint-dev/pylint) | `4.0.1` | `4.0.2` |
| [selenium](https://github.com/SeleniumHQ/Selenium) | `4.36.0` | `4.37.0` |
| [basedpyright](https://github.com/detachhead/basedpyright) | `1.31.7` | `1.32.1` |



Updates `typer-slim` from 0.19.2 to 0.20.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.19.2...0.20.0)

Updates `typing-extensions` from 4.14.1 to 4.15.0
- [Release notes](https://github.com/python/typing_extensions/releases)
- [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/python/typing_extensions/compare/4.14.1...4.15.0)

Updates `pylint` from 4.0.1 to 4.0.2
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v4.0.1...v4.0.2)

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

Updates `basedpyright` from 1.31.7 to 1.32.1
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.31.7...v1.32.1)
2025-10-24 11:11:34 +02:00
github-actions[bot]
2c880f6084 [l10n] update translations from Weblate (#5370)
55c0cab85 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
7705bba80 - 2025-10-21 - Outbreak2096 <outbreak2096@noreply.codeberg.org>
d2ee86058 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
8c4478ca3 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
147ba039a - 2025-10-21 - return42 <return42@noreply.codeberg.org>
2d9a206e8 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
024e2f1c7 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
8059378af - 2025-10-21 - return42 <return42@noreply.codeberg.org>
4b4359eea - 2025-10-21 - return42 <return42@noreply.codeberg.org>
05af879c9 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
0ea9d6393 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
e2e0907ff - 2025-10-21 - return42 <return42@noreply.codeberg.org>
9a7cfc1c1 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
06b7d62f0 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
a3bc054a5 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
34e56b171 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
8cc444358 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
55afa16d1 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
a336dd1ae - 2025-10-21 - return42 <return42@noreply.codeberg.org>
ec68a405a - 2025-10-21 - return42 <return42@noreply.codeberg.org>
beeab8c25 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
44a5c9e04 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
aef218710 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
42923cf46 - 2025-10-21 - Priit Jõerüüt <jrtcdbrg@noreply.codeberg.org>
3cab50a73 - 2025-10-22 - jperegrinm <jperegrinm@noreply.codeberg.org>
410e760d5 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
bb5e921c3 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
eece61f04 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
1f18156d5 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
20026535d - 2025-10-21 - return42 <return42@noreply.codeberg.org>
fcc563bf8 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
ec02a81da - 2025-10-21 - return42 <return42@noreply.codeberg.org>
78125c9e6 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
7a4b89369 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
d02406831 - 2025-10-21 - return42 <return42@noreply.codeberg.org>
8fe4022cb - 2025-10-21 - return42 <return42@noreply.codeberg.org>
0e8cdcaa8 - 2025-10-20 - SomeTr <sometr@noreply.codeberg.org>
4b138b0dc - 2025-10-20 - Juno Takano <jutty@noreply.codeberg.org>
d20e2c9c1 - 2025-10-20 - ghose <ghose@noreply.codeberg.org>
2025-10-24 10:34:09 +02:00
dependabot[bot]
c41b769f97 [upd] github-actions: Bump github/codeql-action from 4.30.8 to 4.30.9 (#5369)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.8 to 4.30.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](f443b600d9...16140ae1a1)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.30.9
  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>
2025-10-24 10:04:49 +02:00
Markus Heiser
e363db970c [fix] Installation Script install fails (msgspec) (#5358)
Since #5280 has been merged, msgspec, like yaml, is a fixed part of the SearXNG
*settings framework* and therefore, like yaml, must be installed in the virtual
environment before installing SearXNG (``searx``).

The actual reason is that in SearXNG we store settings in the configuration that
are required for the installation of the ``searx`` package.  This means that
these settings (from settings.yml) are read in during the installation, and all the
necessary tools for this (pyyaml, msgspec, setuptools, etc.) must be installed
beforehand (chicken or the egg dilemma).

Related:

- https://github.com/searxng/searxng/pull/5353
- https://github.com/searxng/searxng/pull/5346
- https://github.com/searxng/searxng/pull/5280
- https://github.com/searxng/searxng/pull/5254

Closes: https://github.com/searxng/searxng/issues/5357

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-23 09:27:14 +02:00
Ivan Gabaldon
16293132e3 [mod] ci: use custom static podman (#5354)
We only need updated podman on `build`. `test` and `release` can use image
provided container engine binaries.
2025-10-22 14:38:59 +02:00
Markus Heiser
f70120b0b9 [fix] Installation Script install fails (msgspec) (#5353)
Since #5280 has been merged, msgspec, like yaml, is a fixed part of the SearXNG
*settings framework* and therefore, like yaml, must be installed in the virtual
environment before installing SearXNG (``searx``).

The actual reason is that in SearXNG we store settings in the configuration that
are required for the installation of the ``searx`` package.  This means that
these settings (from settings.yml) are read in during the installation, and all the
necessary tools for this (pyyaml, msgspec, setuptools, etc.) must be installed
beforehand (chicken or the egg dilemma).

Related:

- https://github.com/searxng/searxng/pull/5346
- https://github.com/searxng/searxng/pull/5280
- https://github.com/searxng/searxng/pull/5254

Closes: https://github.com/searxng/searxng/issues/5352
2025-10-22 09:34:09 +02:00
Ivan Gabaldon
a8f3644cdc [upd] themes/simple: Bump rolldown-vite from 7.1.17 to 7.1.19 (#5351)
This security update does nothing but creating a new commit to build a container
with the new `base` image.
2025-10-21 20:16:19 +02:00
Markus Heiser
4295e758c0 [fix] Installation Script install fails (msgspec) (#5346)
Since #5280 has been merged, msgspec, like yaml, is a fixed part of the SearXNG
*settings framework* and therefore, like yaml, must be installed in the virtual
environment before installing SearXNG (``searx``).

The actual reason is that in SearXNG we store settings in the configuration that
are required for the installation of the ``searx`` package.  This means that
these settings (from settings.yml) are read in during the installation, and all the
necessary tools for this (pyyaml, msgspec, setuptools, etc.) must be installed
beforehand (chicken or the egg dilemma).

Related:

- https://github.com/searxng/searxng/pull/5280
- https://github.com/searxng/searxng/pull/5254

Closes: https://github.com/searxng/searxng/issues/5343

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-20 11:21:48 +02:00
Markus Heiser
33e798b01b [fix] TrackerPatternsDB.clean_url: don't delete query argument from new_url (#5339)
The query argument for URLs like:

- 'http://example.org?q='       --> query_str is 'q='
- 'http://example.org?/foo/bar' --> query_str is 'foo/bar'

is a *simple string* and not a key/value dict.  This string may only be removed
from the URL if one of the patterns matches.

BTW get_pretty_url(): keep such a *simple string* in the path element.

Closes: https://github.com/searxng/searxng/issues/5299

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-20 11:20:33 +02:00
Markus Heiser
d84ae96cf9 [build] /static 2025-10-20 10:18:33 +02:00
Markus Heiser
9371658531 [mod] typification of SearXNG: add new result type File
This PR adds a new result type: File

    Python class: searx/result_types/file.py
    Jinja template: searx/templates/simple/result_templates/file.html
    CSS (less) client/simple/src/less/result_types/file.less

Class 'File' (singular) replaces template 'files.html' (plural).  The renaming
was carried out because there is only one file (singular) in a result. Not to be
confused with the category 'files' where in multiple results can exist.

As mentioned in issue [1], the class '.category-files' was removed from the CSS
and the stylesheet was adopted in result_types/file.less (there based on the
templates and no longer based on the category).

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

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-20 10:18:33 +02:00
Markus Heiser
ee6d4f322f [mod] engine: reuters - REST-API for Reuter's thumbnail, height:80
The size of the full-size images from ``thumbnail.url`` is usually several
MB. By reducing the full-size image to 80 pixels, the data size for a thumb is
reduced from MB to a few KB.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-18 14:43:35 +02:00
Bnyro
3725aef6f3 [fix] reuters: crash on empty results pages & date parsing
1. On empty result list, return empty EngineResults (#5330)

2. Use ``dateutil.parser`` to avoid ``ValueError``:

    ERROR   searx.engines.reuters : exception : Invalid isoformat string: '2022-06-08T16:07:54Z'
      File "searx/engines/reuters.py", line 91, in response
        publishedDate=datetime.fromisoformat(result["display_time"]),
    ValueError: Invalid isoformat string: '2022-06-08T16:07:54Z'

Closes: https://github.com/searxng/searxng/issues/5330
Co-authored-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-18 14:43:35 +02:00
Markus Heiser
e840e3f960 [fix] engine mullvadleta - ignore HTTP 403 & 429 response
It doesn't matter if you're using Mullvad's VPN and a proper browser, you'll
still get blocked for specific searches [1] with a 403 or 429 HTTP status code.
Mullvad only blocks the search request and doesn't prevent you from doing more
searches.

The logic should handle the blocked requests (403, 429), but not put the engine
on a cooldown.

[1] https://leta.mullvad.net/search?q=site%3Afoo+bar&engine=brave

Closes: https://github.com/searxng/searxng/issues/5328
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-18 09:05:54 +02:00
Bnyro
a6bb1ecf87 [build] /static 2025-10-17 15:59:53 +02:00
Bnyro
636738779e [feat] video results: display video length on video thumbnail 2025-10-17 15:59:53 +02:00
Bnyro
1d138c5968 [mod] bing engine: follow redirects (#5324)
Apparently, in China, Bing redirects from `www.bing.com` to `cn.bing.com`.
So in order to make Bing work for chinese users by default, we have to follow that redirect.

related: https://github.com/searxng/searxng/issues/5243
2025-10-17 15:43:49 +02:00
Markus Heiser
3e7e404fda [fix] issues reported by Pylint 4.0
Last major update of Pylint brings some breaking changes [1], fixed in this
patch.

[1] https://pylint.readthedocs.io/en/latest/whatsnew/4/4.0/index.html#breaking-changes
2025-10-17 15:42:22 +02:00
dependabot[bot]
602a73df9a [upd] pypi: Bump pylint from 3.3.9 to 4.0.1
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.9 to 4.0.1.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.9...v4.0.1)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 4.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-17 15:42:22 +02:00
dependabot[bot]
57622793bf [upd] web-client (simple): Bump the minor group in /client/simple (#5333)
* [upd] web-client (simple): Bump the minor group

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

Updates `@biomejs/biome` from 2.2.5 to 2.2.6
- [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.2.6/packages/@biomejs/biome)

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

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

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

* [build] /static

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ivan Gabaldon <igabaldon@inetol.net>
2025-10-17 10:29:16 +02:00
dependabot[bot]
080f3a5f87 [upd] github-actions: Bump actions/setup-node from 5.0.0 to 6.0.0 (#5334)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](a0853c2454...2028fbc5c2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 10:03:07 +02:00
dependabot[bot]
f54cf643b2 [upd] github-actions: Bump github/codeql-action from 4.30.7 to 4.30.8 (#5335)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.7 to 4.30.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e296a93559...f443b600d9)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.30.8
  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>
2025-10-17 10:02:59 +02:00
dependabot[bot]
dd82d785ce [upd] pypi: Bump basedpyright from 1.31.6 to 1.31.7 in the minor group (#5331)
Bumps the minor group with 1 update: [basedpyright](https://github.com/detachhead/basedpyright).

Updates `basedpyright` from 1.31.6 to 1.31.7
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.31.6...v1.31.7)
2025-10-17 09:43:36 +02:00
github-actions[bot]
f6cdd16449 [l10n] update translations from Weblate (#5336)
e23460caa - 2025-10-13 - aindriu80 <aindriu80@noreply.codeberg.org>
bb7d1cc0e - 2025-10-13 - Juno Takano <jutty@noreply.codeberg.org>
8b8bc1461 - 2025-10-13 - Priit Jõerüüt <jrtcdbrg@noreply.codeberg.org>
f4bec8c6a - 2025-10-13 - Raithlin <raithlin@noreply.codeberg.org>
609efd4e6 - 2025-10-13 - AndersNordh <andersnordh@noreply.codeberg.org>
6c709f898 - 2025-10-13 - return42 <return42@noreply.codeberg.org>
a2b608da4 - 2025-10-13 - AndersNordh <andersnordh@noreply.codeberg.org>
4f0cd2119 - 2025-10-12 - kratos <makesocialfoss32@keemail.me>
8d049e1cb - 2025-10-11 - Outbreak2096 <outbreak2096@noreply.codeberg.org>
4bf5fc5fe - 2025-10-11 - Linerly <linerly@noreply.codeberg.org>
c80cf6e92 - 2025-10-11 - ghose <ghose@noreply.codeberg.org>
92427655d - 2025-10-11 - Fjuro <fjuro@alius.cz>
8efe1bb12 - 2025-10-10 - SomeTr <sometr@noreply.codeberg.org>
2025-10-17 09:41:21 +02:00
benpiano800
576d30ffcd [chore] theme_args.simple_style - mention the black theme style in settings.yml (#5325) 2025-10-15 09:16:19 +02:00
Tommaso Colella
c34bb61284 [feat] engines: add Azure resources engine (#5235)
Adds a new engine `searx/engines/azure.py` to search cloud resources on Azure.

A lot of enterprise users have to deal with Azure Public Cloud.  This helps them
easily search for cloud resources without logging in to the Portal first

How to test this PR locally?

You should create an App Registration on Azure Entra Id with Reader access on
the resources you want to search for.  You should create a Secret for the App
Registration.  After that, you should set up appropriate values in the
`settings.yml` file [1]::

   - name: azure
     engine: azure
     ...
     azure_tenant_id: "your_tenant_id"
     azure_client_id: "your_client_id"
     azure_client_secret: "your_client_secret"
     azure_token_expiration_seconds: 5000

[1] https://github.com/searxng/searxng/pull/5235#issuecomment-3397664928

Co-authored-by: Bnyro <bnyro@tutanota.com>
Co-authored-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-13 16:33:08 +02:00
Bnyro
8baefcc21e [fix] pinterest: crash when there's no link & show image resolution + uploader name (#5314)
closes #5231
2025-10-13 07:43:36 +02:00
Markus Heiser
fc7d8b8be2 [fix] !weather crashes - cls.TURN 'member_descriptor' isn't a float (#5309)
The class method ``Compass.point`` is converted into an instance method to
circumvent the problem described in [1] (without understanding the cause).

[1] https://github.com/searxng/searxng/issues/5304#issuecomment-3394140820

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-13 07:37:42 +02:00
Ivan Gabaldon
5492de15bb [mod] container: move base to own repository (#5310)
The base images will be now built in
[another repository](https://github.com/searxng/base).
2025-10-12 16:30:57 +02:00
Ivan Gabaldon
ced08e12aa [enh] ci: bump to cp3.14 (#5302) 2025-10-11 16:59:40 +02:00
Mehmet Sait Cubukcu
613c1aa8eb docs: remove unsupported --replace flag from Docker command (#5288) 2025-10-10 20:31:48 +02:00
Bnyro
899cf7e08a [build] /static 2025-10-10 18:08:33 +02:00
Bnyro
362cc13aeb [feat] preferences hash: show applied settings in pref page when searching with 'search url of the currently saved preferences'
Previously, when using a search url copied from the cookies tab, clicking
at the settings icon at the top right would show the browser preferences
and not the preferences that were set and used with the search url.
Please see https://github.com/searxng/searxng/issues/5227 for more information.

To test:
- change some preferences
- copy the preferences search url in the settings' cookies tab
- reset the preferences or clear cookies
- paste the copied search url into the search bar to search for something
- press the settings icon
- you can now see/preview the actual settings that were used for the search
- by pressing 'save', you can keep these preferences

closes #5227
2025-10-10 18:08:33 +02:00
Bnyro
d28a1c434f [fix] no results error dialog: link to preferences doesn't work if searxng is hosted as subdirectory 2025-10-10 18:05:54 +02:00
Markus Heiser
21d0428cf2 [mod] brand - partial migration of settings to msgspec.Struct (#5280)
The settings are currently an untyped key/value structure, whose types are
dynamically built at runtime.  The construction process of this structure
is *hand-crafted*.

In the long term, we want a static typing of this structure, based on a standard
tool.  The ``msgspec.Struct`` structures are suitable as a standard tool.

This patch makes a first step towards static typing and implements the "brand"
section using ``msgspec.Struct`` structures.

BTW: searx/settings_defaults.py - ``git_url`` and ``git_branch`` had been
removed in aee613d256, this is a leftover.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-10 16:14:29 +02:00
github-actions[bot]
f0dfe3cc0e [l10n] update translations from Weblate (#5296)
bb1f7a851 - 2025-10-04 - 0ko <0ko@noreply.codeberg.org>
2025-10-10 11:27:35 +02:00
dependabot[bot]
0559b9bfcf [upd] web-client (simple): bump dependencies (#5294)
* [upd] web-client (simple): bump dependencies

* [build] /static

---------

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ivan Gabaldon <igabaldon@inetol.net>
2025-10-10 11:14:54 +02:00
dependabot[bot]
37f7960266 [upd] github-actions: Bump github/codeql-action from 3.30.6 to 4.30.7 (#5295)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.6 to 4.30.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](64d10c1313...e296a93559)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.30.7
  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>
2025-10-10 10:55:50 +02:00
326 changed files with 14573 additions and 9809 deletions

View File

@@ -15,7 +15,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.13" PYTHON_VERSION: "3.14"
jobs: jobs:
search: search:
@@ -24,17 +24,17 @@ jobs:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-" restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"

View File

@@ -18,106 +18,34 @@ concurrency:
permissions: permissions:
contents: read contents: read
# Organization GHCR
packages: read packages: read
env: env:
PYTHON_VERSION: "3.13" PYTHON_VERSION: "3.14"
jobs: jobs:
build-base:
if: |
(github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success')
|| github.event_name == 'workflow_dispatch'
name: Build base
runs-on: ubuntu-24.04
permissions:
# Organization GHCR
packages: write
steps:
- if: github.repository_owner == 'searxng'
name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: "false"
- if: github.repository_owner == 'searxng'
name: Get date
id: date
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
- if: github.repository_owner == 'searxng'
name: Check cache apko
id: cache-apko
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# yamllint disable-line rule:line-length
key: "apko-${{ steps.date.outputs.date }}-${{ hashFiles('./container/base.yml', './container/base-builder.yml') }}"
path: "/tmp/.apko/"
lookup-only: true
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Setup cache apko
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# yamllint disable-line rule:line-length
key: "apko-${{ steps.date.outputs.date }}-${{ hashFiles('./container/base.yml', './container/base-builder.yml') }}"
restore-keys: "apko-${{ steps.date.outputs.date }}-"
path: "/tmp/.apko/"
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Setup apko
run: |
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install apko
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: "ghcr.io"
username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}"
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Build
run: |
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
apko publish ./container/base.yml ghcr.io/${{ github.repository_owner }}/base:searxng \
--cache-dir=/tmp/.apko/ \
--sbom=false \
--vcs=false \
--log-level=debug
apko publish ./container/base-builder.yml ghcr.io/${{ github.repository_owner }}/base:searxng-builder \
--cache-dir=/tmp/.apko/ \
--sbom=false \
--vcs=false \
--log-level=debug
build: build:
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch' if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
name: Build (${{ matrix.arch }}) name: Build (${{ matrix.arch }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: build-base
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- arch: amd64 - arch: amd64
march: amd64
os: ubuntu-24.04 os: ubuntu-24.04
emulation: false emulation: false
- arch: arm64 - arch: arm64
march: arm64
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
emulation: false emulation: false
- arch: armv7 - arch: armv7
march: arm64
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
emulation: true emulation: true
permissions: permissions:
# Organization GHCR
packages: write packages: write
outputs: outputs:
@@ -125,34 +53,64 @@ jobs:
git_url: ${{ steps.build.outputs.git_url }} git_url: ${{ steps.build.outputs.git_url }}
steps: steps:
# yamllint disable rule:line-length
- name: Setup podman
env:
PODMAN_VERSION: "v5.6.2"
run: |
# dpkg man-db trigger is very slow on GHA runners
# https://github.com/actions/runner-images/issues/10977
# https://github.com/actions/runner/issues/4030
sudo rm -f /var/lib/man-db/auto-update
sudo apt-get purge -y podman runc crun conmon
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz"
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz.asc"
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 0CCF102C4F95D89E583FF1D4F8B5AF50344BB503
gpg --batch --verify "podman-linux-${{ matrix.march }}.tar.gz.asc" "podman-linux-${{ matrix.march }}.tar.gz"
tar -xzf "podman-linux-${{ matrix.march }}.tar.gz"
sudo cp -rfv ./podman-linux-${{ matrix.march }}/etc/. /etc/
sudo cp -rfv ./podman-linux-${{ matrix.march }}/usr/. /usr/
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
# yamllint enable rule:line-length
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-" restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
path: "./local/" path: "./local/"
- name: Setup cache container uv - name: Get date
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 id: date
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
- name: Setup cache container
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "container-uv-${{ matrix.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "container-uv-${{ matrix.arch }}-" restore-keys: |
path: "/var/tmp/buildah-cache-1001/uv/" "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-"
"container-${{ matrix.arch }}-"
path: "/var/tmp/buildah-cache-*/*"
- if: ${{ matrix.emulation }} - if: ${{ matrix.emulation }}
name: Setup QEMU name: Setup QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -187,13 +145,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
- if: ${{ matrix.emulation }} - if: ${{ matrix.emulation }}
name: Setup QEMU name: Setup QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -217,12 +175,11 @@ jobs:
- test - test
permissions: permissions:
# Organization GHCR
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
@@ -237,8 +194,8 @@ jobs:
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: "docker.io" registry: "docker.io"
username: "${{ secrets.DOCKERHUB_USERNAME }}" username: "${{ secrets.DOCKER_USER }}"
password: "${{ secrets.DOCKERHUB_TOKEN }}" password: "${{ secrets.DOCKER_TOKEN }}"
- name: Release - name: Release
env: env:

View File

@@ -15,7 +15,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.13" PYTHON_VERSION: "3.14"
jobs: jobs:
data: data:
@@ -40,17 +40,17 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-" restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
@@ -64,7 +64,7 @@ jobs:
- name: Create PR - name: Create PR
id: cpr id: cpr
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with: with:
author: "searxng-bot <searxng-bot@users.noreply.github.com>" author: "searxng-bot <searxng-bot@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>" committer: "searxng-bot <searxng-bot@users.noreply.github.com>"

View File

@@ -19,7 +19,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.13" PYTHON_VERSION: "3.14"
jobs: jobs:
release: release:
@@ -32,18 +32,18 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-" restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
@@ -57,7 +57,7 @@ jobs:
- if: github.ref_name == 'master' - if: github.ref_name == 'master'
name: Release name: Release
uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3 uses: JamesIves/github-pages-deploy-action@9d877eea73427180ae43cf98e8914934fe157a1a # v4.7.6
with: with:
folder: "dist/docs" folder: "dist/docs"
branch: "gh-pages" branch: "gh-pages"

View File

@@ -18,7 +18,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.13" PYTHON_VERSION: "3.14"
jobs: jobs:
test: test:
@@ -35,17 +35,17 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ matrix.python-version }}-${{ runner.arch }}-" restore-keys: "python-${{ matrix.python-version }}-${{ runner.arch }}-"
@@ -62,28 +62,28 @@ jobs:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: "./.nvmrc" node-version-file: "./.nvmrc"
- name: Setup cache Node.js - name: Setup cache Node.js
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}" key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
path: "./client/simple/node_modules/" path: "./client/simple/node_modules/"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-" restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"

View File

@@ -22,7 +22,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.13" PYTHON_VERSION: "3.14"
jobs: jobs:
update: update:
@@ -35,18 +35,18 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-" restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
@@ -82,18 +82,18 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-" restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
@@ -117,7 +117,7 @@ jobs:
- name: Create PR - name: Create PR
id: cpr id: cpr
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with: with:
author: "searxng-bot <searxng-bot@users.noreply.github.com>" author: "searxng-bot <searxng-bot@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>" committer: "searxng-bot <searxng-bot@users.noreply.github.com>"

View File

@@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: "false" persist-credentials: "false"
@@ -32,8 +32,8 @@ jobs:
uses: docker/scout-action@f8c776824083494ab0d56b8105ba2ca85c86e4de # v1.18.2 uses: docker/scout-action@f8c776824083494ab0d56b8105ba2ca85c86e4de # v1.18.2
with: with:
organization: "searxng" organization: "searxng"
dockerhub-user: "${{ secrets.DOCKERHUB_USERNAME }}" dockerhub-user: "${{ secrets.DOCKER_USER }}"
dockerhub-password: "${{ secrets.DOCKERHUB_TOKEN }}" dockerhub-password: "${{ secrets.DOCKER_TOKEN }}"
image: "registry://ghcr.io/searxng/searxng:latest" image: "registry://ghcr.io/searxng/searxng:latest"
command: "cves" command: "cves"
sarif-file: "./scout.sarif" sarif-file: "./scout.sarif"
@@ -41,6 +41,6 @@ jobs:
write-comment: "false" write-comment: "false"
- name: Upload SARIFs - name: Upload SARIFs
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with: with:
sarif_file: "./scout.sarif" sarif_file: "./scout.sarif"

2
.nvmrc
View File

@@ -1 +1 @@
24 25

View File

@@ -162,7 +162,7 @@ no-docstring-rgx=^_
property-classes=abc.abstractproperty property-classes=abc.abstractproperty
# Regular expression matching correct variable names # Regular expression matching correct variable names
variable-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*)|([a-z]))$ variable-rgx=([a-zA-Z0-9_]*)$
[FORMAT] [FORMAT]

View File

@@ -1,8 +1,8 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"files": { "files": {
"ignoreUnknown": true, "ignoreUnknown": true,
"includes": ["**", "!dist", "!node_modules"] "includes": ["**", "!node_modules"]
}, },
"assist": { "assist": {
"enabled": true, "enabled": true,
@@ -15,9 +15,9 @@
} }
}, },
"formatter": { "formatter": {
"enabled": true,
"bracketSameLine": false, "bracketSameLine": false,
"bracketSpacing": true, "bracketSpacing": true,
"enabled": true,
"formatWithErrors": false, "formatWithErrors": false,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 2, "indentWidth": 2,
@@ -35,24 +35,26 @@
}, },
"correctness": { "correctness": {
"noGlobalDirnameFilename": "error", "noGlobalDirnameFilename": "error",
"noUndeclaredVariables": {
"level": "error",
"options": {
"checkTypes": true
}
},
"useImportExtensions": "error", "useImportExtensions": "error",
"useJsonImportAttributes": "error", "useJsonImportAttributes": "error",
"useSingleJsDocAsterisk": "error" "useSingleJsDocAsterisk": "error"
}, },
"nursery": { "nursery": {
"noContinue": "warn",
"noDeprecatedImports": "warn", "noDeprecatedImports": "warn",
"noEqualsToNull": "warn",
"noFloatingPromises": "warn",
"noForIn": "warn",
"noImportCycles": "warn", "noImportCycles": "warn",
"noIncrementDecrement": "warn",
"noMisusedPromises": "warn", "noMisusedPromises": "warn",
"noMultiStr": "warn",
"noParametersOnlyUsedInRecursion": "warn",
"noUselessCatchBinding": "warn", "noUselessCatchBinding": "warn",
"noUselessUndefined": "warn", "noUselessUndefined": "warn",
"useExhaustiveSwitchCases": "warn", "useExhaustiveSwitchCases": "warn",
"useExplicitType": "warn" "useExplicitType": "warn",
"useFind": "warn"
}, },
"performance": { "performance": {
"noAwaitInLoops": "error", "noAwaitInLoops": "error",
@@ -65,6 +67,7 @@
"style": { "style": {
"noCommonJs": "error", "noCommonJs": "error",
"noEnum": "error", "noEnum": "error",
"noImplicitBoolean": "error",
"noInferrableTypes": "error", "noInferrableTypes": "error",
"noNamespace": "error", "noNamespace": "error",
"noNegationElse": "error", "noNegationElse": "error",
@@ -109,6 +112,12 @@
"syntax": "explicit" "syntax": "explicit"
} }
}, },
"useConsistentTypeDefinitions": {
"level": "error",
"options": {
"style": "type"
}
},
"useDefaultSwitchClause": "error", "useDefaultSwitchClause": "error",
"useExplicitLengthCheck": "error", "useExplicitLengthCheck": "error",
"useForOf": "error", "useForOf": "error",
@@ -117,6 +126,7 @@
"useNumericSeparators": "error", "useNumericSeparators": "error",
"useObjectSpread": "error", "useObjectSpread": "error",
"useReadonlyClassProperties": "error", "useReadonlyClassProperties": "error",
"useSelfClosingElements": "error",
"useShorthandAssign": "error", "useShorthandAssign": "error",
"useSingleVarDeclarator": "error", "useSingleVarDeclarator": "error",
"useThrowNewError": "error", "useThrowNewError": "error",

File diff suppressed because it is too large Load Diff

View File

@@ -19,33 +19,31 @@
"lint:tsc": "tsc --noEmit" "lint:tsc": "tsc --noEmit"
}, },
"browserslist": [ "browserslist": [
"Chrome >= 93", "baseline 2022",
"Firefox >= 92",
"Safari >= 15.4",
"not dead" "not dead"
], ],
"dependencies": { "dependencies": {
"ionicons": "~8.0.0", "ionicons": "~8.0.13",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"ol": "~10.6.0", "ol": "~10.7.0",
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.2.5", "@biomejs/biome": "2.3.8",
"@types/node": "~24.6.2", "@types/node": "~25.0.1",
"browserslist": "~4.26.3", "browserslist": "~4.28.1",
"browserslist-to-esbuild": "~2.1.0", "browserslist-to-esbuild": "~2.1.1",
"edge.js": "~6.3.0", "edge.js": "~6.3.0",
"less": "~4.4.1", "less": "~4.4.2",
"lightningcss": "~1.30.2", "mathjs": "~15.1.0",
"sharp": "~0.34.4", "sharp": "~0.34.5",
"sort-package-json": "~3.4.0", "sort-package-json": "~3.5.1",
"stylelint": "~16.24.0", "stylelint": "~16.26.0",
"stylelint-config-standard-less": "~3.0.0", "stylelint-config-standard-less": "~3.0.1",
"stylelint-prettier": "~5.0.0", "stylelint-prettier": "~5.0.3",
"svgo": "~4.0.0", "svgo": "~4.0.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.1.15", "vite": "8.0.0-beta.2",
"vite-bundle-analyzer": "~1.2.3" "vite-bundle-analyzer": "~1.3.1"
} }
} }

View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* Base class for client-side plugins.
*
* @remarks
* Handle conditional loading of the plugin in:
*
* - client/simple/src/js/router.ts
*
* @abstract
*/
export abstract class Plugin {
/**
* Plugin name.
*/
protected readonly id: string;
/**
* @remarks
* Don't hold references of this instance outside the class.
*/
protected constructor(id: string) {
this.id = id;
void this.invoke();
}
private async invoke(): Promise<void> {
try {
console.debug(`[PLUGIN] ${this.id}: Running...`);
const result = await this.run();
if (!result) return;
console.debug(`[PLUGIN] ${this.id}: Running post-exec...`);
// @ts-expect-error
void (await this.post(result as NonNullable<Awaited<ReturnType<this["run"]>>>));
} catch (error) {
console.error(`[PLUGIN] ${this.id}:`, error);
} finally {
console.debug(`[PLUGIN] ${this.id}: Done.`);
}
}
/**
* Plugin goes here.
*
* @remarks
* The plugin is already loaded at this point. If you wish to execute
* conditions to exit early, consider moving the logic to:
*
* - client/simple/src/js/router.ts
*
* ...to avoid unnecessarily loading this plugin on the client.
*/
protected abstract run(): Promise<unknown>;
/**
* Post-execution hook.
*
* @remarks
* The hook is only executed if `#run()` returns a truthy value.
*/
// @ts-expect-error
protected abstract post(result: NonNullable<Awaited<ReturnType<this["run"]>>>): Promise<void>;
}

View File

@@ -1,6 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import "./nojs.ts";
import "./router.ts";
import "./toolkit.ts";
import "./listener.ts";

View File

@@ -1,7 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { listen } from "./toolkit.ts";
listen("click", ".close", function (this: HTMLElement) {
(this.parentNode as HTMLElement)?.classList.add("invisible");
});

View File

@@ -1,8 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { ready } from "./toolkit.ts";
ready(() => {
document.documentElement.classList.remove("no-js");
document.documentElement.classList.add("js");
});

View File

@@ -1,40 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { Endpoints, endpoint, ready, settings } from "./toolkit.ts";
ready(
() => {
import("../main/keyboard.ts");
import("../main/search.ts");
if (settings.autocomplete) {
import("../main/autocomplete.ts");
}
},
{ on: [endpoint === Endpoints.index] }
);
ready(
() => {
import("../main/keyboard.ts");
import("../main/mapresult.ts");
import("../main/results.ts");
import("../main/search.ts");
if (settings.infinite_scroll) {
import("../main/infinite_scroll.ts");
}
if (settings.autocomplete) {
import("../main/autocomplete.ts");
}
},
{ on: [endpoint === Endpoints.results] }
);
ready(
() => {
import("../main/preferences.ts");
},
{ on: [endpoint === Endpoints.preferences] }
);

View File

@@ -0,0 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// core
void import.meta.glob(["./*.ts", "./util/**/.ts"], { eager: true });

View File

@@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type { Plugin } from "./Plugin.ts";
import { type EndpointsKeys, endpoint } from "./toolkit.ts";
type Options =
| {
on: "global";
}
| {
on: "endpoint";
where: EndpointsKeys[];
};
export const load = <T extends Plugin>(instance: () => Promise<T>, options: Options): void => {
if (!check(options)) return;
void instance();
};
const check = (options: Options): boolean => {
// biome-ignore lint/style/useDefaultSwitchClause: options is typed
switch (options.on) {
case "global": {
return true;
}
case "endpoint": {
if (!options.where.includes(endpoint)) {
// not on the expected endpoint
return false;
}
return true;
}
}
};

View File

@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { assertElement, http, listen, settings } from "../core/toolkit.ts"; import { http, listen, settings } from "../toolkit.ts";
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 {

View File

@@ -1,100 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { assertElement, http, settings } from "../core/toolkit.ts";
const newLoadSpinner = (): HTMLDivElement => {
return Object.assign(document.createElement("div"), {
className: "loader"
});
};
const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<void> => {
const searchForm = document.querySelector<HTMLFormElement>("#search");
assertElement(searchForm);
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
assertElement(form);
const action = searchForm.getAttribute("action");
if (!action) {
throw new Error("Form action not defined");
}
const paginationElement = document.querySelector<HTMLElement>("#pagination");
assertElement(paginationElement);
paginationElement.replaceChildren(newLoadSpinner());
try {
const res = await http("POST", action, { body: new FormData(form) });
const nextPage = await res.text();
if (!nextPage) return;
const nextPageDoc = new DOMParser().parseFromString(nextPage, "text/html");
const articleList = nextPageDoc.querySelectorAll<HTMLElement>("#urls article");
const nextPaginationElement = nextPageDoc.querySelector<HTMLElement>("#pagination");
document.querySelector("#pagination")?.remove();
const urlsElement = document.querySelector<HTMLElement>("#urls");
if (!urlsElement) {
throw new Error("URLs element not found");
}
if (articleList.length > 0 && !onlyImages) {
// do not add <hr> element when there are only images
urlsElement.appendChild(document.createElement("hr"));
}
urlsElement.append(...Array.from(articleList));
if (nextPaginationElement) {
const results = document.querySelector<HTMLElement>("#results");
results?.appendChild(nextPaginationElement);
callback();
}
} catch (error) {
console.error("Error loading next page:", error);
const errorElement = Object.assign(document.createElement("div"), {
textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
className: "dialog-error"
});
errorElement.setAttribute("role", "alert");
document.querySelector("#pagination")?.replaceChildren(errorElement);
}
};
const resultsElement: HTMLElement | null = document.getElementById("results");
if (!resultsElement) {
throw new Error("Results element not found");
}
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
const observedSelector = "article.result:last-child";
const intersectionObserveOptions: IntersectionObserverInit = {
rootMargin: "320px"
};
const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
const [paginationEntry] = entries;
if (paginationEntry?.isIntersecting) {
observer.unobserve(paginationEntry.target);
loadNextPage(onlyImages, () => {
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
if (nextObservedElement) {
observer.observe(nextObservedElement);
}
}).then(() => {
// wait until promise is resolved
});
}
}, intersectionObserveOptions);
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
if (initialObservedElement) {
observer.observe(initialObservedElement);
}

View File

@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { assertElement, listen, mutable, settings } from "../core/toolkit.ts"; import { listen, mutable, settings } from "../toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
export type KeyBindingLayout = "default" | "vim"; export type KeyBindingLayout = "default" | "vim";
@@ -407,12 +408,31 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
}; };
const copyURLToClipboard = async (): Promise<void> => { const copyURLToClipboard = async (): Promise<void> => {
const currentUrlElement = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a"); const selectedResult = document.querySelector<HTMLElement>(".result[data-vim-selected]");
assertElement(currentUrlElement); if (!selectedResult) return;
const url = currentUrlElement.getAttribute("href"); const resultAnchor = selectedResult.querySelector<HTMLAnchorElement>("a");
assertElement(resultAnchor);
const url = resultAnchor.getAttribute("href");
if (url) { if (url) {
await navigator.clipboard.writeText(url); if (window.isSecureContext) {
await navigator.clipboard.writeText(url);
} else {
const selection = window.getSelection();
if (selection) {
const node = document.createElement("span");
node.textContent = url;
resultAnchor.appendChild(node);
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
node.remove();
}
}
} }
}; };

View File

@@ -1,86 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { listen } from "../core/toolkit.ts";
listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
event.preventDefault();
this.classList.remove("searxng_init_map");
const {
View,
OlMap,
TileLayer,
VectorLayer,
OSM,
VectorSource,
Style,
Stroke,
Fill,
Circle,
fromLonLat,
GeoJSON,
Feature,
Point
} = await import("../pkg/ol.ts");
import("ol/ol.css");
const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;
const lon = Number.parseFloat(mapLon || "0");
const lat = Number.parseFloat(mapLat || "0");
const view = new View({ maxZoom: 16, enableRotation: false });
const map = new OlMap({
target: target,
layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
view: view
});
try {
const markerSource = new VectorSource({
features: [
new Feature({
geometry: new Point(fromLonLat([lon, lat]))
})
]
});
const markerLayer = new VectorLayer({
source: markerSource,
style: new Style({
image: new Circle({
radius: 6,
fill: new Fill({ color: "#3050ff" })
})
})
});
map.addLayer(markerLayer);
} catch (error) {
console.error("Failed to create marker layer:", error);
}
if (mapGeojson) {
try {
const geoSource = new VectorSource({
features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
})
});
const geoLayer = new VectorLayer({
source: geoSource,
style: new Style({
stroke: new Stroke({ color: "#3050ff", width: 2 }),
fill: new Fill({ color: "#3050ff33" })
})
});
map.addLayer(geoLayer);
view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });
} catch (error) {
console.error("Failed to create GeoJSON layer:", error);
}
}
});

View File

@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { http, listen, settings } from "../core/toolkit.ts"; import { http, listen, settings } from "../toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
let engineDescriptions: Record<string, [string, string]> | undefined; let engineDescriptions: Record<string, [string, string]> | undefined;
@@ -52,19 +53,25 @@ for (const engine of disableAllEngines) {
listen("click", engine, () => toggleEngines(false, engineToggles)); listen("click", engine, () => toggleEngines(false, engineToggles));
} }
const copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>("#copy-hash"); listen("click", "#copy-hash", async function (this: HTMLElement) {
if (copyHashButton) { const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
listen("click", copyHashButton, async (event: Event) => { assertElement(target);
event.preventDefault();
const { copiedText, hash } = copyHashButton.dataset; if (window.isSecureContext) {
if (!(copiedText && hash)) return; await navigator.clipboard.writeText(target.innerText);
} else {
try { const selection = window.getSelection();
await navigator.clipboard.writeText(hash); if (selection) {
copyHashButton.innerText = copiedText; const range = document.createRange();
} catch (error) { range.selectNodeContents(target);
console.error("Failed to copy hash:", error); selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
} }
}); }
}
const copiedText = this.dataset.copiedText;
if (copiedText) {
this.innerText = copiedText;
}
});

View File

@@ -1,7 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import "../../../node_modules/swiped-events/src/swiped-events.js"; import "../../../node_modules/swiped-events/src/swiped-events.js";
import { assertElement, listen, mutable, settings } from "../core/toolkit.ts"; import { listen, mutable, settings } from "../toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
let imgTimeoutID: number; let imgTimeoutID: number;
@@ -121,7 +122,19 @@ listen("click", "#copy_url", async function (this: HTMLElement) {
const target = this.parentElement?.querySelector<HTMLPreElement>("pre"); const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
assertElement(target); assertElement(target);
await navigator.clipboard.writeText(target.innerText); if (window.isSecureContext) {
await navigator.clipboard.writeText(target.innerText);
} else {
const selection = window.getSelection();
if (selection) {
const range = document.createRange();
range.selectNodeContents(target);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
}
}
const copiedText = this.dataset.copiedText; const copiedText = this.dataset.copiedText;
if (copiedText) { if (copiedText) {
this.innerText = copiedText; this.innerText = copiedText;

View File

@@ -1,88 +1,51 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { assertElement, listen, settings } from "../core/toolkit.ts"; import { listen } from "../toolkit.ts";
import { getElement } from "../util/getElement.ts";
const submitIfQuery = (qInput: HTMLInputElement): void => { const searchForm: HTMLFormElement = getElement<HTMLFormElement>("search");
if (qInput.value.length > 0) { const searchInput: HTMLInputElement = getElement<HTMLInputElement>("q");
const search = document.getElementById("search") as HTMLFormElement | null; const searchReset: HTMLButtonElement = getElement<HTMLButtonElement>("clear_search");
search?.submit();
}
};
const updateClearButton = (qInput: HTMLInputElement, cs: HTMLElement): void => {
cs.classList.toggle("empty", qInput.value.length === 0);
};
const createClearButton = (qInput: HTMLInputElement): void => {
const cs = document.getElementById("clear_search");
assertElement(cs);
updateClearButton(qInput, cs);
listen("click", cs, (event: MouseEvent) => {
event.preventDefault();
qInput.value = "";
qInput.focus();
updateClearButton(qInput, cs);
});
listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
};
const qInput = document.getElementById("q") as HTMLInputElement | null;
assertElement(qInput);
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches; const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results"; const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
const categoryButtons: HTMLButtonElement[] = Array.from(
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
);
if (searchInput.value.length === 0) {
searchReset.classList.add("empty");
}
// focus search input on large screens // focus search input on large screens
if (!(isMobile || isResultsPage)) { if (!(isMobile || isResultsPage)) {
qInput.focus(); searchInput.focus();
} }
// On mobile, move cursor to the end of the input on focus // On mobile, move cursor to the end of the input on focus
if (isMobile) { if (isMobile) {
listen("focus", qInput, () => { listen("focus", searchInput, () => {
// Defer cursor move until the next frame to prevent a visual jump // Defer cursor move until the next frame to prevent a visual jump
requestAnimationFrame(() => { requestAnimationFrame(() => {
const end = qInput.value.length; const end = searchInput.value.length;
qInput.setSelectionRange(end, end); searchInput.setSelectionRange(end, end);
qInput.scrollLeft = qInput.scrollWidth; searchInput.scrollLeft = searchInput.scrollWidth;
}); });
}); });
} }
createClearButton(qInput); listen("input", searchInput, () => {
searchReset.classList.toggle("empty", searchInput.value.length === 0);
});
// Additionally to searching when selecting a new category, we also listen("click", searchReset, (event: MouseEvent) => {
// automatically start a new search request when the user changes a search event.preventDefault();
// filter (safesearch, time range or language) (this requires JavaScript searchInput.value = "";
// though) searchInput.focus();
if ( searchReset.classList.add("empty");
settings.search_on_category_select && });
// If .search_filters is undefined (invisible) we are on the homepage and
// hence don't have to set any listeners
document.querySelector(".search_filters")
) {
const safesearchElement = document.getElementById("safesearch");
if (safesearchElement) {
listen("change", safesearchElement, () => submitIfQuery(qInput));
}
const timeRangeElement = document.getElementById("time_range");
if (timeRangeElement) {
listen("change", timeRangeElement, () => submitIfQuery(qInput));
}
const languageElement = document.getElementById("language");
if (languageElement) {
listen("change", languageElement, () => submitIfQuery(qInput));
}
}
const categoryButtons: HTMLButtonElement[] = [
...document.querySelectorAll<HTMLButtonElement>("button.category_button")
];
for (const button of categoryButtons) { for (const button of categoryButtons) {
listen("click", button, (event: MouseEvent) => { listen("click", button, (event: MouseEvent) => {
if (event.shiftKey) { if (event.shiftKey) {
@@ -98,21 +61,34 @@ for (const button of categoryButtons) {
}); });
} }
const form: HTMLFormElement | null = document.querySelector<HTMLFormElement>("#search"); if (document.querySelector("div.search_filters")) {
assertElement(form); const safesearchElement = document.getElementById("safesearch");
if (safesearchElement) {
// override form submit action to update the actually selected categories listen("change", safesearchElement, () => searchForm.submit());
listen("submit", form, (event: Event) => {
event.preventDefault();
const categoryValuesInput = document.querySelector<HTMLInputElement>("#selected-categories");
if (categoryValuesInput) {
const categoryValues = categoryButtons
.filter((button) => button.classList.contains("selected"))
.map((button) => button.name.replace("category_", ""));
categoryValuesInput.value = categoryValues.join(",");
} }
form.submit(); const timeRangeElement = document.getElementById("time_range");
if (timeRangeElement) {
listen("change", timeRangeElement, () => searchForm.submit());
}
const languageElement = document.getElementById("language");
if (languageElement) {
listen("change", languageElement, () => searchForm.submit());
}
}
// override searchForm submit event
listen("submit", searchForm, (event: Event) => {
event.preventDefault();
if (categoryButtons.length > 0) {
const searchCategories = getElement<HTMLInputElement>("selected-categories");
searchCategories.value = categoryButtons
.filter((button) => button.classList.contains("selected"))
.map((button) => button.name.replace("category_", ""))
.join(",");
}
searchForm.submit();
}); });

View File

@@ -1,28 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { Feature, Map as OlMap, View } from "ol";
import { createEmpty } from "ol/extent";
import { GeoJSON } from "ol/format";
import { Point } from "ol/geom";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import { fromLonLat } from "ol/proj";
import { OSM, Vector as VectorSource } from "ol/source";
import { Circle, Fill, Stroke, Style } from "ol/style";
export {
View,
OlMap,
TileLayer,
VectorLayer,
OSM,
createEmpty,
VectorSource,
Style,
Stroke,
Fill,
Circle,
fromLonLat,
GeoJSON,
Feature,
Point
};

View File

@@ -0,0 +1,93 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
absDependencies,
addDependencies,
create,
divideDependencies,
eDependencies,
evaluateDependencies,
expDependencies,
factorialDependencies,
gcdDependencies,
lcmDependencies,
log1pDependencies,
log2Dependencies,
log10Dependencies,
logDependencies,
modDependencies,
multiplyDependencies,
nthRootDependencies,
piDependencies,
powDependencies,
roundDependencies,
signDependencies,
sqrtDependencies,
subtractDependencies
} from "mathjs/number";
import { Plugin } from "../Plugin.ts";
import { appendAnswerElement } from "../util/appendAnswerElement.ts";
import { getElement } from "../util/getElement.ts";
/**
* Parses and solves mathematical expressions. Can do basic arithmetic and
* evaluate some functions.
*
* @example
* "(3 + 5) / 2" = "4"
* "e ^ 2 + pi" = "10.530648752520442"
* "gcd(48, 18) + lcm(4, 5)" = "26"
*
* @remarks
* Depends on `mathjs` library.
*/
export default class Calculator extends Plugin {
public constructor() {
super("calculator");
}
/**
* @remarks
* Compare bundle size after adding or removing features.
*/
private static readonly math = create({
...absDependencies,
...addDependencies,
...divideDependencies,
...eDependencies,
...evaluateDependencies,
...expDependencies,
...factorialDependencies,
...gcdDependencies,
...lcmDependencies,
...log10Dependencies,
...log1pDependencies,
...log2Dependencies,
...logDependencies,
...modDependencies,
...multiplyDependencies,
...nthRootDependencies,
...piDependencies,
...powDependencies,
...roundDependencies,
...signDependencies,
...sqrtDependencies,
...subtractDependencies
});
protected async run(): Promise<string | undefined> {
const searchInput = getElement<HTMLInputElement>("q");
const node = Calculator.math.parse(searchInput.value);
try {
return `${node.toString()} = ${node.evaluate()}`;
} catch {
// not a compatible math expression
return;
}
}
protected async post(result: string): Promise<void> {
appendAnswerElement(result);
}
}

View File

@@ -0,0 +1,110 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { Plugin } from "../Plugin.ts";
import { http, settings } from "../toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
import { getElement } from "../util/getElement.ts";
/**
* Automatically loads the next page when scrolling to bottom of the current page.
*/
export default class InfiniteScroll extends Plugin {
public constructor() {
super("infiniteScroll");
}
protected async run(): Promise<void> {
const resultsElement = getElement<HTMLElement>("results");
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
const observedSelector = "article.result:last-child";
const spinnerElement = document.createElement("div");
spinnerElement.className = "loader";
const loadNextPage = async (callback: () => void): Promise<void> => {
const searchForm = document.querySelector<HTMLFormElement>("#search");
assertElement(searchForm);
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
assertElement(form);
const action = searchForm.getAttribute("action");
if (!action) {
throw new Error("Form action not defined");
}
const paginationElement = document.querySelector<HTMLElement>("#pagination");
assertElement(paginationElement);
paginationElement.replaceChildren(spinnerElement);
try {
const res = await http("POST", action, { body: new FormData(form) });
const nextPage = await res.text();
if (!nextPage) return;
const nextPageDoc = new DOMParser().parseFromString(nextPage, "text/html");
const articleList = nextPageDoc.querySelectorAll<HTMLElement>("#urls article");
const nextPaginationElement = nextPageDoc.querySelector<HTMLElement>("#pagination");
document.querySelector("#pagination")?.remove();
const urlsElement = document.querySelector<HTMLElement>("#urls");
if (!urlsElement) {
throw new Error("URLs element not found");
}
if (articleList.length > 0 && !onlyImages) {
// do not add <hr> element when there are only images
urlsElement.appendChild(document.createElement("hr"));
}
urlsElement.append(...articleList);
if (nextPaginationElement) {
const results = document.querySelector<HTMLElement>("#results");
results?.appendChild(nextPaginationElement);
callback();
}
} catch (error) {
console.error("Error loading next page:", error);
const errorElement = Object.assign(document.createElement("div"), {
textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
className: "dialog-error"
});
errorElement.setAttribute("role", "alert");
document.querySelector("#pagination")?.replaceChildren(errorElement);
}
};
const intersectionObserveOptions: IntersectionObserverInit = {
rootMargin: "320px"
};
const observer: IntersectionObserver = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => {
const [paginationEntry] = entries;
if (paginationEntry?.isIntersecting) {
observer.unobserve(paginationEntry.target);
await loadNextPage(() => {
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
if (nextObservedElement) {
observer.observe(nextObservedElement);
}
});
}
}, intersectionObserveOptions);
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
if (initialObservedElement) {
observer.observe(initialObservedElement);
}
}
protected async post(): Promise<void> {
// noop
}
}

View File

@@ -0,0 +1,90 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import "ol/ol.css?inline";
import { Feature, Map as OlMap, View } from "ol";
import { GeoJSON } from "ol/format";
import { Point } from "ol/geom";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import { fromLonLat } from "ol/proj";
import { OSM, Vector as VectorSource } from "ol/source";
import { Circle, Fill, Stroke, Style } from "ol/style";
import { Plugin } from "../Plugin.ts";
/**
* MapView
*/
export default class MapView extends Plugin {
private readonly map: HTMLElement;
public constructor(map: HTMLElement) {
super("mapView");
this.map = map;
}
protected async run(): Promise<void> {
const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.map.dataset;
const lon = Number.parseFloat(mapLon || "0");
const lat = Number.parseFloat(mapLat || "0");
const view = new View({ maxZoom: 16, enableRotation: false });
const map = new OlMap({
target: target,
layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
view: view
});
try {
const markerSource = new VectorSource({
features: [
new Feature({
geometry: new Point(fromLonLat([lon, lat]))
})
]
});
const markerLayer = new VectorLayer({
source: markerSource,
style: new Style({
image: new Circle({
radius: 6,
fill: new Fill({ color: "#3050ff" })
})
})
});
map.addLayer(markerLayer);
} catch (error) {
console.error("Failed to create marker layer:", error);
}
if (mapGeojson) {
try {
const geoSource = new VectorSource({
features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
})
});
const geoLayer = new VectorLayer({
source: geoSource,
style: new Style({
stroke: new Stroke({ color: "#3050ff", width: 2 }),
fill: new Fill({ color: "#3050ff33" })
})
});
map.addLayer(geoLayer);
view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });
} catch (error) {
console.error("Failed to create GeoJSON layer:", error);
}
}
}
protected async post(): Promise<void> {
// noop
}
}

View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { load } from "./loader.ts";
import { Endpoints, endpoint, listen, ready, settings } from "./toolkit.ts";
ready(() => {
document.documentElement.classList.remove("no-js");
document.documentElement.classList.add("js");
listen("click", ".close", function (this: HTMLElement) {
(this.parentNode as HTMLElement)?.classList.add("invisible");
});
listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
event.preventDefault();
this.classList.remove("searxng_init_map");
load(() => import("./plugin/MapView.ts").then(({ default: Plugin }) => new Plugin(this)), {
on: "endpoint",
where: [Endpoints.results]
});
});
if (settings.plugins?.includes("infiniteScroll")) {
load(() => import("./plugin/InfiniteScroll.ts").then(({ default: Plugin }) => new Plugin()), {
on: "endpoint",
where: [Endpoints.results]
});
}
if (settings.plugins?.includes("calculator")) {
load(() => import("./plugin/Calculator.ts").then(({ default: Plugin }) => new Plugin()), {
on: "endpoint",
where: [Endpoints.results]
});
}
});
ready(
() => {
void import("./main/keyboard.ts");
void import("./main/search.ts");
if (settings.autocomplete) {
void import("./main/autocomplete.ts");
}
},
{ on: [endpoint === Endpoints.index] }
);
ready(
() => {
void import("./main/keyboard.ts");
void import("./main/results.ts");
void import("./main/search.ts");
if (settings.autocomplete) {
void import("./main/autocomplete.ts");
}
},
{ on: [endpoint === Endpoints.results] }
);
ready(
() => {
void import("./main/preferences.ts");
},
{ on: [endpoint === Endpoints.preferences] }
);

View File

@@ -1,16 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import type { KeyBindingLayout } from "../main/keyboard.ts"; import type { KeyBindingLayout } from "./main/keyboard.ts";
// synced with searx/webapp.py get_client_settings // synced with searx/webapp.py get_client_settings
type Settings = { type Settings = {
plugins?: string[];
advanced_search?: boolean; advanced_search?: boolean;
autocomplete?: string; autocomplete?: string;
autocomplete_min?: number; autocomplete_min?: number;
doi_resolver?: string; doi_resolver?: string;
favicon_resolver?: string; favicon_resolver?: string;
hotkeys?: KeyBindingLayout; hotkeys?: KeyBindingLayout;
infinite_scroll?: boolean;
method?: "GET" | "POST"; method?: "GET" | "POST";
query_in_title?: boolean; query_in_title?: boolean;
results_on_new_tab?: boolean; results_on_new_tab?: boolean;
@@ -32,8 +32,6 @@ type ReadyOptions = {
on?: (boolean | undefined)[]; on?: (boolean | undefined)[];
}; };
type AssertElement = (element?: HTMLElement | null) => asserts element is HTMLElement;
export type EndpointsKeys = keyof typeof Endpoints; export type EndpointsKeys = keyof typeof Endpoints;
export const Endpoints = { export const Endpoints = {
@@ -73,12 +71,6 @@ const getSettings = (): Settings => {
} }
}; };
export const assertElement: AssertElement = (element?: HTMLElement | null): asserts element is HTMLElement => {
if (!element) {
throw new Error("Bad assertion: DOM element not found");
}
};
export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => { export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000); const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);

View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { getElement } from "./getElement.ts";
export const appendAnswerElement = (element: HTMLElement | string | number): void => {
const results = getElement<HTMLDivElement>("results");
// ./searx/templates/elements/answers.html
let answers = getElement<HTMLDivElement>("answers", { assert: false });
if (!answers) {
// what is this?
const answersTitle = document.createElement("h4");
answersTitle.setAttribute("class", "title");
answersTitle.setAttribute("id", "answers-title");
answersTitle.textContent = "Answers : ";
answers = document.createElement("div");
answers.setAttribute("id", "answers");
answers.setAttribute("role", "complementary");
answers.setAttribute("aria-labelledby", "answers-title");
answers.appendChild(answersTitle);
}
if (!(element instanceof HTMLElement)) {
const span = document.createElement("span");
span.innerHTML = element.toString();
// biome-ignore lint/style/noParameterAssign: TODO
element = span;
}
answers.appendChild(element);
results.insertAdjacentElement("afterbegin", answers);
};

View File

@@ -0,0 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
type AssertElement = <T>(element?: T | null) => asserts element is T;
export const assertElement: AssertElement = <T>(element?: T | null): asserts element is T => {
if (!element) {
throw new Error("DOM element not found");
}
};

View File

@@ -0,0 +1,21 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { assertElement } from "./assertElement.ts";
type Options = {
assert?: boolean;
};
export function getElement<T>(id: string, options?: { assert: true }): T;
export function getElement<T>(id: string, options?: { assert: false }): T | null;
export function getElement<T>(id: string, options: Options = {}): T | null {
options.assert ??= true;
const element = document.getElementById(id) as T | null;
if (options.assert) {
assertElement(element);
}
return element;
}

View File

@@ -1,19 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
iframe[src^="https://w.soundcloud.com"] iframe[src^="https://w.soundcloud.com"] {
{
height: 120px; height: 120px;
} }
iframe[src^="https://www.deezer.com"] iframe[src^="https://www.deezer.com"] {
{
// The real size is 92px, but 94px are needed to avoid an inner scrollbar of // The real size is 92px, but 94px are needed to avoid an inner scrollbar of
// the embedded HTML. // the embedded HTML.
height: 94px; height: 94px;
} }
iframe[src^="https://www.mixcloud.com"] iframe[src^="https://www.mixcloud.com"] {
{
// the embedded player from mixcloud has some quirks: initial there is an // the embedded player from mixcloud has some quirks: initial there is an
// issue with an image URL that is blocked since it is an a Cross-Origin // issue with an image URL that is blocked since it is an a Cross-Origin
// request. The alternative text (<img alt='Mixcloud Logo'> then cause an // request. The alternative text (<img alt='Mixcloud Logo'> then cause an
@@ -23,19 +20,16 @@ iframe[src^="https://www.mixcloud.com"]
height: 250px; height: 250px;
} }
iframe[src^="https://bandcamp.com/EmbeddedPlayer"] iframe[src^="https://bandcamp.com/EmbeddedPlayer"] {
{
// show playlist // show playlist
height: 350px; height: 350px;
} }
iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"] iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"] {
{
// hide playlist // hide playlist
height: 120px; height: 120px;
} }
iframe[src^="https://genius.com/songs"] iframe[src^="https://genius.com/songs"] {
{
height: 65px; height: 65px;
} }

View File

@@ -8,7 +8,7 @@
text-align: center; text-align: center;
.title { .title {
background: url("../img/searxng.png") no-repeat; background: url("./img/searxng.png") no-repeat;
min-height: 4rem; min-height: 4rem;
margin: 4rem auto; margin: 4rem auto;
background-position: center; background-position: center;

View File

@@ -0,0 +1,22 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
Layout of the Files result class
*/
#main_results .result-file {
border: 1px solid var(--color-result-border);
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
.rounded-corners;
video {
width: 100%;
aspect-ratio: 16 / 9;
padding: 10px 0 0 0;
}
audio {
width: 100%;
padding: 10px 0 0 0;
}
}

View File

@@ -178,7 +178,6 @@ html.no-js #clear_search.hide_if_nojs {
#send_search { #send_search {
display: block; display: block;
margin: 0; margin: 0;
padding: 0.8rem;
background: none repeat scroll 0 0 var(--color-search-background); background: none repeat scroll 0 0 var(--color-search-background);
border: none; border: none;
outline: none; outline: none;
@@ -196,6 +195,7 @@ html.no-js #clear_search.hide_if_nojs {
#send_search { #send_search {
.ltr-rounded-right-corners(0.8rem); .ltr-rounded-right-corners(0.8rem);
padding: 0.8rem;
&:hover { &:hover {
cursor: pointer; cursor: pointer;

View File

@@ -163,12 +163,22 @@ article[data-vim-selected].category-videos,
article[data-vim-selected].category-news, article[data-vim-selected].category-news,
article[data-vim-selected].category-map, article[data-vim-selected].category-map,
article[data-vim-selected].category-music, article[data-vim-selected].category-music,
article[data-vim-selected].category-files,
article[data-vim-selected].category-social { article[data-vim-selected].category-social {
border: 1px solid var(--color-result-vim-arrow); border: 1px solid var(--color-result-vim-arrow);
.rounded-corners; .rounded-corners;
} }
.image-label-bottom-right() {
position: absolute;
right: 0;
bottom: 0;
background: var(--color-image-resolution-background);
padding: 0.3rem 0.5rem;
font-size: 0.9rem;
color: var(--color-image-resolution-font);
border-top-left-radius: 0.3rem;
}
.result { .result {
margin: @results-margin 0; margin: @results-margin 0;
padding: @result-padding; padding: @result-padding;
@@ -295,12 +305,22 @@ article[data-vim-selected].category-social {
color: var(--color-result-description-highlight-font); color: var(--color-result-description-highlight-font);
} }
img.thumbnail { a.thumbnail_link {
position: relative;
margin-top: 0.6rem;
.ltr-margin-right(1rem);
.ltr-float-left(); .ltr-float-left();
padding-top: 0.6rem;
.ltr-padding-right(1rem); img.thumbnail {
width: 7rem; width: 7rem;
height: unset; // remove height value that was needed for lazy loading height: unset; // remove height value that was needed for lazy loading
display: block;
}
.thumbnail_length {
.image-label-bottom-right();
right: 6px;
}
} }
.break { .break {
@@ -366,7 +386,6 @@ article[data-vim-selected].category-social {
.category-news, .category-news,
.category-map, .category-map,
.category-music, .category-music,
.category-files,
.category-social { .category-social {
border: 1px solid var(--color-result-border); border: 1px solid var(--color-result-border);
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important; margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
@@ -391,23 +410,19 @@ article[data-vim-selected].category-social {
} }
.result-videos { .result-videos {
img.thumbnail { a.thumbnail_link img.thumbnail {
.ltr-float-left();
padding-top: 0.6rem;
.ltr-padding-right(1rem);
width: 20rem; width: 20rem;
height: unset; // remove height value that was needed for lazy loading
} }
}
.result-videos .content { .content {
overflow: hidden; overflow: hidden;
} }
.result-videos .embedded-video iframe { .embedded-video iframe {
width: 100%; width: 100%;
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
padding: 10px 0 0 0; padding: 10px 0 0 0;
}
} }
@supports not (aspect-ratio: 1 / 1) { @supports not (aspect-ratio: 1 / 1) {
@@ -472,14 +487,7 @@ article[data-vim-selected].category-social {
} }
.image_resolution { .image_resolution {
position: absolute; .image-label-bottom-right();
right: 0;
bottom: 0;
background: var(--color-image-resolution-background);
padding: 0.3rem 0.5rem;
font-size: 0.9rem;
color: var(--color-image-resolution-font);
border-top-left-radius: 0.3rem;
} }
span.title, span.title,
@@ -1158,3 +1166,4 @@ pre code {
@import "result_types/keyvalue.less"; @import "result_types/keyvalue.less";
@import "result_types/code.less"; @import "result_types/code.less";
@import "result_types/paper.less"; @import "result_types/paper.less";
@import "result_types/file.less";

View File

@@ -193,6 +193,15 @@ div.selectable_url {
border-color: var(--color-warning); border-color: var(--color-warning);
} }
.dialog-warning-block {
.dialog();
display: block;
color: var(--color-warning);
background: var(--color-warning-background);
border-color: var(--color-warning);
}
.dialog-modal { .dialog-modal {
.dialog(); .dialog();

View File

@@ -4,7 +4,7 @@
* Custom vite plugins to build the web-client components of the simple theme. * Custom vite plugins to build the web-client components of the simple theme.
* *
* HINT: * HINT:
* This is an inital implementation for the migration of the build process * This is an initial implementation for the migration of the build process
* from grunt to vite. For fully support (vite: build & serve) more work is * from grunt to vite. For fully support (vite: build & serve) more work is
* needed. * needed.
*/ */

View File

@@ -46,39 +46,34 @@ export default {
sourcemap: true, sourcemap: true,
rolldownOptions: { rolldownOptions: {
input: { input: {
// build CSS files // entrypoint
"searxng-ltr.css": `${PATH.src}/less/style-ltr.less`, core: `${PATH.src}/js/index.ts`,
"searxng-rtl.css": `${PATH.src}/less/style-rtl.less`,
"rss.css": `${PATH.src}/less/rss.less`,
// build script files // stylesheets
"searxng.core": `${PATH.src}/js/core/index.ts`, ltr: `${PATH.src}/less/style-ltr.less`,
rtl: `${PATH.src}/less/style-rtl.less`,
// ol pkg rss: `${PATH.src}/less/rss.less`
ol: `${PATH.src}/js/pkg/ol.ts`,
"ol.css": `${PATH.modules}/ol/ol.css`
}, },
// file naming conventions / pathnames are relative to outDir (PATH.dist) // file naming conventions / pathnames are relative to outDir (PATH.dist)
output: { output: {
entryFileNames: "js/[name].min.js", entryFileNames: "sxng-[name].min.js",
chunkFileNames: "js/[name].min.js", chunkFileNames: "chunk/[hash].min.js",
assetFileNames: ({ names }: PreRenderedAsset): string => { assetFileNames: ({ names }: PreRenderedAsset): string => {
const [name] = names; const [name] = names;
const extension = name?.split(".").pop(); switch (name?.split(".").pop()) {
switch (extension) {
case "css": case "css":
return "css/[name].min[extname]"; return "sxng-[name].min[extname]";
case "js":
return "js/[name].min[extname]";
case "png":
case "svg":
return "img/[name][extname]";
default: default:
console.warn("Unknown asset:", name); return "sxng-[name][extname]";
return "[name][extname]";
} }
},
sanitizeFileName: (name: string): string => {
return name
.normalize("NFD")
.replace(/[^a-zA-Z0-9.-]/g, "_")
.toLowerCase();
} }
} }
} }

View File

@@ -1,26 +0,0 @@
contents:
repositories:
- https://dl-cdn.alpinelinux.org/alpine/edge/main
- https://dl-cdn.alpinelinux.org/alpine/edge/community
packages:
- alpine-base
- build-base
- python3-dev
- uv
- brotli
entrypoint:
command: /bin/sh -l
work-dir: /usr/local/searxng/
environment:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SSL_CERT_DIR: /etc/ssl/certs
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
HISTFILE: /dev/null
archs:
- x86_64
- aarch64
- armv7

View File

@@ -1,62 +0,0 @@
contents:
repositories:
- https://dl-cdn.alpinelinux.org/alpine/edge/main
packages:
- alpine-baselayout
- ca-certificates
- ca-certificates-bundle
- musl-locales
- musl-locales-lang
- tzdata
- busybox
- python3
- wget
entrypoint:
command: /bin/sh -l
work-dir: /usr/local/searxng/
accounts:
groups:
- groupname: searxng
gid: 977
users:
- username: searxng
uid: 977
shell: /bin/ash
environment:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SSL_CERT_DIR: /etc/ssl/certs
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
HISTFILE: /dev/null
CONFIG_PATH: /etc/searxng
DATA_PATH: /var/cache/searxng
paths:
# Workdir
- path: /usr/local/searxng/
type: directory
uid: 977
gid: 977
permissions: 0o555
# Config volume
- path: /etc/searxng/
type: directory
uid: 977
gid: 977
permissions: 0o755
# Data volume
- path: /var/cache/searxng/
type: directory
uid: 977
gid: 977
permissions: 0o755
archs:
- x86_64
- aarch64
- armv7

View File

@@ -19,8 +19,7 @@ RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
find ./.venv/lib/python*/site-packages/*.dist-info/ -type f -name "RECORD" -exec sort -t, -k1,1 -o {} {} \;; \ find ./.venv/lib/python*/site-packages/*.dist-info/ -type f -name "RECORD" -exec sort -t, -k1,1 -o {} {} \;; \
find ./.venv/ -exec touch -h --date="@$TIMESTAMP_VENV" {} + find ./.venv/ -exec touch -h --date="@$TIMESTAMP_VENV" {} +
# use "--exclude=./searx/version_frozen.py" when actions/runner-images updates to Podman 5.0+ COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
COPY ./searx/ ./searx/
ARG TIMESTAMP_SETTINGS="0" ARG TIMESTAMP_SETTINGS="0"

View File

@@ -4,10 +4,10 @@ ARG CONTAINER_IMAGE_NAME="searxng"
FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder
FROM ghcr.io/searxng/base:searxng AS dist FROM ghcr.io/searxng/base:searxng AS dist
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/.venv/ ./.venv/ COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/searx/ ./searx/ COPY --chown=977:977 --from=builder /usr/local/searxng/searx/ ./searx/
COPY --chown=searxng:searxng ./container/ ./ COPY --chown=977:977 ./container/ ./
#COPY --chown=searxng:searxng ./searx/version_frozen.py ./searx/ COPY --chown=977:977 ./searx/version_frozen.py ./searx/
ARG CREATED="0001-01-01T00:00:00Z" ARG CREATED="0001-01-01T00:00:00Z"
ARG VERSION="unknown" ARG VERSION="unknown"

View File

@@ -48,7 +48,7 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
.. group-tab:: Firefox .. group-tab:: Firefox
.. kernel-figure:: answer-captcha/ffox-setting-proxy-socks.png .. kernel-figure:: /assets/answer-captcha/ffox-setting-proxy-socks.png
:alt: FFox proxy on SOCKS5, 127.0.0.1:8080 :alt: FFox proxy on SOCKS5, 127.0.0.1:8080
Firefox's network settings Firefox's network settings
@@ -66,4 +66,3 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
-N -N
Do not execute a remote command. This is useful for just forwarding ports. Do not execute a remote command. This is useful for just forwarding ports.

View File

@@ -100,7 +100,7 @@ Basic container instancing example:
$ cd ./searxng/ $ cd ./searxng/
# Run the container # Run the container
$ docker run --name searxng --replace -d \ $ docker run --name searxng -d \
-p 8888:8080 \ -p 8888:8080 \
-v "./config/:/etc/searxng/" \ -v "./config/:/etc/searxng/" \
-v "./data/:/var/cache/searxng/" \ -v "./data/:/var/cache/searxng/" \

View File

@@ -4,22 +4,5 @@
``brand:`` ``brand:``
========== ==========
.. code:: yaml .. autoclass:: searx.brand.SettingsBrand
:members:
brand:
issue_url: https://github.com/searxng/searxng/issues
docs_url: https://docs.searxng.org
public_instances: https://searx.space
wiki_url: https://github.com/searxng/searxng/wiki
``issue_url`` :
If you host your own issue tracker change this URL.
``docs_url`` :
If you host your own documentation change this URL.
``public_instances`` :
If you host your own https://searx.space change this URL.
``wiki_url`` :
Link to your wiki (or ``false``)

View File

@@ -69,6 +69,9 @@ The built-in plugins are all located in the namespace `searx.plugins`.
searx.plugins.calculator.SXNGPlugin: searx.plugins.calculator.SXNGPlugin:
active: true active: true
searx.plugins.infinite_scroll.SXNGPlugin:
active: false
searx.plugins.hash_plugin.SXNGPlugin: searx.plugins.hash_plugin.SXNGPlugin:
active: true active: true

View File

@@ -12,7 +12,6 @@
ui: ui:
default_locale: "" default_locale: ""
query_in_title: false query_in_title: false
infinite_scroll: false
center_alignment: false center_alignment: false
cache_url: https://web.archive.org/web/ cache_url: https://web.archive.org/web/
default_theme: simple default_theme: simple
@@ -32,9 +31,6 @@
When true, the result page's titles contains the query it decreases the When true, the result page's titles contains the query it decreases the
privacy, since the browser can records the page titles. privacy, since the browser can records the page titles.
``infinite_scroll``:
When true, automatically loads the next page when scrolling to bottom of the current page.
``center_alignment`` : default ``false`` ``center_alignment`` : default ``false``
When enabled, the results are centered instead of being in the left (or RTL) When enabled, the results are centered instead of being in the left (or RTL)
side of the screen. This setting only affects the *desktop layout* side of the screen. This setting only affects the *desktop layout*

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1024 384"><path fill="#410002" d="M479.178 119.294c-.533-.016-.998.357-1.218 1.078l-24.438 78.364.005-.004c-8.59 27.537 4.516 46.485 34.268 46.485 4.336 0 10.006-.357 11.776-.797.885-.264 1.33-.71 1.594-1.506l5.134-17.177c.264-.973-.09-1.77-1.507-1.683-4.517.445-8.588.797-12.308.797-14.964 0-21.075-7.968-16.646-22.224l10.446-33.47h30.373c.797 0 1.506-.445 1.858-1.418l5.401-17.355c.264-.973-.264-1.681-1.417-1.681H492.66l3.895-12.662c.265-.885.089-1.418-.62-2.034l-15.761-14.255c-.332-.3-.676-.45-.996-.459zm173.64 0c-.532-.016-.996.357-1.218 1.078l-24.436 78.364.005-.004c-8.59 27.537 4.517 46.485 34.268 46.485 4.342 0 10.004-.357 11.778-.797.884-.264 1.324-.71 1.593-1.506l5.133-17.177c.26-.973 0-1.77-1.508-1.683-4.517.445-8.59.797-12.307.797-14.966 0-21.077-7.968-16.646-22.224l10.445-33.47H690.3c.795 0 1.504-.445 1.854-1.418l5.402-17.355c.265-.973-.26-1.681-1.414-1.681H666.3l3.896-12.662c.265-.885.087-1.418-.618-2.034l-15.765-14.255c-.332-.3-.676-.45-.996-.459zm-48.32 29.404c-.974 0-1.503.444-1.862 1.417L590.502 188.9c-7.525 23.998-19.478 37.721-31.965 37.721-12.487 0-17.797-9.83-13.105-24.883l16.028-51.178c.351-1.149-.088-1.857-1.328-1.857H539.94c-.97 0-1.505.444-1.86 1.417l-15.497 49.233.008-.005c-8.765 27.982 5.315 46.31 27.452 46.31 12.747 0 22.756-6.111 29.93-16.118l-.176 12.838c0 1.241.621 1.593 1.681 1.593h14.17c1.064 0 1.504-.445 1.859-1.418l28.512-91.997c.35-1.15-.09-1.858-1.33-1.858zm147.96.005c-43.653 0-60.654 37.719-60.654 62.157-.09 21.339 13.282 34.798 31.08 34.798v.004c11.868 0 21.693-5.314 29.133-16.117v12.836c0 1.061.62 1.596 1.594 1.596h14.166c.974 0 1.505-.446 1.86-1.42l28.777-92.086c.265-.973-.266-1.768-1.24-1.768zm-.616 20.54h17.265l-6.197 19.57c-7.35 23.289-18.684 37.896-32.585 37.896-10.094 0-15.585-6.907-15.585-18.15 0-17.976 13.722-39.315 37.102-39.315z"/><path fill="#850122" d="M226.561 106.964c-.558.007-1.043.428-1.043 1.095V251.59c0 1.594 2.04 1.594 2.48 0L261.38 143.3c.445-1.241.446-2.039-.62-3.1l-33.204-32.762c-.299-.332-.66-.478-.996-.474zm55.983 41.739c-1.241 0-1.594.444-2.039 1.417l-43.919 142.203c-.176.797.177 1.594 1.242 1.594h145.747c1.418 0 2.04-.62 2.48-1.858l44.098-141.499c.445-1.417-.18-1.857-1.417-1.857zm-40.022-58.62c-1.418 0-1.594 1.242-.797 2.04l35.065 35.24c.796.798 1.594 1.061 2.836 1.061h149.467c1.065 0 1.68-1.24.62-2.214l-34.63-34.885c-.796-.796-1.592-1.242-3.274-1.242z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -120,6 +120,7 @@ ${fedora_build}
pip install -U setuptools pip install -U setuptools
pip install -U wheel pip install -U wheel
pip install -U pyyaml pip install -U pyyaml
pip install -U msgspec
# jump to SearXNG's working tree and install SearXNG into virtualenv # jump to SearXNG's working tree and install SearXNG into virtualenv
(${SERVICE_USER})$ cd \"$SEARXNG_SRC\" (${SERVICE_USER})$ cd \"$SEARXNG_SRC\"

View File

@@ -0,0 +1,8 @@
.. _azure engine:
===============
Azure Resources
===============
.. automodule:: searx.engines.azure
:members:

View File

@@ -1,8 +0,0 @@
.. _voidlinux mullvad_leta:
============
Mullvad-Leta
============
.. automodule:: searx.engines.mullvad_leta
:members:

View File

@@ -0,0 +1,8 @@
.. _sourcehut engine:
=========
Sourcehut
=========
.. automodule:: searx.engines.sourcehut
:members:

View File

@@ -10,6 +10,7 @@ Built-in Plugins
calculator calculator
hash_plugin hash_plugin
hostnames hostnames
infinite_scroll
self_info self_info
tor_check tor_check
unit_converter unit_converter

View File

@@ -0,0 +1,8 @@
.. _plugins.infinite_scroll:
===============
Infinite scroll
===============
.. automodule:: searx.plugins.infinite_scroll
:members:

View File

@@ -0,0 +1,7 @@
.. _result_types.file:
============
File Results
============
.. automodule:: searx.result_types.file

View File

@@ -17,6 +17,7 @@ following types have been implemented so far ..
main/keyvalue main/keyvalue
main/code main/code
main/paper main/paper
main/file
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that The :ref:`LegacyResult <LegacyResult>` is used internally for the results that
have not yet been typed. The templates can be used as orientation until the have not yet been typed. The templates can be used as orientation until the
@@ -28,5 +29,4 @@ final typing is complete.
- :ref:`template torrent` - :ref:`template torrent`
- :ref:`template map` - :ref:`template map`
- :ref:`template packages` - :ref:`template packages`
- :ref:`template files`
- :ref:`template products` - :ref:`template products`

View File

@@ -60,7 +60,7 @@ Fields used in the template :origin:`macro result_sub_header
publishedDate : :py:obj:`datetime.datetime` publishedDate : :py:obj:`datetime.datetime`
The date on which the object was published. The date on which the object was published.
length: :py:obj:`time.struct_time` length: :py:obj:`datetime.timedelta`
Playing duration in seconds. Playing duration in seconds.
views: :py:class:`str` views: :py:class:`str`
@@ -469,38 +469,6 @@ links : :py:class:`dict`
Additional links in the form of ``{'link_name': 'http://example.com'}`` Additional links in the form of ``{'link_name': 'http://example.com'}``
.. _template files:
``files.html``
--------------
Displays result fields from:
- :ref:`macro result_header` and
- :ref:`macro result_sub_header`
Additional fields used in the :origin:`code.html
<searx/templates/simple/result_templates/files.html>`:
filename, size, time: :py:class:`str`
Filename, Filesize and Date of the file.
mtype : ``audio`` | ``video`` | :py:class:`str`
Mimetype type of the file.
subtype : :py:class:`str`
Mimetype / subtype of the file.
abstract : :py:class:`str`
Abstract of the file.
author : :py:class:`str`
Name of the author of the file
embedded : :py:class:`str`
URL of an embedded media type (``audio`` or ``video``) / is collapsible.
.. _template products: .. _template products:
``products.html`` ``products.html``

View File

@@ -56,4 +56,34 @@ If you don't trust anyone, you can set up your own, see :ref:`installation`.
utils/index utils/index
src/index src/index
----------------
Acknowledgements
----------------
The following organizations have provided SearXNG access to their paid plans at
no cost:
.. flat-table::
:widths: 1 1
* - .. image:: /assets/sponsors/docker.svg
:target: https://docker.com
:alt: Docker
:align: center
:height: 100 px
- .. image:: /assets/sponsors/tuta.svg
:target: https://tuta.com
:alt: Tuta
:align: center
:height: 100 px
* - .. image:: /assets/sponsors/browserstack.svg
:target: https://browserstack.com
:alt: BrowserStack
:align: center
:height: 100 px
.. _searx.space: https://searx.space .. _searx.space: https://searx.space

2
manage
View File

@@ -117,7 +117,7 @@ EOF
dev.env() { dev.env() {
go.env.dev go.env.dev
nvm.env nvm.ensure
node.env.dev node.env.dev
export GOENV export GOENV

View File

@@ -1,7 +1,7 @@
[tools] [tools]
# minimal version we support # minimal version we support
python = "3.10" python = "3.10"
node = "24.3.0" node = "25"
go = "1.24.5" go = "1.24.5"
shellcheck = "0.11.0" shellcheck = "0.11.0"
# python 3.10 uses 3.40.1 (on mac and win) # python 3.10 uses 3.40.1 (on mac and win)

View File

@@ -2,9 +2,9 @@ mock==5.2.0
nose2[coverage_plugin]==0.15.1 nose2[coverage_plugin]==0.15.1
cov-core==1.15.0 cov-core==1.15.0
black==25.9.0 black==25.9.0
pylint==3.3.9 pylint==4.0.4
splinter==0.21.0 splinter==0.21.0
selenium==4.36.0 selenium==4.38.0
Pallets-Sphinx-Themes==2.3.0 Pallets-Sphinx-Themes==2.3.0
Sphinx==8.2.3 ; python_version >= '3.11' Sphinx==8.2.3 ; python_version >= '3.11'
Sphinx==8.1.3 ; python_version < '3.11' Sphinx==8.1.3 ; python_version < '3.11'
@@ -23,6 +23,6 @@ wlc==1.16.1
coloredlogs==15.0.1 coloredlogs==15.0.1
docutils>=0.21.2 docutils>=0.21.2
parameterized==0.9.0 parameterized==0.9.0
granian[reload]==2.5.5 granian[reload]==2.6.0
basedpyright==1.31.6 basedpyright==1.35.0
types-lxml==2025.8.25 types-lxml==2025.11.25

View File

@@ -1 +1,2 @@
granian==2.5.5 granian==2.6.0
granian[pname]==2.6.0

View File

@@ -1,4 +1,4 @@
certifi==2025.10.5 certifi==2025.11.12
babel==2.17.0 babel==2.17.0
flask-babel==4.0.0 flask-babel==4.0.0
flask==3.1.2 flask==3.1.2
@@ -9,14 +9,13 @@ 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.10.0
Brotli==1.1.0 sniffio==1.3.1
setproctitle==1.3.7
valkey==6.1.1 valkey==6.1.1
markdown-it-py==3.0.0 markdown-it-py==3.0.0
fasttext-predict==0.9.2.4 fasttext-predict==0.9.2.4
tomli==2.3.0; python_version < '3.11' tomli==2.3.0; python_version < '3.11'
msgspec==0.19.0 msgspec==0.20.0
typer-slim==0.19.2 typer-slim==0.20.0
isodate==0.7.2 isodate==0.7.2
whitenoise==6.11.0 whitenoise==6.11.0
typing-extensions==4.14.1 typing-extensions==4.15.0

View File

@@ -9,7 +9,7 @@ from os.path import dirname, abspath
import logging import logging
import searx.unixthreadname # pylint: disable=unused-import import msgspec
# Debug # Debug
LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s' LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s'
@@ -76,20 +76,22 @@ def get_setting(name: str, default: t.Any = _unset) -> t.Any:
settings and the ``default`` is unset, a :py:obj:`KeyError` is raised. settings and the ``default`` is unset, a :py:obj:`KeyError` is raised.
""" """
value: dict[str, t.Any] = settings value = settings
for a in name.split('.'): for a in name.split('.'):
if isinstance(value, dict): if isinstance(value, msgspec.Struct):
value = value.get(a, _unset) value = getattr(value, a, _unset)
elif isinstance(value, dict):
value = value.get(a, _unset) # pyright: ignore
else: else:
value = _unset # type: ignore value = _unset
if value is _unset: if value is _unset:
if default is _unset: if default is _unset:
raise KeyError(name) raise KeyError(name)
value = default # type: ignore value = default
break break
return value return value # pyright: ignore
def _is_color_terminal(): def _is_color_terminal():

68
searx/brand.py Normal file
View File

@@ -0,0 +1,68 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementations needed for a branding of SearXNG."""
# pylint: disable=too-few-public-methods
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["SettingsBrand"]
import msgspec
class BrandCustom(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
"""Custom settings in the brand section."""
links: dict[str, str] = {}
"""Custom entries in the footer of the WEB page: ``[title]: [link]``"""
class SettingsBrand(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
"""Options for configuring brand properties.
.. code:: yaml
brand:
issue_url: https://github.com/searxng/searxng/issues
docs_url: https://docs.searxng.org
public_instances: https://searx.space
wiki_url: https://github.com/searxng/searxng/wiki
custom:
links:
Uptime: https://uptime.searxng.org/history/example-org
About: https://example.org/user/about.html
"""
issue_url: str = "https://github.com/searxng/searxng/issues"
"""If you host your own issue tracker change this URL."""
docs_url: str = "https://docs.searxng.org"
"""If you host your own documentation change this URL."""
public_instances: str = "https://searx.space"
"""If you host your own https://searx.space change this URL."""
wiki_url: str = "https://github.com/searxng/searxng/wiki"
"""Link to your wiki (or ``false``)"""
custom: BrandCustom = msgspec.field(default_factory=BrandCustom)
"""Optional customizing.
.. autoclass:: searx.brand.BrandCustom
:members:
"""
# new_issue_url is a hackish solution tailored for only one hoster (GH). As
# long as we don't have a more general solution, we should support it in the
# given function, but it should not be expanded further.
new_issue_url: str = "https://github.com/searxng/searxng/issues/new"
"""If you host your own issue tracker not on GitHub, then unset this URL.
Note: This URL will create a pre-filled GitHub bug report form for an
engine. Since this feature is implemented only for GH (and limited to
engines), it will probably be replaced by another solution in the near
future.
"""

View File

@@ -5,10 +5,6 @@
---- ----
""" """
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["ExpireCacheCfg", "ExpireCacheStats", "ExpireCache", "ExpireCacheSQLite"] __all__ = ["ExpireCacheCfg", "ExpireCacheStats", "ExpireCache", "ExpireCacheSQLite"]
import abc import abc

File diff suppressed because it is too large Load Diff

View File

@@ -321,6 +321,7 @@
"ja": "アルゼンチン・ペソ", "ja": "アルゼンチン・ペソ",
"ko": "아르헨티나 페소", "ko": "아르헨티나 페소",
"lt": "Argentinos pesas", "lt": "Argentinos pesas",
"lv": "Argentīnas peso",
"ms": "Peso Argentina", "ms": "Peso Argentina",
"nl": "Argentijnse peso", "nl": "Argentijnse peso",
"oc": "Peso", "oc": "Peso",
@@ -803,6 +804,7 @@
"ja": "ボリビアーノ", "ja": "ボリビアーノ",
"ko": "볼리비아 볼리비아노", "ko": "볼리비아 볼리비아노",
"lt": "Bolivianas", "lt": "Bolivianas",
"lv": "Bolīvijas boliviano",
"ms": "Boliviano", "ms": "Boliviano",
"nl": "Boliviaanse boliviano", "nl": "Boliviaanse boliviano",
"oc": "Boliviano", "oc": "Boliviano",
@@ -848,6 +850,7 @@
"ja": "レアル", "ja": "レアル",
"ko": "브라질 헤알", "ko": "브라질 헤알",
"lt": "Brazilijos realas", "lt": "Brazilijos realas",
"lv": "Brazīlijas reāls",
"ms": "Real Brazil", "ms": "Real Brazil",
"nl": "Braziliaanse real", "nl": "Braziliaanse real",
"oc": "Real", "oc": "Real",
@@ -932,6 +935,7 @@
"ja": "ニュルタム", "ja": "ニュルタム",
"ko": "부탄 눌트럼", "ko": "부탄 눌트럼",
"lt": "Ngultrumas", "lt": "Ngultrumas",
"lv": "ngultrums",
"ml": "ങൾട്രം", "ml": "ങൾട്രം",
"ms": "Ngultrum Bhutan", "ms": "Ngultrum Bhutan",
"nl": "Bhutaanse ngultrum", "nl": "Bhutaanse ngultrum",
@@ -1327,15 +1331,15 @@
"cs": "Kolumbijské peso", "cs": "Kolumbijské peso",
"da": "Colombiansk peso", "da": "Colombiansk peso",
"de": "kolumbianischer Peso", "de": "kolumbianischer Peso",
"en": "Colombian peso", "en": "peso",
"eo": "kolombia peso", "eo": "kolombia peso",
"es": "peso colombiano", "es": "peso",
"et": "Colombia peeso", "et": "Colombia peeso",
"eu": "Peso kolonbiar", "eu": "Peso kolonbiar",
"fi": "Kolumbian peso", "fi": "Kolumbian peso",
"fr": "peso colombien", "fr": "peso colombien",
"ga": "peso na Colóime", "ga": "peso na Colóime",
"gl": "Peso colombiano", "gl": "peso colombiano",
"he": "פסו קולומביאני", "he": "פסו קולומביאני",
"hr": "Kolumbijski pezo", "hr": "Kolumbijski pezo",
"hu": "kolumbiai peso", "hu": "kolumbiai peso",
@@ -1411,9 +1415,9 @@
"cy": "peso (Ciwba)", "cy": "peso (Ciwba)",
"da": "Cubanske pesos", "da": "Cubanske pesos",
"de": "kubanischer Peso", "de": "kubanischer Peso",
"en": "Cuban peso", "en": "peso",
"eo": "kuba peso", "eo": "kuba peso",
"es": "peso cubano", "es": "peso",
"fi": "Kuuban peso", "fi": "Kuuban peso",
"fr": "peso cubain", "fr": "peso cubain",
"ga": "peso Chúba", "ga": "peso Chúba",
@@ -1465,6 +1469,7 @@
"ja": "カーボベルデ・エスクード", "ja": "カーボベルデ・エスクード",
"ko": "카보베르데 이스쿠두", "ko": "카보베르데 이스쿠두",
"lt": "Žaliojo Kyšulio eskudas", "lt": "Žaliojo Kyšulio eskudas",
"lv": "Kaboverdes eskudo",
"nl": "Kaapverdische escudo", "nl": "Kaapverdische escudo",
"oc": "Escut de Cap Verd", "oc": "Escut de Cap Verd",
"pl": "escudo Zielonego Przylądka", "pl": "escudo Zielonego Przylądka",
@@ -1565,7 +1570,7 @@
"ar": "كرونة دنماركية", "ar": "كرونة دنماركية",
"bg": "Датска крона", "bg": "Датска крона",
"ca": "corona danesa", "ca": "corona danesa",
"cs": "Dánská koruna", "cs": "dánská koruna",
"cy": "Krone Danaidd", "cy": "Krone Danaidd",
"da": "dansk krone", "da": "dansk krone",
"de": "dänische Krone", "de": "dänische Krone",
@@ -1715,7 +1720,7 @@
"nl": "Egyptisch pond", "nl": "Egyptisch pond",
"oc": "Liura egipciana", "oc": "Liura egipciana",
"pa": "ਮਿਸਰੀ ਪਾਊਂਡ", "pa": "ਮਿਸਰੀ ਪਾਊਂਡ",
"pl": "Funt egipski", "pl": "funt egipski",
"pt": "libra egípcia", "pt": "libra egípcia",
"ro": "Liră egipteană", "ro": "Liră egipteană",
"ru": "египетский фунт", "ru": "египетский фунт",
@@ -1772,7 +1777,7 @@
"de": "Äthiopischer Birr", "de": "Äthiopischer Birr",
"en": "bir", "en": "bir",
"eo": "etiopa birro", "eo": "etiopa birro",
"es": "Birr etíope", "es": "bir etíope",
"fi": "Etiopian birr", "fi": "Etiopian birr",
"fr": "Birr", "fr": "Birr",
"ga": "birr", "ga": "birr",
@@ -2035,6 +2040,7 @@
"ja": "セディ", "ja": "セディ",
"ko": "가나 세디", "ko": "가나 세디",
"lt": "Sedis", "lt": "Sedis",
"lv": "Ganas sedi",
"ms": "Cedi Ghana", "ms": "Cedi Ghana",
"nl": "Ghanese cedi", "nl": "Ghanese cedi",
"oc": "Cedi", "oc": "Cedi",
@@ -2149,6 +2155,7 @@
"ja": "ギニア・フラン", "ja": "ギニア・フラン",
"ko": "기니 프랑", "ko": "기니 프랑",
"lt": "Gvinėjos frankas", "lt": "Gvinėjos frankas",
"lv": "Gvinejas franks",
"ms": "Franc Guinea", "ms": "Franc Guinea",
"nl": "Guineese frank", "nl": "Guineese frank",
"oc": "Franc guinean", "oc": "Franc guinean",
@@ -2859,6 +2866,7 @@
"sl": "kirgiški som", "sl": "kirgiški som",
"sr": "киргиски сом", "sr": "киргиски сом",
"sv": "Kirgizistansk som", "sv": "Kirgizistansk som",
"szl": "Sōm (waluta)",
"tr": "Kırgızistan somu", "tr": "Kırgızistan somu",
"tt": "кыргыз сумы", "tt": "кыргыз сумы",
"uk": "сом" "uk": "сом"
@@ -2964,6 +2972,7 @@
"ms": "Won Korea Utara", "ms": "Won Korea Utara",
"nl": "Noord-Koreaanse won", "nl": "Noord-Koreaanse won",
"pa": "ਉੱਤਰੀ ਕੋਰੀਆਈ ਵੌਨ", "pa": "ਉੱਤਰੀ ਕੋਰੀਆਈ ਵੌਨ",
"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",
@@ -3792,9 +3801,9 @@
"cs": "Mexické peso", "cs": "Mexické peso",
"cy": "peso (Mecsico)", "cy": "peso (Mecsico)",
"de": "Mexikanischer Peso", "de": "Mexikanischer Peso",
"en": "Mexican peso", "en": "peso",
"eo": "meksika peso", "eo": "meksika peso",
"es": "peso mexicano", "es": "peso",
"et": "Mehhiko peeso", "et": "Mehhiko peeso",
"eu": "Mexikar peso", "eu": "Mexikar peso",
"fi": "Meksikon peso", "fi": "Meksikon peso",
@@ -3810,6 +3819,7 @@
"ja": "メキシコ・ペソ", "ja": "メキシコ・ペソ",
"ko": "멕시코 페소", "ko": "멕시코 페소",
"lt": "Meksikos pesas", "lt": "Meksikos pesas",
"lv": "Meksikas peso",
"ms": "Peso Mexico", "ms": "Peso Mexico",
"nl": "Mexicaanse peso", "nl": "Mexicaanse peso",
"pa": "ਮੈਕਸੀਕੀ ਪੇਸੋ", "pa": "ਮੈਕਸੀਕੀ ਪੇਸੋ",
@@ -3825,7 +3835,7 @@
"tr": "Meksika pesosu", "tr": "Meksika pesosu",
"tt": "Миксикә писысы", "tt": "Миксикә писысы",
"uk": "мексиканський песо", "uk": "мексиканський песо",
"vi": "Peso Mexico" "vi": "Peso México"
}, },
"MXV": { "MXV": {
"de": "UNIDAD DE INVERSION", "de": "UNIDAD DE INVERSION",
@@ -3879,7 +3889,7 @@
"MZN": { "MZN": {
"ar": "مثقال موزنبيقي", "ar": "مثقال موزنبيقي",
"ca": "metical", "ca": "metical",
"cs": "Mosambický metical", "cs": "mosambický metical",
"cy": "Metical Mosambic", "cy": "Metical Mosambic",
"da": "Metical", "da": "Metical",
"de": "Metical", "de": "Metical",
@@ -3972,6 +3982,7 @@
"ja": "ナイラ", "ja": "ナイラ",
"ko": "나이지리아 나이라", "ko": "나이지리아 나이라",
"lt": "Naira", "lt": "Naira",
"lv": "Nigērijas naira",
"ms": "Naira Nigeria", "ms": "Naira Nigeria",
"nl": "Nigeriaanse naira", "nl": "Nigeriaanse naira",
"oc": "Naira", "oc": "Naira",
@@ -4028,7 +4039,7 @@
"ar": "كرونة نروجية", "ar": "كرونة نروجية",
"bg": "норвежка крона", "bg": "норвежка крона",
"ca": "corona noruega", "ca": "corona noruega",
"cs": "Norská koruna", "cs": "norská koruna",
"cy": "krone Norwy", "cy": "krone Norwy",
"da": "norsk krone", "da": "norsk krone",
"de": "norwegische Krone", "de": "norwegische Krone",
@@ -4208,7 +4219,7 @@
"fi": "Panaman balboa", "fi": "Panaman balboa",
"fr": "Balboa", "fr": "Balboa",
"ga": "balboa Phanama", "ga": "balboa Phanama",
"gl": "Balboa", "gl": "balboa",
"he": "בלבואה", "he": "בלבואה",
"hr": "Panamska balboa", "hr": "Panamska balboa",
"hu": "panamai balboa", "hu": "panamai balboa",
@@ -4255,6 +4266,7 @@
"ja": "ヌエボ・ソル", "ja": "ヌエボ・ソル",
"ko": "페루 솔", "ko": "페루 솔",
"lt": "Naujasis solis", "lt": "Naujasis solis",
"lv": "Peru sols",
"ms": "Nuevo Sol Peru", "ms": "Nuevo Sol Peru",
"nl": "Peruviaanse sol", "nl": "Peruviaanse sol",
"oc": "Nuevo Sol", "oc": "Nuevo Sol",
@@ -4269,7 +4281,7 @@
"tr": "Nuevo Sol", "tr": "Nuevo Sol",
"tt": "Перу яңа соле", "tt": "Перу яңа соле",
"uk": "Новий соль", "uk": "Новий соль",
"vi": "Sol Peru" "vi": "Sol Perú"
}, },
"PGK": { "PGK": {
"ar": "كينا بابوا غينيا الجديدة", "ar": "كينا بابوا غينيا الجديدة",
@@ -4779,7 +4791,7 @@
"en": "Solomon Islands dollar", "en": "Solomon Islands dollar",
"eo": "salomona dolaro", "eo": "salomona dolaro",
"es": "dólar de las Islas Salomón", "es": "dólar de las Islas Salomón",
"fi": "Salomonsaarten dollari", "fi": "Salomoninsaarten dollari",
"fr": "dollar des îles Salomon", "fr": "dollar des îles Salomon",
"ga": "dollar Oileáin Sholaimh", "ga": "dollar Oileáin Sholaimh",
"gl": "Dólar das Illas Salomón", "gl": "Dólar das Illas Salomón",
@@ -4926,7 +4938,7 @@
"ar": "دولار سنغافوري", "ar": "دولار سنغافوري",
"bg": "Сингапурски долар", "bg": "Сингапурски долар",
"bn": "সিঙ্গাপুর ডলার", "bn": "সিঙ্গাপুর ডলার",
"ca": "dòlar de Singapur", "ca": "dòlar singapurès",
"cs": "Singapurský dolar", "cs": "Singapurský dolar",
"da": "singaporeansk dollar", "da": "singaporeansk dollar",
"de": "Singapur-Dollar", "de": "Singapur-Dollar",
@@ -5015,6 +5027,7 @@
"ja": "レオン", "ja": "レオン",
"ko": "시에라리온 레온", "ko": "시에라리온 레온",
"lt": "leonė", "lt": "leonė",
"lv": "Sjerraleones leone",
"ms": "leone", "ms": "leone",
"nl": "Sierra Leoonse leone", "nl": "Sierra Leoonse leone",
"oc": "leone", "oc": "leone",
@@ -5052,6 +5065,7 @@
"ja": "ソマリア・シリング", "ja": "ソマリア・シリング",
"ko": "소말리아 실링", "ko": "소말리아 실링",
"lt": "Somalio šilingas", "lt": "Somalio šilingas",
"lv": "Somālijas šiliņš",
"ms": "Shilling Somalia", "ms": "Shilling Somalia",
"nl": "Somalische shilling", "nl": "Somalische shilling",
"pl": "Szyling somalijski", "pl": "Szyling somalijski",
@@ -5497,7 +5511,7 @@
"TTD": { "TTD": {
"ar": "دولار ترينيداد وتوباغو", "ar": "دولار ترينيداد وتوباغو",
"bg": "Тринидадски и тобагски долар", "bg": "Тринидадски и тобагски долар",
"ca": "dòlar de Trinitat i Tobago", "ca": "dòlar de Trinidad i Tobago",
"cs": "Dolar Trinidadu a Tobaga", "cs": "Dolar Trinidadu a Tobaga",
"cy": "doler Trinidad a Thobago", "cy": "doler Trinidad a Thobago",
"de": "Trinidad-und-Tobago-Dollar", "de": "Trinidad-und-Tobago-Dollar",
@@ -5534,7 +5548,7 @@
"af": "Nuwe Taiwannese dollar", "af": "Nuwe Taiwannese dollar",
"ar": "دولار تايواني جديد", "ar": "دولار تايواني جديد",
"bg": "Нов тайвански долар", "bg": "Нов тайвански долар",
"ca": "nou dòlar de Taiwan", "ca": "Nou dòlar taiwanès",
"cs": "Tchajwanský dolar", "cs": "Tchajwanský dolar",
"cy": "Doler Newydd Taiwan", "cy": "Doler Newydd Taiwan",
"da": "taiwan dollar", "da": "taiwan dollar",
@@ -5715,7 +5729,7 @@
"lv": "ASV dolārs", "lv": "ASV dolārs",
"ml": "യുണൈറ്റഡ് സ്റ്റേറ്റ്സ് ഡോളർ", "ml": "യുണൈറ്റഡ് സ്റ്റേറ്റ്സ് ഡോളർ",
"ms": "Dolar Amerika Syarikat", "ms": "Dolar Amerika Syarikat",
"nl": "US dollar", "nl": "Amerikaanse dollar",
"oc": "dolar american", "oc": "dolar american",
"pa": "ਸੰਯੁਕਤ ਰਾਜ ਡਾਲਰ", "pa": "ਸੰਯੁਕਤ ਰਾਜ ਡਾਲਰ",
"pap": "Dollar merikano", "pap": "Dollar merikano",
@@ -5808,7 +5822,9 @@
"lt": "Uzbekijos sumas", "lt": "Uzbekijos sumas",
"lv": "Uzbekistānas soms", "lv": "Uzbekistānas soms",
"nl": "Oezbeekse sum", "nl": "Oezbeekse sum",
"oc": "som ozbèc",
"pa": "ਉਜ਼ਬੇਕਿਸਤਾਨੀ ਸੋਮ", "pa": "ਉਜ਼ਬੇਕਿਸਤਾਨੀ ਸੋਮ",
"pap": "som usbekistani",
"pl": "Sum", "pl": "Sum",
"pt": "som usbeque", "pt": "som usbeque",
"ro": "Som uzbec", "ro": "Som uzbec",
@@ -5834,6 +5850,7 @@
"en": "sovereign bolivar", "en": "sovereign bolivar",
"es": "bolívar soberano", "es": "bolívar soberano",
"fr": "bolivar souverain", "fr": "bolivar souverain",
"gl": "bolívar soberano",
"hu": "venezuelai bolívar", "hu": "venezuelai bolívar",
"ja": "ボリバル・ソベラノ", "ja": "ボリバル・ソベラノ",
"pt": "Bolívar soberano", "pt": "Bolívar soberano",
@@ -5948,6 +5965,7 @@
"sk": "Tala", "sk": "Tala",
"sr": "самоанска тала", "sr": "самоанска тала",
"sv": "Samoansk Tala", "sv": "Samoansk Tala",
"tr": "Samoa talası",
"tt": "самоа таласы", "tt": "самоа таласы",
"uk": "Самоанська тала" "uk": "Самоанська тала"
}, },
@@ -6095,12 +6113,14 @@
"hu": "karibi forint", "hu": "karibi forint",
"it": "fiorino caraibico", "it": "fiorino caraibico",
"ja": "カリブ・ギルダー", "ja": "カリブ・ギルダー",
"ko": "카리브 휠던",
"nl": "Caribische gulden", "nl": "Caribische gulden",
"pap": "Florin karibense", "pap": "Florin karibense",
"pl": "Gulden karaibski", "pl": "Gulden karaibski",
"pt": "Florim do Caribe", "pt": "Florim do Caribe",
"ro": "Gulden caraibian", "ro": "Gulden caraibian",
"ru": "Карибский гульден", "ru": "Карибский гульден",
"sk": "Karibský gulden",
"sl": "karibski goldinar" "sl": "karibski goldinar"
}, },
"XDR": { "XDR": {
@@ -6571,10 +6591,13 @@
"R": "ZAR", "R": "ZAR",
"R$": "BRL", "R$": "BRL",
"RD$": "DOP", "RD$": "DOP",
"RF": "RWF",
"RM": "MYR", "RM": "MYR",
"RWF": "RWF",
"Rf": "MVR", "Rf": "MVR",
"Rp": "IDR", "Rp": "IDR",
"Rs": "LKR", "Rs": "LKR",
"R₣": "RWF",
"S$": "SGD", "S$": "SGD",
"S/.": "PEN", "S/.": "PEN",
"SI$": "SBD", "SI$": "SBD",
@@ -6594,6 +6617,7 @@
"Ush": "UGX", "Ush": "UGX",
"VT": "VUV", "VT": "VUV",
"WS$": "WST", "WS$": "WST",
"XAF": "XAF",
"XCG": "XCG", "XCG": "XCG",
"XDR": "XDR", "XDR": "XDR",
"Z$": "ZWL", "Z$": "ZWL",
@@ -6719,6 +6743,7 @@
"argentinské peso": "ARS", "argentinské peso": "ARS",
"argentinski peso": "ARS", "argentinski peso": "ARS",
"argentinski pezo": "ARS", "argentinski pezo": "ARS",
"argentīnas peso": "ARS",
"ariari": "MGA", "ariari": "MGA",
"ariari de madagascar": "MGA", "ariari de madagascar": "MGA",
"ariari de madagáscar": "MGA", "ariari de madagáscar": "MGA",
@@ -7038,6 +7063,7 @@
"bolívar soberano": "VES", "bolívar soberano": "VES",
"bolívar sobirà": "VES", "bolívar sobirà": "VES",
"bolíviai boliviano": "BOB", "bolíviai boliviano": "BOB",
"bolīvijas boliviano": "BOB",
"bosenská konvertibilní marka": "BAM", "bosenská konvertibilní marka": "BAM",
"bosna hersek değiştirilebilir markı": "BAM", "bosna hersek değiştirilebilir markı": "BAM",
"bosnia and herzegovina convertible mark": "BAM", "bosnia and herzegovina convertible mark": "BAM",
@@ -7074,6 +7100,7 @@
"brazilski real": "BRL", "brazilski real": "BRL",
"brazilský real": "BRL", "brazilský real": "BRL",
"brazílsky real": "BRL", "brazílsky real": "BRL",
"brazīlijas reāls": "BRL",
"brezilya reali": "BRL", "brezilya reali": "BRL",
"brit font": "GBP", "brit font": "GBP",
"brita pundo": "GBP", "brita pundo": "GBP",
@@ -7147,6 +7174,7 @@
"burundžio frankas": "BIF", "burundžio frankas": "BIF",
"butana ngultrumo": "BTN", "butana ngultrumo": "BTN",
"butanski ngultrum": "BTN", "butanski ngultrum": "BTN",
"butānas ngultrums": "BTN",
"butut": "GMD", "butut": "GMD",
"bututs": "GMD", "bututs": "GMD",
"bwp": "BWP", "bwp": "BWP",
@@ -7818,6 +7846,7 @@
"dirrã marroquino": "MAD", "dirrã marroquino": "MAD",
"dírham de los emiratos árabes unidos": "AED", "dírham de los emiratos árabes unidos": "AED",
"dírham dels emirats àrabs units": "AED", "dírham dels emirats àrabs units": "AED",
"dírham emiratià": "AED",
"dírham marroquí": "MAD", "dírham marroquí": "MAD",
"djf": "DJF", "djf": "DJF",
"djiboeti frank": "DJF", "djiboeti frank": "DJF",
@@ -8232,9 +8261,7 @@
"dòlar de singapur": "SGD", "dòlar de singapur": "SGD",
"dòlar de surinam": "SRD", "dòlar de surinam": "SRD",
"dòlar de taiwan": "TWD", "dòlar de taiwan": "TWD",
"dòlar de trinitat": "TTD", "dòlar de trinidad i tobago": "TTD",
"dòlar de trinitat i tobago": "TTD",
"dòlar de trinitat tobago": "TTD",
"dòlar de zimbàbue": "ZWL", "dòlar de zimbàbue": "ZWL",
"dòlar del canadà": "CAD", "dòlar del canadà": "CAD",
"dòlar del carib oriental": "XCD", "dòlar del carib oriental": "XCD",
@@ -8250,6 +8277,7 @@
"dòlar namibià": "NAD", "dòlar namibià": "NAD",
"dòlar neozelandès": "NZD", "dòlar neozelandès": "NZD",
"dòlar salomonès": "SBD", "dòlar salomonès": "SBD",
"dòlar singapurès": "SGD",
"dòlar surinamès": "SRD", "dòlar surinamès": "SRD",
"dòlar taiwanès": "TWD", "dòlar taiwanès": "TWD",
"dòlars canadencs": "CAD", "dòlars canadencs": "CAD",
@@ -8894,6 +8922,7 @@
"gambijski dalasi": "GMD", "gambijski dalasi": "GMD",
"gambijský dalasi": "GMD", "gambijský dalasi": "GMD",
"ganaa cedio": "GHS", "ganaa cedio": "GHS",
"ganas sedi": "GHS",
"ganski cedi": "GHS", "ganski cedi": "GHS",
"gbp": "GBP", "gbp": "GBP",
"gbp£": "GBP", "gbp£": "GBP",
@@ -9043,6 +9072,7 @@
"gvatemalski kvecal": "GTQ", "gvatemalski kvecal": "GTQ",
"gvatemalski quetzal": "GTQ", "gvatemalski quetzal": "GTQ",
"gvinea franko": "GNF", "gvinea franko": "GNF",
"gvinejas franks": "GNF",
"gvinejski franak": "GNF", "gvinejski franak": "GNF",
"gvinejski frank": "GNF", "gvinejski frank": "GNF",
"gvinėjos frankas": "GNF", "gvinėjos frankas": "GNF",
@@ -9370,6 +9400,7 @@
"kaaimaneilandse dollar": "KYD", "kaaimaneilandse dollar": "KYD",
"kaapverdische escudo": "CVE", "kaapverdische escudo": "CVE",
"kaboverda eskudo": "CVE", "kaboverda eskudo": "CVE",
"kaboverdes eskudo": "CVE",
"kaiman dollar": "KYD", "kaiman dollar": "KYD",
"kaimanu dolārs": "KYD", "kaimanu dolārs": "KYD",
"kaimanu salu dolārs": "KYD", "kaimanu salu dolārs": "KYD",
@@ -9779,6 +9810,7 @@
"lari na seoirsia": "GEL", "lari na seoirsia": "GEL",
"lario": "GEL", "lario": "GEL",
"laris": "GEL", "laris": "GEL",
"lári": "GEL",
"länsi afrikan cfa frangi": "XOF", "länsi afrikan cfa frangi": "XOF",
"lbp": "LBP", "lbp": "LBP",
"ld": "LYD", "ld": "LYD",
@@ -10305,6 +10337,7 @@
"meksika peso": "MXN", "meksika peso": "MXN",
"meksika pesosu": "MXN", "meksika pesosu": "MXN",
"meksikaanse peso": "MXN", "meksikaanse peso": "MXN",
"meksikas peso": "MXN",
"meksikon peso": "MXN", "meksikon peso": "MXN",
"meksikos pesas": "MXN", "meksikos pesas": "MXN",
"meticais": "MZN", "meticais": "MZN",
@@ -10513,6 +10546,7 @@
"ngultrum na bútáine": "BTN", "ngultrum na bútáine": "BTN",
"ngultrumas": "BTN", "ngultrumas": "BTN",
"ngultrumo": "BTN", "ngultrumo": "BTN",
"ngultrums": "BTN",
"ngwee": "ZMW", "ngwee": "ZMW",
"nhân dân tệ": "CNY", "nhân dân tệ": "CNY",
"nhân dân tệ trung quốc": "CNY", "nhân dân tệ trung quốc": "CNY",
@@ -10540,6 +10574,7 @@
"nigerijská naira": "NGN", "nigerijská naira": "NGN",
"nigériai naira": "NGN", "nigériai naira": "NGN",
"nigérijská naira": "NGN", "nigérijská naira": "NGN",
"nigērijas naira": "NGN",
"niĝera najro": "NGN", "niĝera najro": "NGN",
"niĝeria najro": "NGN", "niĝeria najro": "NGN",
"nijerya nairası": "NGN", "nijerya nairası": "NGN",
@@ -10668,7 +10703,6 @@
"nuevo dólar taiwanes": "TWD", "nuevo dólar taiwanes": "TWD",
"nuevo dólar taiwanés": "TWD", "nuevo dólar taiwanés": "TWD",
"nuevo peso": [ "nuevo peso": [
"UYU",
"MXN", "MXN",
"ARS" "ARS"
], ],
@@ -10866,6 +10900,7 @@
"penny": "GBP", "penny": "GBP",
"perak sebagai pelaburan": "XAG", "perak sebagai pelaburan": "XAG",
"peru nueva solü": "PEN", "peru nueva solü": "PEN",
"peru sols": "PEN",
"perua nova suno": "PEN", "perua nova suno": "PEN",
"peruanischer nuevo sol": "PEN", "peruanischer nuevo sol": "PEN",
"peruanischer sol": "PEN", "peruanischer sol": "PEN",
@@ -10940,7 +10975,6 @@
"peso de méxico": "MXN", "peso de méxico": "MXN",
"peso de republica dominicana": "DOP", "peso de republica dominicana": "DOP",
"peso de república dominicana": "DOP", "peso de república dominicana": "DOP",
"peso de uruguay": "UYU",
"peso de xile": "CLP", "peso de xile": "CLP",
"peso do chile": "CLP", "peso do chile": "CLP",
"peso do uruguai": "UYU", "peso do uruguai": "UYU",
@@ -11587,7 +11621,6 @@
"rúpia indiana": "INR", "rúpia indiana": "INR",
"rúpies": "INR", "rúpies": "INR",
"rūpija": "IDR", "rūpija": "IDR",
"rwanda franc": "RWF",
"rwanda frank": "RWF", "rwanda frank": "RWF",
"rwandan franc": "RWF", "rwandan franc": "RWF",
"rwandan frank": "RWF", "rwandan frank": "RWF",
@@ -11629,6 +11662,7 @@
"samoa dolaro": "WST", "samoa dolaro": "WST",
"samoa tala": "WST", "samoa tala": "WST",
"samoa talao": "WST", "samoa talao": "WST",
"samoa talası": "WST",
"samoaanse tala": "WST", "samoaanse tala": "WST",
"samoan tala": "WST", "samoan tala": "WST",
"samoan tālā": "WST", "samoan tālā": "WST",
@@ -11827,6 +11861,7 @@
"sistema unificato di compensazione regionale": "XSU", "sistema unificato di compensazione regionale": "XSU",
"sistema único de compensación regional": "XSU", "sistema único de compensación regional": "XSU",
"sjekel": "ILS", "sjekel": "ILS",
"sjerraleones leone": "SLE",
"sjevernokorejski von": "KPW", "sjevernokorejski von": "KPW",
"sle": "SLE", "sle": "SLE",
"sll": "SLE", "sll": "SLE",
@@ -11839,10 +11874,10 @@
"sol d'or": "PEN", "sol d'or": "PEN",
"sol de oro": "PEN", "sol de oro": "PEN",
"sol novo": "PEN", "sol novo": "PEN",
"sol peru": "PEN",
"sol peruan": "PEN", "sol peruan": "PEN",
"sol peruano": "PEN", "sol peruano": "PEN",
"sol peruviano": "PEN", "sol peruviano": "PEN",
"sol perú": "PEN",
"solomon adaları doları": "SBD", "solomon adaları doları": "SBD",
"solomon dollar": "SBD", "solomon dollar": "SBD",
"solomon islands dollar": "SBD", "solomon islands dollar": "SBD",
@@ -11868,8 +11903,10 @@
"som kîrgîz": "KGS", "som kîrgîz": "KGS",
"som na cirgeastáine": "KGS", "som na cirgeastáine": "KGS",
"som na húisbéiceastáine": "UZS", "som na húisbéiceastáine": "UZS",
"som ozbèc": "UZS",
"som quirguiz": "KGS", "som quirguiz": "KGS",
"som usbeco": "UZS", "som usbeco": "UZS",
"som usbekistani": "UZS",
"som usbeque": "UZS", "som usbeque": "UZS",
"som uzbec": "UZS", "som uzbec": "UZS",
"som uzbeco": "UZS", "som uzbeco": "UZS",
@@ -11892,6 +11929,7 @@
"somas": "KGS", "somas": "KGS",
"somálsky šiling": "SOS", "somálsky šiling": "SOS",
"somálský šilink": "SOS", "somálský šilink": "SOS",
"somālijas šiliņš": "SOS",
"some": "KGS", "some": "KGS",
"somoni": "TJS", "somoni": "TJS",
"somoni na táidsíceastáine": "TJS", "somoni na táidsíceastáine": "TJS",
@@ -11915,6 +11953,7 @@
"sovjetisk rubel": "RUB", "sovjetisk rubel": "RUB",
"soʻm": "UZS", "soʻm": "UZS",
"soʻm uzbekistan": "UZS", "soʻm uzbekistan": "UZS",
"sōm": "KGS",
"söm": "UZS", "söm": "UZS",
"special drawing right": "XDR", "special drawing right": "XDR",
"special drawing rights": "XDR", "special drawing rights": "XDR",
@@ -12660,6 +12699,7 @@
"won nord coréen": "KPW", "won nord coréen": "KPW",
"won nordcoreano": "KPW", "won nordcoreano": "KPW",
"won norte coreano": "KPW", "won norte coreano": "KPW",
"won nortkoreano": "KPW",
"won południowokoreański": "KRW", "won południowokoreański": "KRW",
"won północnokoreański": "KPW", "won północnokoreański": "KPW",
"won sud corean": "KRW", "won sud corean": "KRW",
@@ -14440,6 +14480,7 @@
"דולר פיג'י": "FJD", "דולר פיג'י": "FJD",
"דולר קיימני": "KYD", "דולר קיימני": "KYD",
"דולר קנדי": "CAD", "דולר קנדי": "CAD",
"דולר של איי קיימן": "KYD",
"דונג וייטנאמי ": "VND", "דונג וייטנאמי ": "VND",
"דינר אלג'ירי": "DZD", "דינר אלג'ירי": "DZD",
"דינר בחרייני": "BHD", "דינר בחרייני": "BHD",
@@ -14647,6 +14688,7 @@
"الجنيه الإسترليني": "GBP", "الجنيه الإسترليني": "GBP",
"الجنيه السودانى": "SDG", "الجنيه السودانى": "SDG",
"الجنيه المصري": "EGP", "الجنيه المصري": "EGP",
"الدولار الامريكي": "IQD",
"الدولار البربادوسي": "BBD", "الدولار البربادوسي": "BBD",
"الدولار البهامي": "BSD", "الدولار البهامي": "BSD",
"الدولار الكندي": "CAD", "الدولار الكندي": "CAD",
@@ -14906,6 +14948,7 @@
"شيلينغ كينيي": "KES", "شيلينغ كينيي": "KES",
"عملة السعودية": "SAR", "عملة السعودية": "SAR",
"عملة المملكة العربية السعودية": "SAR", "عملة المملكة العربية السعودية": "SAR",
"عملة ذهبيه": "IQD",
"عملة قطر": "QAR", "عملة قطر": "QAR",
"غواراني": "PYG", "غواراني": "PYG",
"غواراني باراغواي": "PYG", "غواراني باراغواي": "PYG",
@@ -15354,7 +15397,6 @@
"యునైటెడ్ స్టేట్స్ డాలర్": "USD", "యునైటెడ్ స్టేట్స్ డాలర్": "USD",
"యూరో": "EUR", "యూరో": "EUR",
"రూపాయి": "INR", "రూపాయి": "INR",
"సంయుక్త రాష్ట్రాల డాలర్": "USD",
"స్విస్ ఫ్రాంక్": "CHF", "స్విస్ ఫ్రాంక్": "CHF",
"അൾജീരിയൻ ദിനാർ": "DZD", "അൾജീരിയൻ ദിനാർ": "DZD",
"ഇന്തോനേഷ്യൻ റുപിയ": "IDR", "ഇന്തോനേഷ്യൻ റുപിയ": "IDR",
@@ -15735,6 +15777,7 @@
"203" "203"
], ],
"칠레 페소": "CLP", "칠레 페소": "CLP",
"카리브 휠던": "XCG",
"카보베르데 에스쿠도": "CVE", "카보베르데 에스쿠도": "CVE",
"카보베르데 이스쿠두": "CVE", "카보베르데 이스쿠두": "CVE",
"카보베르데에스쿠도": "CVE", "카보베르데에스쿠도": "CVE",

File diff suppressed because one or more lines are too long

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
import typing as t import typing as t
@@ -119,6 +120,12 @@ class TrackerPatternsDB:
for rule in self.rules(): for rule in self.rules():
query_str: str = parsed_new_url.query
if not query_str:
# There are no more query arguments in the parsed_new_url on
# which rules can be applied, stop iterating over the rules.
break
if not re.match(rule[self.Fields.url_regexp], new_url): if not re.match(rule[self.Fields.url_regexp], new_url):
# no match / ignore pattern # no match / ignore pattern
continue continue
@@ -136,18 +143,32 @@ class TrackerPatternsDB:
# overlapping urlPattern like ".*" # overlapping urlPattern like ".*"
continue continue
# remove tracker arguments from the url-query part
query_args: list[tuple[str, str]] = list(parse_qsl(parsed_new_url.query)) query_args: list[tuple[str, str]] = list(parse_qsl(parsed_new_url.query))
if query_args:
# remove tracker arguments from the url-query part
for name, val in query_args.copy():
# remove URL arguments
for pattern in rule[self.Fields.del_args]:
if re.match(pattern, name):
log.debug(
"TRACKER_PATTERNS: %s remove tracker arg: %s='%s'", parsed_new_url.netloc, name, val
)
query_args.remove((name, val))
for name, val in query_args.copy(): parsed_new_url = parsed_new_url._replace(query=urlencode(query_args))
# remove URL arguments new_url = urlunparse(parsed_new_url)
else:
# The query argument for URLs like:
# - 'http://example.org?q=' --> query_str is 'q=' and query_args is []
# - 'http://example.org?/foo/bar' --> query_str is 'foo/bar' and query_args is []
# is a simple string and not a key/value dict.
for pattern in rule[self.Fields.del_args]: for pattern in rule[self.Fields.del_args]:
if re.match(pattern, name): if re.match(pattern, query_str):
log.debug("TRACKER_PATTERNS: %s remove tracker arg: %s='%s'", parsed_new_url.netloc, name, val) log.debug("TRACKER_PATTERNS: %s remove tracker arg: '%s'", parsed_new_url.netloc, query_str)
query_args.remove((name, val)) parsed_new_url = parsed_new_url._replace(query="")
new_url = urlunparse(parsed_new_url)
parsed_new_url = parsed_new_url._replace(query=urlencode(query_args)) break
new_url = urlunparse(parsed_new_url)
if new_url != url: if new_url != url:
return new_url return new_url

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": [
"143.0", "145.0",
"142.0" "144.0"
] ]
} }

View File

@@ -3294,6 +3294,16 @@
"symbol": "slug", "symbol": "slug",
"to_si_factor": 14.593903 "to_si_factor": 14.593903
}, },
"Q136416965": {
"si_name": null,
"symbol": "GT/S",
"to_si_factor": null
},
"Q136417074": {
"si_name": null,
"symbol": "MT/S",
"to_si_factor": null
},
"Q1374438": { "Q1374438": {
"si_name": "Q11574", "si_name": "Q11574",
"symbol": "ks", "symbol": "ks",
@@ -5449,6 +5459,11 @@
"symbol": "T", "symbol": "T",
"to_si_factor": 907.18474 "to_si_factor": 907.18474
}, },
"Q4741": {
"si_name": null,
"symbol": "RF",
"to_si_factor": null
},
"Q474533": { "Q474533": {
"si_name": null, "si_name": null,
"symbol": "At", "symbol": "At",
@@ -6375,9 +6390,9 @@
"to_si_factor": 86400.0 "to_si_factor": 86400.0
}, },
"Q577": { "Q577": {
"si_name": null, "si_name": "Q11574",
"symbol": "a", "symbol": "a",
"to_si_factor": null "to_si_factor": 31557600.0
}, },
"Q57899268": { "Q57899268": {
"si_name": "Q3332095", "si_name": "Q3332095",

View File

@@ -270,7 +270,14 @@ def load_engines(engine_list: list[dict[str, t.Any]]):
categories.clear() categories.clear()
categories['general'] = [] categories['general'] = []
for engine_data in engine_list: for engine_data in engine_list:
if engine_data.get("inactive") is True:
continue
engine = load_engine(engine_data) engine = load_engine(engine_data)
if engine: if engine:
register_engine(engine) register_engine(engine)
else:
# if an engine can't be loaded (if for example the engine is missing
# tor or some other requirements) its set to inactive!
logger.error("loading engine %s failed: set engine to inactive!", engine_data.get("name", "???"))
engine_data["inactive"] = True
return engines return engines

View File

@@ -12,7 +12,7 @@ from urllib.parse import urlencode, urljoin, urlparse
import lxml import lxml
import babel import babel
from searx.utils import extract_text, eval_xpath_list, eval_xpath_getindex from searx.utils import extract_text, eval_xpath_list, eval_xpath_getindex, searxng_useragent
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.locales import language_tag from searx.locales import language_tag
@@ -45,7 +45,7 @@ def request(query, params):
query += ' (' + eng_lang + ')' query += ' (' + eng_lang + ')'
# wiki.archlinux.org is protected by anubis # wiki.archlinux.org is protected by anubis
# - https://github.com/searxng/searxng/issues/4646#issuecomment-2817848019 # - https://github.com/searxng/searxng/issues/4646#issuecomment-2817848019
params['headers']['User-Agent'] = "SearXNG" params['headers']['User-Agent'] = searxng_useragent()
elif netloc == 'wiki.archlinuxcn.org': elif netloc == 'wiki.archlinuxcn.org':
base_url = 'https://' + netloc + '/wzh/index.php?' base_url = 'https://' + netloc + '/wzh/index.php?'
@@ -120,7 +120,7 @@ def fetch_traits(engine_traits: EngineTraits):
'zh': 'Special:搜索', 'zh': 'Special:搜索',
} }
resp = get('https://wiki.archlinux.org/') resp = get('https://wiki.archlinux.org/', timeout=3)
if not resp.ok: # type: ignore if not resp.ok: # type: ignore
print("ERROR: response from wiki.archlinux.org is not OK.") print("ERROR: response from wiki.archlinux.org is not OK.")

View File

@@ -50,7 +50,7 @@ def response(resp):
pos = script.index(end_tag) + len(end_tag) - 1 pos = script.index(end_tag) + len(end_tag) - 1
script = script[:pos] script = script[:pos]
json_resp = utils.js_variable_to_python(script) json_resp = utils.js_obj_str_to_python(script)
results = [] results = []

190
searx/engines/azure.py Normal file
View File

@@ -0,0 +1,190 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Engine for Azure resources. This engine mimics the standard search bar in Azure
Portal (for resources and resource groups).
Configuration
=============
You must `register an application in Microsoft Entra ID`_ and assign it the
'Reader' role in your subscription.
To use this engine, add an entry similar to the following to your engine list in
``settings.yml``:
.. code:: yaml
- name: azure
engine: azure
...
azure_tenant_id: "your_tenant_id"
azure_client_id: "your_client_id"
azure_client_secret: "your_client_secret"
azure_token_expiration_seconds: 5000
.. _register an application in Microsoft Entra ID:
https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
"""
import typing as t
from searx.enginelib import EngineCache
from searx.network import post as http_post
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
engine_type = "online"
categories = ["it", "cloud"]
# Default values, should be overridden in settings.yml
azure_tenant_id = ""
azure_client_id = ""
azure_client_secret = ""
azure_token_expiration_seconds = 5000
"""Time for which an auth token is valid (sec.)"""
azure_batch_endpoint = "https://management.azure.com/batch?api-version=2020-06-01"
about = {
"website": "https://www.portal.azure.com",
"wikidata_id": "Q725967",
"official_api_documentation": "https://learn.microsoft.com/en-us/\
rest/api/azure-resourcegraph/?view=rest-azureresourcegraph-resourcegraph-2024-04-01",
"use_official_api": True,
"require_api_key": True,
"results": "JSON",
"language": "en",
}
CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
seconds."""
def setup(engine_settings: dict[str, t.Any]) -> bool:
"""Initialization of the engine.
- Instantiate a cache for this engine (:py:obj:`CACHE`).
- Checks whether the tenant_id, client_id and client_secret are set,
otherwise the engine is inactive.
"""
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
missing_opts: list[str] = []
for opt in ("azure_tenant_id", "azure_client_id", "azure_client_secret"):
if not engine_settings.get(opt, ""):
missing_opts.append(opt)
if missing_opts:
logger.error("missing values for options: %s", ", ".join(missing_opts))
return False
return True
def authenticate(t_id: str, c_id: str, c_secret: str) -> str:
"""Authenticates to Azure using Oauth2 Client Credentials Flow and returns
an access token."""
url = f"https://login.microsoftonline.com/{t_id}/oauth2/v2.0/token"
body = {
"client_id": c_id,
"client_secret": c_secret,
"grant_type": "client_credentials",
"scope": "https://management.azure.com/.default",
}
resp: SXNG_Response = http_post(url, body, timeout=5)
if resp.status_code != 200:
raise RuntimeError(f"Azure authentication failed (status {resp.status_code}): {resp.text}")
return resp.json()["access_token"]
def get_auth_token(t_id: str, c_id: str, c_secret: str) -> str:
key = f"azure_tenant_id: {t_id:}, azure_client_id: {c_id}, azure_client_secret: {c_secret}"
token: str | None = CACHE.get(key)
if token:
return token
token = authenticate(t_id, c_id, c_secret)
CACHE.set(key=key, value=token, expire=azure_token_expiration_seconds)
return token
def request(query: str, params: "OnlineParams") -> None:
token = get_auth_token(azure_tenant_id, azure_client_id, azure_client_secret)
params["url"] = azure_batch_endpoint
params["method"] = "POST"
params["headers"]["Authorization"] = f"Bearer {token}"
params["headers"]["Content-Type"] = "application/json"
params["json"] = {
"requests": [
{
"url": "/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01",
"httpMethod": "POST",
"name": "resourceGroups",
"requestHeaderDetails": {"commandName": "Microsoft.ResourceGraph"},
"content": {
"query": (
f"ResourceContainers"
f" | where (name contains ('{query}'))"
f" | where (type =~ ('Microsoft.Resources/subscriptions/resourcegroups'))"
f" | project id,name,type,kind,subscriptionId,resourceGroup"
f" | extend matchscore = name startswith '{query}'"
f" | extend normalizedName = tolower(tostring(name))"
f" | sort by matchscore desc, normalizedName asc"
f" | take 30"
)
},
},
{
"url": "/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01",
"httpMethod": "POST",
"name": "resources",
"requestHeaderDetails": {
"commandName": "Microsoft.ResourceGraph",
},
"content": {
"query": f"Resources | where name contains '{query}' | take 30",
},
},
]
}
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_data = resp.json()
for result in json_data["responses"]:
if result["name"] == "resourceGroups":
for data in result["content"]["data"]:
res.add(
res.types.MainResult(
url=(
f"https://portal.azure.com/#@/resource"
f"/subscriptions/{data['subscriptionId']}/resourceGroups/{data['name']}/overview"
),
title=data["name"],
content=f"Resource Group in Subscription: {data['subscriptionId']}",
)
)
elif result["name"] == "resources":
for data in result["content"]["data"]:
res.add(
res.types.MainResult(
url=(
f"https://portal.azure.com/#@/resource"
f"/subscriptions/{data['subscriptionId']}/resourceGroups/{data['resourceGroup']}"
f"/providers/{data['type']}/{data['name']}/overview"
),
title=data["name"],
content=(
f"Resource of type {data['type']} in Subscription:"
f" {data['subscriptionId']}, Resource Group: {data['resourceGroup']}"
),
)
)
return res

View File

@@ -108,6 +108,10 @@ def request(query, params):
time_ranges = {'day': '1', 'week': '2', 'month': '3', 'year': f'5_{unix_day-365}_{unix_day}'} time_ranges = {'day': '1', 'week': '2', 'month': '3', 'year': f'5_{unix_day-365}_{unix_day}'}
params['url'] += f'&filters=ex1:"ez{time_ranges[params["time_range"]]}"' params['url'] += f'&filters=ex1:"ez{time_ranges[params["time_range"]]}"'
# in some regions where geoblocking is employed (e.g. China),
# www.bing.com redirects to the regional version of Bing
params['allow_redirects'] = True
return params return params
@@ -197,7 +201,6 @@ def fetch_traits(engine_traits: EngineTraits):
"User-Agent": gen_useragent(), "User-Agent": gen_useragent(),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US;q=0.5,en;q=0.3", "Accept-Language": "en-US;q=0.5,en;q=0.3",
"Accept-Encoding": "gzip, deflate, br",
"DNT": "1", "DNT": "1",
"Connection": "keep-alive", "Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1", "Upgrade-Insecure-Requests": "1",

View File

@@ -124,17 +124,17 @@ from urllib.parse import (
urlparse, urlparse,
) )
import json
from dateutil import parser from dateutil import parser
from lxml import html from lxml import html
from searx import locales from searx import locales
from searx.utils import ( from searx.utils import (
extr,
extract_text, extract_text,
eval_xpath,
eval_xpath_list, eval_xpath_list,
eval_xpath_getindex, eval_xpath_getindex,
js_variable_to_python, js_obj_str_to_python,
js_obj_str_to_json_str,
get_embeded_stream_url, get_embeded_stream_url,
) )
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
@@ -142,17 +142,17 @@ from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
about = { about = {
"website": 'https://search.brave.com/', "website": "https://search.brave.com/",
"wikidata_id": 'Q22906900', "wikidata_id": "Q22906900",
"official_api_documentation": None, "official_api_documentation": None,
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": 'HTML', "results": "HTML",
} }
base_url = "https://search.brave.com/" base_url = "https://search.brave.com/"
categories = [] categories = []
brave_category: t.Literal["search", "videos", "images", "news", "goggles"] = 'search' brave_category: t.Literal["search", "videos", "images", "news", "goggles"] = "search"
"""Brave supports common web-search, videos, images, news, and goggles search. """Brave supports common web-search, videos, images, news, and goggles search.
- ``search``: Common WEB search - ``search``: Common WEB search
@@ -182,74 +182,87 @@ to do more won't return any result and you will most likely be flagged as a bot.
""" """
safesearch = True safesearch = True
safesearch_map = {2: 'strict', 1: 'moderate', 0: 'off'} # cookie: safesearch=off safesearch_map = {2: "strict", 1: "moderate", 0: "off"} # cookie: safesearch=off
time_range_support = False time_range_support = False
"""Brave only supports time-range in :py:obj:`brave_category` ``search`` (UI """Brave only supports time-range in :py:obj:`brave_category` ``search`` (UI
category All) and in the goggles category.""" category All) and in the goggles category."""
time_range_map: dict[str, str] = { time_range_map: dict[str, str] = {
'day': 'pd', "day": "pd",
'week': 'pw', "week": "pw",
'month': 'pm', "month": "pm",
'year': 'py', "year": "py",
} }
def request(query: str, params: dict[str, t.Any]) -> None: def request(query: str, params: dict[str, t.Any]) -> None:
# Don't accept br encoding / see https://github.com/searxng/searxng/pull/1787
params['headers']['Accept-Encoding'] = 'gzip, deflate'
args: dict[str, t.Any] = { args: dict[str, t.Any] = {
'q': query, "q": query,
'source': 'web', "source": "web",
} }
if brave_spellcheck: if brave_spellcheck:
args['spellcheck'] = '1' args["spellcheck"] = "1"
if brave_category in ('search', 'goggles'): if brave_category in ("search", "goggles"):
if params.get('pageno', 1) - 1: if params.get("pageno", 1) - 1:
args['offset'] = params.get('pageno', 1) - 1 args["offset"] = params.get("pageno", 1) - 1
if time_range_map.get(params['time_range']): if time_range_map.get(params["time_range"]):
args['tf'] = time_range_map.get(params['time_range']) args["tf"] = time_range_map.get(params["time_range"])
if brave_category == 'goggles': if brave_category == "goggles":
args['goggles_id'] = Goggles args["goggles_id"] = Goggles
params["headers"]["Accept-Encoding"] = "gzip, deflate"
params["url"] = f"{base_url}{brave_category}?{urlencode(args)}" params["url"] = f"{base_url}{brave_category}?{urlencode(args)}"
logger.debug("url %s", params["url"])
# set properties in the cookies # set properties in the cookies
params['cookies']['safesearch'] = safesearch_map.get(params['safesearch'], 'off') params["cookies"]["safesearch"] = safesearch_map.get(params["safesearch"], "off")
# the useLocation is IP based, we use cookie 'country' for the region # the useLocation is IP based, we use cookie "country" for the region
params['cookies']['useLocation'] = '0' params["cookies"]["useLocation"] = "0"
params['cookies']['summarizer'] = '0' params["cookies"]["summarizer"] = "0"
engine_region = traits.get_region(params['searxng_locale'], 'all') engine_region = traits.get_region(params["searxng_locale"], "all")
params['cookies']['country'] = engine_region.split('-')[-1].lower() # type: ignore params["cookies"]["country"] = engine_region.split("-")[-1].lower() # type: ignore
ui_lang = locales.get_engine_locale(params['searxng_locale'], traits.custom["ui_lang"], 'en-us') ui_lang = locales.get_engine_locale(params["searxng_locale"], traits.custom["ui_lang"], "en-us")
params['cookies']['ui_lang'] = ui_lang params["cookies"]["ui_lang"] = ui_lang
logger.debug("cookies %s", params["cookies"])
logger.debug("cookies %s", params['cookies'])
params['headers']['Sec-Fetch-Dest'] = "document"
params['headers']['Sec-Fetch-Mode'] = "navigate"
params['headers']['Sec-Fetch-Site'] = "same-origin"
params['headers']['Sec-Fetch-User'] = "?1"
def _extract_published_date(published_date_raw): def _extract_published_date(published_date_raw: str | None):
if published_date_raw is None: if published_date_raw is None:
return None return None
try: try:
return parser.parse(published_date_raw) return parser.parse(published_date_raw)
except parser.ParserError: except parser.ParserError:
return None return None
def extract_json_data(text: str) -> dict[str, t.Any]:
# Example script source containing the data:
#
# kit.start(app, element, {
# node_ids: [0, 19],
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
text = text[text.index("<script") : text.index("</script")]
if not text:
raise ValueError("can't find JS/JSON data in the given text")
start = text.index("data: [{")
end = text.rindex("}}]")
js_obj_str = text[start:end]
js_obj_str = "{" + js_obj_str + "}}]}"
# js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for &nbsp;
# js_obj_str = js_obj_str.replace(r"\u003C", "<").replace(r"\u003c", "<") # fix broken HTML tags in strings
json_str = js_obj_str_to_json_str(js_obj_str)
data: dict[str, t.Any] = json.loads(json_str)
return data
def response(resp: SXNG_Response) -> EngineResults: def response(resp: SXNG_Response) -> EngineResults:
if brave_category in ('search', 'goggles'): if brave_category in ('search', 'goggles'):
@@ -264,11 +277,8 @@ def response(resp: SXNG_Response) -> EngineResults:
# node_ids: [0, 19], # node_ids: [0, 19],
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}] # data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
js_object = "[{" + extr(resp.text, "data: [{", "}}],") + "}}]" json_data: dict[str, t.Any] = extract_json_data(resp.text)
json_data = js_variable_to_python(js_object) json_resp: dict[str, t.Any] = json_data['data'][1]["data"]['body']['response']
# json_data is a list and at the second position (0,1) in this list we find the "response" data we need ..
json_resp = json_data[1]['data']['body']['response']
if brave_category == 'images': if brave_category == 'images':
return _parse_images(json_resp) return _parse_images(json_resp)
@@ -278,150 +288,124 @@ def response(resp: SXNG_Response) -> EngineResults:
raise ValueError(f"Unsupported brave category: {brave_category}") raise ValueError(f"Unsupported brave category: {brave_category}")
def _parse_search(resp) -> EngineResults: def _parse_search(resp: SXNG_Response) -> EngineResults:
result_list = EngineResults() res = EngineResults()
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
# I doubt that Brave is still providing the "answer" class / I haven't seen for result in eval_xpath_list(dom, "//div[contains(@class, 'snippet ')]"):
# answers in brave for a long time.
answer_tag = eval_xpath_getindex(dom, '//div[@class="answer"]', 0, default=None)
if answer_tag:
url = eval_xpath_getindex(dom, '//div[@id="featured_snippet"]/a[@class="result-header"]/@href', 0, default=None)
answer = extract_text(answer_tag)
if answer is not None:
result_list.add(result_list.types.Answer(answer=answer, url=url))
# xpath_results = '//div[contains(@class, "snippet fdb") and @data-type="web"]' url: str | None = eval_xpath_getindex(result, ".//a/@href", 0, default=None)
xpath_results = '//div[contains(@class, "snippet ")]' title_tag = eval_xpath_getindex(result, ".//div[contains(@class, 'title')]", 0, default=None)
for result in eval_xpath_list(dom, xpath_results):
url = eval_xpath_getindex(result, './/a[contains(@class, "h")]/@href', 0, default=None)
title_tag = eval_xpath_getindex(
result, './/a[contains(@class, "h")]//div[contains(@class, "title")]', 0, default=None
)
if url is None or title_tag is None or not urlparse(url).netloc: # partial url likely means it's an ad if url is None or title_tag is None or not urlparse(url).netloc: # partial url likely means it's an ad
continue continue
content: str = extract_text( content: str = ""
eval_xpath_getindex(result, './/div[contains(@class, "snippet-description")]', 0, default='') pub_date = None
) # type: ignore
pub_date_raw = eval_xpath(result, 'substring-before(.//div[contains(@class, "snippet-description")], "-")')
pub_date = _extract_published_date(pub_date_raw)
if pub_date and content.startswith(pub_date_raw):
content = content.lstrip(pub_date_raw).strip("- \n\t")
thumbnail = eval_xpath_getindex(result, './/img[contains(@class, "thumb")]/@src', 0, default='') # there are other classes like 'site-name-content' we don't want to match,
# however only using contains(@class, 'content') would e.g. also match `site-name-content`
# thus, we explicitly also require the spaces as class separator
_content = eval_xpath_getindex(result, ".//div[contains(concat(' ', @class, ' '), ' content ')]", 0, default="")
if len(_content):
content = extract_text(_content) # type: ignore
_pub_date = extract_text(
eval_xpath_getindex(_content, ".//span[contains(@class, 't-secondary')]", 0, default="")
)
if _pub_date:
pub_date = _extract_published_date(_pub_date)
content = content.lstrip(_pub_date).strip("- \n\t")
item = { thumbnail: str = eval_xpath_getindex(result, ".//a[contains(@class, 'thumbnail')]//img/@src", 0, default="")
'url': url,
'title': extract_text(title_tag), item = res.types.LegacyResult(
'content': content, template="default.html",
'publishedDate': pub_date, url=url,
'thumbnail': thumbnail, title=extract_text(title_tag),
} content=content,
publishedDate=pub_date,
thumbnail=thumbnail,
)
res.add(item)
video_tag = eval_xpath_getindex( video_tag = eval_xpath_getindex(
result, './/div[contains(@class, "video-snippet") and @data-macro="video"]', 0, default=None result, ".//div[contains(@class, 'video-snippet') and @data-macro='video']", 0, default=[]
) )
if video_tag is not None: if len(video_tag):
# In my tests a video tag in the WEB search was most often not a # In my tests a video tag in the WEB search was most often not a
# video, except the ones from youtube .. # video, except the ones from youtube ..
iframe_src = get_embeded_stream_url(url) iframe_src = get_embeded_stream_url(url)
if iframe_src: if iframe_src:
item['iframe_src'] = iframe_src item["iframe_src"] = iframe_src
item['template'] = 'videos.html' item["template"] = "videos.html"
item['thumbnail'] = eval_xpath_getindex(video_tag, './/img/@src', 0, default='')
pub_date_raw = extract_text(
eval_xpath(video_tag, './/div[contains(@class, "snippet-attributes")]/div/text()')
)
item['publishedDate'] = _extract_published_date(pub_date_raw)
else:
item['thumbnail'] = eval_xpath_getindex(video_tag, './/img/@src', 0, default='')
result_list.append(item) return res
return result_list
def _parse_news(resp) -> EngineResults: def _parse_news(resp: SXNG_Response) -> EngineResults:
res = EngineResults()
result_list = EngineResults()
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, '//div[contains(@class, "results")]//div[@data-type="news"]'): for result in eval_xpath_list(dom, "//div[contains(@class, 'results')]//div[@data-type='news']"):
# import pdb url = eval_xpath_getindex(result, ".//a[contains(@class, 'result-header')]/@href", 0, default=None)
# pdb.set_trace()
url = eval_xpath_getindex(result, './/a[contains(@class, "result-header")]/@href', 0, default=None)
if url is None: if url is None:
continue continue
title = extract_text(eval_xpath_list(result, './/span[contains(@class, "snippet-title")]')) title = eval_xpath_list(result, ".//span[contains(@class, 'snippet-title')]")
content = extract_text(eval_xpath_list(result, './/p[contains(@class, "desc")]')) content = eval_xpath_list(result, ".//p[contains(@class, 'desc')]")
thumbnail = eval_xpath_getindex(result, './/div[contains(@class, "image-wrapper")]//img/@src', 0, default='') thumbnail = eval_xpath_getindex(result, ".//div[contains(@class, 'image-wrapper')]//img/@src", 0, default="")
item = { item = res.types.LegacyResult(
"url": url, template="default.html",
"title": title, url=url,
"content": content, title=extract_text(title),
"thumbnail": thumbnail, thumbnail=thumbnail,
} content=extract_text(content),
)
res.add(item)
result_list.append(item) return res
return result_list
def _parse_images(json_resp) -> EngineResults: def _parse_images(json_resp: dict[str, t.Any]) -> EngineResults:
result_list = EngineResults() res = EngineResults()
for result in json_resp["results"]: for result in json_resp["results"]:
item = { item = res.types.LegacyResult(
'url': result['url'], template="images.html",
'title': result['title'], url=result["url"],
'content': result['description'], title=result["title"],
'template': 'images.html', source=result["source"],
'resolution': result['properties']['format'], img_src=result["properties"]["url"],
'source': result['source'], thumbnail_src=result["thumbnail"]["src"],
'img_src': result['properties']['url'], )
'thumbnail_src': result['thumbnail']['src'], res.add(item)
}
result_list.append(item)
return result_list return res
def _parse_videos(json_resp) -> EngineResults: def _parse_videos(json_resp: dict[str, t.Any]) -> EngineResults:
result_list = EngineResults() res = EngineResults()
for result in json_resp["results"]: for result in json_resp["results"]:
item = res.types.LegacyResult(
url = result['url'] template="videos.html",
item = { url=result["url"],
'url': url, title=result["title"],
'title': result['title'], content=result["description"],
'content': result['description'], length=result["video"]["duration"],
'template': 'videos.html', duration=result["video"]["duration"],
'length': result['video']['duration'], publishedDate=_extract_published_date(result["age"]),
'duration': result['video']['duration'], )
'publishedDate': _extract_published_date(result['age']), if result["thumbnail"] is not None:
} item["thumbnail"] = result["thumbnail"]["src"]
iframe_src = get_embeded_stream_url(result["url"])
if result['thumbnail'] is not None:
item['thumbnail'] = result['thumbnail']['src']
iframe_src = get_embeded_stream_url(url)
if iframe_src: if iframe_src:
item['iframe_src'] = iframe_src item["iframe_src"] = iframe_src
result_list.append(item) res.add(item)
return result_list return res
def fetch_traits(engine_traits: EngineTraits): def fetch_traits(engine_traits: EngineTraits):
@@ -436,34 +420,31 @@ def fetch_traits(engine_traits: EngineTraits):
engine_traits.custom["ui_lang"] = {} engine_traits.custom["ui_lang"] = {}
headers = {
'Accept-Encoding': 'gzip, deflate',
}
lang_map = {'no': 'nb'} # norway lang_map = {'no': 'nb'} # norway
# languages (UI) # languages (UI)
resp = get('https://search.brave.com/settings', headers=headers) resp = get('https://search.brave.com/settings')
if not resp.ok: # type: ignore if not resp.ok:
print("ERROR: response from Brave is not OK.") print("ERROR: response from Brave is not OK.")
dom = html.fromstring(resp.text) # type: ignore dom = html.fromstring(resp.text)
for option in dom.xpath('//section//option[@value="en-us"]/../option'): for option in dom.xpath("//section//option[@value='en-us']/../option"):
ui_lang = option.get('value') ui_lang = option.get("value")
try: try:
l = babel.Locale.parse(ui_lang, sep='-') l = babel.Locale.parse(ui_lang, sep="-")
if l.territory: if l.territory:
sxng_tag = region_tag(babel.Locale.parse(ui_lang, sep='-')) sxng_tag = region_tag(babel.Locale.parse(ui_lang, sep="-"))
else: else:
sxng_tag = language_tag(babel.Locale.parse(ui_lang, sep='-')) sxng_tag = language_tag(babel.Locale.parse(ui_lang, sep="-"))
except babel.UnknownLocaleError: except babel.UnknownLocaleError:
print("ERROR: can't determine babel locale of Brave's (UI) language %s" % ui_lang) print("ERROR: can't determine babel locale of Brave's (UI) language %s" % ui_lang)
continue continue
conflict = engine_traits.custom["ui_lang"].get(sxng_tag) conflict = engine_traits.custom["ui_lang"].get(sxng_tag) # type: ignore
if conflict: if conflict:
if conflict != ui_lang: if conflict != ui_lang:
print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, ui_lang)) print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, ui_lang))
@@ -472,26 +453,26 @@ def fetch_traits(engine_traits: EngineTraits):
# search regions of brave # search regions of brave
resp = get('https://cdn.search.brave.com/serp/v2/_app/immutable/chunks/parameters.734c106a.js', headers=headers) resp = get("https://cdn.search.brave.com/serp/v2/_app/immutable/chunks/parameters.734c106a.js")
if not resp.ok: # type: ignore if not resp.ok:
print("ERROR: response from Brave is not OK.") print("ERROR: response from Brave is not OK.")
country_js = resp.text[resp.text.index("options:{all") + len('options:') :] # type: ignore country_js = resp.text[resp.text.index("options:{all") + len("options:") :]
country_js = country_js[: country_js.index("},k={default")] country_js = country_js[: country_js.index("},k={default")]
country_tags = js_variable_to_python(country_js) country_tags = js_obj_str_to_python(country_js)
for k, v in country_tags.items(): for k, v in country_tags.items():
if k == 'all': if k == "all":
engine_traits.all_locale = 'all' engine_traits.all_locale = "all"
continue continue
country_tag = v['value'] country_tag = v["value"]
# add official languages of the country .. # add official languages of the country ..
for lang_tag in babel.languages.get_official_languages(country_tag, de_facto=True): for lang_tag in babel.languages.get_official_languages(country_tag, de_facto=True):
lang_tag = lang_map.get(lang_tag, lang_tag) lang_tag = lang_map.get(lang_tag, lang_tag)
sxng_tag = region_tag(babel.Locale.parse('%s_%s' % (lang_tag, country_tag.upper()))) sxng_tag = region_tag(babel.Locale.parse("%s_%s" % (lang_tag, country_tag.upper())))
# print("%-20s: %s <-- %s" % (v['label'], country_tag, sxng_tag)) # print("%-20s: %s <-- %s" % (v["label"], country_tag, sxng_tag))
conflict = engine_traits.regions.get(sxng_tag) conflict = engine_traits.regions.get(sxng_tag)
if conflict: if conflict:

View File

@@ -23,14 +23,14 @@ paging = True
# search-url # search-url
base_url = 'https://www.deviantart.com' base_url = 'https://www.deviantart.com'
results_xpath = '//div[@class="_2pZkk"]/div/div/a' results_xpath = '//div[@class="V_S0t_"]/div/div/a'
url_xpath = './@href' url_xpath = './@href'
thumbnail_src_xpath = './div/img/@src' thumbnail_src_xpath = './div/img/@src'
img_src_xpath = './div/img/@srcset' img_src_xpath = './div/img/@srcset'
title_xpath = './@aria-label' title_xpath = './@aria-label'
premium_xpath = '../div/div/div/text()' premium_xpath = '../div/div/div/text()'
premium_keytext = 'Watch the artist to view this deviation' premium_keytext = 'Watch the artist to view this deviation'
cursor_xpath = '(//a[@class="_1OGeq"]/@href)[last()]' cursor_xpath = '(//a[@class="vQ2brP"]/@href)[last()]'
def request(query, params): def request(query, params):

63
searx/engines/devicons.py Normal file
View File

@@ -0,0 +1,63 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Devicons (icons)"""
import typing as t
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from extended_types import SXNG_Response
from search.processors.online import OnlineParams
about = {
"website": "https://devicon.dev/",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": True,
"results": "JSON",
}
cdn_base_url = "https://cdn.jsdelivr.net/gh/devicons/devicon@latest"
categories = ["images", "icons"]
def request(query: str, params: "OnlineParams"):
params["url"] = f"{cdn_base_url}/devicon.json"
params['query'] = query
return params
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
query_parts = resp.search_params["query"].lower().split(" ")
def is_result_match(result: dict[str, t.Any]) -> bool:
for part in query_parts:
if part in result["name"]:
return True
for tag in result["altnames"] + result["tags"]:
if part in tag:
return True
return False
filtered_results = filter(is_result_match, resp.json())
for result in filtered_results:
for image_type in result["versions"]["svg"]:
img_src = f"{cdn_base_url}/icons/{result['name']}/{result['name']}-{image_type}.svg"
res.add(
res.types.LegacyResult(
{
"template": "images.html",
"url": img_src,
"title": result["name"],
"content": f"Base color: {result['color']}",
"img_src": img_src,
"img_format": "SVG",
}
)
)
return res

View File

@@ -407,7 +407,7 @@ def fetch_traits(engine_traits: EngineTraits):
""" """
# pylint: disable=too-many-branches, too-many-statements, disable=import-outside-toplevel # pylint: disable=too-many-branches, too-many-statements, disable=import-outside-toplevel
from searx.utils import js_variable_to_python from searx.utils import js_obj_str_to_python
# fetch regions # fetch regions
@@ -455,7 +455,7 @@ def fetch_traits(engine_traits: EngineTraits):
js_code = extr(resp.text, 'languages:', ',regions') # type: ignore js_code = extr(resp.text, 'languages:', ',regions') # type: ignore
languages = js_variable_to_python(js_code) languages: dict[str, str] = js_obj_str_to_python(js_code)
for eng_lang, name in languages.items(): for eng_lang, name in languages.items():
if eng_lang == 'wt_WT': if eng_lang == 'wt_WT':

View File

@@ -42,8 +42,8 @@ def response(resp):
results.append( results.append(
{ {
'url': item['source_page_url'], 'url': item.get('source_page_url'),
'title': item['source_site'], 'title': item.get('source_site'),
'img_src': img if item['type'] == 'IMAGE' else thumb, 'img_src': img if item['type'] == 'IMAGE' else thumb,
'filesize': humanize_bytes(item['meme_file_size']), 'filesize': humanize_bytes(item['meme_file_size']),
'publishedDate': formatted_date, 'publishedDate': formatted_date,

View File

@@ -0,0 +1,52 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Grokipedia (general)"""
from urllib.parse import urlencode
from searx.utils import html_to_text
from searx.result_types import EngineResults
about = {
"website": 'https://grokipedia.com',
"wikidata_id": "Q136410803",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://grokipedia.com/api/full-text-search"
categories = ['general']
paging = True
results_per_page = 10
def request(query, params):
start_index = (params["pageno"] - 1) * results_per_page
query_params = {
"query": query,
"limit": results_per_page,
"offset": start_index,
}
params["url"] = f"{base_url}?{urlencode(query_params)}"
return params
def response(resp) -> EngineResults:
results = EngineResults()
search_res = resp.json()
for item in search_res["results"]:
results.add(
results.types.MainResult(
url='https://grokipedia.com/page/' + item["slug"],
title=item["title"],
content=html_to_text(item["snippet"]),
)
)
return results

View File

@@ -6,6 +6,7 @@ from urllib.parse import urlencode
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from flask_babel import gettext from flask_babel import gettext
from searx.utils import html_to_text
# Engine metadata # Engine metadata
about = { about = {
@@ -75,6 +76,7 @@ def response(resp):
object_id = hit["objectID"] object_id = hit["objectID"]
points = hit.get("points") or 0 points = hit.get("points") or 0
num_comments = hit.get("num_comments") or 0 num_comments = hit.get("num_comments") or 0
content = hit.get("url") or html_to_text(hit.get("comment_text")) or html_to_text(hit.get("story_text"))
metadata = "" metadata = ""
if points != 0 or num_comments != 0: if points != 0 or num_comments != 0:
@@ -83,7 +85,7 @@ def response(resp):
{ {
"title": hit.get("title") or f"{gettext('author')}: {hit['author']}", "title": hit.get("title") or f"{gettext('author')}: {hit['author']}",
"url": f"https://news.ycombinator.com/item?id={object_id}", "url": f"https://news.ycombinator.com/item?id={object_id}",
"content": hit.get("url") or hit.get("comment_text") or hit.get("story_text") or "", "content": content,
"metadata": metadata, "metadata": metadata,
"author": hit["author"], "author": hit["author"],
"publishedDate": datetime.fromtimestamp(hit["created_at_i"]), "publishedDate": datetime.fromtimestamp(hit["created_at_i"]),

View File

@@ -31,7 +31,7 @@ paging = True
time_range_support = True time_range_support = True
# base_url can be overwritten by a list of URLs in the settings.yml # base_url can be overwritten by a list of URLs in the settings.yml
base_url: list | str = [] base_url: list[str] | str = []
def init(_): def init(_):

69
searx/engines/lucide.py Normal file
View File

@@ -0,0 +1,69 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Browse one of the largest collections of copyleft icons
that can be used for own projects (e.g. apps, websites).
.. _Website: https://lucide.dev
"""
import typing as t
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from extended_types import SXNG_Response
from search.processors.online import OnlineParams
about = {
"website": "https://lucide.dev/",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": True,
"results": "JSON",
}
cdn_base_url = "https://cdn.jsdelivr.net/npm/lucide-static"
categories = ["images", "icons"]
def request(query: str, params: "OnlineParams"):
params["url"] = f"{cdn_base_url}/tags.json"
params['query'] = query
return params
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
query_parts = resp.search_params["query"].lower().split(" ")
def is_result_match(result: tuple[str, list[str]]) -> bool:
icon_name, tags = result
for part in query_parts:
if part in icon_name:
return True
for tag in tags:
if part in tag:
return True
return False
filtered_results = filter(is_result_match, resp.json().items())
for icon_name, tags in filtered_results:
img_src = f"{cdn_base_url}/icons/{icon_name}.svg"
res.add(
res.types.LegacyResult(
{
"template": "images.html",
"url": img_src,
"title": icon_name,
"content": ", ".join(tags),
"img_src": img_src,
"img_format": "SVG",
}
)
)
return res

View File

@@ -28,7 +28,7 @@ Implementations
""" """
import typing as t import typing as t
from urllib.parse import urlencode, quote_plus from urllib.parse import urlencode
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
@@ -42,7 +42,7 @@ about = {
"results": "JSON", "results": "JSON",
} }
base_url = "https://api.marginalia.nu" base_url = "https://api2.marginalia-search.com"
safesearch = True safesearch = True
categories = ["general"] categories = ["general"]
paging = False paging = False
@@ -85,13 +85,11 @@ class ApiSearchResults(t.TypedDict):
def request(query: str, params: dict[str, t.Any]): def request(query: str, params: dict[str, t.Any]):
query_params = { query_params = {"count": results_per_page, "nsfw": min(params["safesearch"], 1), "query": query}
"count": results_per_page,
"nsfw": min(params["safesearch"], 1),
}
params["url"] = f"{base_url}/{api_key}/search/{quote_plus(query)}?{urlencode(query_params)}" params["url"] = f"{base_url}/search?{urlencode(query_params)}"
params["headers"]["User-Agent"] = searxng_useragent() params["headers"]["User-Agent"] = searxng_useragent()
params["headers"]["API-Key"] = api_key
def response(resp: SXNG_Response): def response(resp: SXNG_Response):

View File

@@ -65,7 +65,8 @@ def request(query, params):
if search_type: if search_type:
args['fmt'] = search_type args['fmt'] = search_type
if search_type == '': # setting the page number on the first page (i.e. s=0) triggers a rate-limit
if search_type == '' and params['pageno'] > 1:
args['s'] = 10 * (params['pageno'] - 1) args['s'] = 10 * (params['pageno'] - 1)
if params['time_range'] and search_type != 'images': if params['time_range'] and search_type != 'images':

View File

@@ -1,264 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Mullvad Leta is a search engine proxy. Currently Leta only offers text
search results not image, news or any other types of search result. Leta acts
as a proxy to Google and Brave search results. You can select which backend
search engine you wish to use, see (:py:obj:`leta_engine`).
.. hint::
Leta caches each search for up to 30 days. For example, if you use search
terms like ``news``, contrary to your intention you'll get very old results!
Configuration
=============
The engine has the following additional settings:
- :py:obj:`leta_engine` (:py:obj:`LetaEnginesType`)
You can configure one Leta engine for Google and one for Brave:
.. code:: yaml
- name: mullvadleta
engine: mullvad_leta
leta_engine: google
shortcut: ml
- name: mullvadleta brave
engine: mullvad_leta
network: mullvadleta # use network from engine "mullvadleta" configured above
leta_engine: brave
shortcut: mlb
Implementations
===============
"""
import typing as t
from urllib.parse import urlencode
import babel
from httpx import Response
from lxml import html
from searx.enginelib.traits import EngineTraits
from searx.locales import get_official_locales, language_tag, region_tag
from searx.utils import eval_xpath_list
from searx.result_types import EngineResults, MainResult
search_url = "https://leta.mullvad.net"
# about
about = {
"website": search_url,
"wikidata_id": 'Q47008412', # the Mullvad id - not leta, but related
"official_api_documentation": 'https://leta.mullvad.net/faq',
"use_official_api": False,
"require_api_key": False,
"results": 'HTML',
}
# engine dependent config
categories = ["general", "web"]
paging = True
max_page = 10
time_range_support = True
time_range_dict = {
"day": "d",
"week": "w",
"month": "m",
"year": "y",
}
LetaEnginesType = t.Literal["google", "brave"]
"""Engine types supported by mullvadleta."""
leta_engine: LetaEnginesType = "google"
"""Select Leta's engine type from :py:obj:`LetaEnginesType`."""
def init(_):
l = t.get_args(LetaEnginesType)
if leta_engine not in l:
raise ValueError(f"leta_engine '{leta_engine}' is invalid, use one of {', '.join(l)}")
class DataNodeQueryMetaDataIndices(t.TypedDict):
"""Indices into query metadata."""
success: int
q: int # pylint: disable=invalid-name
country: int
language: int
lastUpdated: int
engine: int
items: int
infobox: int
news: int
timestamp: int
altered: int
page: int
next: int # if -1, there no more results are available
previous: int
class DataNodeResultIndices(t.TypedDict):
"""Indices into query resultsdata."""
link: int
snippet: int
title: int
favicon: int
def request(query: str, params: dict):
params["method"] = "GET"
args = {
"q": query,
"engine": leta_engine,
"x-sveltekit-invalidated": "001", # hardcoded from all requests seen
}
country = traits.get_region(params.get("searxng_locale"), traits.all_locale) # type: ignore
if country:
args["country"] = country
language = traits.get_language(params.get("searxng_locale"), traits.all_locale) # type: ignore
if language:
args["language"] = language
if params["time_range"] in time_range_dict:
args["lastUpdated"] = time_range_dict[params["time_range"]]
if params["pageno"] > 1:
args["page"] = params["pageno"]
params["url"] = f"{search_url}/search/__data.json?{urlencode(args)}"
return params
def response(resp: Response) -> EngineResults:
json_response = resp.json()
nodes = json_response["nodes"]
# 0: is None
# 1: has "connected=True", not useful
# 2: query results within "data"
data_nodes = nodes[2]["data"]
# Instead of nested object structure, all objects are flattened into a
# list. Rather, the first object in data_node provides indices into the
# "data_nodes" to access each searchresult (which is an object of more
# indices)
#
# Read the relative TypedDict definitions for details
query_meta_data: DataNodeQueryMetaDataIndices = data_nodes[0]
query_items_indices = query_meta_data["items"]
results = EngineResults()
for idx in data_nodes[query_items_indices]:
query_item_indices: DataNodeResultIndices = data_nodes[idx]
results.add(
MainResult(
url=data_nodes[query_item_indices["link"]],
title=data_nodes[query_item_indices["title"]],
content=data_nodes[query_item_indices["snippet"]],
)
)
return results
def fetch_traits(engine_traits: EngineTraits) -> None:
"""Fetch languages and regions from Mullvad-Leta"""
def extract_table_data(table):
for row in table.xpath(".//tr")[2:]:
cells = row.xpath(".//td | .//th") # includes headers and data
if len(cells) > 1: # ensure the column exists
cell0 = cells[0].text_content().strip()
cell1 = cells[1].text_content().strip()
yield [cell0, cell1]
# pylint: disable=import-outside-toplevel
# see https://github.com/searxng/searxng/issues/762
from searx.network import get as http_get
# pylint: enable=import-outside-toplevel
resp = http_get(f"{search_url}/documentation")
if not isinstance(resp, Response):
print("ERROR: failed to get response from mullvad-leta. Are you connected to the VPN?")
return
if not resp.ok:
print("ERROR: response from mullvad-leta is not OK. Are you connected to the VPN?")
return
dom = html.fromstring(resp.text)
# There are 4 HTML tables on the documentation page for extracting information:
# 0. Keyboard Shortcuts
# 1. Query Parameters (shoutout to Mullvad for accessible docs for integration)
# 2. Country Codes [Country, Code]
# 3. Language Codes [Language, Code]
tables = eval_xpath_list(dom.body, "//table")
if tables is None or len(tables) <= 0:
print("ERROR: could not find any tables. Was the page updated?")
language_table = tables[3]
lang_map = {
"zh-hant": "zh_Hans",
"zh-hans": "zh_Hant",
"jp": "ja",
}
for language, code in extract_table_data(language_table):
locale_tag = lang_map.get(code, code).replace("-", "_") # type: ignore
try:
locale = babel.Locale.parse(locale_tag)
except babel.UnknownLocaleError:
print(f"ERROR: Mullvad-Leta language {language} ({code}) is unknown by babel")
continue
sxng_tag = language_tag(locale)
engine_traits.languages[sxng_tag] = code
country_table = tables[2]
country_map = {
"cn": "zh-CN",
"hk": "zh-HK",
"jp": "ja-JP",
"my": "ms-MY",
"tw": "zh-TW",
"uk": "en-GB",
"us": "en-US",
}
for country, code in extract_table_data(country_table):
sxng_tag = country_map.get(code)
if sxng_tag:
engine_traits.regions[sxng_tag] = code
continue
try:
locale = babel.Locale.parse(f"{code.lower()}_{code.upper()}")
except babel.UnknownLocaleError:
locale = None
if locale:
engine_traits.regions[region_tag(locale)] = code
continue
official_locales = get_official_locales(code, engine_traits.languages.keys(), regional=True)
if not official_locales:
print(f"ERROR: Mullvad-Leta country '{code}' ({country}) could not be mapped as expected.")
continue
for locale in official_locales:
engine_traits.regions[region_tag(locale)] = code

View File

@@ -15,7 +15,7 @@ from searx.utils import (
extr, extr,
html_to_text, html_to_text,
parse_duration_string, parse_duration_string,
js_variable_to_python, js_obj_str_to_python,
get_embeded_stream_url, get_embeded_stream_url,
) )
@@ -125,7 +125,7 @@ def parse_images(data):
match = extr(data, '<script>var imageSearchTabData=', '</script>') match = extr(data, '<script>var imageSearchTabData=', '</script>')
if match: if match:
json = js_variable_to_python(match.strip()) json = js_obj_str_to_python(match.strip())
items = json.get('content', {}).get('items', []) items = json.get('content', {}).get('items', [])
for item in items: for item in items:

View File

@@ -55,15 +55,18 @@ def response(resp):
if result['type'] == 'story': if result['type'] == 'story':
continue continue
main_image = result['images']['orig']
results.append( results.append(
{ {
'template': 'images.html', 'template': 'images.html',
'url': result['link'] or f"{base_url}/pin/{result['id']}/", 'url': result.get('link') or f"{base_url}/pin/{result['id']}/",
'title': result.get('title') or result.get('grid_title'), 'title': result.get('title') or result.get('grid_title'),
'content': (result.get('rich_summary') or {}).get('display_description') or "", 'content': (result.get('rich_summary') or {}).get('display_description') or "",
'img_src': result['images']['orig']['url'], 'img_src': main_image['url'],
'thumbnail_src': result['images']['236x']['url'], 'thumbnail_src': result['images']['236x']['url'],
'source': (result.get('rich_summary') or {}).get('site_name'), 'source': (result.get('rich_summary') or {}).get('site_name'),
'resolution': f"{main_image['width']}x{main_image['height']}",
'author': f"{result['pinner'].get('full_name')} ({result['pinner']['username']})",
} }
) )

View File

@@ -40,8 +40,8 @@ Known Quirks
The implementation to support :py:obj:`paging <searx.enginelib.Engine.paging>` The implementation to support :py:obj:`paging <searx.enginelib.Engine.paging>`
is based on the *nextpage* method of Piped's REST API / the :py:obj:`frontend is based on the *nextpage* method of Piped's REST API / the :py:obj:`frontend
API <frontend_url>`. This feature is *next page driven* and plays well with the API <frontend_url>`. This feature is *next page driven* and plays well with the
:ref:`infinite_scroll <settings ui>` setting in SearXNG but it does not really :ref:`infinite_scroll <settings plugins>` plugin in SearXNG but it does not
fit into SearXNG's UI to select a page by number. really fit into SearXNG's UI to select a page by number.
Implementations Implementations
=============== ===============
@@ -72,7 +72,7 @@ categories = []
paging = True paging = True
# search-url # search-url
backend_url: list[str] | str | None = None backend_url: list[str] | str = []
"""Piped-Backend_: The core component behind Piped. The value is an URL or a """Piped-Backend_: The core component behind Piped. The value is an URL or a
list of URLs. In the latter case instance will be selected randomly. For a list of URLs. In the latter case instance will be selected randomly. For a
complete list of official instances see Piped-Instances (`JSON complete list of official instances see Piped-Instances (`JSON

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