113 Commits

Author SHA1 Message Date
ac3dd7b804 replaced favicon
Some checks failed
Security / Container (push) Has been cancelled
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 23:53:49 -05:00
3075003825 changed branding
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 23:38:21 -05:00
c6f1156bc6 changed approach, got rid of settings.yml binding entirely
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 22:50:37 -05:00
cf5dbbbc77 fixed...? 2025-05-10 22:45:56 -05:00
8477fb4846 oh, portainer didn't like that one 2025-05-10 22:43:33 -05:00
8204f4ee72 sets the config volume to read-only 2025-05-10 22:37:36 -05:00
9d9a0fe95e reverted 2025-05-10 22:31:02 -05:00
427a84d823 attempt #2 2025-05-10 22:29:01 -05:00
e971a3d7ab experimental mobile touchscreen support for oneko 2025-05-10 22:20:36 -05:00
89cb563793 fixed issue where oneko.gif was not referenced correctly, causing invisible neko 2025-05-10 21:51:46 -05:00
ac5c83b87d fixed typo in docker compose 2025-05-10 21:45:51 -05:00
34437e8491 implemented oneko 2025-05-10 21:41:29 -05:00
3d30ed1caa updated compose file to be able to define the port in an env var
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 20:46:57 -05:00
2ee495b383 whoops
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 20:31:49 -05:00
c27fd585ce carried over branding from current install
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 20:29:32 -05:00
89425caaa6 changed the embed description to nekosearch-branded desc. I think.
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 20:03:08 -05:00
52347c1843 added pull_policy to docker-compose
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 19:44:39 -05:00
c5b2bff2c5 whoops, teehee, swapped ports on compose
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 19:30:43 -05:00
61059eff9c fixed docker-compose (maybe
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 19:15:00 -05:00
d159f8384f added docker-compose.yml for continuous integration
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 19:08:22 -05:00
Ivan Gabaldon
1b787ed35e [mod] refactor integration.yml (#4763)
Style changes, cleanup and improved integration with CI by leveraging the use of
shared cache between all workflows.
2025-05-10 13:59:31 +02:00
Ivan Gabaldon
8e2e7774d7 [mod] new l10n.yml workflow (#4734)
l10n.yml will run after integration.yml finishes successfully (will defer anything depending on integration.yml until heavy loads like container building are moved to separate workflows) and in master branch.

* After every integration.yml workflow completes successfully, only the `update` job runs.
* Dispatch and Crontab triggers only the `pr` job.

Style changes, cleanup and improved integration with CI by leveraging the use of shared cache between all workflows (not functional until all workflows have been refactored).
2025-05-10 13:08:20 +02:00
Ivan Gabaldon
e982b9f732 [fix] documentation should run on push/pr
Instead of executing the workflow after integration.yml completes correctly, let's run this workflow parallel to integration.yml restoring the original behaviour.
2025-05-10 07:41:42 +02:00
Émilien (perso)
48456caeb3 chore: docker + github-actions dependabot (#4754)
* chore: docker dependabot

* Add github actions too
2025-05-09 20:46:20 +02:00
Markus Heiser
ef158ce1f4 [build] /static 2025-05-09 12:40:34 +02:00
Markus Heiser
cbf9ec7bf4 [fix] static.build.commit: add missing searx/templates/simple/icons.html
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-09 12:40:34 +02:00
Markus Heiser
409ede1530 [fix] simple client: jinja_svg_catalog addClassesToSVGElement
Starting with ionicons-8.0.8 the SVG already contains a class attribute and
instaed of using SVGO plugin ``addAttributesToSVGElement`` we habve to use
``addClassesToSVGElement`` to add out ``__jinja_class_placeholder__``.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-09 12:40:34 +02:00
dependabot[bot]
1326ec7429 [upd] web-client (simple): Bump ionicons in /client/simple
Bumps [ionicons](https://github.com/ionic-team/ionicons) from 7.4.0 to 8.0.8.
- [Release notes](https://github.com/ionic-team/ionicons/releases)
- [Commits](https://github.com/ionic-team/ionicons/compare/v7.4.0...v8.0.8)

---
updated-dependencies:
- dependency-name: ionicons
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 12:40:34 +02:00
dependabot[bot]
50406d4b46 [upd] pypi: Bump pylint from 3.3.6 to 3.3.7 (#4750)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.6 to 3.3.7.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.6...v3.3.7)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 3.3.7
  dependency-type: direct:development
  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-05-09 11:10:37 +02:00
dependabot[bot]
5ce3aa3acf [upd] pypi: Bump yamllint from 1.37.0 to 1.37.1 (#4752)
Bumps [yamllint](https://github.com/adrienverge/yamllint) from 1.37.0 to 1.37.1.
- [Release notes](https://github.com/adrienverge/yamllint/releases)
- [Changelog](https://github.com/adrienverge/yamllint/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/adrienverge/yamllint/compare/v1.37.0...v1.37.1)

---
updated-dependencies:
- dependency-name: yamllint
  dependency-version: 1.37.1
  dependency-type: direct:development
  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-05-09 11:09:44 +02:00
SearXNG Bot
76ebad0b21 [l10n] update translations from Weblate (#4744)
6f8c520f2 - 2025-05-08 - polskiecus <polskiecus@noreply.codeberg.org>
05dd91d5b - 2025-05-08 - return42 <return42@noreply.codeberg.org>
686b8e5fb - 2025-05-08 - return42 <return42@noreply.codeberg.org>
f40b42bd8 - 2025-05-05 - ehsanrs2 <ehsanrs2@noreply.codeberg.org>
b8013bc99 - 2025-05-03 - polskiecus <polskiecus@noreply.codeberg.org>
5affaa104 - 2025-05-02 - SomeTr <sometr@noreply.codeberg.org>

Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2025-05-09 09:31:50 +02:00
github-actions[bot]
d76f030cb3 [data] update searx.data - update_wikidata_units.py (#4738) 2025-05-09 07:09:58 +02:00
github-actions[bot]
b3b15ecc72 [data] update searx.data - update_ahmia_blacklist.py (#4739)
Co-authored-by: inetol <inetol@users.noreply.github.com>
2025-05-09 07:09:00 +02:00
github-actions[bot]
1319b250af [data] update searx.data - update_currencies.py (#4740)
Co-authored-by: inetol <inetol@users.noreply.github.com>
2025-05-09 07:08:26 +02:00
github-actions[bot]
198928de05 [data] update searx.data - update_engine_traits.py (#4741)
Co-authored-by: inetol <inetol@users.noreply.github.com>
2025-05-09 07:07:33 +02:00
github-actions[bot]
11d9c830b8 [data] update searx.data - update_engine_descriptions.py (#4742)
Co-authored-by: inetol <inetol@users.noreply.github.com>
2025-05-09 07:06:52 +02:00
Ivan Gabaldon
743f90514b [fix] missing PR perm data-update.yml workflow (#4737)
We actually don't need to keep the token on checkout because `peter-evans/create-pull-request` will read from `github.token`. The obvious `pull-requests` write permission wasn't set in the last fix, so I added it now.
2025-05-08 23:55:23 +02:00
Ivan Gabaldon
48801dbc9a [mod] CI move build of online docs to dedicated workflow documentation.yml (#4733)
documentation.yml will run after integration.yml COMPLETES successfully (will
defer anything depending on integration.yml until heavy loads like container
building are moved to separate workflows) and in master branch.

Style changes, cleanup and improved integration with CI by leveraging the use of
shared cache between all workflows (not functional until all workflows have been
refactored).
2025-05-08 17:40:05 +02:00
Ivan Gabaldon
5451ab243a [fix] fix security.yml workflow (#4735)
Uploading SARIFs needs to write into the repository GitHub security tab
2025-05-08 17:13:07 +02:00
Ivan Gabaldon
7ca24eee45 [fix] missing perm data-update.yml workflow (#4736)
We need to keep the token on checkout and allow writing into the repository to create the branch
2025-05-08 16:51:21 +02:00
Ivan Gabaldon
c6a70782b2 [mod] CI: refactor data-update.yml - searxng_extra/update scripts (#4732)
Style changes, cleanup and improved integration with CI by leveraging the use of
shared cache between all workflows (not functional until all workflows have been
refactored).
2025-05-08 15:13:22 +02:00
Ivan Gabaldon
01a07f34b2 [mod] CI refactor security.yml - style and cleanup changes (#4731) 2025-05-08 14:44:22 +02:00
Ivan Gabaldon
f32fcb1243 [mod] CI: refactor checker.yml - make search.checker (#4730)
Style changes, cleanup and improved integration with CI by leveraging the use of
shared cache between all workflows (not functional until all workflows have been
refactored).
2025-05-08 13:58:06 +02:00
benpiano800
bc06b1aece [enh] plugins: tor_check: Add more keywords (#4726)
Previously, there was only one usable keyword for the tor_check plugin. Adding more keywords eliminates confusion.
2025-05-07 10:39:46 +02:00
Brock Vojkovic
ff60fe635f [fix] sec-fetch-* blocking infinite scroll (#4728) 2025-05-07 10:38:21 +02:00
Markus Heiser
6e7119fa4e [fix] references from searx.botdetection.http_sec_fetch (#4723) 2025-05-07 10:25:47 +02:00
Ivan Gabaldon
f52cd3f008 missing dependency for armv7 (#4727) 2025-05-07 08:53:34 +02:00
Ivan Gabaldon
a2fa7de880 [mod] Rework Dockerfile - migrate to glibc (debian) (#4721) 2025-05-06 11:56:59 +02:00
Bnyro
0315988f5a fix] revert searxng/searxng#4699 due to breaking issues (#4720)
This reverts commit 2e74d86321.
2025-05-05 09:46:37 +02:00
Ivan Gabaldon
2e74d86321 Rework Dockerfile (#4699)
This is one of various PR to refactor the entire SearXNG Docker workflow.

Switches to Python glibc based images, all dependencies are installed via pip and not from system repositories, and several minor changes.

This PR will increase the image size from 194.9 MB to 345.47 MB (amd64), this is due to ARMv7 images (needs dependencies for wheels compilation and runtime (?)) and uWSGI webserver. Later PR will reduce the final image size.
2025-05-04 22:27:53 +02:00
Émilien (perso)
19b116f1d7 fix: check if the browser supports Sec-Fetch headers (#4696) 2025-05-04 10:12:25 +02:00
Markus Heiser
fe08bb1d90 [mod] botdetection: HTTP Fetch Metadata Request Headers
HTTP Fetch Metadata Request Headers [1][2] are used to detect bot requests. Bots
with invalid *Fetch Metadata* will be redirected to the intro (`index`)  page.

[1] https://www.w3.org/TR/fetch-metadata/
[2] https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-04 02:07:26 +02:00
Markus Heiser
8ef5fbca4e [fix] cache.ExpireCache: definition of a context name for the key
The definition of a context name belongs in the abstract base class (was
previously only in the concrete implementation for the SQLite adapter).

Suggested-by: @dalf https://github.com/searxng/searxng/pull/4650#discussion_r2069873853
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-03 08:39:12 +02:00
Markus Heiser
7351c38e6c [fix] (armv7) cache.ExpireCache: remove option ENCRYPT_VALUE
Prophylactic encryption of the value currently makes no sense; on the contrary,
since the ``cryptography`` package is not available on armv7, it would cause
further problems.

Suggested-by: @dalf https://github.com/searxng/searxng/pull/4650#issuecomment-2830786661
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-03 08:39:12 +02:00
Markus Heiser
bdfe1c2a15 [mod] engines: migration of the individual cache solutions to EngineCache
The EngineCache class replaces all previously individual solutions for caches in
the context of the engines.

- demo_offline.py
- duckduckgo.py
- radio_browser.py
- soundcloud.py
- startpage.py
- wolframalpha_api.py
- wolframalpha_noapi.py

Search term to test most of the modified engines::

    !ddg !rb !sc !sp !wa test

    !ddg !rb !sc !sp !wa foo

For introspection of the DB, jump into developer environment and run command to
show cache state::

    $ ./manage pyenv.cmd bash --norc --noprofile
    (py3) python -m searx.enginelib cache state

    cache tables and key/values
    ===========================
    [demo_offline        ] 2025-04-22 11:32:50 count        --> (int) 4
    [startpage           ] 2025-04-22 12:32:30 SC_CODE      --> (str) fSOBnhEMlDfE20
    [duckduckgo          ] 2025-04-22 12:32:31 4dff493e.... --> (str) 4-128634958369380006627592672385352473325
    [duckduckgo          ] 2025-04-22 12:40:06 3e2583e2.... --> (str) 4-263126175288871260472289814259666848451
    [radio_browser       ] 2025-04-23 11:33:08 servers      --> (list) ['https://de2.api.radio-browser.info',  ...]
    [soundcloud          ] 2025-04-29 11:40:06 guest_client_id --> (str) EjkRJG0BLNEZquRiPZYdNtJdyGtTuHdp
    [wolframalpha        ] 2025-04-22 12:40:06 code         --> (str) 5aa79f86205ad26188e0e26e28fb7ae7
    number of tables: 6
    number of key/value pairs: 7

In the "cache tables and key/values" section, the table name (engine name) is at
first position on the second there is the calculated expire date and on the
third and fourth position the key/value is shown.

About duckduckgo: The *vqd coode* of ddg depends on the query term and therefore
the key is a hash value of the query term (to not to store the raw query term).

In the "properties of ENGINES_CACHE" section all properties of the SQLiteAppl /
ExpireCache and their last modification date are shown::

    properties of ENGINES_CACHE
    ===========================
    [last modified: 2025-04-22 11:32:27] DB_SCHEMA           : 1
    [last modified: 2025-04-22 11:32:27] LAST_MAINTENANCE    :
    [last modified: 2025-04-22 11:32:27] crypt_hash          : ca612e3566fdfd7cf7efe2b1c9349f461158d07cb78a3750e5c5be686aa8ebdc
    [last modified: 2025-04-22 11:32:30] CACHE-TABLE--demo_offline: demo_offline
    [last modified: 2025-04-22 11:32:30] CACHE-TABLE--startpage: startpage
    [last modified: 2025-04-22 11:32:31] CACHE-TABLE--duckduckgo: duckduckgo
    [last modified: 2025-04-22 11:33:08] CACHE-TABLE--radio_browser: radio_browser
    [last modified: 2025-04-22 11:40:06] CACHE-TABLE--soundcloud: soundcloud
    [last modified: 2025-04-22 11:40:06] CACHE-TABLE--wolframalpha: wolframalpha

These properties provide information about the state of the ExpireCache and
control the behavior.  For example, the maintenance intervals are controlled by
the last modification date of the LAST_MAINTENANCE property and the hash value
of the password can be used to detect whether the password has been changed (in
this case the DB entries can no longer be decrypted and the entire cache must be
discarded).

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-03 08:39:12 +02:00
Markus Heiser
4cbfba9d7b [mod] ExpireCache - sqlite based key/value cache with expire time 2025-05-03 08:39:12 +02:00
Markus Heiser
4a594f1b53 [fix] ResourceWarning: unclosed database in sqlite3
Reported:

- https://github.com/inetol-infrastructure/searxng-container/issues/5

Related:

- https://github.com/searxng/searxng/issues/4405#issuecomment-2692352352

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-03 08:39:12 +02:00
Bnyro
590b211652 [fix] semantic scholar: method not allowed / engine doesn't work
Fixes the semantic scholar engine by extracting a ui version token.

BTW: remove html tags from the content.

Author's checklist:

- they are ratelimiting very fast, if you do approx more than 2 requests per
  minute, you have to wait some time again...

- they also have an official api at api.semanticscholar.org, but it's ratelimits
  are even harder

Closes: https://github.com/searxng/searxng/issues/4685
2025-05-02 16:46:38 +02:00
return42
41e3a0baa7 [data] update searx.data - update_engine_descriptions.py 2025-05-02 16:45:47 +02:00
dependabot[bot]
11204459da [upd] web-client (simple): Bump less-loader in /client/simple
Bumps [less-loader](https://github.com/webpack-contrib/less-loader) from 12.2.0 to 12.3.0.
- [Release notes](https://github.com/webpack-contrib/less-loader/releases)
- [Changelog](https://github.com/webpack-contrib/less-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/less-loader/compare/v12.2.0...v12.3.0)

---
updated-dependencies:
- dependency-name: less-loader
  dependency-version: 12.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 16:06:36 +02:00
Markus Heiser
03787b2e5a [docs] fix link searx/languages.py to new file searx/sxng_locales.py
The language settings page in the documentation reference languages.py which was
removed in commit c9cd376

Closes: https://github.com/searxng/searxng/issues/4690
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-02 15:19:55 +02:00
dependabot[bot]
72294a9ffa [upd] web-client (simple): Bump webpack in /client/simple
Bumps [webpack](https://github.com/webpack/webpack) from 5.99.6 to 5.99.7.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.99.6...v5.99.7)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.99.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 14:50:13 +02:00
BrandonStudio
d47cf9db24 [feat] engine ChinaSo: support source filter for ChinaSo-News
* filtering ChinaSo-News results by source, option ``chinaso_news_source``
* add ChinaSo engine to the online docs https://docs.searxng.org/dev/engines/online/chinaso.html
* fix SearXNG categories in the settings.yml
* deactivate ChinaSo engines ``inactive: true`` until [1] is fixed
* configure network of the ChinaSo engines

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

Signed-off-by: @BrandonStudio
Co-authored-by: Markus Heiser <markus.heiser@darmarit.de>
2025-05-02 14:22:51 +02:00
dependabot[bot]
705ebe64b5 [upd] pypi: Bump typer-slim from 0.15.2 to 0.15.3
Bumps [typer-slim](https://github.com/fastapi/typer) from 0.15.2 to 0.15.3.
- [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.15.2...0.15.3)

---
updated-dependencies:
- dependency-name: typer-slim
  dependency-version: 0.15.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 13:46:47 +02:00
dependabot[bot]
b072b0f66b [upd] pypi: Bump setproctitle from 1.3.5 to 1.3.6
Bumps [setproctitle](https://github.com/dvarrazzo/py-setproctitle) from 1.3.5 to 1.3.6.
- [Changelog](https://github.com/dvarrazzo/py-setproctitle/blob/master/HISTORY.rst)
- [Commits](https://github.com/dvarrazzo/py-setproctitle/compare/version-1.3.5...version-1.3.6)

---
updated-dependencies:
- dependency-name: setproctitle
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 13:37:39 +02:00
dependabot[bot]
4beb5523f5 [upd] pypi: Bump certifi from 2025.1.31 to 2025.4.26
Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.1.31 to 2025.4.26.
- [Commits](https://github.com/certifi/python-certifi/compare/2025.01.31...2025.04.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 13:37:11 +02:00
dependabot[bot]
54e6e2a96d [upd] web-client (simple): Bump stylelint in /client/simple
Bumps [stylelint](https://github.com/stylelint/stylelint) from 16.19.0 to 16.19.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/16.19.0...16.19.1)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-version: 16.19.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 13:36:01 +02:00
searxng-bot
abcf8ca073 [l10n] update translations from Weblate
244e1f3f2 - 2025-04-30 - return42 <return42@noreply.codeberg.org>
2025-05-02 13:29:05 +02:00
dependabot[bot]
1a16281490 Bump vite from 6.3.3 to 6.3.4 in /client/simple
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.3 to 6.3.4.
- [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/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 09:43:39 +02:00
Bnyro
fd33559cfb [fix] brave: fix images and videos engines 2025-04-30 08:28:04 +02:00
Denperidge
60e31eacfc [fix] pdia: dynamically fetch API key config file location
As suggested by @Bnyro at
https://github.com/searxng/searxng/pull/4652#discussion_r2055760390 !
2025-04-29 20:45:08 +02:00
return42
c45b970546 [data] update searx.data - update_currencies.py 2025-04-29 09:12:06 +02:00
Markus Heiser
a4251be397 [data] update searx.data - make data.traits
Related:

- https://github.com/searxng/searxng/pull/4687

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-29 09:11:33 +02:00
Markus Heiser
c20038e7c3 [fix] engine yahoo: replace fetch_traits by a list of languages
The Yahoo engine's fetch_traits function has been encountering an error in CI
jobs for several months [1], thus aborting the process for all other engines as
well.

The language selection dialog (which fetch_traits calls) requires an `EuConsent`
cookie. Strangely, the cookie is not needed for searching, which is why the
engine itself still works.

Since Yahoo won't be conquering any new marketplaces in the foreseeable future,
it should be sufficient to hard-implement the list of currently available
languages ​​(`yahoo_languages`).

[1] https://github.com/searxng/searxng/actions/runs/14720458830/job/41313149268

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-29 08:48:56 +02:00
return42
20e40ded6d [data] update searx.data - update_wikidata_units.py 2025-04-29 07:07:15 +02:00
return42
4de0766b76 [data] update searx.data - update_ahmia_blacklist.py 2025-04-29 07:06:49 +02:00
return42
6de83ca47f [data] update searx.data - update_firefox_version.py 2025-04-29 07:06:26 +02:00
return42
a32bcd54c5 [data] update searx.data - update_external_bangs.py 2025-04-29 07:06:10 +02:00
searxng-bot
c733aa83e8 [l10n] update translations from Weblate
bec89c8a4 - 2025-04-24 - Atul_Eterno <atul_eterno@noreply.codeberg.org>
97edb4d63 - 2025-04-23 - whytf <whytf@noreply.codeberg.org>
e7111d6ec - 2025-04-22 - prashere <prashere@noreply.codeberg.org>
f5eeda966 - 2025-04-20 - return42 <return42@noreply.codeberg.org>
2025-04-27 17:13:43 +02:00
dependabot[bot]
9dfc6f38d5 [upd] web-client (simple): Bump vite in /client/simple
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.2 to 6.3.3.
- [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/v6.3.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 10:16:44 +02:00
dependabot[bot]
e1076f5c35 [upd] web-client (simple): Bump @eslint/js in /client/simple
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.24.0 to 9.25.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.25.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.25.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 10:16:26 +02:00
dependabot[bot]
8489817561 [upd] web-client (simple): Bump webpack in /client/simple
Bumps [webpack](https://github.com/webpack/webpack) from 5.99.5 to 5.99.6.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.99.5...v5.99.6)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.99.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 10:16:03 +02:00
dependabot[bot]
38ff1e4a7d [upd] web-client (simple): Bump stylelint in /client/simple
Bumps [stylelint](https://github.com/stylelint/stylelint) from 16.18.0 to 16.19.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.18.0...16.19.0)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-version: 16.19.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 10:15:40 +02:00
dependabot[bot]
21cdb0332b [upd] web-client (simple): Bump eslint in /client/simple
Bumps [eslint](https://github.com/eslint/eslint) from 9.24.0 to 9.25.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.24.0...v9.25.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.25.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 10:15:33 +02:00
dependabot[bot]
30330617d9 [upd] pypi: Bump lxml from 5.3.2 to 5.4.0
Bumps [lxml](https://github.com/lxml/lxml) from 5.3.2 to 5.4.0.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-5.3.2...lxml-5.4.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-26 13:46:56 +02:00
Markus Heiser
33729439c5 [fix] is_werkzeug_reload_active is not realted to python -m
Werkzeug's reloader is not active when was server is launched by uWSGI.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-24 17:24:51 +02:00
Markus Heiser
20ce88a274 Revert "[fix] is_werkzeug_reload_active is not realted to python -m"
This reverts commit 3392beb914.
2025-04-24 17:03:20 +02:00
Zhijie He
8595e467ce [fix] fix Quark engine calling 2025-04-24 16:17:34 +02:00
Markus Heiser
3392beb914 [fix] is_werkzeug_reload_active is not realted to python -m
Werkzeug's reloader is not active when was server is launched by::

    python -m searx.webapp

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-24 15:33:59 +02:00
Markus Heiser
c6c6d3027c [mod] internal ! and external !! bangs: ignore upper/lower case
Closes: https://github.com/searxng/searxng/issues/1223

Suggested-by: @dalf https://github.com/searxng/searxng/issues/1223#issuecomment-1133772363
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-24 10:40:18 +02:00
Markus Heiser
d19eb3903e [clean] drop unusable engine: server/project on curlie.org is broken
The websites of https://curlie.org are no longer usable, long runtimes and
recurring "Bad Gateway" messages .. the project is no longer maintained and is
therefore no longer useful in SearXNG.

Related: https://github.com/searxng/searxng/issues/1190
Closes: https://github.com/searxng/searxng/issues/3482

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-24 09:57:50 +02:00
Markus Heiser
f45d4145e6 [fix] typo in soundcloud engine
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-23 18:42:40 +02:00
Markus Heiser
937eb907d3 [data] update searx.data - make data.traits (mullvad leta)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-23 14:06:32 +02:00
Grant Lanham
851c0e5cc0 [fix] engine: re-implement mullvad leta integration
Re-writes the Mullvad Leta integration to work with the new breaking changes.

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.

- Remove docstring comments regarding requiring the use of Mullvad VPN, which is
  no longer a hard requirement.

- configured two engines: ``mullvadleta`` (uses google) and
  ``mullvadleta brave`` (uses brave)

- since leta may not provide up-to-date search results, both search engines are
  disabled by default.

.. 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!

Co-authored-by: Markus Heiser <markus.heiser@darmarit.de>
Signed-off-by: Grant Lanham <contact@grantlanham.com>
2025-04-23 14:06:32 +02:00
Aadniz
07a94d4d2e [mod] include SEARXNG_METHOD environment variable 2025-04-23 07:29:26 +02:00
Markus Heiser
e9157b3c1a [fix] issues when launching a local development server
A local development server can be launched by one of these command lines::

    $ flask --app searx.webapp run
    $ python -m searx.webapp

The different ways of starting the server should lead to the same result, which
is generally the case.  However, if the modules are reloaded after code
changes (reload option), it must be avoided that the application is initialized
twice at startup.  We have already discussed this in 2022 [1][2].

Further information on this topic can be found in [3][4][5].

To test a bash in the ./local environment was started and the follwing commands
had been executed::

    $ ./manage pyenv.cmd bash --norc --noprofile
    (py3) SEARXNG_DEBUG=1 flask --app searx.webapp run --reload
    (py3) SEARXNG_DEBUG=1 python -m searx.webapp

Since the generic parts of the docs also initialize the app to generate doc from
it, the build of the docs was also tested::

    $ make docs.clean docs.live

[1] https://github.com/searxng/searxng/pull/1656#issuecomment-1214198941
[2] https://github.com/searxng/searxng/pull/1616#issuecomment-1206137468
[3] https://flask.palletsprojects.com/en/stable/api/#flask.Flask.run
[4] https://github.com/pallets/flask/issues/5307#issuecomment-1774646119
[5] https://stackoverflow.com/a/25504196

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-22 14:26:11 +02:00
dependabot[bot]
5ae3b3f17e [upd] web-client (simple): Bump vite-plugin-static-copy
Bumps [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases)
- [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@2.3.0...vite-plugin-static-copy@2.3.1)

---
updated-dependencies:
- dependency-name: vite-plugin-static-copy
  dependency-version: 2.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-18 09:46:01 +02:00
dependabot[bot]
50ac76ce94 [upd] web-client (simple): Bump vite in /client/simple
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.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/v6.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-18 09:43:05 +02:00
searxng-bot
5aea044eb5 [l10n] update translations from Weblate
f290f4ffe - 2025-04-15 - return42 <return42@noreply.codeberg.org>
4a9134b03 - 2025-04-14 - DZDevelopers <dzdevelopers@noreply.codeberg.org>
a8c86581d - 2025-04-14 - aindriu80 <aindriu80@noreply.codeberg.org>
2b5395719 - 2025-04-11 - ayame30 <ayame30@noreply.codeberg.org>
448a443fa - 2025-04-11 - ayame30 <ayame30@noreply.codeberg.org>
2025-04-18 09:30:55 +02:00
Zhijie He
808dcaf1e2 [feat] engine: add Steam engine 2025-04-18 09:30:17 +02:00
Markus Heiser
777ba6ddf8 [fix] disable engine ansa by default
Reported-by: https://github.com/searxng/searxng/pull/4575#issuecomment-2813007107
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-17 16:50:57 +02:00
Tommaso Colella
8ca57748ff [fix] docs: changed "many" to "some" for clarity in contribution guide's privacy by design section 2025-04-17 16:47:52 +02:00
Tommaso Colella
30399c50e2 [fix] docs: typos and minor refinements in contribution_guide.rst 2025-04-17 16:47:52 +02:00
Tommaso Colella
e6467bce7c [fix] docs: better phrasing for many sections of contribution_guide.rst 2025-04-17 16:47:52 +02:00
Tommaso Colella
8ca2bbc4e9 [fix] docs: typos in dev contrib guide "it's might be because
of the tool" -> "it might be because the tool"
2025-04-17 16:47:52 +02:00
Tommaso Colella
a74593419f [fix] docs: typo in templates dev doc developerat -> developer at 2025-04-17 16:47:52 +02:00
Zhijie He
f94802f2d2 [feat] engines: add Hugging Face engine 2025-04-17 16:43:32 +02:00
Tommaso Colella
d1c584b961 [feat] engine: add engine for italian press agency ansa 2025-04-17 15:33:57 +02:00
Markus Heiser
81f3d15665 [fix] settings.yml files: doc & obsolete settings in the template
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-17 13:31:10 +02:00
Tommaso Colella
99ad69450d [fix] Result type: remove rstrip "/" form url normalization 2025-04-17 10:24:05 +02:00
RobinFrcd
087da66565 [feat] add SensCritique (FR) engine
Closes: https://github.com/searxng/searxng/issues/4623
2025-04-17 10:19:22 +02:00
Markus Heiser
b84ae39978 [fix] doc of setting server.base_url, env is SEARXNG_BASE_URL
Related:

- https://github.com/searxng/searxng/issues/4634

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-04-16 14:54:46 +02:00
Tommaso Colella
391bb1268d [feat] engine: add microsoft learn engine 2025-04-12 11:14:13 +02:00
126 changed files with 10643 additions and 6146 deletions

View File

@@ -20,3 +20,21 @@ updates:
target-branch: "master" target-branch: "master"
commit-message: commit-message:
prefix: "[upd] web-client (simple):" prefix: "[upd] web-client (simple):"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "friday"
target-branch: "master"
commit-message:
prefix: "[upd] docker:"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "friday"
target-branch: "master"
commit-message:
prefix: "[upd] github-actions:"

View File

@@ -1,31 +1,46 @@
name: "Checker" ---
on: # yamllint disable-line rule:truthy name: Checker
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule: schedule:
- cron: "0 4 * * 5" - cron: "0 4 * * 5"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
env:
PYTHON_VERSION: "3.13"
jobs: jobs:
checker: search:
name: Checker name: Search
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04-arm
steps: steps:
- name: Checkout - name: Setup Python
uses: actions/checkout@v4
- name: Install Ubuntu packages
run: |
sudo ./utils/searxng.sh install packages
- name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.13' python-version: "${{ env.PYTHON_VERSION }}"
architecture: 'x64'
- name: Install Python dependencies - name: Checkout
run: | uses: actions/checkout@v4
make V=1 install with:
persist-credentials: "false"
- name: Checker - name: Setup cache Python
run: | uses: actions/cache@v4
make search.checker with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
path: "./local"
- name: Setup venv
run: make V=1 install
- name: Search checker
run: make search.checker

View File

@@ -1,14 +1,27 @@
name: "Update searx.data" ---
on: # yamllint disable-line rule:truthy name: Update searx.data
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule: schedule:
- cron: "59 23 28 * *" - cron: "59 23 28 * *"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
env:
PYTHON_VERSION: "3.13"
jobs: jobs:
updateData: data:
name: Update data - ${{ matrix.fetch }} if: github.repository_owner == 'searxng'
runs-on: ubuntu-24.04 name: ${{ matrix.fetch }}
if: ${{ github.repository_owner == 'searxng'}} runs-on: ubuntu-24.04-arm
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -20,48 +33,51 @@ jobs:
- update_engine_traits.py - update_engine_traits.py
- update_wikidata_units.py - update_wikidata_units.py
- update_engine_descriptions.py - update_engine_descriptions.py
permissions:
contents: write
pull-requests: write
steps: steps:
- name: Checkout - name: Setup Python
uses: actions/checkout@v4
- name: Install Ubuntu packages
run: |
sudo ./utils/searxng.sh install packages
- name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.12' python-version: "${{ env.PYTHON_VERSION }}"
architecture: 'x64'
- name: Install Python dependencies - name: Checkout
run: | uses: actions/checkout@v4
make V=1 install with:
persist-credentials: "false"
- name: Setup cache Python
uses: actions/cache@v4
with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
path: "./local/"
- name: Setup venv
run: make V=1 install
- name: Fetch data - name: Fetch data
env: run: V=1 ./manage pyenv.cmd python "./searxng_extra/update/${{ matrix.fetch }}"
FETCH_SCRIPT: ./searxng_extra/update/${{ matrix.fetch }}
run: |
V=1 ./manage pyenv.cmd python "$FETCH_SCRIPT"
- name: Create Pull Request - name: Create PR
id: cpr id: cpr
uses: peter-evans/create-pull-request@v6 uses: peter-evans/create-pull-request@v7
with: with:
commit-message: '[data] update searx.data - ${{ matrix.fetch }}' author: "${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>"
committer: searxng-bot <noreply@github.com> committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> title: "[data] update searx.data - ${{ matrix.fetch }}"
signoff: false commit-message: "[data] update searx.data - ${{ matrix.fetch }}"
branch: update_data_${{ matrix.fetch }} branch: "update_data_${{ matrix.fetch }}"
delete-branch: true delete-branch: "true"
draft: false draft: "false"
title: '[data] update searx.data - ${{ matrix.fetch }}' signoff: "false"
body: |
update searx.data - ${{ matrix.fetch }}
labels: | labels: |
data data
- name: Check outputs - name: Display information
run: | run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

66
.github/workflows/documentation.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
---
name: Documentation
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
push:
branches:
- master
pull_request:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
env:
PYTHON_VERSION: "3.13"
jobs:
release:
name: Release
runs-on: ubuntu-24.04-arm
permissions:
# for JamesIves/github-pages-deploy-action to push
contents: write
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: "false"
fetch-depth: "0"
- name: Setup cache Python
uses: actions/cache@v4
with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
path: "./local/"
- name: Setup venv
run: make V=1 install
- name: Build documentation
run: make V=1 docs.clean docs.html
- if: github.ref_name == 'master'
name: Release
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: "dist/docs"
branch: "gh-pages"
commit-message: "[doc] build from commit ${{ github.sha }}"
# Automatically remove deleted files from the deploy branch
clean: "true"
single-commit: "true"

View File

@@ -1,143 +1,106 @@
---
name: Integration name: Integration
on: # yamllint disable-line rule:truthy # yamllint disable-line rule:truthy
on:
push: push:
branches: ["master"] branches:
- master
pull_request: pull_request:
branches: ["master"] branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
permissions: permissions:
contents: read contents: read
env:
PYTHON_VERSION: "3.13"
jobs: jobs:
python: test:
name: Python ${{ matrix.python-version }} name: Python ${{ matrix.python-version }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
os: [ubuntu-24.04] python-version:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
steps: steps:
- name: Checkout - name: Setup Python
uses: actions/checkout@v4
- name: Install Ubuntu packages
run: |
sudo ./utils/searxng.sh install packages
- name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: "${{ matrix.python-version }}"
architecture: 'x64'
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: "false"
- name: Setup cache Python
uses: actions/cache@v4
with:
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ matrix.python-version }}-${{ runner.arch }}-"
path: "./local/"
- name: Setup venv
run: make V=1 install
- name: Run tests - name: Run tests
run: make V=1 ci.test run: make V=1 ci.test
themes: theme:
name: Themes name: Theme
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04-arm
steps: steps:
- name: Checkout - name: Setup Python
uses: actions/checkout@v4
- name: Install Ubuntu packages
run: sudo ./utils/searxng.sh install buildhost
- name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.12' python-version: "${{ env.PYTHON_VERSION }}"
architecture: 'x64'
- name: Build themes - name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: "false"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: "./.nvmrc"
- name: Setup cache Node.js
uses: actions/cache@v4
with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
path: "./client/simple/node_modules/"
- name: Setup cache Python
uses: actions/cache@v4
with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
path: "./local/"
- name: Setup venv
run: make V=1 install
- name: Build
run: make themes.all run: make themes.all
documentation:
name: Documentation
runs-on: ubuntu-24.04
permissions:
contents: write # for JamesIves/github-pages-deploy-action to push changes in repo
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: '0'
persist-credentials: false
- name: Install Ubuntu packages
run: sudo ./utils/searxng.sh install buildhost
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
architecture: 'x64'
- name: Cache Python dependencies
id: cache-python
uses: actions/cache@v4
with:
path: |
./local
./.nvm
./node_modules
key: python-ubuntu-24.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
- name: Build documentation
run: |
make V=1 docs.clean docs.html
- name: Deploy
if: github.ref == 'refs/heads/master'
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ github.token }}
BRANCH: gh-pages
FOLDER: dist/docs
CLEAN: true # Automatically remove deleted files from the deploy branch
SINGLE_COMMIT: true
COMMIT_MESSAGE: '[doc] build from commit ${{ github.sha }}'
babel:
name: Update translations branch
runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
needs:
- python
- themes
- documentation
permissions:
contents: write # for make V=1 weblate.push.translations
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: '0'
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
architecture: 'x64'
- name: Cache Python dependencies
id: cache-python
uses: actions/cache@v4
with:
path: |
./local
./.nvm
./node_modules
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
- name: weblate & git setup
env:
WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
run: |
mkdir -p ~/.config
echo "${WEBLATE_CONFIG}" > ~/.config/weblate
git config --global user.email "searxng-bot@users.noreply.github.com"
git config --global user.name "searxng-bot"
- name: Update transations
id: update
run: |
make V=1 weblate.push.translations
dockers: dockers:
name: Docker name: Docker
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
needs: needs:
- python - test
- themes - theme
- documentation
env: env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

136
.github/workflows/l10n.yml vendored Normal file
View File

@@ -0,0 +1,136 @@
---
name: Translation
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
workflow_run:
workflows:
- Integration
types:
- completed
branches:
- master
schedule:
- cron: "05 07 * * 5"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
env:
PYTHON_VERSION: "3.13"
jobs:
update:
if: github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success'
name: Update
runs-on: ubuntu-24.04-arm
permissions:
# For "make V=1 weblate.push.translations"
contents: write
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@v4
with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0"
- name: Setup cache Python
uses: actions/cache@v4
with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
path: "./local/"
- name: Setup venv
run: make V=1 install
- name: Setup Weblate
run: |
mkdir -p ~/.config
echo "${{ secrets.WEBLATE_CONFIG }}" > ~/.config/weblate
- name: Setup Git
run: |
git config --global user.email "searxng-bot@users.noreply.github.com"
git config --global user.name "searxng-bot"
- name: Update translations
run: make V=1 weblate.push.translations
pr:
if: |
github.repository_owner == 'searxng'
&& (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule')
name: Pull Request
runs-on: ubuntu-24.04-arm
permissions:
# For "make V=1 weblate.translations.commit"
contents: write
# For action "peter-evans/create-pull-request"
pull-requests: write
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@v4
with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0"
- name: Setup cache Python
uses: actions/cache@v4
with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
path: "./local/"
- name: Setup venv
run: make V=1 install
- name: Setup Weblate
run: |
mkdir -p ~/.config
echo "${{ secrets.WEBLATE_CONFIG }}" > ~/.config/weblate
- name: Setup Git
run: |
git config --global user.email "searxng-bot@users.noreply.github.com"
git config --global user.name "searxng-bot"
- name: Merge and push translation updates
run: make V=1 weblate.translations.commit
- name: Create PR
id: cpr
uses: peter-evans/create-pull-request@v7
with:
author: "${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
title: "[l10n] update translations from Weblate"
commit-message: "[l10n] update translations from Weblate"
branch: "translations_update"
delete-branch: "true"
draft: "false"
signoff: "false"
labels: |
translation
- name: Display information
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

View File

@@ -1,28 +1,43 @@
name: "Security checks" ---
on: # yamllint disable-line rule:truthy name: Security
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule: schedule:
- cron: "42 05 * * *" - cron: "42 05 * * *"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
jobs: jobs:
dockers: container:
name: Trivy ${{ matrix.image }} name: Container
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04-arm
permissions:
security-events: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with: with:
image-ref: 'searxng/searxng:latest' persist-credentials: "false"
ignore-unfixed: false
vuln-type: 'os,library'
severity: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab - name: Run Trivy scanner
uses: github/codeql-action/upload-sarif@v2 uses: aquasecurity/trivy-action@0.30.0
with: with:
sarif_file: 'trivy-results.sarif' image-ref: "docker.io/searxng/searxng:latest"
vuln-type: "os,library"
severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
ignore-unfixed: "false"
format: "sarif"
output: "./trivy-results.sarif"
- name: Upload SARIFs
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: "./trivy-results.sarif"

View File

@@ -1,59 +0,0 @@
name: "Update translations"
on: # yamllint disable-line rule:truthy
schedule:
- cron: "05 07 * * 5"
workflow_dispatch:
jobs:
babel:
name: "create PR for additions from weblate"
runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: '0'
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
architecture: 'x64'
- name: Cache Python dependencies
id: cache-python
uses: actions/cache@v4
with:
path: |
./local
./.nvm
./node_modules
key: python-ubuntu-24.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
- name: weblate & git setup
env:
WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
run: |
mkdir -p ~/.config
echo "${WEBLATE_CONFIG}" > ~/.config/weblate
git config --global user.email "searxng-bot@users.noreply.github.com"
git config --global user.name "searxng-bot"
- name: Merge and push transation updates
run: |
make V=1 weblate.translations.commit
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
commit-message: '[l10n] update translations from Weblate'
committer: searxng-bot <searxng-bot@users.noreply.github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
signoff: false
branch: translations_update
delete-branch: true
draft: false
title: '[l10n] update translations from Weblate'
body: |
update translations from Weblate
labels: |
translation

View File

@@ -1,92 +1,107 @@
FROM alpine:3.20 FROM docker.io/library/python:3.13-slim AS builder
ENTRYPOINT ["/sbin/tini","--","/usr/local/searxng/dockerfiles/docker-entrypoint.sh"]
EXPOSE 8080
VOLUME /etc/searxng
ARG SEARXNG_GID=977 RUN apt-get update \
ARG SEARXNG_UID=977 && apt-get install -y --no-install-recommends \
build-essential \
RUN addgroup -g ${SEARXNG_GID} searxng && \
adduser -u ${SEARXNG_UID} -D -h /usr/local/searxng -s /bin/sh -G searxng searxng
ENV INSTANCE_NAME=searxng \
AUTOCOMPLETE= \
BASE_URL= \
BIND_ADDRESS=[::]:8080 \
MORTY_KEY= \
MORTY_URL= \
SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml \
UWSGI_SETTINGS_PATH=/etc/searxng/uwsgi.ini \
UWSGI_WORKERS=%k \
UWSGI_THREADS=4
WORKDIR /usr/local/searxng
COPY requirements.txt ./requirements.txt
RUN apk add --no-cache -t build-dependencies \
build-base \
py3-setuptools \
python3-dev \
libffi-dev \
libxslt-dev \
libxml2-dev \
openssl-dev \
tar \
git \
&& apk add --no-cache \
ca-certificates \
python3 \
py3-pip \
libxml2 \
libxslt \
openssl \
tini \
uwsgi \
uwsgi-python3 \
brotli \ brotli \
&& pip3 install --break-system-packages --no-cache -r requirements.txt \ # lxml
&& apk del build-dependencies \ libxml2-dev \
&& rm -rf /root/.cache libxslt1-dev \
zlib1g-dev \
# uwsgi
libpcre3-dev \
&& rm -rf /var/lib/apt/lists/*
COPY --chown=searxng:searxng dockerfiles ./dockerfiles WORKDIR /usr/local/searxng/
COPY --chown=searxng:searxng searx ./searx
COPY ./requirements.txt ./requirements.txt
# Readd on #4707 "--mount=type=cache,id=pip,target=/root/.cache/pip"
RUN python -m venv ./venv \
&& . ./venv/bin/activate \
&& pip install -r requirements.txt \
&& pip install "uwsgi~=2.0"
COPY ./searx/ ./searx/
ARG TIMESTAMP_SETTINGS=0 ARG TIMESTAMP_SETTINGS=0
ARG TIMESTAMP_UWSGI=0 ARG TIMESTAMP_UWSGI=0
ARG VERSION_GITCOMMIT=unknown
RUN su searxng -c "/usr/bin/python3 -m compileall -q searx" \ RUN python -m compileall -q searx \
&& touch -c --date=@${TIMESTAMP_SETTINGS} searx/settings.yml \ && touch -c --date=@$TIMESTAMP_SETTINGS ./searx/settings.yml \
&& touch -c --date=@${TIMESTAMP_UWSGI} dockerfiles/uwsgi.ini \ && touch -c --date=@$TIMESTAMP_UWSGI ./dockerfiles/uwsgi.ini \
&& find /usr/local/searxng/searx/static -a \( -name '*.html' -o -name '*.css' -o -name '*.js' \ && find /usr/local/searxng/searx/static \
-o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \ \( -name '*.html' -o -name '*.css' -o -name '*.js' -o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \
-type f -exec gzip -9 -k {} \+ -exec brotli --best {} \+ -type f -exec gzip -9 -k {} + -exec brotli --best {} +
ARG SEARXNG_UID=977
ARG SEARXNG_GID=977
RUN grep -m1 root /etc/group > /tmp/.searxng.group \
&& grep -m1 root /etc/passwd > /tmp/.searxng.passwd \
&& echo "searxng:x:$SEARXNG_GID:" >> /tmp/.searxng.group \
&& echo "searxng:x:$SEARXNG_UID:$SEARXNG_GID:searxng:/usr/local/searxng:/bin/bash" >> /tmp/.searxng.passwd
FROM docker.io/library/python:3.13-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
# healthcheck
wget \
# lxml (ARMv7)
libxslt1.1 \
# uwsgi
libpcre3 \
libxml2 \
mailcap \
&& rm -rf /var/lib/apt/lists/*
COPY --chown=root:root --from=builder /tmp/.searxng.passwd /etc/passwd
COPY --chown=root:root --from=builder /tmp/.searxng.group /etc/group
ARG LABEL_DATE="0001-01-01T00:00:00Z"
ARG GIT_URL="unspecified"
ARG SEARXNG_GIT_VERSION="unspecified"
ARG LABEL_VCS_REF="unspecified"
ARG LABEL_VCS_URL="unspecified"
WORKDIR /usr/local/searxng/
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/venv/ ./venv/
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/searx/ ./searx/
COPY --chown=searxng:searxng ./dockerfiles/ ./dockerfiles/
LABEL org.opencontainers.image.authors="searxng <$GIT_URL>" \
org.opencontainers.image.created=$LABEL_DATE \
org.opencontainers.image.description="A privacy-respecting, hackable metasearch engine" \
org.opencontainers.image.documentation="https://github.com/searxng/searxng-docker" \
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
org.opencontainers.image.revision=$LABEL_VCS_REF \
org.opencontainers.image.source=$LABEL_VCS_URL \
org.opencontainers.image.title="searxng" \
org.opencontainers.image.url=$LABEL_VCS_URL \
org.opencontainers.image.version=$SEARXNG_GIT_VERSION
ENV CONFIG_PATH=/etc/searxng \
DATA_PATH=/var/cache/searxng
ENV SEARXNG_VERSION=$SEARXNG_GIT_VERSION \
INSTANCE_NAME=searxng \
AUTOCOMPLETE="" \
BASE_URL="" \
BIND_ADDRESS=[::]:8080 \
MORTY_KEY="" \
MORTY_URL="" \
SEARXNG_SETTINGS_PATH=$CONFIG_PATH/settings.yml \
UWSGI_SETTINGS_PATH=$CONFIG_PATH/uwsgi.ini \
UWSGI_WORKERS=%k \
UWSGI_THREADS=4
VOLUME $CONFIG_PATH
VOLUME $DATA_PATH
EXPOSE 8080
HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:8080/healthz || exit 1 HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:8080/healthz || exit 1
# Keep these arguments at the end to prevent redundant layer rebuilds ENTRYPOINT ["/usr/local/searxng/dockerfiles/docker-entrypoint.sh"]
ARG LABEL_DATE=
ARG GIT_URL=unknown
ARG SEARXNG_GIT_VERSION=unknown
ARG SEARXNG_DOCKER_TAG=unknown
ARG LABEL_VCS_REF=
ARG LABEL_VCS_URL=
LABEL maintainer="searxng <${GIT_URL}>" \
description="A privacy-respecting, hackable metasearch engine." \
version="${SEARXNG_GIT_VERSION}" \
org.label-schema.schema-version="1.0" \
org.label-schema.name="searxng" \
org.label-schema.version="${SEARXNG_GIT_VERSION}" \
org.label-schema.url="${LABEL_VCS_URL}" \
org.label-schema.vcs-ref=${LABEL_VCS_REF} \
org.label-schema.vcs-url=${LABEL_VCS_URL} \
org.label-schema.build-date="${LABEL_DATE}" \
org.label-schema.usage="https://github.com/searxng/searxng-docker" \
org.opencontainers.image.title="searxng" \
org.opencontainers.image.version="${SEARXNG_DOCKER_TAG}" \
org.opencontainers.image.url="${LABEL_VCS_URL}" \
org.opencontainers.image.revision=${LABEL_VCS_REF} \
org.opencontainers.image.source=${LABEL_VCS_URL} \
org.opencontainers.image.created="${LABEL_DATE}" \
org.opencontainers.image.documentation="https://github.com/searxng/searxng-docker"

View File

@@ -11,30 +11,30 @@
"autocomplete-js": "^2.7.1" "autocomplete-js": "^2.7.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.22.0", "@eslint/js": "^9.25.1",
"copy-webpack-plugin": "^13.0.0", "copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"edge.js": "^6.2.1", "edge.js": "^6.2.1",
"eslint": "^9.24.0", "eslint": "^9.25.1",
"filemanager-webpack-plugin": "^8.0.0", "filemanager-webpack-plugin": "^8.0.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"ionicons": "^7.4.0", "ionicons": "^8.0.8",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"less": "^4.3.0", "less": "^4.3.0",
"less-loader": "^12.2.0", "less-loader": "^12.3.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"sharp": "^0.34.1", "sharp": "^0.34.1",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"stylelint": "^16.17.0", "stylelint": "^16.19.1",
"stylelint-config-standard": "^38.0.0", "stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-less": "^3.0.1", "stylelint-config-standard-less": "^3.0.1",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svgo": "^3.3.2", "svgo": "^3.3.2",
"swiped-events": "^1.2.0", "swiped-events": "^1.2.0",
"vite": "^6.2.6", "vite": "^6.3.4",
"vite-plugin-static-copy": "^2.3.0", "vite-plugin-static-copy": "^2.3.1",
"vite-plugin-stylelint": "^6.0.0", "vite-plugin-stylelint": "^6.0.0",
"webpack": "^5.99.5", "webpack": "^5.99.7",
"webpack-cli": "^6.0.1" "webpack-cli": "^6.0.1"
} }
}, },
@@ -668,9 +668,9 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.2.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
"integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -678,9 +678,9 @@
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.12.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -728,9 +728,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.24.0", "version": "9.25.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
"integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -748,13 +748,13 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.2.7", "version": "0.2.8",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.12.0", "@eslint/core": "^0.13.0",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
@@ -1486,9 +1486,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
"integrity": "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==", "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1500,9 +1500,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz",
"integrity": "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==", "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1514,9 +1514,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz",
"integrity": "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==", "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1528,9 +1528,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz",
"integrity": "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==", "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1542,9 +1542,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz",
"integrity": "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==", "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1556,9 +1556,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz",
"integrity": "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==", "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1570,9 +1570,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz",
"integrity": "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==", "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1584,9 +1584,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz",
"integrity": "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==", "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1598,9 +1598,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz",
"integrity": "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==", "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1612,9 +1612,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz",
"integrity": "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==", "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1626,9 +1626,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz",
"integrity": "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==", "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -1640,9 +1640,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz",
"integrity": "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==", "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -1654,9 +1654,23 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz",
"integrity": "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==", "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz",
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -1668,9 +1682,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz",
"integrity": "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==", "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -1682,9 +1696,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz",
"integrity": "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==", "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1696,9 +1710,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz",
"integrity": "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==", "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1710,9 +1724,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz",
"integrity": "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==", "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1724,9 +1738,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz",
"integrity": "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==", "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1738,9 +1752,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz",
"integrity": "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==", "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1752,9 +1766,9 @@
] ]
}, },
"node_modules/@stencil/core": { "node_modules/@stencil/core": {
"version": "4.26.0", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.26.0.tgz", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.31.0.tgz",
"integrity": "sha512-+0Inu+dJ9/LgWSskcZwx7v17v4GILcwIYxNgD+OuK0U+D5z61WsxWw7yHkYG5OqGPBijsJMVssYRx/Tn+e7F9A==", "integrity": "sha512-Ei9MFJ6LPD9BMFs+klkHylbVOOYhG10Jv4bvoFf3GMH15kA41rSYkEdr4DiX84ZdErQE2qtFV/2SUyWoXh0AhA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -1763,8 +1777,130 @@
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=16.0.0",
"npm": ">=7.10.0" "npm": ">=7.10.0"
},
"optionalDependencies": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
"@rollup/rollup-linux-arm64-musl": "4.34.9",
"@rollup/rollup-linux-x64-gnu": "4.34.9",
"@rollup/rollup-linux-x64-musl": "4.34.9",
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
"@rollup/rollup-win32-x64-msvc": "4.34.9"
} }
}, },
"node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
"integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
"integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
"integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
"integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@stencil/core/node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@stencil/core/node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@trysound/sax": { "node_modules/@trysound/sax": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -1815,9 +1951,9 @@
} }
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -2507,20 +2643,20 @@
} }
}, },
"node_modules/cacheable": { "node_modules/cacheable": {
"version": "1.8.9", "version": "1.8.10",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.9.tgz", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.10.tgz",
"integrity": "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ==", "integrity": "sha512-0ZnbicB/N2R6uziva8l6O6BieBklArWyiGx4GkwAhLKhSHyQtRfM9T1nx7HHuHDKkYB/efJQhz3QJ6x/YqoZzA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"hookified": "^1.7.1", "hookified": "^1.8.1",
"keyv": "^5.3.1" "keyv": "^5.3.2"
} }
}, },
"node_modules/cacheable/node_modules/keyv": { "node_modules/cacheable/node_modules/keyv": {
"version": "5.3.2", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.2.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz",
"integrity": "sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ==", "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3440,20 +3576,20 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.24.0", "version": "9.25.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz",
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.0", "@eslint/config-array": "^0.20.0",
"@eslint/config-helpers": "^0.2.0", "@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.12.0", "@eslint/core": "^0.13.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.24.0", "@eslint/js": "9.25.1",
"@eslint/plugin-kit": "^0.2.7", "@eslint/plugin-kit": "^0.2.8",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
@@ -4018,9 +4154,9 @@
} }
}, },
"node_modules/hookified": { "node_modules/hookified": {
"version": "1.8.1", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.1.tgz", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.2.tgz",
"integrity": "sha512-GrO2l93P8xCWBSTBX9l2BxI78VU/MAAYag+pG8curS3aBGy0++ZlxrQ7PdUOUVMbn5BwkGb6+eRrnf43ipnFEA==", "integrity": "sha512-5nZbBNP44sFCDjSoB//0N7m508APCgbQ4mGGo1KJGBYyCKNHfry1Pvd0JVHZIxjdnqn8nFRBAN/eFB6Rk/4w5w==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -4203,13 +4339,13 @@
} }
}, },
"node_modules/ionicons": { "node_modules/ionicons": {
"version": "7.4.0", "version": "8.0.8",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz", "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.8.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==", "integrity": "sha512-CRHhDQA5vsNxC7raeEPqddgO90iR2F13VZS7hB0Vx7JrxJMM2060E3ddgYbpMQNJjzeBsgpkNwdBeK5qQ7RbLA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@stencil/core": "^4.0.3" "@stencil/core": "^4.30.0"
} }
}, },
"node_modules/is-arrayish": { "node_modules/is-arrayish": {
@@ -4472,9 +4608,9 @@
} }
}, },
"node_modules/known-css-properties": { "node_modules/known-css-properties": {
"version": "0.35.0", "version": "0.36.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz",
"integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -4559,9 +4695,9 @@
} }
}, },
"node_modules/less-loader": { "node_modules/less-loader": {
"version": "12.2.0", "version": "12.3.0",
"resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.3.0.tgz",
"integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", "integrity": "sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -5623,13 +5759,13 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.34.7", "version": "4.40.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.7.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
"integrity": "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==", "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.6" "@types/estree": "1.0.7"
}, },
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@@ -5639,25 +5775,26 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.34.7", "@rollup/rollup-android-arm-eabi": "4.40.0",
"@rollup/rollup-android-arm64": "4.34.7", "@rollup/rollup-android-arm64": "4.40.0",
"@rollup/rollup-darwin-arm64": "4.34.7", "@rollup/rollup-darwin-arm64": "4.40.0",
"@rollup/rollup-darwin-x64": "4.34.7", "@rollup/rollup-darwin-x64": "4.40.0",
"@rollup/rollup-freebsd-arm64": "4.34.7", "@rollup/rollup-freebsd-arm64": "4.40.0",
"@rollup/rollup-freebsd-x64": "4.34.7", "@rollup/rollup-freebsd-x64": "4.40.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.7", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0",
"@rollup/rollup-linux-arm-musleabihf": "4.34.7", "@rollup/rollup-linux-arm-musleabihf": "4.40.0",
"@rollup/rollup-linux-arm64-gnu": "4.34.7", "@rollup/rollup-linux-arm64-gnu": "4.40.0",
"@rollup/rollup-linux-arm64-musl": "4.34.7", "@rollup/rollup-linux-arm64-musl": "4.40.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.7", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.7", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0",
"@rollup/rollup-linux-riscv64-gnu": "4.34.7", "@rollup/rollup-linux-riscv64-gnu": "4.40.0",
"@rollup/rollup-linux-s390x-gnu": "4.34.7", "@rollup/rollup-linux-riscv64-musl": "4.40.0",
"@rollup/rollup-linux-x64-gnu": "4.34.7", "@rollup/rollup-linux-s390x-gnu": "4.40.0",
"@rollup/rollup-linux-x64-musl": "4.34.7", "@rollup/rollup-linux-x64-gnu": "4.40.0",
"@rollup/rollup-win32-arm64-msvc": "4.34.7", "@rollup/rollup-linux-x64-musl": "4.40.0",
"@rollup/rollup-win32-ia32-msvc": "4.34.7", "@rollup/rollup-win32-arm64-msvc": "4.40.0",
"@rollup/rollup-win32-x64-msvc": "4.34.7", "@rollup/rollup-win32-ia32-msvc": "4.40.0",
"@rollup/rollup-win32-x64-msvc": "4.40.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -5733,9 +5870,9 @@
"optional": true "optional": true
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -6083,9 +6220,9 @@
} }
}, },
"node_modules/stylelint": { "node_modules/stylelint": {
"version": "16.18.0", "version": "16.19.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.18.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.19.1.tgz",
"integrity": "sha512-OXb68qzesv7J70BSbFwfK3yTVLEVXiQ/ro6wUE4UrSbKCMjLLA02S8Qq3LC01DxKyVjk7z8xh35aB4JzO3/sNA==", "integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -6112,7 +6249,7 @@
"debug": "^4.3.7", "debug": "^4.3.7",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^10.0.7", "file-entry-cache": "^10.0.8",
"global-modules": "^2.0.0", "global-modules": "^2.0.0",
"globby": "^11.1.0", "globby": "^11.1.0",
"globjoin": "^0.1.4", "globjoin": "^0.1.4",
@@ -6120,7 +6257,7 @@
"ignore": "^7.0.3", "ignore": "^7.0.3",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0", "is-plain-object": "^5.0.0",
"known-css-properties": "^0.35.0", "known-css-properties": "^0.36.0",
"mathml-tag-names": "^2.1.3", "mathml-tag-names": "^2.1.3",
"meow": "^13.2.0", "meow": "^13.2.0",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
@@ -6337,25 +6474,25 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/stylelint/node_modules/file-entry-cache": { "node_modules/stylelint/node_modules/file-entry-cache": {
"version": "10.0.7", "version": "10.0.8",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.7.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.8.tgz",
"integrity": "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw==", "integrity": "sha512-FGXHpfmI4XyzbLd3HQ8cbUcsFGohJpZtmQRHr8z8FxxtCe2PcpgIlVLwIgunqjvRmXypBETvwhV4ptJizA+Y1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"flat-cache": "^6.1.7" "flat-cache": "^6.1.8"
} }
}, },
"node_modules/stylelint/node_modules/flat-cache": { "node_modules/stylelint/node_modules/flat-cache": {
"version": "6.1.7", "version": "6.1.8",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.7.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.8.tgz",
"integrity": "sha512-qwZ4xf1v1m7Rc9XiORly31YaChvKt6oNVHuqqZcoED/7O+ToyNVGobKsIAopY9ODcWpEDKEBAbrSOCBHtNQvew==", "integrity": "sha512-R6MaD3nrJAtO7C3QOuS79ficm2pEAy++TgEUD8ii1LVlbcgZ9DtASLkt9B+RZSFCzm7QHDMlXPsqqB6W2Pfr1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cacheable": "^1.8.9", "cacheable": "^1.8.9",
"flatted": "^3.3.3", "flatted": "^3.3.3",
"hookified": "^1.7.1" "hookified": "^1.8.1"
} }
}, },
"node_modules/stylelint/node_modules/globby": { "node_modules/stylelint/node_modules/globby": {
@@ -6642,13 +6779,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.12", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.4.3", "fdir": "^6.4.4",
"picomatch": "^4.0.2" "picomatch": "^4.0.2"
}, },
"engines": { "engines": {
@@ -6659,9 +6796,9 @@
} }
}, },
"node_modules/tinyglobby/node_modules/fdir": { "node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.3", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
@@ -6792,15 +6929,18 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.6", "version": "6.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"rollup": "^4.30.1" "rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -6864,9 +7004,9 @@
} }
}, },
"node_modules/vite-plugin-static-copy": { "node_modules/vite-plugin-static-copy": {
"version": "2.3.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.1.tgz",
"integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==", "integrity": "sha512-EfsPcBm3ewg3UMG8RJaC0ADq6/qnUZnokXx4By4+2cAcipjT9i0Y0owIJGqmZI7d6nxk4qB1q5aXOwNuSyPdyA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -6943,6 +7083,34 @@
} }
} }
}, },
"node_modules/vite/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
@@ -6958,14 +7126,15 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.99.5", "version": "5.99.7",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz",
"integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==", "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@@ -6982,7 +7151,7 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^4.3.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.11", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",

View File

@@ -9,30 +9,30 @@
"icons.html": "node theme_icons.js" "icons.html": "node theme_icons.js"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.22.0", "@eslint/js": "^9.25.1",
"copy-webpack-plugin": "^13.0.0", "copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"edge.js": "^6.2.1", "edge.js": "^6.2.1",
"eslint": "^9.24.0", "eslint": "^9.25.1",
"filemanager-webpack-plugin": "^8.0.0", "filemanager-webpack-plugin": "^8.0.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"ionicons": "^7.4.0", "ionicons": "^8.0.8",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"less": "^4.3.0", "less": "^4.3.0",
"less-loader": "^12.2.0", "less-loader": "^12.3.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"sharp": "^0.34.1", "sharp": "^0.34.1",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"stylelint": "^16.17.0", "stylelint": "^16.19.1",
"stylelint-config-standard": "^38.0.0", "stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-less": "^3.0.1", "stylelint-config-standard-less": "^3.0.1",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svgo": "^3.3.2", "svgo": "^3.3.2",
"swiped-events": "^1.2.0", "swiped-events": "^1.2.0",
"vite": "^6.2.6", "vite": "^6.3.4",
"vite-plugin-static-copy": "^2.3.0", "vite-plugin-static-copy": "^2.3.1",
"vite-plugin-stylelint": "^6.0.0", "vite-plugin-stylelint": "^6.0.0",
"webpack": "^5.99.5", "webpack": "^5.99.7",
"webpack-cli": "^6.0.1" "webpack-cli": "^6.0.1"
}, },
"dependencies": { "dependencies": {

View File

@@ -1,56 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" width="21.508125mm"
xmlns:cc="http://creativecommons.org/ns#" height="21.508017mm"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" viewBox="0 0 21.508125 21.508015"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1" version="1.1"
viewBox="0 0 92 92" id="svg1"
height="92mm" xmlns="http://www.w3.org/2000/svg"
width="92mm"> xmlns:svg="http://www.w3.org/2000/svg">
<defs <defs
id="defs2" /> id="defs1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g <g
transform="translate(-40.921303,-17.416526)" id="layer1"
id="layer1"> transform="translate(-54.245938,-114.24604)">
<circle
r="0"
style="fill:none;stroke:#000000;stroke-width:12;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cy="92"
cx="75"
id="path3713" />
<circle
r="30"
cy="53.902557"
cx="75.921303"
id="path834"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path <path
d="m 67.514849,37.91524 a 18,18 0 0 1 21.051475,3.312407 18,18 0 0 1 3.137312,21.078282" style="fill:#222222;fill-opacity:1;stroke:#ffffff;stroke-width:1.50812;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path852" d="m 55,115 5.000001,4.99999 h 5 L 75,130 70.000002,135 H 60.000001 L 55,130 Z"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> id="path2" />
<rect <path
transform="rotate(-46.234709)" style="fill:#222222;fill-opacity:1;stroke:#ffffff;stroke-width:1.50812;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
ry="1.8669105e-13" d="m 67,119.99999 h 3.000002 L 75,115 v 12.99999 z"
y="122.08995" id="path3" />
x="3.7063529"
height="39.963303"
width="18.846331"
id="rect912"
style="opacity:1;fill:#3050ff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 980 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 310 KiB

View File

@@ -51,11 +51,13 @@ function jinja_svg_catalog(dest, macros, items) {
(item) => { (item) => {
/** @type {import("svgo").Config} */ /** @type {import("svgo").Config} */
// JSON.stringify & JSON.parse are used to create a deep copy of the
// item.svgo_opts object
const svgo_opts = JSON.parse(JSON.stringify(item.svgo_opts)); const svgo_opts = JSON.parse(JSON.stringify(item.svgo_opts));
svgo_opts.plugins.push({ svgo_opts.plugins.push({
name: "addAttributesToSVGElement", name: "addClassesToSVGElement",
params: { params: {
attributes: [{ "class": __jinja_class_placeholder__, }] classNames: [__jinja_class_placeholder__]
}} }}
); );

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
services:
nekosearch:
build: .
pull_policy: build
restart: unless-stopped
ports:
- "${NEKOSEARCH_PORT}:8080"

View File

@@ -43,15 +43,7 @@ do
esac esac
done done
get_searxng_version(){ echo "SearXNG version $SEARXNG_VERSION"
su searxng -c \
'python3 -c "import six; import searx.version; six.print_(searx.version.VERSION_STRING)"' \
2>/dev/null
}
SEARXNG_VERSION="$(get_searxng_version)"
export SEARXNG_VERSION
echo "SearXNG version ${SEARXNG_VERSION}"
# helpers to update the configuration files # helpers to update the configuration files
patch_uwsgi_settings() { patch_uwsgi_settings() {
@@ -76,7 +68,7 @@ patch_searxng_settings() {
-e "s|base_url: false|base_url: ${BASE_URL}|g" \ -e "s|base_url: false|base_url: ${BASE_URL}|g" \
-e "s/instance_name: \"SearXNG\"/instance_name: \"${INSTANCE_NAME}\"/g" \ -e "s/instance_name: \"SearXNG\"/instance_name: \"${INSTANCE_NAME}\"/g" \
-e "s/autocomplete: \"\"/autocomplete: \"${AUTOCOMPLETE}\"/g" \ -e "s/autocomplete: \"\"/autocomplete: \"${AUTOCOMPLETE}\"/g" \
-e "s/ultrasecretkey/$(openssl rand -hex 32)/g" \ -e "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" \
"${CONF}" "${CONF}"
# Morty configuration # Morty configuration
@@ -172,4 +164,4 @@ printf 'Listen on %s\n' "${BIND_ADDRESS}"
# Start uwsgi # Start uwsgi
# TODO: "--http-socket" will be removed in the future (see uwsgi.ini.new config file): https://github.com/searxng/searxng/pull/4578 # TODO: "--http-socket" will be removed in the future (see uwsgi.ini.new config file): https://github.com/searxng/searxng/pull/4578
exec uwsgi --http-socket "${BIND_ADDRESS}" "${UWSGI_SETTINGS_PATH}" exec /usr/local/searxng/venv/bin/uwsgi --http-socket "${BIND_ADDRESS}" "${UWSGI_SETTINGS_PATH}"

View File

@@ -21,7 +21,6 @@ chmod-socket = 666
# Plugin to use and interpreter config # Plugin to use and interpreter config
single-interpreter = true single-interpreter = true
master = true master = true
plugin = python3
lazy-apps = true lazy-apps = true
enable-threads = true enable-threads = true

View File

@@ -96,7 +96,7 @@ Modify the ``/etc/searxng/settings.yml`` to your needs:
.. literalinclude:: ../../utils/templates/etc/searxng/settings.yml .. literalinclude:: ../../utils/templates/etc/searxng/settings.yml
:language: yaml :language: yaml
:end-before: # hostnames: :end-before: # preferences:
To see the entire file jump to :origin:`utils/templates/etc/searxng/settings.yml` To see the entire file jump to :origin:`utils/templates/etc/searxng/settings.yml`

View File

@@ -148,6 +148,8 @@ engine is shown. Most of the options have a default value or even are optional.
``display_error_messages`` : default ``true`` ``display_error_messages`` : default ``true``
When an engine returns an error, the message is displayed on the user interface. When an engine returns an error, the message is displayed on the user interface.
.. _engine network:
``network`` : optional ``network`` : optional
Use the network configuration from another engine. Use the network configuration from another engine.
In addition, there are two default networks: In addition, there are two default networks:
@@ -257,4 +259,3 @@ Example configuration in settings.yml for a German and English speaker:
When searching, the default google engine will return German results and When searching, the default google engine will return German results and
"google english" will return English results. "google english" will return English results.

View File

@@ -16,8 +16,14 @@
open_metrics: '' open_metrics: ''
``debug`` : ``$SEARXNG_DEBUG`` ``debug`` : ``$SEARXNG_DEBUG``
Allow a more detailed log if you run SearXNG directly. Display *detailed* error In debug mode, the server provides an interactive debugger, will reload when
messages in the browser too, so this must be deactivated in production. code is changed and activates a verbose logging.
.. attention::
The debug setting is intended for local development server. Don't
activate debug (don't use a development server) when deploying to
production.
``donation_url`` : ``donation_url`` :
Set value to ``true`` to use your own donation page written in the Set value to ``true`` to use your own donation page written in the

View File

@@ -57,11 +57,11 @@
``default_lang``: ``default_lang``:
Default search language - leave blank to detect from browser information or Default search language - leave blank to detect from browser information or
use codes from :origin:`searx/languages.py`. use codes from :origin:`searx/sxng_locales.py`.
``languages``: ``languages``:
List of available languages - leave unset to use all codes from List of available languages - leave unset to use all codes from
:origin:`searx/languages.py`. Otherwise list codes of available languages. :origin:`searx/sxng_locales.py`. Otherwise list codes of available languages.
The ``all`` value is shown as the ``Default language`` in the user interface The ``all`` value is shown as the ``Default language`` in the user interface
(in most cases, it is meant to send the query without a language parameter ; (in most cases, it is meant to send the query without a language parameter ;
in some cases, it means the English language) Example: in some cases, it means the English language) Example:

View File

@@ -14,13 +14,14 @@
limiter: false limiter: false
public_instance: false public_instance: false
image_proxy: false image_proxy: false
method: "POST"
default_http_headers: default_http_headers:
X-Content-Type-Options : nosniff X-Content-Type-Options : nosniff
X-Download-Options : noopen X-Download-Options : noopen
X-Robots-Tag : noindex, nofollow X-Robots-Tag : noindex, nofollow
Referrer-Policy : no-referrer Referrer-Policy : no-referrer
``base_url`` : ``$SEARXNG_URL`` ``base_url`` : ``$SEARXNG_BASE_URL``
The base URL where SearXNG is deployed. Used to create correct inbound links. The base URL where SearXNG is deployed. Used to create correct inbound links.
``port`` & ``bind_address``: ``$SEARXNG_PORT`` & ``$SEARXNG_BIND_ADDRESS`` ``port`` & ``bind_address``: ``$SEARXNG_PORT`` & ``$SEARXNG_BIND_ADDRESS``
@@ -28,6 +29,8 @@
directly using ``python searx/webapp.py``. Doesn't apply to a SearXNG directly using ``python searx/webapp.py``. Doesn't apply to a SearXNG
services running behind a proxy and using socket communications. services running behind a proxy and using socket communications.
.. _server.secret_key:
``secret_key`` : ``$SEARXNG_SECRET`` ``secret_key`` : ``$SEARXNG_SECRET``
Used for cryptography purpose. Used for cryptography purpose.
@@ -50,6 +53,11 @@
``image_proxy`` : ``$SEARXNG_IMAGE_PROXY`` ``image_proxy`` : ``$SEARXNG_IMAGE_PROXY``
Allow your instance of SearXNG of being able to proxy images. Uses memory space. Allow your instance of SearXNG of being able to proxy images. Uses memory space.
.. _method:
``method`` : ``$SEARXNG_METHOD``
Whether to use ``GET`` or ``POST`` HTTP method when searching.
.. _HTTP headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers .. _HTTP headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
``default_http_headers`` : ``default_http_headers`` :

View File

@@ -27,23 +27,24 @@ Privacy-by-design
----------------- -----------------
SearXNG was born out of the need for a **privacy-respecting** search tool which SearXNG was born out of the need for a **privacy-respecting** search tool which
can be extended easily to maximize both, its search and its privacy protecting can be extended easily to maximize both its search and its privacy protecting
capabilities. capabilities.
A few widely used features work differently or turned off by default or not Some widely used search engine features may work differently,
implemented at all **as a consequence of privacy-by-design**. may be turned off by default, or may not be implemented at all in SearXNG
**as a consequence of a privacy-by-design approach**.
If a feature reduces the privacy preserving aspects of searx, it should be Following this approach, features reducing the privacy preserving aspects of SearXNG should be
switched off by default or should not implemented at all. There are plenty of switched off by default or should not be implemented at all. There are plenty of
search engines already providing such features. If a feature reduces the search engines already providing such features. If a feature reduces
protection of searx, users must be informed about the effect of choosing to SearXNG's efficacy in protecting a user's privacy, the user must be informed about
enable it. Features that protect privacy but differ from the expectations of the effect of choosing to enable it. Features that protect privacy but differ from the
the user should also be explained. expectations of the user should also be carefully explained to them.
Also, if you think that something works weird with searx, it's might be because Also, if you think that something works weird with SearXNG, it might be because
of the tool you use is designed in a way to interfere with the privacy respect. the tool you are using is designed in a way that interferes with SearXNG's privacy aspects.
Submitting a bugreport to the vendor of the tool that misbehaves might be a good Submitting a bug report to the vendor of the tool that misbehaves might be a good
feedback to reconsider the disrespect to its customers (e.g. ``GET`` vs ``POST`` feedback for them to reconsider the disrespect to their customers (e.g., ``GET`` vs ``POST``
requests in various browsers). requests in various browsers).
Remember the other prime directive of SearXNG is to be hackable, so if the above Remember the other prime directive of SearXNG is to be hackable, so if the above
@@ -134,7 +135,7 @@ Here is an example which makes a complete rebuild:
.. _make docs.live: .. _make docs.live:
live build Live build
---------- ----------
.. _sphinx-autobuild: .. _sphinx-autobuild:
@@ -145,8 +146,8 @@ live build
It is recommended to assert a complete rebuild before deploying (use It is recommended to assert a complete rebuild before deploying (use
``docs.clean``). ``docs.clean``).
Live build is like WYSIWYG. If you want to edit the documentation, its Live build is like WYSIWYG. It's the recommended way to go if you want to edit the documentation.
recommended to use. The Makefile target ``docs.live`` builds the docs, opens The Makefile target ``docs.live`` builds the docs, opens
URL in your favorite browser and rebuilds every time a reST file has been URL in your favorite browser and rebuilds every time a reST file has been
changed (:ref:`make docs.clean`). changed (:ref:`make docs.clean`).
@@ -159,9 +160,9 @@ changed (:ref:`make docs.clean`).
... Start watching changes ... Start watching changes
Live builds are implemented by sphinx-autobuild_. Use environment Live builds are implemented by sphinx-autobuild_. Use environment
``$(SPHINXOPTS)`` to pass arguments to the sphinx-autobuild_ command. Except ``$(SPHINXOPTS)`` to pass arguments to the sphinx-autobuild_ command. You can
option ``--host`` (which is always set to ``0.0.0.0``) you can pass any pass any argument except for the ``--host`` option (which is always set to ``0.0.0.0``).
argument. E.g to find and use a free port, use: E.g., to find and use a free port, use:
.. code:: sh .. code:: sh

View File

@@ -4,19 +4,13 @@
Engine Library Engine Library
============== ==============
.. contents::
:depth: 2
:local:
:backlinks: entry
.. automodule:: searx.enginelib .. automodule:: searx.enginelib
:members: :members:
.. _searx.enginelib.traits: .. _searx.enginelib.traits:
Engine traits Engine traits
============= =============
.. automodule:: searx.enginelib.traits .. automodule:: searx.enginelib.traits
:members: :members:

View File

@@ -29,7 +29,7 @@ Programming Interface
parameter. This function can be omitted, if there is no need to setup anything parameter. This function can be omitted, if there is no need to setup anything
in advance. in advance.
:py:func:`search(query, params) <searx.engines.demo_offline.searc>` :py:func:`search(query, params) <searx.engines.demo_offline.search>`
Each offline engine has a function named ``search``. This function is Each offline engine has a function named ``search``. This function is
responsible to perform a search and return the results in a presentable responsible to perform a search and return the results in a presentable
format. (Where *presentable* means presentable by the selected result format. (Where *presentable* means presentable by the selected result

View File

@@ -0,0 +1,8 @@
.. _chinaso engine:
=======
ChinaSo
=======
.. automodule:: searx.engines.chinaso
:members:

View File

@@ -0,0 +1,8 @@
.. _huggingface engine:
============
Hugging Face
============
.. automodule:: searx.engines.huggingface
:members:

View File

@@ -4,10 +4,5 @@
Mullvad-Leta Mullvad-Leta
============ ============
.. contents:: Contents
:depth: 2
:local:
:backlinks: entry
.. automodule:: searx.engines.mullvad_leta .. automodule:: searx.engines.mullvad_leta
:members: :members:

View File

@@ -6,7 +6,7 @@ Simple Theme Templates
The simple template is complex, it consists of many different elements and also The simple template is complex, it consists of many different elements and also
uses macros and include statements. The following is a rough overview that we uses macros and include statements. The following is a rough overview that we
would like to give the developerat hand, details must still be taken from the would like to give the developer at hand, details must still be taken from the
:origin:`sources <searx/templates/simple/>`. :origin:`sources <searx/templates/simple/>`.
A :ref:`result item <result types>` can be of different media types. The media A :ref:`result item <result types>` can be of different media types. The media

View File

@@ -53,6 +53,9 @@ Probe HTTP headers
.. automodule:: searx.botdetection.http_user_agent .. automodule:: searx.botdetection.http_user_agent
:members: :members:
.. automodule:: searx.botdetection.http_sec_fetch
:members:
.. _botdetection config: .. _botdetection config:
Config Config

8
docs/src/searx.cache.rst Normal file
View File

@@ -0,0 +1,8 @@
.. _searx.cache:
======
Caches
======
.. automodule:: searx.cache
:members:

View File

@@ -2,7 +2,7 @@ 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==24.3.0 black==24.3.0
pylint==3.3.6 pylint==3.3.7
splinter==0.21.0 splinter==0.21.0
selenium==4.31.0 selenium==4.31.0
Pallets-Sphinx-Themes==2.3.0 Pallets-Sphinx-Themes==2.3.0
@@ -16,7 +16,7 @@ sphinx-notfound-page==1.1.0
myst-parser==3.0.1 myst-parser==3.0.1
linuxdoc==20240924 linuxdoc==20240924
aiounittest==1.5.0 aiounittest==1.5.0
yamllint==1.37.0 yamllint==1.37.1
wlc==1.15 wlc==1.15
coloredlogs==15.0.1 coloredlogs==15.0.1
docutils>=0.21.2 docutils>=0.21.2

View File

@@ -1,9 +1,9 @@
certifi==2025.1.31 certifi==2025.4.26
babel==2.17.0 babel==2.17.0
flask-babel==4.0.0 flask-babel==4.0.0
flask==3.1.0 flask==3.1.0
jinja2==3.1.6 jinja2==3.1.6
lxml==5.3.2 lxml==5.4.0
pygments==2.19.1 pygments==2.19.1
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pyyaml==6.0.2 pyyaml==6.0.2
@@ -11,11 +11,11 @@ httpx[http2]==0.24.1
Brotli==1.1.0 Brotli==1.1.0
uvloop==0.21.0 uvloop==0.21.0
httpx-socks[asyncio]==0.7.7 httpx-socks[asyncio]==0.7.7
setproctitle==1.3.5 setproctitle==1.3.6
redis==5.0.8 redis==5.0.8
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.2.1; python_version < '3.11' tomli==2.2.1; python_version < '3.11'
msgspec==0.19.0 msgspec==0.19.0
typer-slim==0.15.2 typer-slim==0.15.3
isodate==0.7.2 isodate==0.7.2

View File

@@ -22,18 +22,18 @@ searx_dir = abspath(dirname(__file__))
searx_parent_dir = abspath(dirname(dirname(__file__))) searx_parent_dir = abspath(dirname(dirname(__file__)))
settings = {} settings = {}
searx_debug = False sxng_debug = False
logger = logging.getLogger('searx') logger = logging.getLogger('searx')
_unset = object() _unset = object()
def init_settings(): def init_settings():
"""Initialize global ``settings`` and ``searx_debug`` variables and """Initialize global ``settings`` and ``sxng_debug`` variables and
``logger`` from ``SEARXNG_SETTINGS_PATH``. ``logger`` from ``SEARXNG_SETTINGS_PATH``.
""" """
global settings, searx_debug # pylint: disable=global-variable-not-assigned global settings, sxng_debug # pylint: disable=global-variable-not-assigned
cfg, msg = searx.settings_loader.load_settings(load_user_settings=True) cfg, msg = searx.settings_loader.load_settings(load_user_settings=True)
cfg = cfg or {} cfg = cfg or {}
@@ -42,8 +42,8 @@ def init_settings():
settings.clear() settings.clear()
settings.update(cfg) settings.update(cfg)
searx_debug = settings['general']['debug'] sxng_debug = get_setting("general.debug")
if searx_debug: if sxng_debug:
_logging_config_debug() _logging_config_debug()
else: else:
logging.basicConfig(level=LOG_LEVEL_PROD, format=LOG_FORMAT_PROD) logging.basicConfig(level=LOG_LEVEL_PROD, format=LOG_FORMAT_PROD)

View File

@@ -34,6 +34,9 @@ def dump_request(request: SXNG_Request):
+ " || Content-Length: %s" % request.headers.get('Content-Length') + " || Content-Length: %s" % request.headers.get('Content-Length')
+ " || Connection: %s" % request.headers.get('Connection') + " || Connection: %s" % request.headers.get('Connection')
+ " || User-Agent: %s" % request.headers.get('User-Agent') + " || User-Agent: %s" % request.headers.get('User-Agent')
+ " || Sec-Fetch-Site: %s" % request.headers.get('Sec-Fetch-Site')
+ " || Sec-Fetch-Mode: %s" % request.headers.get('Sec-Fetch-Mode')
+ " || Sec-Fetch-Dest: %s" % request.headers.get('Sec-Fetch-Dest')
) )

View File

@@ -0,0 +1,103 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Method ``http_sec_fetch``
-------------------------
The ``http_sec_fetch`` method protect resources from web attacks with `Fetch
Metadata`_. A request is filtered out in case of:
- http header Sec-Fetch-Mode_ is invalid
- http header Sec-Fetch-Dest_ is invalid
.. _Fetch Metadata:
https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header
.. _Sec-Fetch-Dest:
https://developer.mozilla.org/en-US/docs/Web/API/Request/destination
.. _Sec-Fetch-Mode:
https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
"""
# pylint: disable=unused-argument
from __future__ import annotations
from ipaddress import (
IPv4Network,
IPv6Network,
)
import re
import flask
import werkzeug
from searx.extended_types import SXNG_Request
from . import config
from ._helpers import logger
def is_browser_supported(user_agent: str) -> bool:
"""Check if the browser supports Sec-Fetch headers.
https://caniuse.com/mdn-http_headers_sec-fetch-dest
https://caniuse.com/mdn-http_headers_sec-fetch-mode
https://caniuse.com/mdn-http_headers_sec-fetch-site
Supported browsers:
- Chrome >= 80
- Firefox >= 90
- Safari >= 16.4
- Edge (mirrors Chrome)
- Opera (mirrors Chrome)
"""
user_agent = user_agent.lower()
# Chrome/Chromium/Edge/Opera
chrome_match = re.search(r'chrome/(\d+)', user_agent)
if chrome_match:
version = int(chrome_match.group(1))
return version >= 80
# Firefox
firefox_match = re.search(r'firefox/(\d+)', user_agent)
if firefox_match:
version = int(firefox_match.group(1))
return version >= 90
# Safari
safari_match = re.search(r'version/(\d+)\.(\d+)', user_agent)
if safari_match:
major = int(safari_match.group(1))
minor = int(safari_match.group(2))
return major > 16 or (major == 16 and minor >= 4)
return False
def filter_request(
network: IPv4Network | IPv6Network,
request: SXNG_Request,
cfg: config.Config,
) -> werkzeug.Response | None:
# Only check Sec-Fetch headers for supported browsers
user_agent = request.headers.get('User-Agent', '')
if is_browser_supported(user_agent):
val = request.headers.get("Sec-Fetch-Mode", "")
if val not in ('navigate', 'cors'):
logger.debug("invalid Sec-Fetch-Mode '%s'", val)
return flask.redirect(flask.url_for('index'), code=302)
val = request.headers.get("Sec-Fetch-Site", "")
if val not in ('same-origin', 'same-site', 'none'):
logger.debug("invalid Sec-Fetch-Site '%s'", val)
flask.redirect(flask.url_for('index'), code=302)
val = request.headers.get("Sec-Fetch-Dest", "")
if val not in ('document', 'empty'):
logger.debug("invalid Sec-Fetch-Dest '%s'", val)
flask.redirect(flask.url_for('index'), code=302)
return None

405
searx/cache.py Normal file
View File

@@ -0,0 +1,405 @@
"""Implementation of caching solutions.
- :py:obj:`searx.cache.ExpireCache` and its :py:obj:`searx.cache.ExpireCacheCfg`
----
"""
from __future__ import annotations
__all__ = ["ExpireCacheCfg", "ExpireCacheStats", "ExpireCache", "ExpireCacheSQLite"]
import abc
import dataclasses
import datetime
import hashlib
import hmac
import os
import pickle
import sqlite3
import string
import tempfile
import time
import typing
import msgspec
from searx import sqlitedb
from searx import logger
from searx import get_setting
log = logger.getChild("cache")
class ExpireCacheCfg(msgspec.Struct): # pylint: disable=too-few-public-methods
"""Configuration of a :py:obj:`ExpireCache` cache."""
name: str
"""Name of the cache."""
db_url: str = ""
"""URL of the SQLite DB, the path to the database file. If unset a default
DB will be created in `/tmp/sxng_cache_{self.name}.db`"""
MAX_VALUE_LEN: int = 1024 * 10
"""Max lenght of a *serialized* value."""
MAXHOLD_TIME: int = 60 * 60 * 24 * 7 # 7 days
"""Hold time (default in sec.), after which a value is removed from the cache."""
MAINTENANCE_PERIOD: int = 60 * 60 # 2h
"""Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to
``auto``."""
MAINTENANCE_MODE: typing.Literal["auto", "off"] = "auto"
"""Type of maintenance mode
``auto``:
Maintenance is carried out automatically as part of the maintenance
intervals (:py:obj:`MAINTENANCE_PERIOD`); no external process is required.
``off``:
Maintenance is switched off and must be carried out by an external process
if required.
"""
password: bytes = get_setting("server.secret_key").encode() # type: ignore
"""Password used by :py:obj:`ExpireCache.secret_hash`.
The default password is taken from :ref:`secret_key <server.secret_key>`.
When the password is changed, the hashed keys in the cache can no longer be
used, which is why all values in the cache are deleted when the password is
changed.
"""
def __post_init__(self):
# if db_url is unset, use a default DB in /tmp/sxng_cache_{name}.db
if not self.db_url:
self.db_url = tempfile.gettempdir() + os.sep + f"sxng_cache_{ExpireCache.normalize_name(self.name)}.db"
@dataclasses.dataclass
class ExpireCacheStats:
"""Dataclass wich provides information on the status of the cache."""
cached_items: dict[str, list[tuple[str, typing.Any, int]]]
"""Values in the cache mapped by context name.
.. code: python
{
"context name": [
("foo key": "foo value", <expire>),
("bar key": "bar value", <expire>),
# ...
],
# ...
}
"""
def report(self):
c_ctx = 0
c_kv = 0
lines = []
for ctx_name, kv_list in self.cached_items.items():
c_ctx += 1
if not kv_list:
lines.append(f"[{ctx_name:20s}] empty")
continue
for key, value, expire in kv_list:
valid_until = datetime.datetime.fromtimestamp(expire).strftime("%Y-%m-%d %H:%M:%S")
c_kv += 1
lines.append(f"[{ctx_name:20s}] {valid_until} {key:12}" f" --> ({type(value).__name__}) {value} ")
lines.append(f"Number of contexts: {c_ctx}")
lines.append(f"number of key/value pairs: {c_kv}")
return "\n".join(lines)
class ExpireCache(abc.ABC):
"""Abstract base class for the implementation of a key/value cache
with expire date."""
cfg: ExpireCacheCfg
hash_token = "hash_token"
@abc.abstractmethod
def set(self, key: str, value: typing.Any, expire: int | None, ctx: str | None = None) -> bool:
"""Set *key* to *value*. To set a timeout on key use argument
``expire`` (in sec.). If expire is unset the default is taken from
:py:obj:`ExpireCacheCfg.MAXHOLD_TIME`. After the timeout has expired,
the key will automatically be deleted.
The ``ctx`` argument specifies the context of the ``key``. A key is
only unique in its context.
The concrete implementations of this abstraction determine how the
context is mapped in the connected database. In SQL databases, for
example, the context is a DB table or in a Key/Value DB it could be
a prefix for the key.
If the context is not specified (the default is ``None``) then a
default context should be used, e.g. a default table for SQL databases
or a default prefix in a Key/Value DB.
"""
@abc.abstractmethod
def get(self, key: str, default=None, ctx: str | None = None) -> typing.Any:
"""Return *value* of *key*. If key is unset, ``None`` is returned."""
@abc.abstractmethod
def maintenance(self, force: bool = False, truncate: bool = False) -> bool:
"""Performs maintenance on the cache.
``force``:
Maintenance should be carried out even if the maintenance interval has
not yet been reached.
``truncate``:
Truncate the entire cache, which is necessary, for example, if the
password has changed.
"""
@abc.abstractmethod
def state(self) -> ExpireCacheStats:
"""Returns a :py:obj:`ExpireCacheStats`, which provides information
about the status of the cache."""
@staticmethod
def build_cache(cfg: ExpireCacheCfg) -> ExpireCache:
"""Factory to build a caching instance.
.. note::
Currently, only the SQLite adapter is available, but other database
types could be implemented in the future, e.g. a Valkey (Redis)
adapter.
"""
return ExpireCacheSQLite(cfg)
@staticmethod
def normalize_name(name: str) -> str:
"""Returns a normalized name that can be used as a file name or as a SQL
table name (is used, for example, to normalize the context name)."""
_valid = "-_." + string.ascii_letters + string.digits
return "".join([c for c in name if c in _valid])
def serialize(self, value: typing.Any) -> bytes:
dump: bytes = pickle.dumps(value)
return dump
def deserialize(self, value: bytes) -> typing.Any:
obj = pickle.loads(value)
return obj
def secret_hash(self, name: str | bytes) -> str:
"""Creates a hash of the argument ``name``. The hash value is formed
from the ``name`` combined with the :py:obj:`password
<ExpireCacheCfg.password>`. Can be used, for example, to make the
``key`` stored in the DB unreadable for third parties."""
if isinstance(name, str):
name = bytes(name, encoding='utf-8')
m = hmac.new(name + self.cfg.password, digestmod='sha256')
return m.hexdigest()
class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
"""Cache that manages key/value pairs in a SQLite DB. The DB model in the
SQLite DB is implemented in abstract class :py:obj:`SQLiteAppl
<searx.sqlitedb.SQLiteAppl>`.
The following configurations are required / supported:
- :py:obj:`ExpireCacheCfg.db_url`
- :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`
- :py:obj:`ExpireCacheCfg.MAINTENANCE_PERIOD`
- :py:obj:`ExpireCacheCfg.MAINTENANCE_MODE`
"""
DB_SCHEMA = 1
# The key/value tables will be created on demand by self.create_table
DDL_CREATE_TABLES = {}
CACHE_TABLE_PREFIX = "CACHE-TABLE-"
def __init__(self, cfg: ExpireCacheCfg):
"""An instance of the SQLite expire cache is build up from a
:py:obj:`config <ExpireCacheCfg>`."""
self.cfg = cfg
if cfg.db_url == ":memory:":
log.critical("don't use SQLite DB in :memory: in production!!")
super().__init__(cfg.db_url)
def init(self, conn: sqlite3.Connection) -> bool:
ret_val = super().init(conn)
if not ret_val:
return False
new = hashlib.sha256(self.cfg.password).hexdigest()
old = self.properties(self.hash_token)
if old != new:
if old is not None:
log.warning("[%s] hash token changed: truncate all cache tables", self.cfg.name)
self.maintenance(force=True, truncate=True)
self.properties.set(self.hash_token, new)
return True
def maintenance(self, force: bool = False, truncate: bool = False) -> bool:
if not force and int(time.time()) < self.next_maintenance_time:
# log.debug("no maintenance required yet, next maintenance interval is in the future")
return False
# Prevent parallel DB maintenance cycles from other DB connections
# (e.g. in multi thread or process environments).
self.properties.set("LAST_MAINTENANCE", "") # hint: this (also) sets the m_time of the property!
if truncate:
self.truncate_tables(self.table_names)
return True
# drop items by expire time stamp ..
expire = int(time.time())
with self.connect() as conn:
for table in self.table_names:
res = conn.execute(f"DELETE FROM {table} WHERE expire < ?", (expire,))
log.debug("deleted %s keys from table %s (expire date reached)", res.rowcount, table)
# Vacuuming the WALs
# https://www.theunterminatedstring.com/sqlite-vacuuming/
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
conn.close()
return True
def create_table(self, table: str) -> bool:
"""Create DB ``table`` if it has not yet been created, no recreates are
initiated if the table already exists.
"""
if table in self.table_names:
# log.debug("key/value table %s exists in DB (no need to recreate)", table)
return False
log.info("key/value table '%s' NOT exists in DB -> create DB table ..", table)
sql_table = "\n".join(
[
f"CREATE TABLE IF NOT EXISTS {table} (",
" key TEXT,",
" value BLOB,",
f" expire INTEGER DEFAULT (strftime('%s', 'now') + {self.cfg.MAXHOLD_TIME}),",
"PRIMARY KEY (key))",
]
)
sql_index = f"CREATE INDEX IF NOT EXISTS index_expire_{table} ON {table}(expire);"
with self.connect() as conn:
conn.execute(sql_table)
conn.execute(sql_index)
conn.close()
self.properties.set(f"{self.CACHE_TABLE_PREFIX}-{table}", table)
return True
@property
def table_names(self) -> list[str]:
"""List of key/value tables already created in the DB."""
sql = f"SELECT value FROM properties WHERE name LIKE '{self.CACHE_TABLE_PREFIX}%%'"
rows = self.DB.execute(sql).fetchall() or []
return [r[0] for r in rows]
def truncate_tables(self, table_names: list[str]):
log.debug("truncate table: %s", ",".join(table_names))
with self.connect() as conn:
for table in table_names:
conn.execute(f"DELETE FROM {table}")
conn.close()
return True
@property
def next_maintenance_time(self) -> int:
"""Returns (unix epoch) time of the next maintenance."""
return self.cfg.MAINTENANCE_PERIOD + self.properties.m_time("LAST_MAINTENANCE", int(time.time()))
# implement ABC methods of ExpireCache
def set(self, key: str, value: typing.Any, expire: int | None, ctx: str | None = None) -> bool:
"""Set key/value in DB table given by argument ``ctx``. If expire is
unset the default is taken from :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`.
If ``ctx`` argument is ``None`` (the default), a table name is
generated from the :py:obj:`ExpireCacheCfg.name`. If DB table does not
exists, it will be created (on demand) by :py:obj:`self.create_table
<ExpireCacheSQLite.create_table>`.
"""
table = ctx
self.maintenance()
value = self.serialize(value=value)
if len(value) > self.cfg.MAX_VALUE_LEN:
log.warning("ExpireCache.set(): %s.key='%s' - value too big to cache (len: %s) ", table, value, len(value))
return False
if not expire:
expire = self.cfg.MAXHOLD_TIME
expire = int(time.time()) + expire
table_name = table
if not table_name:
table_name = self.normalize_name(self.cfg.name)
self.create_table(table_name)
sql = (
f"INSERT INTO {table_name} (key, value, expire) VALUES (?, ?, ?)"
f" ON CONFLICT DO "
f"UPDATE SET value=?, expire=?"
)
if table:
with self.DB:
self.DB.execute(sql, (key, value, expire, value, expire))
else:
with self.connect() as conn:
conn.execute(sql, (key, value, expire, value, expire))
conn.close()
return True
def get(self, key: str, default=None, ctx: str | None = None) -> typing.Any:
"""Get value of ``key`` from table given by argument ``ctx``. If
``ctx`` argument is ``None`` (the default), a table name is generated
from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists (in
table), the ``default`` value is returned.
"""
table = ctx
self.maintenance()
if not table:
table = self.normalize_name(self.cfg.name)
if table not in self.table_names:
return default
sql = f"SELECT value FROM {table} WHERE key = ?"
row = self.DB.execute(sql, (key,)).fetchone()
if row is None:
return default
return self.deserialize(row[0])
def state(self) -> ExpireCacheStats:
cached_items = {}
for table in self.table_names:
cached_items[table] = []
for row in self.DB.execute(f"SELECT key, value, expire FROM {table}"):
cached_items[table].append((row[0], self.deserialize(row[1]), row[2]))
return ExpireCacheStats(cached_items=cached_items)

File diff suppressed because it is too large Load Diff

View File

@@ -839,6 +839,7 @@
"ja": "バハマ・ドル", "ja": "バハマ・ドル",
"ko": "바하마 달러", "ko": "바하마 달러",
"lt": "Bahamų doleris", "lt": "Bahamų doleris",
"lv": "Bahamu dolārs",
"nl": "Bahamaanse dollar", "nl": "Bahamaanse dollar",
"oc": "Dolar de las Bahamas", "oc": "Dolar de las Bahamas",
"pa": "ਬਹਾਮਾਸੀ ਡਾਲਰ", "pa": "ਬਹਾਮਾਸੀ ਡਾਲਰ",
@@ -2119,7 +2120,7 @@
"de": "guatemaltekischer Quetzal", "de": "guatemaltekischer Quetzal",
"en": "quetzal", "en": "quetzal",
"eo": "gvatemala kecalo", "eo": "gvatemala kecalo",
"es": "quetzal", "es": "Quetzal",
"eu": "Quetzal", "eu": "Quetzal",
"fi": "Guatemalan quetzal", "fi": "Guatemalan quetzal",
"fr": "Quetzal", "fr": "Quetzal",
@@ -2211,6 +2212,7 @@
"ja": "香港ドル", "ja": "香港ドル",
"ko": "홍콩 달러", "ko": "홍콩 달러",
"lt": "Honkongo doleris", "lt": "Honkongo doleris",
"lv": "Honkongas dolārs",
"ml": "ഹോങ്കോങ്ങ് ഡോളർ", "ml": "ഹോങ്കോങ്ങ് ഡോളർ",
"ms": "dolar Hong Kong", "ms": "dolar Hong Kong",
"nl": "Hongkongse dollar", "nl": "Hongkongse dollar",
@@ -2424,7 +2426,7 @@
"pa": "ਇਜ਼ਰਾਇਲੀ ਨਵਾਂ ਸ਼ੇਕਲ", "pa": "ਇਜ਼ਰਾਇਲੀ ਨਵਾਂ ਸ਼ੇਕਲ",
"pl": "Nowy izraelski szekel", "pl": "Nowy izraelski szekel",
"pt": "novo shekel israelense", "pt": "novo shekel israelense",
"ro": "Shekel", "ro": "shekel",
"ru": "новый израильский шекель", "ru": "новый израильский шекель",
"si": "සෙකල්", "si": "සෙකල්",
"sk": "Nový izraelský šekel", "sk": "Nový izraelský šekel",
@@ -2591,6 +2593,7 @@
"ja": "アイスランド・クローナ", "ja": "アイスランド・クローナ",
"ko": "아이슬란드 크로나", "ko": "아이슬란드 크로나",
"lt": "Islandijos krona", "lt": "Islandijos krona",
"lv": "Islandes krona",
"nl": "IJslandse kroon", "nl": "IJslandse kroon",
"pa": "ਆਈਸਲੈਂਡੀ ਕਰੋਨਾ", "pa": "ਆਈਸਲੈਂਡੀ ਕਰੋਨਾ",
"pl": "Korona islandzka", "pl": "Korona islandzka",
@@ -2628,6 +2631,7 @@
"ja": "ジャマイカ・ドル", "ja": "ジャマイカ・ドル",
"ko": "자메이카 달러", "ko": "자메이카 달러",
"lt": "Jamaikos doleris", "lt": "Jamaikos doleris",
"lv": "Jamaikas dolārs",
"nl": "Jamaicaanse dollar", "nl": "Jamaicaanse dollar",
"pa": "ਜਮੈਕੀ ਡਾਲਰ", "pa": "ਜਮੈਕੀ ਡਾਲਰ",
"pl": "Dolar jamajski", "pl": "Dolar jamajski",
@@ -3104,6 +3108,7 @@
"ja": "キープ", "ja": "キープ",
"ko": "라오스 킵", "ko": "라오스 킵",
"lt": "Laoso kipas", "lt": "Laoso kipas",
"lv": "Laosas kips",
"ms": "Kip", "ms": "Kip",
"nl": "Laotiaanse kip", "nl": "Laotiaanse kip",
"oc": "kip laossian", "oc": "kip laossian",
@@ -3221,6 +3226,7 @@
"ja": "リベリア・ドル", "ja": "リベリア・ドル",
"ko": "라이베리아 달러", "ko": "라이베리아 달러",
"lt": "Liberijos doleris", "lt": "Liberijos doleris",
"lv": "Libērijas dolārs",
"ms": "Dolar Liberia", "ms": "Dolar Liberia",
"nl": "Liberiaanse dollar", "nl": "Liberiaanse dollar",
"oc": "Dolar liberian", "oc": "Dolar liberian",
@@ -3427,7 +3433,7 @@
"cy": "denar (Macedonia)", "cy": "denar (Macedonia)",
"da": "Makedonske denarer", "da": "Makedonske denarer",
"de": "mazedonischer Denar", "de": "mazedonischer Denar",
"en": "North Macedonian denar", "en": "Macedonian denar",
"eo": "makedona denaro", "eo": "makedona denaro",
"es": "denar macedonio", "es": "denar macedonio",
"et": "Makedoonia denaar", "et": "Makedoonia denaar",
@@ -3691,7 +3697,7 @@
"de": "Malawi-Kwacha", "de": "Malawi-Kwacha",
"en": "Malawian kwacha", "en": "Malawian kwacha",
"eo": "malavia kvaĉo", "eo": "malavia kvaĉo",
"es": "kwacha malauí", "es": "kuacha malauí",
"et": "Malawi kvatša", "et": "Malawi kvatša",
"fi": "Malawin kwacha", "fi": "Malawin kwacha",
"fr": "kwacha malawien", "fr": "kwacha malawien",
@@ -3867,6 +3873,7 @@
"ja": "ナミビア・ドル", "ja": "ナミビア・ドル",
"ko": "나미비아 달러", "ko": "나미비아 달러",
"lt": "Namibijos doleris", "lt": "Namibijos doleris",
"lv": "Namībijas dolārs",
"ms": "Dolar Namibia", "ms": "Dolar Namibia",
"nl": "Namibische dollar", "nl": "Namibische dollar",
"oc": "Dolar namibian", "oc": "Dolar namibian",
@@ -4874,6 +4881,7 @@
"ja": "シンガポールドル", "ja": "シンガポールドル",
"ko": "싱가포르 달러", "ko": "싱가포르 달러",
"lt": "Singapūro doleris", "lt": "Singapūro doleris",
"lv": "Singapūras dolārs",
"ml": "സിംഗപ്പൂർ ഡോളർ", "ml": "സിംഗപ്പൂർ ഡോളർ",
"ms": "Dolar Singapura", "ms": "Dolar Singapura",
"nl": "Singaporese dollar", "nl": "Singaporese dollar",
@@ -5245,6 +5253,7 @@
"ja": "ソモニ", "ja": "ソモニ",
"ko": "타지키스탄 소모니", "ko": "타지키스탄 소모니",
"lt": "Somonis", "lt": "Somonis",
"lv": "somoni",
"ms": "Somoni", "ms": "Somoni",
"nl": "Tadzjiekse somoni", "nl": "Tadzjiekse somoni",
"pa": "ਤਾਜਿਕਿਸਤਾਨੀ ਸੋਮੋਨੀ", "pa": "ਤਾਜਿਕਿਸਤਾਨੀ ਸੋਮੋਨੀ",
@@ -5843,7 +5852,7 @@
"ca": "tala", "ca": "tala",
"cs": "Samojská tala", "cs": "Samojská tala",
"de": "samoanischer Tala", "de": "samoanischer Tala",
"en": "Samoan Tālā", "en": "Samoan tālā",
"eo": "samoa talao", "eo": "samoa talao",
"es": "tālā", "es": "tālā",
"et": "Samoa tala", "et": "Samoa tala",
@@ -5998,6 +6007,7 @@
"XCG": { "XCG": {
"ar": "الجلدر الكاريبي", "ar": "الجلدر الكاريبي",
"ca": "florí caribeny", "ca": "florí caribeny",
"cs": "Karibský gulden",
"de": "Karibischer Gulden", "de": "Karibischer Gulden",
"en": "Caribbean guilder", "en": "Caribbean guilder",
"eo": "Karibia guldeno", "eo": "Karibia guldeno",
@@ -6006,6 +6016,7 @@
"hr": "Karipski gulden", "hr": "Karipski gulden",
"hu": "karibi forint", "hu": "karibi forint",
"it": "fiorino caraibico", "it": "fiorino caraibico",
"ja": "カリブ・ギルダー",
"nl": "Caribische gulden", "nl": "Caribische gulden",
"pap": "Florin karibense", "pap": "Florin karibense",
"pt": "Florim do Caribe", "pt": "Florim do Caribe",
@@ -6251,7 +6262,7 @@
"de": "sambischer Kwacha", "de": "sambischer Kwacha",
"en": "Zambian Kwacha", "en": "Zambian Kwacha",
"eo": "zambia kvaĉo", "eo": "zambia kvaĉo",
"es": "kwacha zambiano", "es": "kuacha zambiano",
"et": "Sambia kvatša", "et": "Sambia kvatša",
"fi": "Sambian kwacha", "fi": "Sambian kwacha",
"fr": "kwacha zambien", "fr": "kwacha zambien",
@@ -6745,6 +6756,7 @@
"bahamski dolar": "BSD", "bahamski dolar": "BSD",
"bahamský dolar": "BSD", "bahamský dolar": "BSD",
"bahamský dolár": "BSD", "bahamský dolár": "BSD",
"bahamu dolārs": "BSD",
"bahamų doleris": "BSD", "bahamų doleris": "BSD",
"bahrain dinar": "BHD", "bahrain dinar": "BHD",
"bahraini dinar": "BHD", "bahraini dinar": "BHD",
@@ -8011,6 +8023,10 @@
"dop": "DOP", "dop": "DOP",
"dopene": "DOP", "dopene": "DOP",
"dòlar australià": "AUD", "dòlar australià": "AUD",
"dòlar bahamià": "BSD",
"dòlar barbadià": "BBD",
"dòlar belizià": "BZD",
"dòlar bruneiès": "BND",
"dòlar canadenc": "CAD", "dòlar canadenc": "CAD",
"dòlar de bahames": "BSD", "dòlar de bahames": "BSD",
"dòlar de barbados": "BBD", "dòlar de barbados": "BBD",
@@ -8042,14 +8058,18 @@
"dòlar del canadà": "CAD", "dòlar del canadà": "CAD",
"dòlar del carib oriental": "XCD", "dòlar del carib oriental": "XCD",
"dòlar dels estats units": "USD", "dòlar dels estats units": "USD",
"dòlar estatunidenc": "USD",
"dòlar etíop": "ETB", "dòlar etíop": "ETB",
"dòlar fijià": "FJD", "dòlar fijià": "FJD",
"dòlar guyanès": "GYD",
"dòlar jamaicà": "JMD", "dòlar jamaicà": "JMD",
"dòlar liberià": "LRD", "dòlar liberià": "LRD",
"dòlar malai": "MYR", "dòlar malai": "MYR",
"dòlar namibi": "NAD", "dòlar namibi": "NAD",
"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 surinamès": "SRD",
"dòlar taiwanès": "TWD", "dòlar taiwanès": "TWD",
"dòlars canadencs": "CAD", "dòlars canadencs": "CAD",
"dòlars neozelandesos": "NZD", "dòlars neozelandesos": "NZD",
@@ -8252,6 +8272,7 @@
"egyptská libra": "EGP", "egyptská libra": "EGP",
"eiro": "EUR", "eiro": "EUR",
"ekialdeko karibeko dolar": "XCD", "ekialdeko karibeko dolar": "XCD",
"el peso": "GTQ",
"emalangeni": "SZL", "emalangeni": "SZL",
"emas sebagai pelaburan": "XAU", "emas sebagai pelaburan": "XAU",
"emirati dirham": "AED", "emirati dirham": "AED",
@@ -8304,6 +8325,7 @@
"escudo tanjung verde": "CVE", "escudo tanjung verde": "CVE",
"escudo zielonego przylądka": "CVE", "escudo zielonego przylądka": "CVE",
"escudos cabo verdianos": "CVE", "escudos cabo verdianos": "CVE",
"escut capverdià": "CVE",
"escut de cap verd": "CVE", "escut de cap verd": "CVE",
"esloti": [ "esloti": [
"PLZ", "PLZ",
@@ -8490,6 +8512,7 @@
"franc an chongó": "CDF", "franc an chongó": "CDF",
"franc burundais": "BIF", "franc burundais": "BIF",
"franc burundez": "BIF", "franc burundez": "BIF",
"franc burundès": "BIF",
"franc burundi": "BIF", "franc burundi": "BIF",
"franc centrafrican cfa": "XAF", "franc centrafrican cfa": "XAF",
"franc central african cfa": "XAF", "franc central african cfa": "XAF",
@@ -8514,6 +8537,7 @@
"franc cfa vest african": "XOF", "franc cfa vest african": "XOF",
"franc cfp": "XPF", "franc cfp": "XPF",
"franc comorian": "KMF", "franc comorian": "KMF",
"franc comorià": "KMF",
"franc comorien": "KMF", "franc comorien": "KMF",
"franc comoros": "KMF", "franc comoros": "KMF",
"franc congo": "CDF", "franc congo": "CDF",
@@ -8530,6 +8554,7 @@
"franc del congo belga": "CDF", "franc del congo belga": "CDF",
"franc des collectivités françaises du pacifique": "XPF", "franc des collectivités françaises du pacifique": "XPF",
"franc djibouti": "DJF", "franc djibouti": "DJF",
"franc djiboutià": "DJF",
"franc djiboutien": "DJF", "franc djiboutien": "DJF",
"franc elvețian": "CHF", "franc elvețian": "CHF",
"franc elveţian": "CHF", "franc elveţian": "CHF",
@@ -8741,6 +8766,7 @@
"greenback": "USD", "greenback": "USD",
"grivina": "UAH", "grivina": "UAH",
"grivna": "UAH", "grivna": "UAH",
"grivna ucraina": "UAH",
"grivna ucraniana": "UAH", "grivna ucraniana": "UAH",
"grivnas": "UAH", "grivnas": "UAH",
"grivnă": "UAH", "grivnă": "UAH",
@@ -8892,6 +8918,7 @@
"hongkonský dolár": "HKD", "hongkonský dolár": "HKD",
"hongkonški dolar": "HKD", "hongkonški dolar": "HKD",
"honkonga dolaro": "HKD", "honkonga dolaro": "HKD",
"honkongas dolārs": "HKD",
"honkongo doleris": "HKD", "honkongo doleris": "HKD",
"honkonški dolar": "HKD", "honkonški dolar": "HKD",
"hrivna": "UAH", "hrivna": "UAH",
@@ -9013,6 +9040,7 @@
"irr": "IRR", "irr": "IRR",
"isk": "ISK", "isk": "ISK",
"islanda krono": "ISK", "islanda krono": "ISK",
"islandes krona": "ISK",
"islandi kroon": "ISK", "islandi kroon": "ISK",
"islandiar koroa": "ISK", "islandiar koroa": "ISK",
"islandijos krona": "ISK", "islandijos krona": "ISK",
@@ -9077,6 +9105,7 @@
"jamaika doları": "JMD", "jamaika doları": "JMD",
"jamaika dollar": "JMD", "jamaika dollar": "JMD",
"jamaikan dollari": "JMD", "jamaikan dollari": "JMD",
"jamaikas dolārs": "JMD",
"jamaikos doleris": "JMD", "jamaikos doleris": "JMD",
"jamajčanski dolar": "JMD", "jamajčanski dolar": "JMD",
"jamajka dolaro": "JMD", "jamajka dolaro": "JMD",
@@ -9202,6 +9231,7 @@
"karibia guldeno": "XCG", "karibia guldeno": "XCG",
"karibischer gulden": "XCG", "karibischer gulden": "XCG",
"karibski goldinar": "XCG", "karibski goldinar": "XCG",
"karibský gulden": "XCG",
"karipski gulden": "XCG", "karipski gulden": "XCG",
"karod": "NPR", "karod": "NPR",
"kartvela lario": "GEL", "kartvela lario": "GEL",
@@ -9395,6 +9425,8 @@
"krw": "KRW", "krw": "KRW",
"ks": "MMK", "ks": "MMK",
"ksh": "KES", "ksh": "KES",
"kuacha malauí": "MWK",
"kuacha zambiano": "ZMW",
"kuanza angoleño": "AOA", "kuanza angoleño": "AOA",
"kuba peso": "CUP", "kuba peso": "CUP",
"kubai peso": "CUP", "kubai peso": "CUP",
@@ -9445,10 +9477,8 @@
"kwacha do malawi": "MWK", "kwacha do malawi": "MWK",
"kwacha do maláui": "MWK", "kwacha do maláui": "MWK",
"kwacha do malávi": "MWK", "kwacha do malávi": "MWK",
"kwacha malaui": "MWK",
"kwacha malauiana": "MWK", "kwacha malauiana": "MWK",
"kwacha malauiano": "MWK", "kwacha malauiano": "MWK",
"kwacha malauí": "MWK",
"kwacha malaviana": "MWK", "kwacha malaviana": "MWK",
"kwacha malawi": "MWK", "kwacha malawi": "MWK",
"kwacha malawiana": "MWK", "kwacha malawiana": "MWK",
@@ -9510,6 +9540,7 @@
"lao kip": "LAK", "lao kip": "LAK",
"laos kipi": "LAK", "laos kipi": "LAK",
"laosa kipo": "LAK", "laosa kipo": "LAK",
"laosas kips": "LAK",
"laosin kip": "LAK", "laosin kip": "LAK",
"laoski kip": "LAK", "laoski kip": "LAK",
"laoský kip": "LAK", "laoský kip": "LAK",
@@ -9671,6 +9702,7 @@
"liberisk dollar": "LRD", "liberisk dollar": "LRD",
"liberya doları": "LRD", "liberya doları": "LRD",
"libériai dollár": "LRD", "libériai dollár": "LRD",
"libērijas dolārs": "LRD",
"libia dinaro": "LYD", "libia dinaro": "LYD",
"libijos dinaras": "LYD", "libijos dinaras": "LYD",
"libijski dinar": "LYD", "libijski dinar": "LYD",
@@ -10213,6 +10245,7 @@
"namibya doları": "NAD", "namibya doları": "NAD",
"namíbiai dollár": "NAD", "namíbiai dollár": "NAD",
"namíbijský dolár": "NAD", "namíbijský dolár": "NAD",
"namībijas dolārs": "NAD",
"naujasis solis": "PEN", "naujasis solis": "PEN",
"naujasis taivano doleris": "TWD", "naujasis taivano doleris": "TWD",
"naujoji rumunijos lėja": "RON", "naujoji rumunijos lėja": "RON",
@@ -11075,6 +11108,7 @@
"riyal oman": "OMR", "riyal oman": "OMR",
"riyal qatar": "QAR", "riyal qatar": "QAR",
"riyal qatari": "QAR", "riyal qatari": "QAR",
"riyal qatarià": "QAR",
"riyal qatarien": "QAR", "riyal qatarien": "QAR",
"riyal qatariota": "QAR", "riyal qatariota": "QAR",
"riyal qatarita": "QAR", "riyal qatarita": "QAR",
@@ -11256,6 +11290,7 @@
"rupia índia": "INR", "rupia índia": "INR",
"rupia lankijska": "LKR", "rupia lankijska": "LKR",
"rupia maldiva": "MVR", "rupia maldiva": "MVR",
"rupia maldiviana": "MVR",
"rupia maldívia": "MVR", "rupia maldívia": "MVR",
"rupia malediwska": "MVR", "rupia malediwska": "MVR",
"rupia mauricia": "MUR", "rupia mauricia": "MUR",
@@ -11276,6 +11311,7 @@
"rupia seszelska": "SCR", "rupia seszelska": "SCR",
"rupia seychelense": "SCR", "rupia seychelense": "SCR",
"rupia seychellense": "SCR", "rupia seychellense": "SCR",
"rupia seychellesa": "SCR",
"rupia singalesa": "LKR", "rupia singalesa": "LKR",
"rupia singalese": "LKR", "rupia singalese": "LKR",
"rupia sri lanki": "LKR", "rupia sri lanki": "LKR",
@@ -11556,6 +11592,7 @@
"singapurski dolar": "SGD", "singapurski dolar": "SGD",
"singapurský dolar": "SGD", "singapurský dolar": "SGD",
"singapurský dolár": "SGD", "singapurský dolár": "SGD",
"singapūras dolārs": "SGD",
"singapūro doleris": "SGD", "singapūro doleris": "SGD",
"sint heleens pond": "SHP", "sint heleens pond": "SHP",
"siria pundo": "SYP", "siria pundo": "SYP",
@@ -11870,7 +11907,6 @@
"švýcarský frank": "CHF", "švýcarský frank": "CHF",
"șekel nou": "ILS", "șekel nou": "ILS",
"şekel": "ILS", "şekel": "ILS",
"şekel nou": "ILS",
"şili pesosu": "CLP", "şili pesosu": "CLP",
"s₣": "CHF", "s₣": "CHF",
"t": "TMT", "t": "TMT",
@@ -11883,6 +11919,8 @@
"tadžikistani somoni": "TJS", "tadžikistani somoni": "TJS",
"tadžikistanin somoni": "TJS", "tadžikistanin somoni": "TJS",
"tadžikistanski somoni": "TJS", "tadžikistanski somoni": "TJS",
"tadžikistānas somoni": "TJS",
"tadžiku somoni": "TJS",
"taĝika somonio": "TJS", "taĝika somonio": "TJS",
"tai baat": "THB", "tai baat": "THB",
"tailando batas": "THB", "tailando batas": "THB",
@@ -11918,6 +11956,7 @@
"tala samoana": "WST", "tala samoana": "WST",
"tala samoano": "WST", "tala samoano": "WST",
"tala samoà": "WST", "tala samoà": "WST",
"talao": "WST",
"tambala": "MWK", "tambala": "MWK",
"tamil rupee": "LKR", "tamil rupee": "LKR",
"tamilska rupija": "LKR", "tamilska rupija": "LKR",
@@ -14413,6 +14452,9 @@
"دوبرا": "STN", "دوبرا": "STN",
"دوبرا ساو تومي وبرينسيب": "STN", "دوبرا ساو تومي وبرينسيب": "STN",
"دوبرا ساو تومية وبرينسيبية": "STN", "دوبرا ساو تومية وبرينسيبية": "STN",
"دولار الامريكي": "USD",
"دولار الأمريكي": "USD",
"دولار امريكي": "USD",
"دولار أسترالي": "AUD", "دولار أسترالي": "AUD",
"دولار أمريكي": "USD", "دولار أمريكي": "USD",
"دولار بربادوسي": "BBD", "دولار بربادوسي": "BBD",
@@ -15109,6 +15151,7 @@
"เยน": "JPY", "เยน": "JPY",
"เรนมินบิ": "CNY", "เรนมินบิ": "CNY",
"เรอัลบราซิล": "BRL", "เรอัลบราซิล": "BRL",
"เรียล": "KHR",
"เรียลกัมพูชา": "KHR", "เรียลกัมพูชา": "KHR",
"เรียลบราซิล": "BRL", "เรียลบราซิล": "BRL",
"เลวูโรมาเนีย": "RON", "เลวูโรมาเนีย": "RON",
@@ -15479,7 +15522,6 @@
"₽": "RUB", "₽": "RUB",
"₾": "GEL", "₾": "GEL",
"⃀": "KGS", "⃀": "KGS",
"⃁": "SAR",
"〒": "KZT", "〒": "KZT",
"アイスランドクローナ": "ISK", "アイスランドクローナ": "ISK",
"アイスランド・クローナ": "ISK", "アイスランド・クローナ": "ISK",
@@ -15535,6 +15577,7 @@
"カタール・リヤル": "QAR", "カタール・リヤル": "QAR",
"カナダドル": "CAD", "カナダドル": "CAD",
"カナダ・ドル": "CAD", "カナダ・ドル": "CAD",
"カリブ・ギルダー": "XCG",
"カーボベルデ・エスクード": "CVE", "カーボベルデ・エスクード": "CVE",
"ギニア・フラン": "GNF", "ギニア・フラン": "GNF",
"ギニー": "EGP", "ギニー": "EGP",
@@ -15635,6 +15678,7 @@
"チュニジア・ディナール": "TND", "チュニジア・ディナール": "TND",
"チリの通貨": "CLP", "チリの通貨": "CLP",
"チリ・ペソ": "CLP", "チリ・ペソ": "CLP",
"デジタルルピー": "INR",
"デンマーククローネ": "DKK", "デンマーククローネ": "DKK",
"デンマーク・クローネ": "DKK", "デンマーク・クローネ": "DKK",
"テンゲ": "KZT", "テンゲ": "KZT",

File diff suppressed because one or more lines are too long

View File

@@ -1684,8 +1684,10 @@
"custom": { "custom": {
"ui_lang": { "ui_lang": {
"bg": "bg", "bg": "bg",
"br": "br",
"ca": "ca", "ca": "ca",
"cs": "cs", "cs": "cs",
"cy": "cy",
"da": "da", "da": "da",
"de-DE": "de-de", "de-DE": "de-de",
"el": "el", "el": "el",
@@ -1695,9 +1697,11 @@
"en-US": "en-us", "en-US": "en-us",
"es": "es", "es": "es",
"et": "et", "et": "et",
"eu": "eu",
"fi-FI": "fi-fi", "fi-FI": "fi-fi",
"fr-CA": "fr-ca", "fr-CA": "fr-ca",
"fr-FR": "fr-fr", "fr-FR": "fr-fr",
"gl": "gl",
"hr": "hr", "hr": "hr",
"hu": "hu", "hu": "hu",
"id": "id", "id": "id",
@@ -1786,8 +1790,10 @@
"custom": { "custom": {
"ui_lang": { "ui_lang": {
"bg": "bg", "bg": "bg",
"br": "br",
"ca": "ca", "ca": "ca",
"cs": "cs", "cs": "cs",
"cy": "cy",
"da": "da", "da": "da",
"de-DE": "de-de", "de-DE": "de-de",
"el": "el", "el": "el",
@@ -1797,9 +1803,11 @@
"en-US": "en-us", "en-US": "en-us",
"es": "es", "es": "es",
"et": "et", "et": "et",
"eu": "eu",
"fi-FI": "fi-fi", "fi-FI": "fi-fi",
"fr-CA": "fr-ca", "fr-CA": "fr-ca",
"fr-FR": "fr-fr", "fr-FR": "fr-fr",
"gl": "gl",
"hr": "hr", "hr": "hr",
"hu": "hu", "hu": "hu",
"id": "id", "id": "id",
@@ -1888,8 +1896,10 @@
"custom": { "custom": {
"ui_lang": { "ui_lang": {
"bg": "bg", "bg": "bg",
"br": "br",
"ca": "ca", "ca": "ca",
"cs": "cs", "cs": "cs",
"cy": "cy",
"da": "da", "da": "da",
"de-DE": "de-de", "de-DE": "de-de",
"el": "el", "el": "el",
@@ -1899,9 +1909,11 @@
"en-US": "en-us", "en-US": "en-us",
"es": "es", "es": "es",
"et": "et", "et": "et",
"eu": "eu",
"fi-FI": "fi-fi", "fi-FI": "fi-fi",
"fr-CA": "fr-ca", "fr-CA": "fr-ca",
"fr-FR": "fr-fr", "fr-FR": "fr-fr",
"gl": "gl",
"hr": "hr", "hr": "hr",
"hu": "hu", "hu": "hu",
"id": "id", "id": "id",
@@ -1990,8 +2002,10 @@
"custom": { "custom": {
"ui_lang": { "ui_lang": {
"bg": "bg", "bg": "bg",
"br": "br",
"ca": "ca", "ca": "ca",
"cs": "cs", "cs": "cs",
"cy": "cy",
"da": "da", "da": "da",
"de-DE": "de-de", "de-DE": "de-de",
"el": "el", "el": "el",
@@ -2001,9 +2015,11 @@
"en-US": "en-us", "en-US": "en-us",
"es": "es", "es": "es",
"et": "et", "et": "et",
"eu": "eu",
"fi-FI": "fi-fi", "fi-FI": "fi-fi",
"fr-CA": "fr-ca", "fr-CA": "fr-ca",
"fr-FR": "fr-fr", "fr-FR": "fr-fr",
"gl": "gl",
"hr": "hr", "hr": "hr",
"hu": "hu", "hu": "hu",
"id": "id", "id": "id",
@@ -6628,6 +6644,172 @@
"to-TO": "to" "to-TO": "to"
} }
}, },
"mullvadleta": {
"all_locale": null,
"custom": {},
"data_type": "traits_v1",
"languages": {
"ar": "ar",
"bg": "bg",
"ca": "ca",
"cs": "cs",
"da": "da",
"de": "de",
"en": "en",
"es": "es",
"et": "et",
"fi": "fi",
"fr": "fr",
"he": "he",
"hr": "hr",
"hu": "hu",
"is": "is",
"it": "it",
"ja": "jp",
"ko": "ko",
"lt": "lt",
"lv": "lv",
"nb": "nb",
"nl": "nl",
"pl": "pl",
"pt": "pt",
"ro": "ro",
"ru": "ru",
"sk": "sk",
"sl": "sl",
"sr": "sr",
"sv": "sv",
"tr": "tr",
"zh_Hans": "zh-hant",
"zh_Hant": "zh-hans"
},
"regions": {
"ar-SA": "sa",
"da-DK": "dk",
"de-AT": "at",
"de-BE": "be",
"de-CH": "ch",
"de-DE": "de",
"en-AU": "au",
"en-CA": "ca",
"en-GB": "uk",
"en-IN": "in",
"en-NZ": "nz",
"en-PH": "ph",
"en-US": "us",
"en-ZA": "za",
"es-AR": "ar",
"es-CL": "cl",
"es-ES": "es",
"es-MX": "mx",
"fi-FI": "fi",
"fr-BE": "be",
"fr-CA": "ca",
"fr-CH": "ch",
"fr-FR": "fr",
"id-ID": "id",
"it-CH": "ch",
"it-IT": "it",
"ja-JP": "jp",
"ko-KR": "kr",
"ms-MY": "my",
"nb-NO": "no",
"nl-BE": "be",
"nl-NL": "nl",
"pl-PL": "pl",
"pt-BR": "br",
"pt-PT": "pt",
"ru-RU": "ru",
"se-SE": "se",
"tr-TR": "tr",
"zh-CN": "cn",
"zh-HK": "hk",
"zh-TW": "tw"
}
},
"mullvadleta brave": {
"all_locale": null,
"custom": {},
"data_type": "traits_v1",
"languages": {
"ar": "ar",
"bg": "bg",
"ca": "ca",
"cs": "cs",
"da": "da",
"de": "de",
"en": "en",
"es": "es",
"et": "et",
"fi": "fi",
"fr": "fr",
"he": "he",
"hr": "hr",
"hu": "hu",
"is": "is",
"it": "it",
"ja": "jp",
"ko": "ko",
"lt": "lt",
"lv": "lv",
"nb": "nb",
"nl": "nl",
"pl": "pl",
"pt": "pt",
"ro": "ro",
"ru": "ru",
"sk": "sk",
"sl": "sl",
"sr": "sr",
"sv": "sv",
"tr": "tr",
"zh_Hans": "zh-hant",
"zh_Hant": "zh-hans"
},
"regions": {
"ar-SA": "sa",
"da-DK": "dk",
"de-AT": "at",
"de-BE": "be",
"de-CH": "ch",
"de-DE": "de",
"en-AU": "au",
"en-CA": "ca",
"en-GB": "uk",
"en-IN": "in",
"en-NZ": "nz",
"en-PH": "ph",
"en-US": "us",
"en-ZA": "za",
"es-AR": "ar",
"es-CL": "cl",
"es-ES": "es",
"es-MX": "mx",
"fi-FI": "fi",
"fr-BE": "be",
"fr-CA": "ca",
"fr-CH": "ch",
"fr-FR": "fr",
"id-ID": "id",
"it-CH": "ch",
"it-IT": "it",
"ja-JP": "jp",
"ko-KR": "kr",
"ms-MY": "my",
"nb-NO": "no",
"nl-BE": "be",
"nl-NL": "nl",
"pl-PL": "pl",
"pt-BR": "br",
"pt-PT": "pt",
"ru-RU": "ru",
"se-SE": "se",
"tr-TR": "tr",
"zh-CN": "cn",
"zh-HK": "hk",
"zh-TW": "tw"
}
},
"odysee": { "odysee": {
"all_locale": null, "all_locale": null,
"custom": {}, "custom": {},
@@ -6934,6 +7116,7 @@
"BY", "BY",
"BZ", "BZ",
"CA", "CA",
"CC",
"CD", "CD",
"CF", "CF",
"CG", "CG",
@@ -8105,6 +8288,7 @@
"nr", "nr",
"nrm", "nrm",
"nso", "nso",
"nup",
"nv", "nv",
"ny", "ny",
"oc", "oc",
@@ -8121,7 +8305,6 @@
"pdc", "pdc",
"pfl", "pfl",
"pi", "pi",
"pih",
"pl", "pl",
"pms", "pms",
"pnb", "pnb",
@@ -8410,46 +8593,6 @@
"zh-classical": "zh-classical" "zh-classical": "zh-classical"
} }
}, },
"yahoo": {
"all_locale": "any",
"custom": {},
"data_type": "traits_v1",
"languages": {
"ar": "ar",
"bg": "bg",
"cs": "cs",
"da": "da",
"de": "de",
"el": "el",
"en": "en",
"es": "es",
"et": "et",
"fi": "fi",
"fr": "fr",
"he": "he",
"hr": "hr",
"hu": "hu",
"it": "it",
"ja": "ja",
"ko": "ko",
"lt": "lt",
"lv": "lv",
"nl": "nl",
"no": "no",
"pl": "pl",
"pt": "pt",
"ro": "ro",
"ru": "ru",
"sk": "sk",
"sl": "sl",
"sv": "sv",
"th": "th",
"tr": "tr",
"zh_Hans": "zh_chs",
"zh_Hant": "zh_cht"
},
"regions": {}
},
"z-library": { "z-library": {
"all_locale": "", "all_locale": "",
"custom": { "custom": {

File diff suppressed because it is too large Load Diff

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": [
"136.0", "138.0",
"135.0" "137.0"
] ]
} }

View File

@@ -4244,6 +4244,11 @@
"symbol": "cm³/mol", "symbol": "cm³/mol",
"to_si_factor": 1e-06 "to_si_factor": 1e-06
}, },
"Q240468": {
"si_name": null,
"symbol": "syr£",
"to_si_factor": null
},
"Q242988": { "Q242988": {
"si_name": null, "si_name": null,
"symbol": "Lib$", "symbol": "Lib$",
@@ -5279,6 +5284,11 @@
"symbol": "bhp EDR", "symbol": "bhp EDR",
"to_si_factor": 12.958174 "to_si_factor": 12.958174
}, },
"Q3984193": {
"si_name": "Q25269",
"symbol": "TeV",
"to_si_factor": 1.602176634e-07
},
"Q39978339": { "Q39978339": {
"si_name": "Q25377184", "si_name": "Q25377184",
"symbol": "kg/cm²", "symbol": "kg/cm²",
@@ -6595,14 +6605,14 @@
"to_si_factor": null "to_si_factor": null
}, },
"Q68725821": { "Q68725821": {
"si_name": null, "si_name": "Q11579",
"symbol": "°Rø", "symbol": "°Rø",
"to_si_factor": null "to_si_factor": 1.90476190476
}, },
"Q68726230": { "Q68726230": {
"si_name": null, "si_name": "Q11579",
"symbol": "°De", "symbol": "°De",
"to_si_factor": null "to_si_factor": 0.66667
}, },
"Q68726625": { "Q68726625": {
"si_name": null, "si_name": null,
@@ -6917,7 +6927,7 @@
"Q72081071": { "Q72081071": {
"si_name": "Q25269", "si_name": "Q25269",
"symbol": "MeV", "symbol": "MeV",
"to_si_factor": 1.602176634e-13 "to_si_factor": 1.60217656535e-13
}, },
"Q7235648": { "Q7235648": {
"si_name": null, "si_name": null,

View File

@@ -1,6 +1,16 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementations of the framework for the SearXNG engines. """Implementations of the framework for the SearXNG engines.
- :py:obj:`searx.enginelib.EngineCache`
- :py:obj:`searx.enginelib.Engine`
- :py:obj:`searx.enginelib.traits`
There is a command line for developer purposes and for deeper analysis. Here is
an example in which the command line is called in the development environment::
$ ./manage pyenv.cmd bash --norc --noprofile
(py3) python -m searx.enginelib --help
.. hint:: .. hint::
The long term goal is to modularize all implementations of the engine The long term goal is to modularize all implementations of the engine
@@ -9,16 +19,158 @@
- move implementations of the :ref:`searx.engines loader` to a new module in - move implementations of the :ref:`searx.engines loader` to a new module in
the :py:obj:`searx.enginelib` namespace. the :py:obj:`searx.enginelib` namespace.
-----
""" """
from __future__ import annotations from __future__ import annotations
from typing import List, Callable, TYPE_CHECKING
__all__ = ["EngineCache", "Engine", "ENGINES_CACHE"]
from typing import List, Callable, TYPE_CHECKING, Any
import string
import typer
from ..cache import ExpireCache, ExpireCacheCfg
if TYPE_CHECKING: if TYPE_CHECKING:
from searx.enginelib import traits from searx.enginelib import traits
ENGINES_CACHE = ExpireCache.build_cache(
ExpireCacheCfg(
name="ENGINES_CACHE",
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
MAINTENANCE_PERIOD=60 * 60, # 2h
)
)
"""Global :py:obj:`searx.cache.ExpireCacheSQLite` instance where the cached
values from all engines are stored. The `MAXHOLD_TIME` is 7 days and the
`MAINTENANCE_PERIOD` is set to two hours."""
app = typer.Typer()
@app.command()
def state():
"""Show state for the caches of the engines."""
title = "cache tables and key/values"
print(title)
print("=" * len(title))
print(ENGINES_CACHE.state().report())
print()
title = f"properties of {ENGINES_CACHE.cfg.name}"
print(title)
print("=" * len(title))
print(str(ENGINES_CACHE.properties)) # type: ignore
@app.command()
def maintenance(force: bool = True):
"""Carry out maintenance on cache of the engines."""
ENGINES_CACHE.maintenance(force=force)
class EngineCache:
"""Persistent (SQLite) key/value cache that deletes its values again after
``expire`` seconds (default/max: :py:obj:`MAXHOLD_TIME
<searx.cache.ExpireCacheCfg.MAXHOLD_TIME>`). This class is a wrapper around
:py:obj:`ENGINES_CACHE` (:py:obj:`ExpireCacheSQLite
<searx.cache.ExpireCacheSQLite>`).
In the :origin:`searx/engines/demo_offline.py` engine you can find an
exemplary implementation of such a cache other exaples are implemeted
in:
- :origin:`searx/engines/radio_browser.py`
- :origin:`searx/engines/soundcloud.py`
- :origin:`searx/engines/startpage.py`
.. code: python
from searx.enginelib import EngineCache
CACHE: EngineCache
def init(engine_settings):
global CACHE
CACHE = EngineCache(engine_settings["name"])
def request(query, params):
token = CACHE.get(key="token")
if token is None:
token = get_token()
# cache token of this engine for 1h
CACHE.set(key="token", value=token, expire=3600)
...
For introspection of the DB, jump into developer environment and run command to
show cache state::
$ ./manage pyenv.cmd bash --norc --noprofile
(py3) python -m searx.enginelib cache state
cache tables and key/values
===========================
[demo_offline ] 2025-04-22 11:32:50 count --> (int) 4
[startpage ] 2025-04-22 12:32:30 SC_CODE --> (str) fSOBnhEMlDfE20
[duckduckgo ] 2025-04-22 12:32:31 4dff493e.... --> (str) 4-128634958369380006627592672385352473325
[duckduckgo ] 2025-04-22 12:40:06 3e2583e2.... --> (str) 4-263126175288871260472289814259666848451
[radio_browser ] 2025-04-23 11:33:08 servers --> (list) ['https://de2.api.radio-browser.info', ...]
[soundcloud ] 2025-04-29 11:40:06 guest_client_id --> (str) EjkRJG0BLNEZquRiPZYdNtJdyGtTuHdp
[wolframalpha ] 2025-04-22 12:40:06 code --> (str) 5aa79f86205ad26188e0e26e28fb7ae7
number of tables: 6
number of key/value pairs: 7
In the "cache tables and key/values" section, the table name (engine name) is at
first position on the second there is the calculated expire date and on the
third and fourth position the key/value is shown.
About duckduckgo: The *vqd coode* of ddg depends on the query term and therefore
the key is a hash value of the query term (to not to store the raw query term).
In the "properties of ENGINES_CACHE" section all properties of the SQLiteAppl /
ExpireCache and their last modification date are shown::
properties of ENGINES_CACHE
===========================
[last modified: 2025-04-22 11:32:27] DB_SCHEMA : 1
[last modified: 2025-04-22 11:32:27] LAST_MAINTENANCE :
[last modified: 2025-04-22 11:32:27] crypt_hash : ca612e3566fdfd7cf7efe...
[last modified: 2025-04-22 11:32:30] CACHE-TABLE--demo_offline: demo_offline
[last modified: 2025-04-22 11:32:30] CACHE-TABLE--startpage: startpage
[last modified: 2025-04-22 11:32:31] CACHE-TABLE--duckduckgo: duckduckgo
[last modified: 2025-04-22 11:33:08] CACHE-TABLE--radio_browser: radio_browser
[last modified: 2025-04-22 11:40:06] CACHE-TABLE--soundcloud: soundcloud
[last modified: 2025-04-22 11:40:06] CACHE-TABLE--wolframalpha: wolframalpha
These properties provide information about the state of the ExpireCache and
control the behavior. For example, the maintenance intervals are controlled by
the last modification date of the LAST_MAINTENANCE property and the hash value
of the password can be used to detect whether the password has been changed (in
this case the DB entries can no longer be decrypted and the entire cache must be
discarded).
"""
def __init__(self, engine_name: str, expire: int | None = None):
self.expire = expire or ENGINES_CACHE.cfg.MAXHOLD_TIME
_valid = "-_." + string.ascii_letters + string.digits
self.table_name = "".join([c if c in _valid else "_" for c in engine_name])
def set(self, key: str, value: Any, expire: int | None = None) -> bool:
return ENGINES_CACHE.set(
key=key,
value=value,
expire=expire or self.expire,
ctx=self.table_name,
)
def get(self, key: str, default=None) -> Any:
return ENGINES_CACHE.get(key, default=default, ctx=self.table_name)
def secret_hash(self, name: str | bytes) -> str:
return ENGINES_CACHE.secret_hash(name=name)
class Engine: # pylint: disable=too-few-public-methods class Engine: # pylint: disable=too-few-public-methods
"""Class of engine instances build from YAML settings. """Class of engine instances build from YAML settings.

View File

@@ -0,0 +1,21 @@
"""Implementation of a command line for development purposes. To start a
command, switch to the environment and run library module as a script::
$ ./manage pyenv.cmd bash --norc --noprofile
(py3) python -m searx.enginelib --help
The following commands can be used for maintenance and introspection
(development) of the engine cache::
(py3) python -m searx.enginelib cache state
(py3) python -m searx.enginelib cache maintenance
"""
import typer
from .. import enginelib
app = typer.Typer()
app.add_typer(enginelib.app, name="cache", help="Commands related to the cache of the engines.")
app()

81
searx/engines/ansa.py Normal file
View File

@@ -0,0 +1,81 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Engine for Ansa, Italy's oldest news agency.
To use this engine add the following entry to your engines
list in ``settings.yml``:
.. code:: yaml
- name: ansa
engine: ansa
shortcut: ans
disabled: false
"""
from urllib.parse import urlencode
from lxml import html
from searx.result_types import EngineResults, MainResult
from searx.utils import eval_xpath, eval_xpath_list, extract_text
engine_type = 'online'
language_support = False
categories = ['news']
paging = True
page_size = 12
base_url = 'https://www.ansa.it'
time_range_support = True
time_range_args = {
'day': 1,
'week': 7,
'month': 31,
'year': 365,
}
# https://www.ansa.it/ricerca/ansait/search.shtml?start=0&any=houthi&periodo=&sort=data%3Adesc
search_api = 'https://www.ansa.it/ricerca/ansait/search.shtml?'
about = {
'website': 'https://www.ansa.it',
'wikidata_id': 'Q392934',
'official_api_documentation': None,
'use_official_api': False,
'require_api_key': False,
'results': 'HTML',
'language': 'it',
}
def request(query, params):
query_params = {
'any': query,
'start': (params['pageno'] - 1) * page_size,
'sort': "data:desc",
}
if params['time_range']:
query_params['periodo'] = time_range_args.get(params['time_range'])
params['url'] = search_api + urlencode(query_params)
return params
def response(resp) -> EngineResults:
res = EngineResults()
doc = html.fromstring(resp.text)
for result in eval_xpath_list(doc, "//div[@class='article']"):
res_obj = MainResult(
title=extract_text(eval_xpath(result, "./div[@class='content']/h2[@class='title']/a")),
content=extract_text(eval_xpath(result, "./div[@class='content']/div[@class='text']")),
url=base_url + extract_text(eval_xpath(result, "./div[@class='content']/h2[@class='title']/a/@href")),
)
thumbnail = extract_text(eval_xpath(result, "./div[@class='image']/a/img/@src"))
if thumbnail:
res_obj.thumbnail = base_url + thumbnail
res.append(res_obj)
return res

View File

@@ -129,6 +129,7 @@ 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,
eval_xpath_list, eval_xpath_list,
@@ -253,33 +254,6 @@ def _extract_published_date(published_date_raw):
return None return None
def parse_data_string(resp):
# kit.start(app, element, {
# node_ids: [0, 19],
# data: [{"type":"data","data" .... ["q","goggles_id"],"route":1,"url":1}}]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
kit_start = resp.text.index("kit.start(app,")
start = resp.text[kit_start:].index('data: [{"type":"data"')
start = kit_start + start + len('data: ')
lev = 0
end = start
inner = False
for c in resp.text[start:]:
if inner and lev == 0:
break
end += 1
if c == "[":
lev += 1
inner = True
continue
if c == "]":
lev -= 1
json_data = js_variable_to_python(resp.text[start:end])
return json_data
def response(resp) -> EngineResults: def response(resp) -> EngineResults:
if brave_category in ('search', 'goggles'): if brave_category in ('search', 'goggles'):
@@ -288,7 +262,15 @@ def response(resp) -> EngineResults:
if brave_category in ('news'): if brave_category in ('news'):
return _parse_news(resp) return _parse_news(resp)
json_data = parse_data_string(resp) # 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}}]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
js_object = "[{" + extr(resp.text, "data: [{", "}}],") + "}}]"
json_data = js_variable_to_python(js_object)
# json_data is a list and at the second position (0,1) in this list we find the "response" data we need .. # 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'] json_resp = json_data[1]['data']['body']['response']

View File

@@ -1,5 +1,60 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""ChinaSo: A search engine from ChinaSo.""" """ChinaSo_, a search engine for the chinese language area.
.. attention::
ChinaSo engine does not return real URL, the links from these search
engines violate the privacy of the users!!
We try to find a solution for this problem, please follow `issue #4694`_.
As long as the problem has not been resolved, these engines are
not active in a standard setup (``inactive: true``).
.. _ChinaSo: https://www.chinaso.com/
.. _issue #4694: https://github.com/searxng/searxng/issues/4694
Configuration
=============
The engine has the following additional settings:
- :py:obj:`chinaso_category` (:py:obj:`ChinasoCategoryType`)
- :py:obj:`chinaso_news_source` (:py:obj:`ChinasoNewsSourceType`)
In the example below, all three ChinaSO engines are using the :ref:`network
<engine network>` from the ``chinaso news`` engine.
.. code:: yaml
- name: chinaso news
engine: chinaso
shortcut: chinaso
categories: [news]
chinaso_category: news
chinaso_news_source: all
- name: chinaso images
engine: chinaso
network: chinaso news
shortcut: chinasoi
categories: [images]
chinaso_category: images
- name: chinaso videos
engine: chinaso
network: chinaso news
shortcut: chinasov
categories: [videos]
chinaso_category: videos
Implementations
===============
"""
import typing
from urllib.parse import urlencode from urllib.parse import urlencode
from datetime import datetime from datetime import datetime
@@ -20,13 +75,31 @@ paging = True
time_range_support = True time_range_support = True
results_per_page = 10 results_per_page = 10
categories = [] categories = []
chinaso_category = 'news'
ChinasoCategoryType = typing.Literal['news', 'videos', 'images']
"""ChinaSo supports news, videos, images search. """ChinaSo supports news, videos, images search.
- ``news``: search for news - ``news``: search for news
- ``videos``: search for videos - ``videos``: search for videos
- ``images``: search for images - ``images``: search for images
In the category ``news`` you can additionally filter by option
:py:obj:`chinaso_news_source`.
""" """
chinaso_category = 'news'
"""Configure ChinaSo category (:py:obj:`ChinasoCategoryType`)."""
ChinasoNewsSourceType = typing.Literal['CENTRAL', 'LOCAL', 'BUSINESS', 'EPAPER', 'all']
"""Filtering ChinaSo-News results by source:
- ``CENTRAL``: central publication
- ``LOCAL``: local publication
- ``BUSINESS``: business publication
- ``EPAPER``: E-Paper
- ``all``: all sources
"""
chinaso_news_source: ChinasoNewsSourceType = 'all'
"""Configure ChinaSo-News type (:py:obj:`ChinasoNewsSourceType`)."""
time_range_dict = {'day': '24h', 'week': '1w', 'month': '1m', 'year': '1y'} time_range_dict = {'day': '24h', 'week': '1w', 'month': '1m', 'year': '1y'}
@@ -35,7 +108,9 @@ base_url = "https://www.chinaso.com"
def init(_): def init(_):
if chinaso_category not in ('news', 'videos', 'images'): if chinaso_category not in ('news', 'videos', 'images'):
raise SearxEngineAPIException(f"Unsupported category: {chinaso_category}") raise ValueError(f"Unsupported category: {chinaso_category}")
if chinaso_category == 'news' and chinaso_news_source not in typing.get_args(ChinasoNewsSourceType):
raise ValueError(f"Unsupported news source: {chinaso_news_source}")
def request(query, params): def request(query, params):
@@ -56,6 +131,11 @@ def request(query, params):
'params': {'start_index': (params["pageno"] - 1) * results_per_page, 'rn': results_per_page}, 'params': {'start_index': (params["pageno"] - 1) * results_per_page, 'rn': results_per_page},
}, },
} }
if chinaso_news_source != 'all':
if chinaso_news_source == 'EPAPER':
category_config['news']['params']["type"] = 'EPAPER'
else:
category_config['news']['params']["cate"] = chinaso_news_source
query_params.update(category_config[chinaso_category]['params']) query_params.update(category_config[chinaso_category]['params'])

View File

@@ -15,6 +15,7 @@ close to the implementation, its just a simple example. To get in use of this
import json import json
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineCache
engine_type = 'offline' engine_type = 'offline'
categories = ['general'] categories = ['general']
@@ -32,14 +33,18 @@ about = {
# if there is a need for globals, use a leading underline # if there is a need for globals, use a leading underline
_my_offline_engine: str = "" _my_offline_engine: str = ""
CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
seconds."""
def init(engine_settings=None):
def init(engine_settings):
"""Initialization of the (offline) engine. The origin of this demo engine is a """Initialization of the (offline) engine. The origin of this demo engine is a
simple json string which is loaded in this example while the engine is simple json string which is loaded in this example while the engine is
initialized. initialized."""
global _my_offline_engine, CACHE # pylint: disable=global-statement
""" CACHE = EngineCache(engine_settings["name"]) # type:ignore
global _my_offline_engine # pylint: disable=global-statement
_my_offline_engine = ( _my_offline_engine = (
'[ {"value": "%s"}' '[ {"value": "%s"}'
@@ -57,8 +62,8 @@ def search(query, request_params) -> EngineResults:
results. results.
""" """
res = EngineResults() res = EngineResults()
count = CACHE.get("count", 0)
count = 0
for row in json.loads(_my_offline_engine): for row in json.loads(_my_offline_engine):
count += 1 count += 1
kvmap = { kvmap = {
@@ -75,4 +80,7 @@ def search(query, request_params) -> EngineResults:
) )
) )
res.add(res.types.LegacyResult(number_of_results=count)) res.add(res.types.LegacyResult(number_of_results=count))
# cache counter value for 20sec
CACHE.set("count", count, expire=20)
return res return res

View File

@@ -6,16 +6,17 @@ DuckDuckGo WEB
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
import re
from urllib.parse import quote_plus
import json import json
import re
import typing
from urllib.parse import quote_plus
import babel import babel
import lxml.html import lxml.html
from searx import ( from searx import (
locales, locales,
redislib,
external_bang, external_bang,
) )
from searx.utils import ( from searx.utils import (
@@ -25,12 +26,12 @@ from searx.utils import (
extract_text, extract_text,
) )
from searx.network import get # see https://github.com/searxng/searxng/issues/762 from searx.network import get # see https://github.com/searxng/searxng/issues/762
from searx import redisdb
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineCaptchaException from searx.exceptions import SearxEngineCaptchaException
from searx.result_types import EngineResults from searx.result_types import EngineResults
if TYPE_CHECKING: if typing.TYPE_CHECKING:
import logging import logging
logger: logging.Logger logger: logging.Logger
@@ -61,28 +62,18 @@ url = "https://html.duckduckgo.com/html"
time_range_dict = {'day': 'd', 'week': 'w', 'month': 'm', 'year': 'y'} time_range_dict = {'day': 'd', 'week': 'w', 'month': 'm', 'year': 'y'}
form_data = {'v': 'l', 'api': 'd.js', 'o': 'json'} form_data = {'v': 'l', 'api': 'd.js', 'o': 'json'}
__CACHE = []
CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
seconds."""
def _cache_key(query: str, region: str): def init(_): # pylint: disable=unused-argument
return 'SearXNG_ddg_web_vqd' + redislib.secret_hash(f"{query}//{region}") global CACHE # pylint: disable=global-statement
CACHE = EngineCache("duckduckgo") # type:ignore
def cache_vqd(query: str, region: str, value: str): def get_vqd(query: str, region: str, force_request: bool = False) -> str:
"""Caches a ``vqd`` value from a query."""
c = redisdb.client()
if c:
logger.debug("VALKEY cache vqd value: %s (%s)", value, region)
c.set(_cache_key(query, region), value, ex=600)
else:
logger.debug("MEM cache vqd value: %s (%s)", value, region)
if len(__CACHE) > 100: # cache vqd from last 100 queries
__CACHE.pop(0)
__CACHE.append((_cache_key(query, region), value))
def get_vqd(query: str, region: str, force_request: bool = False):
"""Returns the ``vqd`` that fits to the *query*. """Returns the ``vqd`` that fits to the *query*.
:param query: The query term :param query: The query term
@@ -114,31 +105,34 @@ def get_vqd(query: str, region: str, force_request: bool = False):
seems the block list is a sliding window: to get my IP rid from the bot list seems the block list is a sliding window: to get my IP rid from the bot list
I had to cool down my IP for 1h (send no requests from that IP to DDG). I had to cool down my IP for 1h (send no requests from that IP to DDG).
""" """
key = _cache_key(query, region) key = CACHE.secret_hash(f"{query}//{region}")
value = CACHE.get(key=key)
if value is not None and not force_request:
logger.debug("vqd: re-use cached value: %s", value)
return value
c = redisdb.client() logger.debug("vqd: request value from from duckduckgo.com")
if c: resp = get(f'https://duckduckgo.com/?q={quote_plus(query)}')
value = c.get(key) if resp.status_code == 200: # type: ignore
if value or value == b'': value = extr(resp.text, 'vqd="', '"') # type: ignore
value = value.decode('utf-8') # type: ignore if value:
logger.debug("re-use CACHED vqd value: %s", value) logger.debug("vqd value from duckduckgo.com request: '%s'", value)
return value else:
logger.error("vqd: can't parse value from ddg response (return empty string)")
return ""
else:
logger.error("vqd: got HTTP %s from duckduckgo.com", resp.status_code)
for k, value in __CACHE: if value:
if k == key: CACHE.set(key=key, value=value)
logger.debug("MEM re-use CACHED vqd value: %s", value) else:
return value logger.error("vqd value from duckduckgo.com ", resp.status_code)
return value
if force_request:
resp = get(f'https://duckduckgo.com/?q={quote_plus(query)}')
if resp.status_code == 200: # type: ignore
value = extr(resp.text, 'vqd="', '"') # type: ignore
if value:
logger.debug("vqd value from DDG request: %s", value)
cache_vqd(query, region, value)
return value
return None def set_vqd(query: str, region: str, value: str):
key = CACHE.secret_hash(f"{query}//{region}")
CACHE.set(key=key, value=value, expire=3600)
def get_ddg_lang(eng_traits: EngineTraits, sxng_locale, default='en_US'): def get_ddg_lang(eng_traits: EngineTraits, sxng_locale, default='en_US'):
@@ -373,8 +367,11 @@ def response(resp) -> EngineResults:
# some locales (at least China) does not have a "next page" button # some locales (at least China) does not have a "next page" button
form = form[0] form = form[0]
form_vqd = eval_xpath(form, '//input[@name="vqd"]/@value')[0] form_vqd = eval_xpath(form, '//input[@name="vqd"]/@value')[0]
set_vqd(
cache_vqd(resp.search_params['data']['q'], resp.search_params['data']['kl'], form_vqd) query=resp.search_params['data']['q'],
region=resp.search_params['data']['kl'],
value=str(form_vqd),
)
# just select "web-result" and ignore results of class "result--ad result--ad--small" # just select "web-result" and ignore results of class "result--ad result--ad--small"
for div_result in eval_xpath(doc, '//div[@id="links"]/div[contains(@class, "web-result")]'): for div_result in eval_xpath(doc, '//div[@id="links"]/div[contains(@class, "web-result")]'):
@@ -401,7 +398,7 @@ def response(resp) -> EngineResults:
results.add( results.add(
results.types.Answer( results.types.Answer(
answer=zero_click, answer=zero_click,
url=eval_xpath_getindex(doc, '//div[@id="zero_click_abstract"]/a/@href', 0), url=eval_xpath_getindex(doc, '//div[@id="zero_click_abstract"]/a/@href', 0), # type: ignore
) )
) )

View File

@@ -0,0 +1,116 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""`Hugging Face`_ search engine for SearXNG.
.. _Hugging Face: https://huggingface.co
Configuration
=============
The engine has the following additional settings:
- :py:obj:`huggingface_endpoint`
Configurations for endpoints:
.. code:: yaml
- name: huggingface
engine: huggingface
shortcut: hf
- name: huggingface datasets
huggingface_endpoint: datasets
engine: huggingface
shortcut: hfd
- name: huggingface spaces
huggingface_endpoint: spaces
engine: huggingface
shortcut: hfs
Implementations
===============
"""
from urllib.parse import urlencode
from datetime import datetime
from searx.exceptions import SearxEngineAPIException
from searx.utils import html_to_text
from searx.result_types import EngineResults, MainResult
about = {
"website": "https://huggingface.co/",
"wikidata_id": "Q108943604",
"official_api_documentation": "https://huggingface.co/docs/hub/en/api",
"use_official_api": True,
"require_api_key": False,
"results": "JSON",
}
categories = ['it', 'repos']
base_url = "https://huggingface.co"
huggingface_endpoint = 'models'
"""Hugging Face supports datasets, models, spaces as search endpoint.
- ``datasets``: search for datasets
- ``models``: search for models
- ``spaces``: search for spaces
"""
def init(_):
if huggingface_endpoint not in ('datasets', 'models', 'spaces'):
raise SearxEngineAPIException(f"Unsupported Hugging Face endpoint: {huggingface_endpoint}")
def request(query, params):
query_params = {
"direction": -1,
"search": query,
}
params["url"] = f"{base_url}/api/{huggingface_endpoint}?{urlencode(query_params)}"
return params
def response(resp) -> EngineResults:
results = EngineResults()
data = resp.json()
for entry in data:
if huggingface_endpoint != 'models':
url = f"{base_url}/{huggingface_endpoint}/{entry['id']}"
else:
url = f"{base_url}/{entry['id']}"
published_date = None
try:
published_date = datetime.strptime(entry["createdAt"], "%Y-%m-%dT%H:%M:%S.%fZ")
except (ValueError, TypeError):
pass
contents = []
if entry.get("likes"):
contents.append(f"Likes: {entry['likes']}")
if entry.get("downloads"):
contents.append(f"Downloads: {entry['downloads']:,}")
if entry.get("tags"):
contents.append(f"Tags: {', '.join(entry['tags'])}")
if entry.get("description"):
contents.append(f"Description: {entry['description']}")
item = MainResult(
title=entry["id"],
content=html_to_text(" | ".join(contents)),
url=url,
publishedDate=published_date,
)
results.add(item)
return results

View File

@@ -0,0 +1,68 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Engine for Microsoft Learn, Microsoft's technical knowledge base.
To use this engine add the following entry to your engines list
in ``settings.yml``:
.. code:: yaml
- name: microsoft learn
engine: microsoft_learn
shortcut: msl
disabled: false
"""
from urllib.parse import urlencode
from searx.result_types import EngineResults
engine_type = "online"
language_support = True
categories = ["it"]
paging = True
page_size = 10
time_range_support = False
search_api = "https://learn.microsoft.com/api/search?"
about = {
"website": "https://learn.microsoft.com",
"wikidata_id": "Q123663245",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
def request(query, params):
if params['language'] == 'all':
params['language'] = 'en-us'
query_params = [
("search", query),
("locale", params["language"]),
("scoringprofile", "semantic-answers"),
("facet", "category"),
("facet", "products"),
("facet", "tags"),
("$top", "10"),
("$skip", (params["pageno"] - 1) * page_size),
("expandScope", "true"),
("includeQuestion", "false"),
("applyOperator", "false"),
("partnerId", "LearnSite"),
]
params["url"] = search_api + urlencode(query_params)
return params
def response(resp) -> EngineResults:
res = EngineResults()
json_data = resp.json()
for result in json_data["results"]:
res.add(res.types.MainResult(url=result["url"], title=result["title"], content=result.get("description", "")))
return res

View File

@@ -1,46 +1,61 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Mullvad Leta is a search engine proxy. Currently Leta only offers text
"""This is the implementation of the Mullvad-Leta meta-search engine. 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
This engine **REQUIRES** that searxng operate within a Mullvad VPN search engine you wish to use, see (:py:obj:`leta_engine`).
If using docker, consider using gluetun for easily connecting to the Mullvad
- https://github.com/qdm12/gluetun
Otherwise, follow instructions provided by Mullvad for enabling the VPN on Linux
- https://mullvad.net/en/help/install-mullvad-app-linux
.. hint:: .. hint::
The :py:obj:`EngineTraits` is empty by default. Maintainers have to run Leta caches each search for up to 30 days. For example, if you use search
``make data.traits`` (in the Mullvad VPN / :py:obj:`fetch_traits`) and rebase terms like ``news``, contrary to your intention you'll get very old results!
the modified JSON file ``searx/data/engine_traits.json`` on every single
update of SearXNG!
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
===============
""" """
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING import typing
from urllib.parse import urlencode
import babel
from httpx import Response from httpx import Response
from lxml import html from lxml import html
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.locales import region_tag, get_official_locales from searx.locales import get_official_locales, language_tag, region_tag
from searx.utils import eval_xpath, extract_text, eval_xpath_list from searx.utils import eval_xpath_list
from searx.exceptions import SearxEngineResponseException from searx.result_types import EngineResults, MainResult
if TYPE_CHECKING: if typing.TYPE_CHECKING:
import logging import logging
logger = logging.getLogger() logger = logging.getLogger()
traits: EngineTraits traits: EngineTraits
use_cache: bool = True # non-cache use only has 100 searches per day!
leta_engine: str = 'google'
search_url = "https://leta.mullvad.net" search_url = "https://leta.mullvad.net"
# about # about
@@ -54,154 +69,205 @@ about = {
} }
# engine dependent config # engine dependent config
categories = ['general', 'web'] categories = ["general", "web"]
paging = True paging = True
max_page = 50 max_page = 10
time_range_support = True time_range_support = True
time_range_dict = { time_range_dict = {
"day": "d1", "day": "d",
"week": "w1", "week": "w",
"month": "m1", "month": "m",
"year": "y1", "year": "y",
} }
available_leta_engines = [ LetaEnginesType = typing.Literal["google", "brave"]
'google', # first will be default if provided engine is invalid """Engine types supported by mullvadleta."""
'brave',
] leta_engine: LetaEnginesType = "google"
"""Select Leta's engine type from :py:obj:`LetaEnginesType`."""
def is_vpn_connected(dom: html.HtmlElement) -> bool: def init(_):
"""Returns true if the VPN is connected, False otherwise""" l = typing.get_args(LetaEnginesType)
connected_text = extract_text(eval_xpath(dom, '//main/div/p[1]')) if leta_engine not in l:
return connected_text != 'You are not connected to Mullvad VPN.' raise ValueError(f"leta_engine '{leta_engine}' is invalid, use one of {', '.join(l)}")
def assign_headers(headers: dict) -> dict: class DataNodeQueryMetaDataIndices(typing.TypedDict):
"""Assigns the headers to make a request to Mullvad Leta""" """Indices into query metadata."""
headers['Accept'] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
headers['Content-Type'] = "application/x-www-form-urlencoded" success: int
headers['Host'] = "leta.mullvad.net" q: int # pylint: disable=invalid-name
headers['Origin'] = "https://leta.mullvad.net" country: int
return headers 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(typing.TypedDict):
"""Indices into query resultsdata."""
link: int
snippet: int
title: int
favicon: int
def request(query: str, params: dict): def request(query: str, params: dict):
country = traits.get_region(params.get('searxng_locale', 'all'), traits.all_locale) # type: ignore params["method"] = "GET"
args = {
result_engine = leta_engine
if leta_engine not in available_leta_engines:
result_engine = available_leta_engines[0]
logger.warning(
'Configured engine "%s" not one of the available engines %s, defaulting to "%s"',
leta_engine,
available_leta_engines,
result_engine,
)
params['url'] = search_url
params['method'] = 'POST'
params['data'] = {
"q": query, "q": query,
"gl": country if country is str else '', "engine": leta_engine,
'engine': result_engine, "x-sveltekit-invalidated": "001", # hardcoded from all requests seen
} }
# pylint: disable=undefined-variable
if use_cache:
params['data']['oc'] = "on"
# pylint: enable=undefined-variable
if params['time_range'] in time_range_dict: country = traits.get_region(params.get("searxng_locale"), traits.all_locale) # type: ignore
params['dateRestrict'] = time_range_dict[params['time_range']] if country:
else: args["country"] = country
params['dateRestrict'] = ''
if params['pageno'] > 1: language = traits.get_language(params.get("searxng_locale"), traits.all_locale) # type: ignore
# Page 1 is n/a, Page 2 is 11, page 3 is 21, ... if language:
params['data']['start'] = ''.join([str(params['pageno'] - 1), "1"]) args["language"] = language
if params['headers'] is None: if params["time_range"] in time_range_dict:
params['headers'] = {} 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)}"
assign_headers(params['headers'])
return params return params
def extract_result(dom_result: list[html.HtmlElement]): def response(resp: Response) -> EngineResults:
# Infoboxes sometimes appear in the beginning and will have a length of 0 json_response = resp.json()
if len(dom_result) == 3:
[a_elem, h3_elem, p_elem] = dom_result
elif len(dom_result) == 4:
[_, a_elem, h3_elem, p_elem] = dom_result
else:
return None
return { nodes = json_response["nodes"]
'url': extract_text(a_elem.text), # 0: is None
'title': extract_text(h3_elem), # 1: has "connected=True", not useful
'content': extract_text(p_elem), # 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 extract_results(search_results: html.HtmlElement): def fetch_traits(engine_traits: EngineTraits) -> None:
for search_result in search_results: """Fetch languages and regions from Mullvad-Leta"""
dom_result = eval_xpath_list(search_result, 'div/div/*')
result = extract_result(dom_result)
if result is not None:
yield result
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]
def response(resp: Response):
"""Checks if connected to Mullvad VPN, then extracts the search results from
the DOM resp: requests response object"""
dom = html.fromstring(resp.text)
if not is_vpn_connected(dom):
raise SearxEngineResponseException('Not connected to Mullvad VPN')
search_results = eval_xpath(dom.body, '//main/div[2]/div')
return list(extract_results(search_results))
def fetch_traits(engine_traits: EngineTraits):
"""Fetch languages and regions from Mullvad-Leta
.. warning::
Fetching the engine traits also requires a Mullvad VPN connection. If
not connected, then an error message will print and no traits will be
updated.
"""
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
# see https://github.com/searxng/searxng/issues/762 # see https://github.com/searxng/searxng/issues/762
from searx.network import post as http_post from searx.network import get as http_get
# pylint: enable=import-outside-toplevel # pylint: enable=import-outside-toplevel
resp = http_post(search_url, headers=assign_headers({}))
resp = http_get(f"{search_url}/documentation")
if not isinstance(resp, Response): if not isinstance(resp, Response):
print("ERROR: failed to get response from mullvad-leta. Are you connected to the VPN?") print("ERROR: failed to get response from mullvad-leta. Are you connected to the VPN?")
return return
if not resp.ok: if not resp.ok:
print("ERROR: response from mullvad-leta is not OK. Are you connected to the VPN?") print("ERROR: response from mullvad-leta is not OK. Are you connected to the VPN?")
return return
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
if not is_vpn_connected(dom):
print('ERROR: Not connected to Mullvad VPN')
return
# supported region codes
options = eval_xpath_list(dom.body, '//main/div/form/div[2]/div/select[1]/option')
if options is None or len(options) <= 0:
print('ERROR: could not find any results. Are you connected to the VPN?')
for x in options:
eng_country = x.get("value")
sxng_locales = get_official_locales(eng_country, engine_traits.languages.keys(), regional=True) # 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?")
if not sxng_locales: language_table = tables[3]
print( lang_map = {
"ERROR: can't map from Mullvad-Leta country %s (%s) to a babel region." "zh-hant": "zh_Hans",
% (x.get('data-name'), eng_country) "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 continue
for sxng_locale in sxng_locales: sxng_tag = language_tag(locale)
engine_traits.regions[region_tag(sxng_locale)] = eng_country 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

@@ -40,7 +40,10 @@ about = {
} }
base_url = 'https://oqi2j6v4iz-dsn.algolia.net' base_url = 'https://oqi2j6v4iz-dsn.algolia.net'
pdia_config_url = 'https://pdimagearchive.org/_astro/config.BiNvrvzG.js' pdia_base_url = 'https://pdimagearchive.org'
pdia_search_url = pdia_base_url + '/search/?q='
pdia_config_start = "/_astro/InfiniteSearch."
pdia_config_end = ".js"
categories = ['images'] categories = ['images']
page_size = 20 page_size = 20
paging = True paging = True
@@ -62,11 +65,17 @@ def _get_algolia_api_key():
if __CACHED_API_KEY: if __CACHED_API_KEY:
return __CACHED_API_KEY return __CACHED_API_KEY
resp = get(pdia_search_url)
if resp.status_code != 200:
raise LookupError("Failed to fetch config location (and as such the API key) for PDImageArchive")
pdia_config_filepart = extr(resp.text, pdia_config_start, pdia_config_end)
pdia_config_url = pdia_base_url + pdia_config_start + pdia_config_filepart + pdia_config_end
resp = get(pdia_config_url) resp = get(pdia_config_url)
if resp.status_code != 200: if resp.status_code != 200:
raise LookupError("Failed to obtain Algolia API key for PDImageArchive") raise LookupError("Failed to obtain Algolia API key for PDImageArchive")
api_key = extr(resp.text, 'r="', '"', default=None) api_key = extr(resp.text, 'const r="', '"', default=None)
if api_key is None: if api_key is None:
raise LookupError("Couldn't obtain Algolia API key for PDImageArchive") raise LookupError("Couldn't obtain Algolia API key for PDImageArchive")

View File

@@ -11,7 +11,7 @@ from searx.exceptions import SearxEngineAPIException, SearxEngineCaptchaExceptio
# Metadata # Metadata
about = { about = {
"website": "https://m.quark.cn/", "website": "https://quark.sm.cn/",
"wikidata_id": "Q48816502", "wikidata_id": "Q48816502",
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
@@ -53,7 +53,7 @@ def request(query, params):
category_config = { category_config = {
'general': { 'general': {
'endpoint': 'https://m.quark.cn/s', 'endpoint': 'https://quark.sm.cn/s',
'params': { 'params': {
"q": query, "q": query,
"layout": "html", "layout": "html",

View File

@@ -5,7 +5,9 @@
https://de1.api.radio-browser.info/#Advanced_station_search https://de1.api.radio-browser.info/#Advanced_station_search
""" """
from __future__ import annotations
import typing
import random import random
import socket import socket
from urllib.parse import urlencode from urllib.parse import urlencode
@@ -13,9 +15,15 @@ import babel
from flask_babel import gettext from flask_babel import gettext
from searx.network import get from searx.network import get
from searx.enginelib import EngineCache
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.locales import language_tag from searx.locales import language_tag
if typing.TYPE_CHECKING:
import logging
logger = logging.getLogger()
traits: EngineTraits traits: EngineTraits
about = { about = {
@@ -52,11 +60,24 @@ none filters are applied. Valid filters are:
""" """
servers = [] CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
seconds."""
def init(_): def init(_):
# see https://api.radio-browser.info global CACHE # pylint: disable=global-statement
CACHE = EngineCache("radio_browser")
server_list()
def server_list() -> list[str]:
servers = CACHE.get("servers", [])
if servers:
return servers
# hint: can take up to 40sec!
ips = socket.getaddrinfo("all.api.radio-browser.info", 80, 0, 0, socket.IPPROTO_TCP) ips = socket.getaddrinfo("all.api.radio-browser.info", 80, 0, 0, socket.IPPROTO_TCP)
for ip_tuple in ips: for ip_tuple in ips:
_ip: str = ip_tuple[4][0] # type: ignore _ip: str = ip_tuple[4][0] # type: ignore
@@ -65,8 +86,22 @@ def init(_):
if srv not in servers: if srv not in servers:
servers.append(srv) servers.append(srv)
# update server list once in 24h
CACHE.set(key="servers", value=servers, expire=60 * 60 * 24)
return servers
def request(query, params): def request(query, params):
servers = server_list()
if not servers:
logger.error("Fetched server list is empty!")
params["url"] = None
return
server = random.choice(servers)
args = { args = {
'name': query, 'name': query,
'order': 'votes', 'order': 'votes',
@@ -87,8 +122,7 @@ def request(query, params):
if countrycode in traits.custom['countrycodes']: # type: ignore if countrycode in traits.custom['countrycodes']: # type: ignore
args['countrycode'] = countrycode args['countrycode'] = countrycode
params['url'] = f"{random.choice(servers)}/json/stations/search?{urlencode(args)}" params['url'] = f"{server}/json/stations/search?{urlencode(args)}"
return params
def response(resp): def response(resp):
@@ -154,8 +188,9 @@ def fetch_traits(engine_traits: EngineTraits):
babel_reg_list = get_global("territory_languages").keys() babel_reg_list = get_global("territory_languages").keys()
language_list = get(f'{servers[0]}/json/languages').json() # type: ignore server = server_list()[0]
country_list = get(f'{servers[0]}/json/countries').json() # type: ignore language_list = get(f'{server}/json/languages').json() # type: ignore
country_list = get(f'{server}/json/countries').json() # type: ignore
for lang in language_list: for lang in language_list:

View File

@@ -1,11 +1,14 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Semantic Scholar (Science) """Semantic Scholar (Science)"""
"""
from json import dumps, loads from json import dumps
from datetime import datetime from datetime import datetime
from lxml import html
from flask_babel import gettext from flask_babel import gettext
from searx.network import get
from searx.utils import eval_xpath_getindex, gen_useragent, html_to_text
about = { about = {
"website": 'https://www.semanticscholar.org/', "website": 'https://www.semanticscholar.org/',
@@ -19,13 +22,31 @@ about = {
categories = ['science', 'scientific publications'] categories = ['science', 'scientific publications']
paging = True paging = True
search_url = 'https://www.semanticscholar.org/api/1/search' search_url = 'https://www.semanticscholar.org/api/1/search'
paper_url = 'https://www.semanticscholar.org/paper' base_url = 'https://www.semanticscholar.org'
def _get_ui_version():
resp = get(base_url)
if not resp.ok:
raise RuntimeError("Can't determine Semantic Scholar UI version")
doc = html.fromstring(resp.text)
ui_version = eval_xpath_getindex(doc, "//meta[@name='s2-ui-version']/@content", 0)
if not ui_version:
raise RuntimeError("Can't determine Semantic Scholar UI version")
return ui_version
def request(query, params): def request(query, params):
params['url'] = search_url params['url'] = search_url
params['method'] = 'POST' params['method'] = 'POST'
params['headers']['content-type'] = 'application/json' params['headers'] = {
'Content-Type': 'application/json',
'X-S2-UI-Version': _get_ui_version(),
'X-S2-Client': "webapp-browser",
'User-Agent': gen_useragent(),
}
params['data'] = dumps( params['data'] = dumps(
{ {
"queryString": query, "queryString": query,
@@ -43,7 +64,8 @@ def request(query, params):
def response(resp): def response(resp):
res = loads(resp.text) res = resp.json()
results = [] results = []
for result in res['results']: for result in res['results']:
url = result.get('primaryPaperLink', {}).get('url') url = result.get('primaryPaperLink', {}).get('url')
@@ -54,7 +76,7 @@ def response(resp):
if alternatePaperLinks: if alternatePaperLinks:
url = alternatePaperLinks[0].get('url') url = alternatePaperLinks[0].get('url')
if not url: if not url:
url = paper_url + '/%s' % result['id'] url = base_url + '/paper/%s' % result['id']
# publishedDate # publishedDate
if 'pubDate' in result: if 'pubDate' in result:
@@ -88,7 +110,7 @@ def response(resp):
'template': 'paper.html', 'template': 'paper.html',
'url': url, 'url': url,
'title': result['title']['text'], 'title': result['title']['text'],
'content': result['paperAbstract']['text'], 'content': html_to_text(result['paperAbstract']['text']),
'journal': result.get('venue', {}).get('text') or result.get('journal', {}).get('name'), 'journal': result.get('venue', {}).get('text') or result.get('journal', {}).get('name'),
'doi': result.get('doiInfo', {}).get('doi'), 'doi': result.get('doiInfo', {}).get('doi'),
'tags': result.get('fieldsOfStudy'), 'tags': result.get('fieldsOfStudy'),

View File

@@ -0,0 +1,151 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""SensCritique (movies)
"""
from __future__ import annotations
from json import dumps, loads
from typing import Any, Optional
from searx.result_types import EngineResults, MainResult
about = {
"website": 'https://www.senscritique.com/',
"wikidata_id": 'Q16676060',
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": 'JSON',
'language': 'fr',
}
categories = ['movies']
paging = True
page_size = 16
graphql_url = 'https://apollo.senscritique.com/'
graphql_query = """query SearchProductExplorer($query: String, $offset: Int, $limit: Int,
$sortBy: SearchProductExplorerSort) {
searchProductExplorer(
query: $query
filters: []
sortBy: $sortBy
offset: $offset
limit: $limit
) {
items {
category
dateRelease
duration
id
originalTitle
rating
title
url
yearOfProduction
medias {
picture
}
countries {
name
}
genresInfos {
label
}
directors {
name
}
stats {
ratingCount
}
}
}
}"""
def request(query: str, params: dict[str, Any]) -> dict[str, Any]:
offset = (params['pageno'] - 1) * page_size
data = {
"operationName": "SearchProductExplorer",
"variables": {"offset": offset, "limit": page_size, "query": query, "sortBy": "RELEVANCE"},
"query": graphql_query,
}
params['url'] = graphql_url
params['method'] = 'POST'
params['headers']['Content-Type'] = 'application/json'
params['data'] = dumps(data)
return params
def response(resp) -> EngineResults:
res = EngineResults()
response_data = loads(resp.text)
items = response_data.get('data', {}).get('searchProductExplorer', {}).get('items', [])
if not items:
return res
for item in items:
result = parse_item(item)
if not result:
continue
res.add(result=result)
return res
def parse_item(item: dict[str, Any]) -> MainResult | None:
"""Parse a single item from the SensCritique API response"""
title = item.get('title', '')
if not title:
return None
year = item.get('yearOfProduction')
original_title = item.get('originalTitle')
thumbnail: str = ""
if item.get('medias', {}) and item['medias'].get('picture'):
thumbnail = item['medias']['picture']
content_parts = build_content_parts(item, title, original_title)
url = f"https://www.senscritique.com{item['url']}"
return MainResult(
url=url,
title=title + (f' ({year})' if year else ''),
content=' | '.join(content_parts),
thumbnail=thumbnail,
)
def build_content_parts(item: dict[str, Any], title: str, original_title: Optional[str]) -> list[str]:
"""Build the content parts for an item"""
content_parts = []
if item.get('category'):
content_parts.append(item['category'])
if original_title and original_title != title:
content_parts.append(f"Original title: {original_title}")
if item.get('directors'):
directors = [director['name'] for director in item['directors']]
content_parts.append(f"Director(s): {', '.join(directors)}")
if item.get('countries'):
countries = [country['name'] for country in item['countries']]
content_parts.append(f"Country: {', '.join(countries)}")
if item.get('genresInfos'):
genres = [genre['label'] for genre in item['genresInfos']]
content_parts.append(f"Genre(s): {', '.join(genres)}")
if item.get('duration'):
minutes = item['duration'] // 60
if minutes > 0:
content_parts.append(f"Duration: {minutes} min")
if item.get('rating') and item.get('stats', {}).get('ratingCount'):
content_parts.append(f"Rating: {item['rating']}/10 ({item['stats']['ratingCount']} votes)")
return content_parts

View File

@@ -1,17 +1,26 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""SoundCloud is a German audio streaming service.""" """SoundCloud is a German audio streaming service."""
from __future__ import annotations
import re import re
from urllib.parse import quote_plus, urlencode import typing
import datetime import datetime
from urllib.parse import quote_plus, urlencode
from dateutil import parser from dateutil import parser
from lxml import html from lxml import html
from searx.network import get as http_get from searx.network import get as http_get
from searx.enginelib import EngineCache
if typing.TYPE_CHECKING:
import logging
logger: logging.Logger
about = { about = {
"website": "ttps://soundcloud.com", "website": "https://soundcloud.com",
"wikidata_id": "Q568769", "wikidata_id": "Q568769",
"official_api_documentation": "https://developers.soundcloud.com/docs/api/guide", "official_api_documentation": "https://developers.soundcloud.com/docs/api/guide",
"use_official_api": False, "use_official_api": False,
@@ -28,7 +37,6 @@ HTML frontend of the common WEB site.
""" """
cid_re = re.compile(r'client_id:"([^"]*)"', re.I | re.U) cid_re = re.compile(r'client_id:"([^"]*)"', re.I | re.U)
guest_client_id = ""
results_per_page = 10 results_per_page = 10
soundcloud_facet = "model" soundcloud_facet = "model"
@@ -48,6 +56,10 @@ app_locale_map = {
"sv": "sv", "sv": "sv",
} }
CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
seconds."""
def request(query, params): def request(query, params):
@@ -55,6 +67,12 @@ def request(query, params):
# - user_id=451561-497874-703312-310156 # - user_id=451561-497874-703312-310156
# - app_version=1740727428 # - app_version=1740727428
guest_client_id = CACHE.get("guest_client_id")
if guest_client_id is None:
guest_client_id = get_client_id()
if guest_client_id:
CACHE.set(key="guest_client_id", value=guest_client_id)
args = { args = {
"q": query, "q": query,
"offset": (params['pageno'] - 1) * results_per_page, "offset": (params['pageno'] - 1) * results_per_page,
@@ -104,12 +122,12 @@ def response(resp):
return results return results
def init(engine_settings=None): # pylint: disable=unused-argument def init(engine_settings): # pylint: disable=unused-argument
global guest_client_id # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
guest_client_id = get_client_id() CACHE = EngineCache(engine_settings["name"]) # type:ignore
def get_client_id() -> str: def get_client_id() -> str | None:
client_id = "" client_id = ""
url = "https://soundcloud.com" url = "https://soundcloud.com"
@@ -143,4 +161,4 @@ def get_client_id() -> str:
logger.info("using client_id '%s' for soundclud queries", client_id) logger.info("using client_id '%s' for soundclud queries", client_id)
else: else:
logger.warning("missing valid client_id for soundclud queries") logger.warning("missing valid client_id for soundclud queries")
return client_id return client_id or None

View File

@@ -84,7 +84,6 @@ from typing import TYPE_CHECKING, Any
from collections import OrderedDict from collections import OrderedDict
import re import re
from unicodedata import normalize, combining from unicodedata import normalize, combining
from time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from json import loads from json import loads
@@ -97,6 +96,7 @@ from searx.network import get # see https://github.com/searxng/searxng/issues/7
from searx.exceptions import SearxEngineCaptchaException from searx.exceptions import SearxEngineCaptchaException
from searx.locales import region_tag from searx.locales import region_tag
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.enginelib import EngineCache
if TYPE_CHECKING: if TYPE_CHECKING:
import logging import logging
@@ -159,10 +159,21 @@ search_form_xpath = '//form[@id="search"]'
</form> </form>
""" """
# timestamp of the last fetch of 'sc' code
sc_code_ts = 0 CACHE: EngineCache
sc_code = '' """Persistent (SQLite) key/value cache that deletes its values after ``expire``
sc_code_cache_sec = 30 seconds."""
def init(_):
global CACHE # pylint: disable=global-statement
# hint: all three startpage engines (WEB, Images & News) can/should use the
# same sc_code ..
CACHE = EngineCache("startpage") # type:ignore
sc_code_cache_sec = 3600
"""Time in seconds the sc-code is cached in memory :py:obj:`get_sc_code`.""" """Time in seconds the sc-code is cached in memory :py:obj:`get_sc_code`."""
@@ -176,14 +187,10 @@ def get_sc_code(searxng_locale, params):
Startpage's search form generates a new sc-code on each request. This Startpage's search form generates a new sc-code on each request. This
function scrap a new sc-code from Startpage's home page every function scrap a new sc-code from Startpage's home page every
:py:obj:`sc_code_cache_sec` seconds. :py:obj:`sc_code_cache_sec` seconds."""
""" sc_code = CACHE.get("SC_CODE", "")
if sc_code:
global sc_code_ts, sc_code # pylint: disable=global-statement
if sc_code and (time() < (sc_code_ts + sc_code_cache_sec)):
logger.debug("get_sc_code: reuse '%s'", sc_code)
return sc_code return sc_code
headers = {**params['headers']} headers = {**params['headers']}
@@ -233,8 +240,9 @@ def get_sc_code(searxng_locale, params):
message="get_sc_code: [PR-695] query new sc time-stamp failed! (%s)" % resp.url, # type: ignore message="get_sc_code: [PR-695] query new sc time-stamp failed! (%s)" % resp.url, # type: ignore
) from exc ) from exc
sc_code_ts = time() sc_code = str(sc_code)
logger.debug("get_sc_code: new value is: %s", sc_code) logger.debug("get_sc_code: new value is: %s", sc_code)
CACHE.set(key="SC_CODE", value=sc_code, expire=sc_code_cache_sec)
return sc_code return sc_code

51
searx/engines/steam.py Normal file
View File

@@ -0,0 +1,51 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Steam (store) for SearXNG."""
from urllib.parse import urlencode
from searx.utils import html_to_text
from searx.result_types import EngineResults, MainResult
about = {
"website": 'https://store.steampowered.com/',
"wikidata_id": 'Q337535',
"use_official_api": False,
"require_api_key": False,
"results": 'JSON',
}
categories = []
base_url = "https://store.steampowered.com"
def request(query, params):
query_params = {"term": query, "cc": "us", "l": "en"}
params['url'] = f'{base_url}/api/storesearch/?{urlencode(query_params)}'
return params
def response(resp) -> EngineResults:
results = EngineResults()
search_results = resp.json()
for item in search_results.get('items', []):
app_id = item.get('id')
currency = item.get('price', {}).get('currency', 'USD')
price = item.get('price', {}).get('final', 0) / 100
platforms = ', '.join([platform for platform, supported in item.get('platforms', {}).items() if supported])
content = [f'Price: {price:.2f} {currency}', f'Platforms: {platforms}']
results.add(
MainResult(
title=item.get('name'),
content=html_to_text(' | '.join(content)),
url=f'{base_url}/app/{app_id}',
thumbnail=item.get('tiny_image', ''),
)
)
return results

View File

@@ -5,7 +5,7 @@
from urllib.parse import urlencode from urllib.parse import urlencode
from lxml import etree import lxml.etree
# about # about
about = { about = {
@@ -72,7 +72,7 @@ def replace_pua_chars(text):
def response(resp): def response(resp):
results = [] results = []
search_results = etree.XML(resp.content) search_results = lxml.etree.XML(resp.content)
# return empty array if there are no results # return empty array if there are no results
if search_results.xpath(failure_xpath): if search_results.xpath(failure_xpath):

View File

@@ -3,11 +3,13 @@
Wolfram|Alpha (Science) Wolfram|Alpha (Science)
""" """
from __future__ import annotations
from json import loads from json import loads
from time import time
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.network import get as http_get from searx.network import get as http_get
from searx.enginelib import EngineCache
# about # about
about = { about = {
@@ -40,41 +42,39 @@ search_url = (
referer_url = url + 'input/?{query}' referer_url = url + 'input/?{query}'
token = {'value': '', 'last_updated': None}
# pods to display as image in infobox # pods to display as image in infobox
# this pods do return a plaintext, but they look better and are more useful as images # this pods do return a plaintext, but they look better and are more useful as images
image_pods = {'VisualRepresentation', 'Illustration', 'Symbol'} image_pods = {'VisualRepresentation', 'Illustration', 'Symbol'}
# seems, wolframalpha resets its token in every hour CACHE: EngineCache
def obtain_token(): """Persistent (SQLite) key/value cache that deletes its values after ``expire``
update_time = time() - (time() % 3600) seconds."""
try:
token_response = http_get('https://www.wolframalpha.com/input/api/v1/code?ts=9999999999999999999', timeout=2.0)
token['value'] = loads(token_response.text)['code'] def init(engine_settings):
token['last_updated'] = update_time global CACHE # pylint: disable=global-statement
except: # pylint: disable=bare-except CACHE = EngineCache(engine_settings["name"]) # type:ignore
pass
def obtain_token() -> str:
token = CACHE.get(key="token")
if token is None:
resp = http_get('https://www.wolframalpha.com/input/api/v1/code?ts=9999999999999999999', timeout=2.0)
token = resp.json()["code"]
# seems, wolframalpha resets its token in every hour
CACHE.set(key="code", value=token, expire=3600)
return token return token
def init(engine_settings=None): # pylint: disable=unused-argument
obtain_token()
# do search-request
def request(query, params): def request(query, params):
# obtain token if last update was more than an hour token = obtain_token()
if time() - (token['last_updated'] or 0) > 3600: params['url'] = search_url.format(query=urlencode({'input': query}), token=token)
obtain_token()
params['url'] = search_url.format(query=urlencode({'input': query}), token=token['value'])
params['headers']['Referer'] = referer_url.format(query=urlencode({'i': query})) params['headers']['Referer'] = referer_url.format(query=urlencode({'i': query}))
return params return params
# get response from search-request
def response(resp): def response(resp):
results = [] results = []

View File

@@ -63,21 +63,52 @@ lang2domain = {
} }
"""Map language to domain""" """Map language to domain"""
locale_aliases = { yahoo_languages = {
'zh': 'zh_Hans', "all": "any",
'zh-HK': 'zh_Hans', "ar": "ar",
'zh-CN': 'zh_Hans', # dead since 2015 / routed to hk.search.yahoo.com "bg": "bg",
'zh-TW': 'zh_Hant', "cs": "cs",
"da": "da",
"de": "de",
"el": "el",
"en": "en",
"es": "es",
"et": "et",
"fi": "fi",
"fr": "fr",
"he": "he",
"hr": "hr",
"hu": "hu",
"it": "it",
"ja": "ja",
"ko": "ko",
"lt": "lt",
"lv": "lv",
"nl": "nl",
"no": "no",
"pl": "pl",
"pt": "pt",
"ro": "ro",
"ru": "ru",
"sk": "sk",
"sl": "sl",
"sv": "sv",
"th": "th",
"tr": "tr",
"zh": "zh_chs",
"zh_Hans": "zh_chs",
'zh-CN': "zh_chs",
"zh_Hant": "zh_cht",
"zh-HK": "zh_cht",
'zh-TW': "zh_cht",
} }
def request(query, params): def request(query, params):
"""build request""" """build request"""
lang = locale_aliases.get(params['language'], None) lang = params["language"].split("-")[0]
if not lang: lang = yahoo_languages.get(lang, "any")
lang = params['language'].split('-')[0]
lang = traits.get_language(lang, traits.all_locale)
offset = (params['pageno'] - 1) * 7 + 1 offset = (params['pageno'] - 1) * 7 + 1
age, btf = time_range_dict.get(params['time_range'], ('', '')) age, btf = time_range_dict.get(params['time_range'], ('', ''))
@@ -154,39 +185,3 @@ def response(resp):
results.append({'suggestion': extract_text(suggestion)}) results.append({'suggestion': extract_text(suggestion)})
return results return results
def fetch_traits(engine_traits: EngineTraits):
"""Fetch languages from yahoo"""
# pylint: disable=import-outside-toplevel
import babel
from searx import network
from searx.locales import language_tag
engine_traits.all_locale = 'any'
resp = network.get('https://search.yahoo.com/preferences/languages')
if not resp.ok:
print("ERROR: response from yahoo is not OK.")
dom = html.fromstring(resp.text)
offset = len('lang_')
eng2sxng = {'zh_chs': 'zh_Hans', 'zh_cht': 'zh_Hant'}
for val in eval_xpath_list(dom, '//div[contains(@class, "lang-item")]/input/@value'):
eng_tag = val[offset:]
try:
sxng_tag = language_tag(babel.Locale.parse(eng2sxng.get(eng_tag, eng_tag)))
except babel.UnknownLocaleError:
print('ERROR: unknown language --> %s' % eng_tag)
continue
conflict = engine_traits.languages.get(sxng_tag)
if conflict:
if conflict != eng_tag:
print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, eng_tag))
continue
engine_traits.languages[sxng_tag] = eng_tag

View File

@@ -1,5 +1,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementations for providing the favicons in SearXNG""" """Implementations for providing the favicons in SearXNG.
There is a command line for developer purposes and for deeper analysis. Here is
an example in which the command line is called in the development environment::
$ ./manage pyenv.cmd bash --norc --noprofile
(py3) python -m searx.favicons --help
"""
from __future__ import annotations from __future__ import annotations

View File

@@ -236,6 +236,12 @@ class FaviconCacheSQLite(sqlitedb.SQLiteAppl, FaviconCache):
model in the SQLite DB is implemented using the abstract class model in the SQLite DB is implemented using the abstract class
:py:obj:`sqlitedb.SQLiteAppl`. :py:obj:`sqlitedb.SQLiteAppl`.
For introspection of the DB, jump into developer environment and run command
to show cache state::
$ ./manage pyenv.cmd bash --norc --noprofile
(py3) python -m searx.favicons cache state
The following configurations are required / supported: The following configurations are required / supported:
- :py:obj:`FaviconCacheConfig.db_url` - :py:obj:`FaviconCacheConfig.db_url`
@@ -357,6 +363,10 @@ CREATE TABLE IF NOT EXISTS blob_map (
if sha256 != FALLBACK_ICON: if sha256 != FALLBACK_ICON:
conn.execute(self.SQL_INSERT_BLOBS, (sha256, bytes_c, mime, data)) conn.execute(self.SQL_INSERT_BLOBS, (sha256, bytes_c, mime, data))
conn.execute(self.SQL_INSERT_BLOB_MAP, (sha256, resolver, authority)) conn.execute(self.SQL_INSERT_BLOB_MAP, (sha256, resolver, authority))
# hint: the with context of the connection object closes the transaction
# but not the DB connection. The connection has to be closed by the
# caller of self.connect()!
conn.close()
return True return True
@@ -376,7 +386,8 @@ CREATE TABLE IF NOT EXISTS blob_map (
return return
self.properties.set("LAST_MAINTENANCE", "") # hint: this (also) sets the m_time of the property! self.properties.set("LAST_MAINTENANCE", "") # hint: this (also) sets the m_time of the property!
# do maintenance tasks # Do maintenance tasks. This can be take a little more time, to avoid
# DB locks, etablish a new DB connecton.
with self.connect() as conn: with self.connect() as conn:
@@ -407,6 +418,12 @@ CREATE TABLE IF NOT EXISTS blob_map (
conn.execute("DELETE FROM blob_map WHERE sha256 IN ('%s')" % "','".join(sha_list)) conn.execute("DELETE FROM blob_map WHERE sha256 IN ('%s')" % "','".join(sha_list))
logger.debug("dropped %s blobs with total size of %s bytes", len(sha_list), c) logger.debug("dropped %s blobs with total size of %s bytes", len(sha_list), c)
# Vacuuming the WALs
# https://www.theunterminatedstring.com/sqlite-vacuuming/
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
conn.close()
def _query_val(self, sql, default=None): def _query_val(self, sql, default=None):
val = self.DB.execute(sql).fetchone() val = self.DB.execute(sql).fetchone()
if val is not None: if val is not None:

View File

@@ -112,6 +112,7 @@ from searx.botdetection import (
http_accept_encoding, http_accept_encoding,
http_accept_language, http_accept_language,
http_user_agent, http_user_agent,
http_sec_fetch,
ip_limit, ip_limit,
ip_lists, ip_lists,
get_network, get_network,
@@ -179,16 +180,17 @@ def filter_request(request: SXNG_Request) -> werkzeug.Response | None:
logger.error("BLOCK %s: matched BLOCKLIST - %s", network.compressed, msg) logger.error("BLOCK %s: matched BLOCKLIST - %s", network.compressed, msg)
return flask.make_response(('IP is on BLOCKLIST - %s' % msg, 429)) return flask.make_response(('IP is on BLOCKLIST - %s' % msg, 429))
# methods applied on / # methods applied on all requests
for func in [ for func in [
http_user_agent, http_user_agent,
]: ]:
val = func.filter_request(network, request, cfg) val = func.filter_request(network, request, cfg)
if val is not None: if val is not None:
logger.debug(f"NOT OK ({func.__name__}): {network}: %s", dump_request(sxng_request))
return val return val
# methods applied on /search # methods applied on /search requests
if request.path == '/search': if request.path == '/search':
@@ -197,11 +199,14 @@ def filter_request(request: SXNG_Request) -> werkzeug.Response | None:
http_accept_encoding, http_accept_encoding,
http_accept_language, http_accept_language,
http_user_agent, http_user_agent,
http_sec_fetch,
ip_limit, ip_limit,
]: ]:
val = func.filter_request(network, request, cfg) val = func.filter_request(network, request, cfg)
if val is not None: if val is not None:
logger.debug(f"NOT OK ({func.__name__}): {network}: %s", dump_request(sxng_request))
return val return val
logger.debug(f"OK {network}: %s", dump_request(sxng_request)) logger.debug(f"OK {network}: %s", dump_request(sxng_request))
return None return None

View File

@@ -12,7 +12,7 @@ from typing import Dict
import httpx import httpx
from searx import logger, searx_debug from searx import logger, sxng_debug
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
from .client import new_client, get_loop, AsyncHTTPTransportNoHttp from .client import new_client, get_loop, AsyncHTTPTransportNoHttp
from .raise_for_httperror import raise_for_httperror from .raise_for_httperror import raise_for_httperror
@@ -186,7 +186,7 @@ class Network:
local_address = next(self._local_addresses_cycle) local_address = next(self._local_addresses_cycle)
proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key
key = (verify, max_redirects, local_address, proxies) key = (verify, max_redirects, local_address, proxies)
hook_log_response = self.log_response if searx_debug else None hook_log_response = self.log_response if sxng_debug else None
if key not in self._clients or self._clients[key].is_closed: if key not in self._clients or self._clients[key].is_closed:
client = new_client( client = new_client(
self.enable_http, self.enable_http,

View File

@@ -33,7 +33,7 @@ class SXNGPlugin(Plugin):
"""Rewrite hostnames, remove results or prioritize them.""" """Rewrite hostnames, remove results or prioritize them."""
id = "tor_check" id = "tor_check"
keywords = ["tor-check"] keywords = ["tor-check", "tor_check", "torcheck", "tor", "tor check"]
def __init__(self, plg_cfg: "PluginCfg") -> None: def __init__(self, plg_cfg: "PluginCfg") -> None:
super().__init__(plg_cfg) super().__init__(plg_cfg)
@@ -53,7 +53,7 @@ class SXNGPlugin(Plugin):
if search.search_query.pageno > 1: if search.search_query.pageno > 1:
return results return results
if search.search_query.query.lower() == "tor-check": if search.search_query.query.lower() in self.keywords:
# Request the list of tor exit nodes. # Request the list of tor exit nodes.
try: try:

View File

@@ -155,7 +155,7 @@ class ExternalBangParser(QueryPartParser):
return raw_value.startswith('!!') and len(raw_value) > 2 return raw_value.startswith('!!') and len(raw_value) > 2
def __call__(self, raw_value): def __call__(self, raw_value):
value = raw_value[2:] value = raw_value[2:].lower()
found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, []) found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, [])
if self.enable_autocomplete: if self.enable_autocomplete:
self._autocomplete(bang_ac_list) self._autocomplete(bang_ac_list)
@@ -183,7 +183,7 @@ class BangParser(QueryPartParser):
return raw_value[0] == '!' and (len(raw_value) < 2 or raw_value[1] != '!') return raw_value[0] == '!' and (len(raw_value) < 2 or raw_value[1] != '!')
def __call__(self, raw_value): def __call__(self, raw_value):
value = raw_value[1:].replace('-', ' ').replace('_', ' ') value = raw_value[1:].replace('-', ' ').replace('_', ' ').lower()
found = self._parse(value) if len(value) > 0 else False found = self._parse(value) if len(value) > 0 else False
if found and raw_value[0] == '!': if found and raw_value[0] == '!':
self.raw_text_query.specific = True self.raw_text_query.specific = True

View File

@@ -55,8 +55,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
result.parsed_url = result.parsed_url._replace( result.parsed_url = result.parsed_url._replace(
# if the result has no scheme, use http as default # if the result has no scheme, use http as default
scheme=result.parsed_url.scheme or "http", scheme=result.parsed_url.scheme or "http",
# normalize ``example.com/path/`` to ``example.com/path`` path=result.parsed_url.path,
path=result.parsed_url.path.rstrip("/"),
) )
result.url = result.parsed_url.geturl() result.url = result.parsed_url.geturl()
@@ -73,7 +72,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
item["url"] = _url._replace( item["url"] = _url._replace(
scheme=_url.scheme or "http", scheme=_url.scheme or "http",
# netloc=_url.netloc.replace("www.", ""), # netloc=_url.netloc.replace("www.", ""),
path=_url.path.rstrip("/"), path=_url.path,
).geturl() ).geturl()
infobox_id = getattr(result, "id", None) infobox_id = getattr(result, "id", None)
@@ -82,7 +81,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
result.id = _url._replace( result.id = _url._replace(
scheme=_url.scheme or "http", scheme=_url.scheme or "http",
# netloc=_url.netloc.replace("www.", ""), # netloc=_url.netloc.replace("www.", ""),
path=_url.path.rstrip("/"), path=_url.path,
).geturl() ).geturl()
@@ -259,9 +258,6 @@ class Result(msgspec.Struct, kw_only=True):
``parse_url`` from field ``url``. The ``url`` field is initialized ``parse_url`` from field ``url``. The ``url`` field is initialized
with the resulting value in ``parse_url``, if ``url`` and with the resulting value in ``parse_url``, if ``url`` and
``parse_url`` are not equal. ``parse_url`` are not equal.
- ``example.com/path/`` and ``example.com/path`` are equivalent and are
normalized to ``example.com/path``.
""" """
_normalize_url_fields(self) _normalize_url_fields(self)

View File

@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
import redis.exceptions import redis.exceptions
from searx import logger, settings, searx_debug from searx import logger, settings, sxng_debug
from searx.redisdb import client as get_redis_client from searx.redisdb import client as get_redis_client
from searx.exceptions import SearxSettingsException from searx.exceptions import SearxSettingsException
from searx.search.processors import PROCESSORS from searx.search.processors import PROCESSORS
@@ -139,7 +139,7 @@ def initialize():
signal.signal(signal.SIGUSR1, _signal_handler) signal.signal(signal.SIGUSR1, _signal_handler)
# special case when debug is activate # special case when debug is activate
if searx_debug and settings['checker']['off_when_debug']: if sxng_debug and settings['checker']['off_when_debug']:
logger.info('debug mode: checker is disabled') logger.info('debug mode: checker is disabled')
return return

View File

@@ -2,7 +2,7 @@ general:
# Debug mode, only for development. Is overwritten by ${SEARXNG_DEBUG} # Debug mode, only for development. Is overwritten by ${SEARXNG_DEBUG}
debug: false debug: false
# displayed name # displayed name
instance_name: "SearXNG" instance_name: "nekosearch"
# For example: https://example.com/privacy # For example: https://example.com/privacy
privacypolicy_url: false privacypolicy_url: false
# use true to use your own donation page written in searx/info/en/donate.md # use true to use your own donation page written in searx/info/en/donate.md
@@ -32,16 +32,16 @@ brand:
search: search:
# Filter results. 0: None, 1: Moderate, 2: Strict # Filter results. 0: None, 1: Moderate, 2: Strict
safe_search: 0 safe_search: 2
# Existing autocomplete backends: "360search", "baidu", "brave", "dbpedia", "duckduckgo", "google", "yandex", # Existing autocomplete backends: "360search", "baidu", "brave", "dbpedia", "duckduckgo", "google", "yandex",
# "mwmbl", "seznam", "sogou", "stract", "swisscows", "quark", "qwant", "wikipedia" - # "mwmbl", "seznam", "sogou", "stract", "swisscows", "qwant", "wikipedia" -
# leave blank to turn it off by default. # leave blank to turn it off by default.
autocomplete: "" autocomplete: "duckduckgo"
# minimun characters to type before autocompleter starts # minimun characters to type before autocompleter starts
autocomplete_min: 4 autocomplete_min: 4
# backend for the favicon near URL in search results. # backend for the favicon near URL in search results.
# Available resolvers: "allesedv", "duckduckgo", "google", "yandex" - leave blank to turn it off by default. # Available resolvers: "allesedv", "duckduckgo", "google", "yandex" - leave blank to turn it off by default.
favicon_resolver: "" favicon_resolver: "duckduckgo"
# Default search language - leave blank to detect from browser information or # Default search language - leave blank to detect from browser information or
# use codes from 'languages.py' # use codes from 'languages.py'
default_lang: "auto" default_lang: "auto"
@@ -84,7 +84,7 @@ server:
bind_address: "127.0.0.1" bind_address: "127.0.0.1"
# public URL of the instance, to ensure correct inbound links. Is overwritten # public URL of the instance, to ensure correct inbound links. Is overwritten
# by ${SEARXNG_URL}. # by ${SEARXNG_URL}.
base_url: false # "http://example.com/location" base_url: https://nekosear.ch # "http://example.com/location"
# rate limit the number of request on the instance, block some bots. # rate limit the number of request on the instance, block some bots.
# Is overwritten by ${SEARXNG_LIMITER} # Is overwritten by ${SEARXNG_LIMITER}
limiter: false limiter: false
@@ -95,7 +95,7 @@ server:
# If your instance owns a /etc/searxng/settings.yml file, then set the following # If your instance owns a /etc/searxng/settings.yml file, then set the following
# values there. # values there.
secret_key: "ultrasecretkey" # Is overwritten by ${SEARXNG_SECRET} secret_key: "a9b0aafe71f3dbe131d66762f03e27ab02dfa409541184f3d34036926d7dd79e" # Is overwritten by ${SEARXNG_SECRET}
# Proxy image results through SearXNG. Is overwritten by ${SEARXNG_IMAGE_PROXY} # Proxy image results through SearXNG. Is overwritten by ${SEARXNG_IMAGE_PROXY}
image_proxy: false image_proxy: false
# 1.0 and 1.1 are supported # 1.0 and 1.1 are supported
@@ -123,13 +123,13 @@ ui:
templates_path: "" templates_path: ""
# query_in_title: When true, the result page's titles contains the query # query_in_title: When true, the result page's titles contains the query
# it decreases the privacy, since the browser can records the page titles. # it decreases the privacy, since the browser can records the page titles.
query_in_title: false query_in_title: true
# infinite_scroll: When true, automatically loads the next page when scrolling to bottom of the current page. # infinite_scroll: When true, automatically loads the next page when scrolling to bottom of the current page.
infinite_scroll: false infinite_scroll: true
# ui theme # ui theme
default_theme: simple default_theme: simple
# center the results ? # center the results ?
center_alignment: false center_alignment: true
# URL prefix of the internet archive, don't forget trailing slash (if needed). # URL prefix of the internet archive, don't forget trailing slash (if needed).
# cache_url: "https://webcache.googleusercontent.com/search?q=cache:" # cache_url: "https://webcache.googleusercontent.com/search?q=cache:"
# Default interface locale - leave blank to detect from browser information or # Default interface locale - leave blank to detect from browser information or
@@ -139,7 +139,7 @@ ui:
# results_on_new_tab: false # results_on_new_tab: false
theme_args: theme_args:
# style of simple theme: auto, light, dark # style of simple theme: auto, light, dark
simple_style: auto simple_style: dark
# Perform search immediately if a category selected. # Perform search immediately if a category selected.
# Disable to select multiple categories at once and start the search manually. # Disable to select multiple categories at once and start the search manually.
search_on_category_select: true search_on_category_select: true
@@ -151,7 +151,7 @@ ui:
# Lock arbitrary settings on the preferences page. # Lock arbitrary settings on the preferences page.
# #
# preferences: # preferences:
# lock: # lock:
# - categories # - categories
# - language # - language
# - autocomplete # - autocomplete
@@ -207,8 +207,16 @@ outgoing:
# are also supported: see # are also supported: see
# https://2.python-requests.org/en/latest/user/advanced/#socks # https://2.python-requests.org/en/latest/user/advanced/#socks
# #
# proxies: proxies:
# all://: all://:
- http://38.33.146.13:2000
- http://38.33.148.3:2000
- http://38.38.254.150:2000
- http://38.33.146.96:2000
#https: http://192.168.1.78:8888
#proxies:
#all://:
#- http://192.168.1.78:8888
# - http://proxy1:8080 # - http://proxy1:8080
# - http://proxy2:8080 # - http://proxy2:8080
# #
@@ -216,7 +224,7 @@ outgoing:
# #
# Extra seconds to add in order to account for the time taken by the proxy # Extra seconds to add in order to account for the time taken by the proxy
# #
# extra_proxy_timeout: 10 # extra_proxy_timeout: 20
# #
# uncomment below section only if you have more than one network interface # uncomment below section only if you have more than one network interface
# which can be the source of outgoing search requests # which can be the source of outgoing search requests
@@ -226,38 +234,29 @@ outgoing:
# - 1.1.1.2 # - 1.1.1.2
# - fe80::/126 # - fe80::/126
# Plugin configuration, for more details see # External plugin configuration, for more details see
# https://docs.searxng.org/admin/settings/settings_plugins.html # https://docs.searxng.org/admin/settings/settings_plugins.html
# #
plugins: # plugins:
# - mypackage.mymodule.MyPlugin
searx.plugins.calculator.SXNGPlugin: # - mypackage.mymodule.MyOtherPlugin
active: true # - ...
searx.plugins.hash_plugin.SXNGPlugin:
active: true
searx.plugins.self_info.SXNGPlugin:
active: true
searx.plugins.unit_converter.SXNGPlugin:
active: true
searx.plugins.ahmia_filter.SXNGPlugin:
active: true
searx.plugins.hostnames.SXNGPlugin:
active: true
searx.plugins.oa_doi_rewrite.SXNGPlugin:
active: false
searx.plugins.tor_check.SXNGPlugin:
active: false
searx.plugins.tracker_url_remover.SXNGPlugin:
active: false
# Comment or un-comment plugin to activate / deactivate by default.
# https://docs.searxng.org/admin/settings/settings_plugins.html
#
# enabled_plugins:
# # these plugins are enabled if nothing is configured ..
# - 'Basic Calculator'
# - 'Hash plugin'
# - 'Self Information'
# - 'Tracker URL remover'
# - 'Unit converter plugin'
# - 'Ahmia blacklist' # activation depends on outgoing.using_tor_proxy
# # these plugins are disabled if nothing is configured ..
# - 'Hostnames plugin' # see 'hostnames' configuration below
# - 'Open Access DOI rewrite'
# - 'Tor check plugin'
# Configuration of the "Hostnames plugin": # Configuration of the "Hostnames plugin":
# #
@@ -361,11 +360,6 @@ engines:
shortcut: 9g shortcut: 9g
disabled: true disabled: true
- name: acfun
engine: acfun
shortcut: acf
disabled: true
- name: adobe stock - name: adobe stock
engine: adobe_stock engine: adobe_stock
shortcut: asi shortcut: asi
@@ -489,6 +483,7 @@ engines:
engine: artic engine: artic
shortcut: arc shortcut: arc
timeout: 4.0 timeout: 4.0
disabled: true
- name: arxiv - name: arxiv
engine: arxiv engine: arxiv
@@ -510,27 +505,6 @@ engines:
shortcut: bc shortcut: bc
categories: music categories: music
- name: baidu
baidu_category: general
categories: [general]
engine: baidu
shortcut: bd
disabled: true
- name: baidu images
baidu_category: images
categories: [images]
engine: baidu
shortcut: bdi
disabled: true
- name: baidu kaifa
baidu_category: it
categories: [it]
engine: baidu
shortcut: bdk
disabled: true
- name: wikipedia - name: wikipedia
engine: wikipedia engine: wikipedia
shortcut: wp shortcut: wp
@@ -546,7 +520,6 @@ engines:
- name: bing - name: bing
engine: bing engine: bing
shortcut: bi shortcut: bi
disabled: true
- name: bing images - name: bing images
engine: bing_images engine: bing_images
@@ -560,11 +533,6 @@ engines:
engine: bing_videos engine: bing_videos
shortcut: biv shortcut: biv
- name: bitchute
engine: bitchute
shortcut: bit
disabled: true
- name: bitbucket - name: bitbucket
engine: xpath engine: xpath
paging: true paging: true
@@ -613,24 +581,6 @@ engines:
# to show premium or plus results too: # to show premium or plus results too:
# skip_premium: false # skip_premium: false
- name: chinaso news
chinaso_category: news
engine: chinaso
shortcut: chinaso
disabled: true
- name: chinaso images
chinaso_category: images
engine: chinaso
shortcut: chinasoi
disabled: true
- name: chinaso videos
chinaso_category: videos
engine: chinaso
shortcut: chinasov
disabled: true
- name: cloudflareai - name: cloudflareai
engine: cloudflareai engine: cloudflareai
shortcut: cfai shortcut: cfai
@@ -744,6 +694,7 @@ engines:
engine: deviantart engine: deviantart
shortcut: da shortcut: da
timeout: 3.0 timeout: 3.0
disabled: true
- name: ddg definitions - name: ddg definitions
engine: duckduckgo_definitions engine: duckduckgo_definitions
@@ -803,18 +754,17 @@ engines:
results: HTML results: HTML
# - name: elasticsearch # - name: elasticsearch
# shortcut: els # shortcut: es
# engine: elasticsearch # engine: elasticsearch
# base_url: http://localhost:9200 # base_url: http://localhost:9200
# username: elastic # username: elastic
# password: changeme # password: changeme
# index: my-index # index: my-index
# enable_http: true
# # available options: match, simple_query_string, term, terms, custom # # available options: match, simple_query_string, term, terms, custom
# query_type: match # query_type: match
# # if query_type is set to custom, provide your query here # # if query_type is set to custom, provide your query here
# # custom_query_json: {"query":{"match_all": {}}} # #custom_query_json: {"query":{"match_all": {}}}
# # show_metadata: false # #show_metadata: false
# disabled: true # disabled: true
- name: wikidata - name: wikidata
@@ -925,6 +875,7 @@ engines:
# api_key: 'apikey' # required! # api_key: 'apikey' # required!
# Or you can use the html non-stable engine, activated by default # Or you can use the html non-stable engine, activated by default
engine: flickr_noapi engine: flickr_noapi
disabled: true
- name: free software directory - name: free software directory
engine: mediawiki engine: mediawiki
@@ -1013,11 +964,11 @@ engines:
engine: goodreads engine: goodreads
shortcut: good shortcut: good
timeout: 4.0 timeout: 4.0
disabled: true
- name: google - name: google
engine: google engine: google
shortcut: go shortcut: go
disabled: true
# additional_tests: # additional_tests:
# android: *test_android # android: *test_android
@@ -1126,11 +1077,6 @@ engines:
require_api_key: false require_api_key: false
results: JSON results: JSON
- name: il post
engine: il_post
shortcut: pst
disabled: true
- name: imdb - name: imdb
engine: imdb engine: imdb
shortcut: imdb shortcut: imdb
@@ -1164,11 +1110,6 @@ engines:
shortcut: ip shortcut: ip
disabled: true disabled: true
- name: iqiyi
engine: iqiyi
shortcut: iq
disabled: true
- name: jisho - name: jisho
engine: jisho engine: jisho
shortcut: js shortcut: js
@@ -1317,14 +1258,12 @@ engines:
disabled: true disabled: true
number_of_results: 20 number_of_results: 20
# https://docs.searxng.org/dev/engines/offline/search-indexer-engines.html#module-searx.engines.meilisearch
# - name: meilisearch # - name: meilisearch
# engine: meilisearch # engine: meilisearch
# shortcut: mes # shortcut: mes
# enable_http: true # enable_http: true
# base_url: http://localhost:7700 # base_url: http://localhost:7700
# index: my-index # index: my-index
# auth_key: Bearer XXXX
- name: mixcloud - name: mixcloud
engine: mixcloud engine: mixcloud
@@ -1361,11 +1300,6 @@ engines:
shortcut: mwm shortcut: mwm
disabled: true disabled: true
- name: niconico
engine: niconico
shortcut: nico
disabled: true
- name: npm - name: npm
engine: npm engine: npm
shortcut: npm shortcut: npm
@@ -1407,11 +1341,6 @@ engines:
shortcut: od shortcut: od
disabled: true disabled: true
- name: ollama
engine: ollama
shortcut: ollama
disabled: true
- name: openairedatasets - name: openairedatasets
engine: json_engine engine: json_engine
paging: true paging: true
@@ -1673,24 +1602,11 @@ engines:
shortcut: pypi shortcut: pypi
engine: pypi engine: pypi
- name: quark
quark_category: general
categories: [general]
engine: quark
shortcut: qk
disabled: true
- name: quark images
quark_category: images
categories: [images]
engine: quark
shortcut: qki
disabled: true
- name: qwant - name: qwant
qwant_categ: web qwant_categ: web
engine: qwant engine: qwant
shortcut: qw shortcut: qw
disabled: true
categories: [general, web] categories: [general, web]
additional_tests: additional_tests:
rosebud: *test_rosebud rosebud: *test_rosebud
@@ -1748,12 +1664,6 @@ engines:
page_size: 25 page_size: 25
disabled: true disabled: true
- name: reuters
engine: reuters
shortcut: reu
# https://docs.searxng.org/dev/engines/online/reuters.html
# sort_order = "relevance"
- name: right dao - name: right dao
engine: xpath engine: xpath
paging: true paging: true
@@ -1808,12 +1718,6 @@ engines:
about: about:
website: https://searchmysite.net website: https://searchmysite.net
- name: selfhst icons
engine: selfhst
shortcut: si
inactive: true
disabled: true
- name: sepiasearch - name: sepiasearch
engine: sepiasearch engine: sepiasearch
shortcut: sep shortcut: sep
@@ -1944,6 +1848,7 @@ engines:
startpage_categ: images startpage_categ: images
categories: [images, web] categories: [images, web]
shortcut: spi shortcut: spi
disabled: true
- name: tokyotoshokan - name: tokyotoshokan
engine: tokyotoshokan engine: tokyotoshokan
@@ -1962,15 +1867,15 @@ engines:
# For this demo of the sqlite engine download: # For this demo of the sqlite engine download:
# https://liste.mediathekview.de/filmliste-v2.db.bz2 # https://liste.mediathekview.de/filmliste-v2.db.bz2
# and unpack into searx/data/filmliste-v2.db # and unpack into searx/data/filmliste-v2.db
# Query to test: "!mediathekview concert" # Query to test: "!demo concert"
# #
# - name: mediathekview # - name: demo
# engine: sqlite # engine: sqlite
# shortcut: mediathekview # shortcut: demo
# categories: [general, videos] # categories: general
# result_type: MainResult # result_template: default.html
# database: searx/data/filmliste-v2.db # database: searx/data/filmliste-v2.db
# query_str: >- # query_str: >-
# SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title, # SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title,
# COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url, # COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url,
# description AS content # description AS content
@@ -2104,7 +2009,6 @@ engines:
content_query: Snippet content_query: Snippet
categories: [general, web] categories: [general, web]
shortcut: wib shortcut: wib
disabled: true
about: about:
website: https://wiby.me/ website: https://wiby.me/
@@ -2313,8 +2217,8 @@ engines:
- name: mojeek - name: mojeek
shortcut: mjk shortcut: mjk
engine: mojeek engine: mojeek
categories: [general, web]
disabled: true disabled: true
categories: [general, web]
- name: mojeek images - name: mojeek images
shortcut: mjkimg shortcut: mjkimg
@@ -2348,7 +2252,6 @@ engines:
content_xpath: //div[@class="total_dsc_wrap"]/a content_xpath: //div[@class="total_dsc_wrap"]/a
first_page_num: 1 first_page_num: 1
page_size: 10 page_size: 10
disabled: true
about: about:
website: https://www.naver.com/ website: https://www.naver.com/
wikidata_id: Q485639 wikidata_id: Q485639

View File

@@ -182,7 +182,7 @@ SCHEMA = {
'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'), 'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'),
'image_proxy': SettingsValue(bool, False, 'SEARXNG_IMAGE_PROXY'), 'image_proxy': SettingsValue(bool, False, 'SEARXNG_IMAGE_PROXY'),
'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'), 'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),
'method': SettingsValue(('POST', 'GET'), 'POST'), 'method': SettingsValue(('POST', 'GET'), 'POST', 'SEARXNG_METHOD'),
'default_http_headers': SettingsValue(dict, {}), 'default_http_headers': SettingsValue(dict, {}),
}, },
'redis': { 'redis': {

View File

@@ -7,20 +7,83 @@
:py:obj:`SQLiteProperties`: :py:obj:`SQLiteProperties`:
Class to manage properties stored in a database. Class to manage properties stored in a database.
---- Examplarical implementations based on :py:obj:`SQLiteAppl`:
:py:obj:`searx.cache.ExpireCacheSQLite` :
Cache that manages key/value pairs in a SQLite DB, in which the key/value
pairs are deleted after an "expire" time. This type of cache is used, for
example, for the engines, see :py:obj:`searx.enginelib.EngineCache`.
:py:obj:`searx.favicons.cache.FaviconCacheSQLite` :
Favicon cache that manages the favicon BLOBs in a SQLite DB.
----
""" """
from __future__ import annotations from __future__ import annotations
import sys import abc
import datetime
import re import re
import sqlite3 import sqlite3
import sys
import threading import threading
import abc import uuid
from searx import logger from searx import logger
logger = logger.getChild('sqlitedb') logger = logger.getChild("sqlitedb")
THREAD_LOCAL = threading.local()
class DBSession:
"""A *thead-local* DB session"""
@classmethod
def get_connect(cls, app: SQLiteAppl) -> sqlite3.Connection:
"""Returns a thread local DB connection. The connection is only
established once per thread.
"""
if getattr(THREAD_LOCAL, "DBSession_map", None) is None:
THREAD_LOCAL.DBSession_map = {}
session = THREAD_LOCAL.DBSession_map.get(app.db_url)
if session is None:
session = cls(app)
return session.conn
def __init__(self, app: SQLiteAppl):
self.uuid = uuid.uuid4()
self.app = app
self._conn = None
# self.__del__ will be called, when thread ends
if getattr(THREAD_LOCAL, "DBSession_map", None) is None:
THREAD_LOCAL.DBSession_map = {}
THREAD_LOCAL.DBSession_map[self.app.db_url] = self
@property
def conn(self) -> sqlite3.Connection:
msg = f"[{threading.current_thread().ident}] DBSession: " f"{self.app.__class__.__name__}({self.app.db_url})"
if self._conn is None:
self._conn = self.app.connect()
logger.debug("%s --> created new connection", msg)
# else:
# logger.debug("%s --> already connected", msg)
return self._conn
def __del__(self):
try:
if self._conn is not None:
# HINT: Don't use Python's logging facility in a destructor, it
# will produce error reports when python aborts the process or
# thread, because at this point objects that the logging module
# needs, do not exist anymore.
# msg = f"DBSession: close [{self.uuid}] {self.app.__class__.__name__}({self.app.db_url})"
# logger.debug(msg)
self._conn.close()
except Exception: # pylint: disable=broad-exception-caught
pass
class SQLiteAppl(abc.ABC): class SQLiteAppl(abc.ABC):
@@ -51,13 +114,18 @@ class SQLiteAppl(abc.ABC):
""" """
SQLITE_JOURNAL_MODE = "WAL" SQLITE_JOURNAL_MODE = "WAL"
"""``SQLiteAppl`` applications are optimzed for WAL_ mode, its not recommend
to change the journal mode (see :py:obj:`SQLiteAppl.tear_down`).
.. _WAL: https://sqlite.org/wal.html
"""
SQLITE_CONNECT_ARGS = { SQLITE_CONNECT_ARGS = {
# "timeout": 5.0, # "timeout": 5.0,
# "detect_types": 0, # "detect_types": 0,
"check_same_thread": bool(SQLITE_THREADING_MODE != "serialized"), "check_same_thread": bool(SQLITE_THREADING_MODE != "serialized"),
"cached_statements": 0, # https://github.com/python/cpython/issues/118172 "cached_statements": 0, # https://github.com/python/cpython/issues/118172
# "uri": False, # "uri": False,
"autocommit": False, "isolation_level": None,
} # fmt:skip } # fmt:skip
"""Connection arguments (:py:obj:`sqlite3.connect`) """Connection arguments (:py:obj:`sqlite3.connect`)
@@ -66,10 +134,6 @@ class SQLiteAppl(abc.ABC):
``serialized``. The check is more of a hindrance in this case because it ``serialized``. The check is more of a hindrance in this case because it
would prevent a DB connector from being used in multiple threads. would prevent a DB connector from being used in multiple threads.
``autocommit``:
Is disabled by default. Note: autocommit option has been added in Python
3.12.
``cached_statements``: ``cached_statements``:
Is set to ``0`` by default. Note: Python 3.12+ fetch result are not Is set to ``0`` by default. Note: Python 3.12+ fetch result are not
consistent in multi-threading application and causing an API misuse error. consistent in multi-threading application and causing an API misuse error.
@@ -89,9 +153,17 @@ class SQLiteAppl(abc.ABC):
self.db_url = db_url self.db_url = db_url
self.properties = SQLiteProperties(db_url) self.properties = SQLiteProperties(db_url)
self.thread_local = threading.local()
self._init_done = False self._init_done = False
self._compatibility() self._compatibility()
# atexit.register(self.tear_down)
# def tear_down(self):
# """:ref:`Vacuuming the WALs` upon normal interpreter termination
# (:py:obj:`atexit.register`).
# .. _SQLite: Vacuuming the WALs: https://www.theunterminatedstring.com/sqlite-vacuuming/
# """
# self.DB.execute("PRAGMA wal_checkpoint(TRUNCATE)")
def _compatibility(self): def _compatibility(self):
@@ -113,19 +185,31 @@ class SQLiteAppl(abc.ABC):
"SQLite runtime library version %s is not supported (require >= 3.35)", sqlite3.sqlite_version "SQLite runtime library version %s is not supported (require >= 3.35)", sqlite3.sqlite_version
) )
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.Connection(self.db_url, **self.SQLITE_CONNECT_ARGS) # type: ignore
conn.execute(f"PRAGMA journal_mode={self.SQLITE_JOURNAL_MODE}")
self.register_functions(conn)
return conn
def connect(self) -> sqlite3.Connection: def connect(self) -> sqlite3.Connection:
"""Creates a new DB connection (:py:obj:`SQLITE_CONNECT_ARGS`). If not """Creates a new DB connection (:py:obj:`SQLITE_CONNECT_ARGS`). If not
already done, the DB schema is set up already done, the DB schema is set up. The caller must take care of
closing the resource. Alternatively, :py:obj:`SQLiteAppl.DB` can also
be used (the resource behind `self.DB` is automatically closed when the
process or thread is terminated).
""" """
if sys.version_info < (3, 12): if sys.version_info < (3, 12):
# Prior Python 3.12 there is no "autocommit" option # Prior Python 3.12 there is no "autocommit" option
self.SQLITE_CONNECT_ARGS.pop("autocommit", None) self.SQLITE_CONNECT_ARGS.pop("autocommit", None)
self.init() msg = (
logger.debug("%s: connect to DB: %s // %s", self.__class__.__name__, self.db_url, self.SQLITE_CONNECT_ARGS) f"[{threading.current_thread().ident}] {self.__class__.__name__}({self.db_url})"
conn = sqlite3.Connection(self.db_url, **self.SQLITE_CONNECT_ARGS) # type: ignore f" {self.SQLITE_CONNECT_ARGS} // {self.SQLITE_JOURNAL_MODE}"
conn.execute(f"PRAGMA journal_mode={self.SQLITE_JOURNAL_MODE}") )
self.register_functions(conn) logger.debug(msg)
with self._connect() as conn:
self.init(conn)
return conn return conn
def register_functions(self, conn): def register_functions(self, conn):
@@ -150,7 +234,7 @@ class SQLiteAppl(abc.ABC):
.. _re.search: https://docs.python.org/3/library/re.html#re.search .. _re.search: https://docs.python.org/3/library/re.html#re.search
""" """
conn.create_function('regexp', 2, lambda x, y: 1 if re.search(x, y) else 0, deterministic=True) conn.create_function("regexp", 2, lambda x, y: 1 if re.search(x, y) else 0, deterministic=True)
@property @property
def DB(self) -> sqlite3.Connection: def DB(self) -> sqlite3.Connection:
@@ -168,57 +252,66 @@ class SQLiteAppl(abc.ABC):
https://docs.python.org/3/library/sqlite3.html#sqlite3-controlling-transactions https://docs.python.org/3/library/sqlite3.html#sqlite3-controlling-transactions
""" """
if getattr(self.thread_local, 'DB', None) is None: conn = None
self.thread_local.DB = self.connect()
# Theoretically it is possible to reuse the DB cursor across threads as if self.SQLITE_THREADING_MODE == "serialized":
# of Python 3.12, in practice the threading of the cursor seems to me to # Theoretically it is possible to reuse the DB cursor across threads
# be so faulty that I prefer to establish one connection per thread # as of Python 3.12, in practice the threading of the cursor seems
# to me a little faulty that I prefer to establish one connection
# per thread.
#
# may we can activate this code one day ..
# if self._DB is None:
# self._DB = self.connect()
# conn = self._DB
conn = DBSession.get_connect(self)
else:
conn = DBSession.get_connect(self)
self.thread_local.DB.commit() # Since more than one instance of SQLiteAppl share the same DB
return self.thread_local.DB # connection, we need to make sure that each SQLiteAppl instance has run
# its init method at least once.
self.init(conn)
# In "serialized" mode, SQLite can be safely used by multiple threads return conn
# with no restriction.
#
# if self.SQLITE_THREADING_MODE != "serialized":
# if getattr(self.thread_local, 'DB', None) is None:
# self.thread_local.DB = self.connect()
# return self.thread_local.DB
#
# if self._DB is None:
# self._DB = self.connect() # pylint: disable=attribute-defined-outside-init
# return self._DB
def init(self): def init(self, conn: sqlite3.Connection) -> bool:
"""Initializes the DB schema and properties, is only executed once even """Initializes the DB schema and properties, is only executed once even
if called several times.""" if called several times.
If the initialization has not yet taken place, it is carried out and a
`True` is returned to the caller at the end. If the initialization has
already been carried out in the past, `False` is returned.
"""
if self._init_done: if self._init_done:
return return False
self._init_done = True self._init_done = True
logger.debug("init DB: %s", self.db_url) logger.debug("init DB: %s", self.db_url)
self.properties.init() self.properties.init(conn)
ver = self.properties("DB_SCHEMA") ver = self.properties("DB_SCHEMA")
if ver is None: if ver is None:
with self.properties.DB: with conn:
self.create_schema(self.properties.DB) self.create_schema(conn)
else: else:
ver = int(ver) ver = int(ver)
if ver != self.DB_SCHEMA: if ver != self.DB_SCHEMA:
raise sqlite3.DatabaseError("Expected DB schema v%s, DB schema is v%s" % (self.DB_SCHEMA, ver)) raise sqlite3.DatabaseError("Expected DB schema v%s, DB schema is v%s" % (self.DB_SCHEMA, ver))
logger.debug("DB_SCHEMA = %s", ver) logger.debug("DB_SCHEMA = %s", ver)
def create_schema(self, conn): return True
def create_schema(self, conn: sqlite3.Connection):
logger.debug("create schema ..") logger.debug("create schema ..")
self.properties.set("DB_SCHEMA", self.DB_SCHEMA)
self.properties.set("LAST_MAINTENANCE", "")
with conn: with conn:
for table_name, sql in self.DDL_CREATE_TABLES.items(): for table_name, sql in self.DDL_CREATE_TABLES.items():
conn.execute(sql) conn.execute(sql)
self.properties.set(f"Table {table_name} created", table_name) self.properties.set(f"Table {table_name} created", table_name)
self.properties.set("DB_SCHEMA", self.DB_SCHEMA)
self.properties.set("LAST_MAINTENANCE", "")
class SQLiteProperties(SQLiteAppl): class SQLiteProperties(SQLiteAppl):
@@ -253,33 +346,32 @@ CREATE TABLE IF NOT EXISTS properties (
" ON CONFLICT(name) DO UPDATE" " ON CONFLICT(name) DO UPDATE"
" SET value=excluded.value, m_time=strftime('%s', 'now')" " SET value=excluded.value, m_time=strftime('%s', 'now')"
) )
SQL_DELETE = "DELETE FROM properties WHERE name = ?"
SQL_TABLE_EXISTS = ( SQL_TABLE_EXISTS = (
"SELECT name FROM sqlite_master" "SELECT name FROM sqlite_master"
" WHERE type='table' AND name='properties'" " WHERE type='table' AND name='properties'"
) # fmt:skip ) # fmt:skip
SQLITE_CONNECT_ARGS = dict(SQLiteAppl.SQLITE_CONNECT_ARGS) SQLITE_CONNECT_ARGS = dict(SQLiteAppl.SQLITE_CONNECT_ARGS)
SQLITE_CONNECT_ARGS["autocommit"] = True # This option has no effect before Python 3.12
def __init__(self, db_url: str): # pylint: disable=super-init-not-called def __init__(self, db_url: str): # pylint: disable=super-init-not-called
self.db_url = db_url self.db_url = db_url
self.thread_local = threading.local()
self._init_done = False self._init_done = False
self._compatibility() self._compatibility()
def init(self): def init(self, conn: sqlite3.Connection) -> bool:
"""Initializes DB schema of the properties in the DB.""" """Initializes DB schema of the properties in the DB."""
if self._init_done: if self._init_done:
return return False
self._init_done = True self._init_done = True
logger.debug("init properties of DB: %s", self.db_url) logger.debug("init properties of DB: %s", self.db_url)
with self.DB as conn: res = conn.execute(self.SQL_TABLE_EXISTS)
res = conn.execute(self.SQL_TABLE_EXISTS) if res.fetchone() is None: # DB schema needs to be be created
if res.fetchone() is None: # DB schema needs to be be created self.create_schema(conn)
self.create_schema(conn) return True
def __call__(self, name, default=None): def __call__(self, name: str, default=None):
"""Returns the value of the property ``name`` or ``default`` if property """Returns the value of the property ``name`` or ``default`` if property
not exists in DB.""" not exists in DB."""
@@ -288,36 +380,47 @@ CREATE TABLE IF NOT EXISTS properties (
return default return default
return res[0] return res[0]
def set(self, name, value): def set(self, name: str, value: str | int):
"""Set ``value`` of property ``name`` in DB. If property already """Set ``value`` of property ``name`` in DB. If property already
exists, update the ``m_time`` (and the value).""" exists, update the ``m_time`` (and the value)."""
self.DB.execute(self.SQL_SET, (name, value)) with self.DB:
self.DB.execute(self.SQL_SET, (name, value))
if sys.version_info <= (3, 12): def delete(self, name: str) -> int:
# Prior Python 3.12 there is no "autocommit" option / lets commit """Delete of property ``name`` from DB."""
# explicitely. with self.DB:
self.DB.commit() cur = self.DB.execute(self.SQL_DELETE, (name,))
return cur.rowcount
def row(self, name, default=None): def row(self, name: str, default=None):
"""Returns the DB row of property ``name`` or ``default`` if property """Returns the DB row of property ``name`` or ``default`` if property
not exists in DB.""" not exists in DB."""
cur = self.DB.cursor() res = self.DB.execute("SELECT * FROM properties WHERE name = ?", (name,))
cur.execute("SELECT * FROM properties WHERE name = ?", (name,)) row = res.fetchone()
res = cur.fetchone() if row is None:
if res is None:
return default return default
col_names = [column[0] for column in cur.description]
return dict(zip(col_names, res))
def m_time(self, name, default: int = 0) -> int: col_names = [column[0] for column in row.description]
return dict(zip(col_names, row))
def m_time(self, name: str, default: int = 0) -> int:
"""Last modification time of this property.""" """Last modification time of this property."""
res = self.DB.execute(self.SQL_M_TIME, (name,)).fetchone() res = self.DB.execute(self.SQL_M_TIME, (name,))
if res is None: row = res.fetchone()
if row is None:
return default return default
return int(res[0]) return int(row[0])
def create_schema(self, conn): def create_schema(self, conn):
with conn: with conn:
conn.execute(self.DDL_PROPERTIES) conn.execute(self.DDL_PROPERTIES)
def __str__(self) -> str:
lines = []
for row in self.DB.execute("SELECT name, value, m_time FROM properties"):
name, value, m_time = row
m_time = datetime.datetime.fromtimestamp(m_time).strftime("%Y-%m-%d %H:%M:%S")
lines.append(f"[last modified: {m_time}] {name:20s}: {value}")
return "\n".join(lines)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="92mm" height="92mm" viewBox="0 0 92 92"><g transform="translate(-40.921 -17.417)"><circle cx="75.921" cy="53.903" r="30" style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/><path d="M67.515 37.915a18 18 0 0 1 21.051 3.313 18 18 0 0 1 3.138 21.078" style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/><rect width="18.846" height="39.963" x="3.706" y="122.09" ry="0" style="opacity:1;fill:#3050ff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" transform="rotate(-46.235)"/></g></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="21.508125mm"
height="21.508017mm"
viewBox="0 0 21.508125 21.508015"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-54.245938,-114.24604)">
<path
style="fill:#222222;fill-opacity:1;stroke:#ffffff;stroke-width:1.50812;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 55,115 5.000001,4.99999 h 5 L 75,130 70.000002,135 H 60.000001 L 55,130 Z"
id="path2" />
<path
style="fill:#222222;fill-opacity:1;stroke:#ffffff;stroke-width:1.50812;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 67,119.99999 h 3.000002 L 75,115 v 12.99999 z"
id="path3" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 310 KiB

View File

@@ -0,0 +1,242 @@
function neko() {
var nekoEl = document.createElement("div");
var nekoPosX = 32;
var nekoPosY = 32;
var mousePosX = 0;
var mousePosY = 0;
var frameCount = 0;
var idleTime = 0;
var idleAnimation = null;
var idleAnimationFrame = 0;
var direction;
var IE = document.all ? true : false;
var nekoSpeed = 10;
var spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "absolute";
nekoEl.style.pointerEvents = "none";
nekoEl.style.backgroundImage = "url('oneko.gif')";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = nekoPosX - 16 + "px";
nekoEl.style.top = nekoPosY - 16 + "px";
nekoEl.style.zIndex = Number.MAX_VALUE;
document.body.appendChild(nekoEl);
function mousePos(event) {
if (IE) {
event = window.event;
}
mousePosX = event.clientX;
mousePosY = event.clientY - 16;
}
document.onmousemove = mousePos;
window.onekoInterval = setInterval(frame, 100);
}
function setSprite(name, frame) {
var length = spriteSets[name].length;
if (IE) {
length = 0;
// Internet explorer is really fucking dumb
while (length < spriteSets[name].length) {
if (spriteSets[name][length] != null) {
length = length + 1;
continue;
}
break;
}
}
var sprite = spriteSets[name][frame % length];
nekoEl.style.backgroundPosition =
sprite["0"] * 32 + "px " + sprite["1"] * 32 + "px";
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime = idleTime + 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
var avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame = idleAnimationFrame + 1;
}
function frame() {
frameCount = frameCount + 1;
var diffX = nekoPosX - mousePosX;
var diffY = nekoPosY - mousePosY;
var distance = Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2));
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime = idleTime - 1;
return;
}
direction = "";
if (diffY / distance > 0.5) {
direction = "N";
} else if (diffY / distance < -0.5) {
direction = "S";
}
if (diffX / distance > 0.5) {
direction = direction + "W";
} else if (diffX / distance < -0.5) {
direction = direction + "E";
}
setSprite(direction, frameCount);
if (distance > nekoSpeed) {
nekoPosX = nekoPosX - (diffX / distance) * nekoSpeed;
nekoPosY = nekoPosY - (diffY / distance) * nekoSpeed;
} else {
nekoPosX = mousePosX;
nekoPosY = mousePosY;
}
nekoPosX = Math.min(
Math.max(16, nekoPosX),
document.getElementsByTagName("body")[0].clientWidth - 16
);
nekoPosY = Math.min(
Math.max(16, nekoPosY),
document.getElementsByTagName("body")[0].clientHeight - 16
);
nekoEl.style.left = nekoPosX - 16 + "px";
nekoEl.style.top = nekoPosY - 16 + "px";
}
init();
}
neko();

View File

@@ -0,0 +1,291 @@
// oneko.js: https://github.com/adryd325/oneko.js (webring variant)
(function oneko() {
const isReducedMotion =
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
if (isReducedMotion) return;
const nekoEl = document.createElement("div");
let nekoPosX = 32;
let nekoPosY = 32;
let mousePosX = 0;
let mousePosY = 0;
// please use data-neko="true" on your A elements that link to another site with oneko-webring.js instead of this
// this is deprecated and will eventually be removed
const nekoSites = [
"localhost",
];
try {
const searchParams = location.search
.replace("?", "")
.split("&")
.map((keyvaluepair) => keyvaluepair.split("="));
// This is so much repeated code, I don't like it
tmp = searchParams.find((a) => a[0] == "catx");
if (tmp && tmp[1]) nekoPosX = parseInt(tmp[1]);
tmp = searchParams.find((a) => a[0] == "caty");
if (tmp && tmp[1]) nekoPosY = parseInt(tmp[1]);
tmp = searchParams.find((a) => a[0] == "catdx");
if (tmp && tmp[1]) mousePosX = parseInt(tmp[1]);
tmp = searchParams.find((a) => a[0] == "catdy");
if (tmp && tmp[1]) mousePosY = parseInt(tmp[1]);
} catch (e) {
console.error("oneko.js: failed to parse query params.");
console.error(e);
}
function onClick(event) {
const target = event.target.closest("A");
if (target === null || !target.getAttribute("href")) {
return;
}
let newLocation;
try {
newLocation = new URL(target.href);
} catch (e) {
return;
}
if (
(nekoSites.includes(newLocation.host) && newLocation.pathname == "/") ||
target.dataset.neko
) {
newLocation.searchParams.append("catx", Math.floor(nekoPosX));
newLocation.searchParams.append("caty", Math.floor(nekoPosY));
newLocation.searchParams.append("catdx", Math.floor(mousePosX));
newLocation.searchParams.append("catdy", Math.floor(mousePosY));
event.preventDefault();
window.location.href = newLocation.toString();
}
}
document.addEventListener("click", onClick);
let frameCount = 0;
let idleTime = 0;
let idleAnimation = null;
let idleAnimationFrame = 0;
const nekoSpeed = 10;
const spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "fixed";
nekoEl.style.pointerEvents = "none";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
nekoEl.style.zIndex = Number.MAX_VALUE;
let nekoFile = "./oneko.gif"
const curScript = document.currentScript
if (curScript && curScript.dataset.cat) {
nekoFile = curScript.dataset.cat
}
nekoEl.style.backgroundImage = `url(${nekoFile})`;
document.body.appendChild(nekoEl);
document.addEventListener("mousemove", function (event) {
mousePosX = event.clientX;
mousePosY = event.clientY;
});
window.requestAnimationFrame(onAnimationFrame);
}
let lastFrameTimestamp;
function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM
if (!nekoEl.isConnected) {
return;
}
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp
frame()
}
window.requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) {
const sprite = spriteSets[name][frame % spriteSets[name].length];
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime += 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame += 1;
}
function frame() {
frameCount += 1;
const diffX = nekoPosX - mousePosX;
const diffY = nekoPosY - mousePosY;
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
let direction;
direction = diffY / distance > 0.5 ? "N" : "";
direction += diffY / distance < -0.5 ? "S" : "";
direction += diffX / distance > 0.5 ? "W" : "";
direction += diffX / distance < -0.5 ? "E" : "";
setSprite(direction, frameCount);
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
}
init();
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,240 @@
// oneko.js: https://github.com/adryd325/oneko.js
(function oneko() {
const isReducedMotion =
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
if (isReducedMotion) return;
const nekoEl = document.createElement("div");
let nekoPosX = 32;
let nekoPosY = 32;
let mousePosX = 0;
let mousePosY = 0;
let frameCount = 0;
let idleTime = 0;
let idleAnimation = null;
let idleAnimationFrame = 0;
const nekoSpeed = 10;
const spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "fixed";
nekoEl.style.pointerEvents = "none";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
nekoEl.style.zIndex = 2147483647;
let nekoFile = "/static/themes/simple/js/oneko.gif"
const curScript = document.currentScript
if (curScript && curScript.dataset.cat) {
nekoFile = curScript.dataset.cat
}
nekoEl.style.backgroundImage = `url(${nekoFile})`;
document.body.appendChild(nekoEl);
document.addEventListener("mousemove", function (event) {
mousePosX = event.clientX;
mousePosY = event.clientY;
});
window.requestAnimationFrame(onAnimationFrame);
}
let lastFrameTimestamp;
function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM
if (!nekoEl.isConnected) {
return;
}
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp
frame()
}
window.requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) {
const sprite = spriteSets[name][frame % spriteSets[name].length];
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime += 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame += 1;
}
function frame() {
frameCount += 1;
const diffX = nekoPosX - mousePosX;
const diffY = nekoPosY - mousePosY;
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
let direction;
direction = diffY / distance > 0.5 ? "N" : "";
direction += diffY / distance < -0.5 ? "S" : "";
direction += diffX / distance > 0.5 ? "W" : "";
direction += diffX / distance < -0.5 ? "E" : "";
setSprite(direction, frameCount);
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
}
init();
})();

View File

@@ -0,0 +1,7 @@
Copyright © 2022 adryd
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,19 @@
# oneko.js
A hacky script I wrote to put a cat on my site.
The default image is `oneko.gif` in the same directory as the script. This can be changed by adding `data-cat="yourimage.png"` to your `<script>` tag.
demo: https://adryd.com
This script is meant to be simple so that it can easily be extended upon. Pull requests adding features not seen in the [original neko program
](https://en.wikipedia.org/wiki/Neko_(software)) will probably not be merged.
implemented in a few different places
- Userscript: https://openuserjs.org/scripts/sjehuda/Oneko_WebMate
- Vencord: https://vencord.dev/plugins/oneko
- Spicetify: https://github.com/kyrie25/spicetify-oneko
feature forks
- Pet the cat: https://github.com/tylxr59/oneko.js/tree/main
- Move the cat using scroll wheel: https://github.com/rozbrajaczpoziomow/fork-oneko.js/tree/main

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oneko.js</title>
</head>
<body>
<!--[if IE]>
<script src="./oneko-ie6.js"></script>
<![endif]-->
<!--[if !IE]><!-->
<script src="./oneko.js"></script>
<!--<![endif]-->
</body>
</html>

View File

@@ -5,6 +5,12 @@
"src": "js/searxng.head.js", "src": "js/searxng.head.js",
"isEntry": true "isEntry": true
}, },
"js/oneko.js": {
"file": "js/oneko.js",
"name": "js/oneko.js",
"src": "js/oneko.js",
"isEntry": true
},
"js/searxng.js": { "js/searxng.js": {
"file": "js/searxng.min.js", "file": "js/searxng.min.js",
"name": "js/searxng.min", "name": "js/searxng.min",
@@ -26,4 +32,4 @@
"src": "less/style-rtl.less", "src": "less/style-rtl.less",
"isEntry": true "isEntry": true
} }
} }

View File

@@ -23,6 +23,7 @@ sxng_locales = (
('da-DK', 'Dansk', 'Danmark', 'Danish', '\U0001f1e9\U0001f1f0'), ('da-DK', 'Dansk', 'Danmark', 'Danish', '\U0001f1e9\U0001f1f0'),
('de', 'Deutsch', '', 'German', '\U0001f310'), ('de', 'Deutsch', '', 'German', '\U0001f310'),
('de-AT', 'Deutsch', 'Österreich', 'German', '\U0001f1e6\U0001f1f9'), ('de-AT', 'Deutsch', 'Österreich', 'German', '\U0001f1e6\U0001f1f9'),
('de-BE', 'Deutsch', 'Belgien', 'German', '\U0001f1e7\U0001f1ea'),
('de-CH', 'Deutsch', 'Schweiz', 'German', '\U0001f1e8\U0001f1ed'), ('de-CH', 'Deutsch', 'Schweiz', 'German', '\U0001f1e8\U0001f1ed'),
('de-DE', 'Deutsch', 'Deutschland', 'German', '\U0001f1e9\U0001f1ea'), ('de-DE', 'Deutsch', 'Deutschland', 'German', '\U0001f1e9\U0001f1ea'),
('el', 'Ελληνικά', '', 'Greek', '\U0001f310'), ('el', 'Ελληνικά', '', 'Greek', '\U0001f310'),

Binary file not shown.

View File

@@ -2,8 +2,8 @@
<html class="no-js theme-{{ preferences.get_value('simple_style') or 'auto' }} center-alignment-{{ preferences.get_value('center_alignment') and 'yes' or 'no' }}" lang="{{ locale_rfc5646 }}" {% if rtl %} dir="rtl"{% endif %}> <html class="no-js theme-{{ preferences.get_value('simple_style') or 'auto' }} center-alignment-{{ preferences.get_value('center_alignment') and 'yes' or 'no' }}" lang="{{ locale_rfc5646 }}" {% if rtl %} dir="rtl"{% endif %}>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="description" content="SearXNG — a privacy-respecting, open metasearch engine"> <meta name="description" content="nekosearch — a privacy-respecting, open metasearch engine for nekos">
<meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search"> <meta name="keywords" content="nekosearch, SearXNG, search, search engine, metasearch, meta search">
<meta name="generator" content="searxng/{{ searx_version }}"> <meta name="generator" content="searxng/{{ searx_version }}">
<meta name="referrer" content="no-referrer"> <meta name="referrer" content="no-referrer">
<meta name="robots" content="noarchive"> <meta name="robots" content="noarchive">
@@ -84,6 +84,7 @@
</footer> </footer>
<!--[if gte IE 9]>--> <!--[if gte IE 9]>-->
<script src="{{ url_for('static', filename='js/searxng.min.js') }}"></script> <script src="{{ url_for('static', filename='js/searxng.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/oneko.js') }}"></script>
<!--<![endif]--> <!--<![endif]-->
</body> </body>
</html> </html>

View File

@@ -7,35 +7,35 @@ template. This file from:
{%- {%-
set catalog = { set catalog = {
'alert' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 80c-8.66 0-16.58 7.36-16 16l8 216a8 8 0 008 8h0a8 8 0 008-8l8-216c.58-8.64-7.34-16-16-16z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="256" cy="416" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'alert' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 80c-8.66 0-16.58 7.36-16 16l8 216a8 8 0 0 0 8 8h0a8 8 0 0 0 8-8l8-216c.58-8.64-7.34-16-16-16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><circle cx="256" cy="416" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'appstore' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="64" y="64" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="216" y="64" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="368" y="64" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="64" y="216" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="216" y="216" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="368" y="216" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="64" y="368" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="216" y="368" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="368" y="368" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/></svg>', 'appstore' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="80" height="80" x="64" y="64" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="216" y="64" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="368" y="64" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="64" y="216" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="216" y="216" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="368" y="216" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="64" y="368" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="216" y="368" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="368" y="368" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
'book' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 160c16-63.16 76.43-95.41 208-96a15.94 15.94 0 0116 16v288a16 16 0 01-16 16c-128 0-177.45 25.81-208 64-30.37-38-80-64-208-64-9.88 0-16-8.05-16-17.93V80a15.94 15.94 0 0116-16c131.57.59 192 32.84 208 96zM256 160v288" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'book' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 160c16-63.16 76.43-95.41 208-96a15.94 15.94 0 0 1 16 16v288a16 16 0 0 1-16 16c-128 0-177.45 25.81-208 64-30.37-38-80-64-208-64-9.88 0-16-8.05-16-17.93V80a15.94 15.94 0 0 1 16-16c131.57.59 192 32.84 208 96M256 160v288" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'close' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368"/></svg>', 'close' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M368 368 144 144M368 144 144 368" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'download' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M336 176h40a40 40 0 0140 40v208a40 40 0 01-40 40H136a40 40 0 01-40-40V216a40 40 0 0140-40h40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M176 272l80 80 80-80M256 48v288"/></svg>', 'download' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M336 176h40a40 40 0 0 1 40 40v208a40 40 0 0 1-40 40H136a40 40 0 0 1-40-40V216a40 40 0 0 1 40-40h40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="m176 272 80 80 80-80M256 48v288" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'ellipsis-vertical' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><circle cx="256" cy="256" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><circle cx="256" cy="416" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><circle cx="256" cy="96" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/></svg>', 'ellipsis-vertical' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><circle cx="256" cy="256" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><circle cx="256" cy="416" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><circle cx="256" cy="96" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
'file-tray-full' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M384 80H128c-26 0-43 14-48 40L48 272v112a48.14 48.14 0 0048 48h320a48.14 48.14 0 0048-48V272l-32-152c-5-27-23-40-48-40z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M48 272h144M320 272h144M192 272a64 64 0 00128 0M144 144h224M128 208h256"/></svg>', 'file-tray-full' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M384 80H128c-26 0-43 14-48 40L48 272v112a48.14 48.14 0 0 0 48 48h320a48.14 48.14 0 0 0 48-48V272l-32-152c-5-27-23-40-48-40Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><path d="M48 272h144M320 272h144M192 272a64 64 0 0 0 128 0M144 144h224M128 208h256" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'film' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="48" y="96" width="416" height="320" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="336" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="256" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="176" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="96" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="336" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="256" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="176" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="96" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="128" y="96" width="256" height="160" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="128" y="256" width="256" height="160" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/></svg>', 'film' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="416" height="320" x="48" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="336" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="256" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="176" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="336" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="256" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="176" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="256" height="160" x="128" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="256" height="160" x="128" y="256" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/></svg>',
'globe' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 48C141.13 48 48 141.13 48 256s93.13 208 208 208 208-93.13 208-208S370.87 48 256 48z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M256 48c-58.07 0-112.67 93.13-112.67 208S197.93 464 256 464s112.67-93.13 112.67-208S314.07 48 256 48z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M117.33 117.33c38.24 27.15 86.38 43.34 138.67 43.34s100.43-16.19 138.67-43.34M394.67 394.67c-38.24-27.15-86.38-43.34-138.67-43.34s-100.43 16.19-138.67 43.34" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32" d="M256 48v416M464 256H48"/></svg>', 'globe' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 48C141.13 48 48 141.13 48 256s93.13 208 208 208 208-93.13 208-208S370.87 48 256 48Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M256 48c-58.07 0-112.67 93.13-112.67 208S197.93 464 256 464s112.67-93.13 112.67-208S314.07 48 256 48Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M117.33 117.33c38.24 27.15 86.38 43.34 138.67 43.34s100.43-16.19 138.67-43.34M394.67 394.67c-38.24-27.15-86.38-43.34-138.67-43.34s-100.43 16.19-138.67 43.34" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M256 48v416M464 256H48" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
'heart' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M352.92 80C288 80 256 144 256 144s-32-64-96.92-64c-52.76 0-94.54 44.14-95.08 96.81-1.1 109.33 86.73 187.08 183 252.42a16 16 0 0018 0c96.26-65.34 184.09-143.09 183-252.42-.54-52.67-42.32-96.81-95.08-96.81z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'heart' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M352.92 80C288 80 256 144 256 144s-32-64-96.92-64c-52.76 0-94.54 44.14-95.08 96.81-1.1 109.33 86.73 187.08 183 252.42a16 16 0 0 0 18 0c96.26-65.34 184.09-143.09 183-252.42-.54-52.67-42.32-96.81-95.08-96.81" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'image' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="48" y="80" width="416" height="352" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><circle cx="336" cy="176" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M304 335.79l-90.66-90.49a32 32 0 00-43.87-1.3L48 352M224 432l123.34-123.34a32 32 0 0143.11-2L464 368" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'image' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="416" height="352" x="48" y="80" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><circle cx="336" cy="176" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="m304 335.79-90.66-90.49a32 32 0 0 0-43.87-1.3L48 352M224 432l123.34-123.34a32 32 0 0 1 43.11-2L464 368" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'layers' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M434.8 137.65l-149.36-68.1c-16.19-7.4-42.69-7.4-58.88 0L77.3 137.65c-17.6 8-17.6 21.09 0 29.09l148 67.5c16.89 7.7 44.69 7.7 61.58 0l148-67.5c17.52-8 17.52-21.1-.08-29.09zM160 308.52l-82.7 37.11c-17.6 8-17.6 21.1 0 29.1l148 67.5c16.89 7.69 44.69 7.69 61.58 0l148-67.5c17.6-8 17.6-21.1 0-29.1l-79.94-38.47" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M160 204.48l-82.8 37.16c-17.6 8-17.6 21.1 0 29.1l148 67.49c16.89 7.7 44.69 7.7 61.58 0l148-67.49c17.7-8 17.7-21.1.1-29.1L352 204.48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'layers' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m434.8 137.65-149.36-68.1c-16.19-7.4-42.69-7.4-58.88 0L77.3 137.65c-17.6 8-17.6 21.09 0 29.09l148 67.5c16.89 7.7 44.69 7.7 61.58 0l148-67.5c17.52-8 17.52-21.1-.08-29.09M160 308.52l-82.7 37.11c-17.6 8-17.6 21.1 0 29.1l148 67.5c16.89 7.69 44.69 7.69 61.58 0l148-67.5c17.6-8 17.6-21.1 0-29.1l-79.94-38.47" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="m160 204.48-82.8 37.16c-17.6 8-17.6 21.1 0 29.1l148 67.49c16.89 7.7 44.69 7.7 61.58 0l148-67.49c17.7-8 17.7-21.1.1-29.1L352 204.48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'leecher' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 268l144 144 144-144M256 392V100"/></svg>', 'leecher' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m112 268 144 144 144-144M256 392V100" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
'location' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 48c-79.5 0-144 61.39-144 137 0 87 96 224.87 131.25 272.49a15.77 15.77 0 0025.5 0C304 409.89 400 272.07 400 185c0-75.61-64.5-137-144-137z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="256" cy="192" r="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'location' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 48c-79.5 0-144 61.39-144 137 0 87 96 224.87 131.25 272.49a15.77 15.77 0 0 0 25.5 0C304 409.89 400 272.07 400 185c0-75.61-64.5-137-144-137" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><circle cx="256" cy="192" r="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'magnet' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M421.83 293.82A144 144 0 00218.18 90.17M353.94 225.94a48 48 0 00-67.88-67.88" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M192 464v-48M90.18 421.82l33.94-33.94M48 320h48"/><path d="M286.06 158.06L172.92 271.19a32 32 0 01-45.25 0L105 248.57a32 32 0 010-45.26L218.18 90.17M421.83 293.82L308.69 407a32 32 0 01-45.26 0l-22.62-22.63a32 32 0 010-45.26l113.13-113.17M139.6 169.98l67.88 67.89M275.36 305.75l67.89 67.88" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/></svg>', 'magnet' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M421.83 293.82A144 144 0 0 0 218.18 90.17" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M353.94 225.94a48 48 0 0 0-67.88-67.88" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M192 464v-48M90.18 421.82l33.94-33.94M48 320h48" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/><path d="M286.06 158.06 172.92 271.19a32 32 0 0 1-45.25 0L105 248.57a32 32 0 0 1 0-45.26L218.18 90.17M421.83 293.82 308.69 407a32 32 0 0 1-45.26 0l-22.62-22.63a32 32 0 0 1 0-45.26l113.13-113.17M139.6 169.98l67.88 67.89M275.36 305.75l67.89 67.88" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/></svg>',
'musical-notes' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M192 218v-6c0-14.84 10-27 24.24-30.59l174.59-46.68A20 20 0 01416 154v22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M416 295.94v80c0 13.91-8.93 25.59-22 30l-22 8c-25.9 8.72-52-10.42-52-38h0a33.37 33.37 0 0123-32l51-18.15c13.07-4.4 22-15.94 22-29.85V58a10 10 0 00-12.6-9.61L204 102a16.48 16.48 0 00-12 16v226c0 13.91-8.93 25.6-22 30l-52 18c-13.88 4.68-22 17.22-22 32h0c0 27.58 26.52 46.55 52 38l22-8c13.07-4.4 22-16.08 22-30v-80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'musical-notes' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M192 218v-6c0-14.84 10-27 24.24-30.59l174.59-46.68A20 20 0 0 1 416 154v22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M416 295.94v80c0 13.91-8.93 25.59-22 30l-22 8c-25.9 8.72-52-10.42-52-38h0a33.37 33.37 0 0 1 23-32l51-18.15c13.07-4.4 22-15.94 22-29.85V58a10 10 0 0 0-12.6-9.61L204 102a16.48 16.48 0 0 0-12 16v226c0 13.91-8.93 25.6-22 30l-52 18c-13.88 4.68-22 17.22-22 32h0c0 27.58 26.52 46.55 52 38l22-8c13.07-4.4 22-16.08 22-30v-80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'navigate-down' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 184l144 144 144-144"/></svg>', 'navigate-down' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m112 184 144 144 144-144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
'navigate-left' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M328 112L184 256l144 144"/></svg>', 'navigate-left' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M328 112 184 256l144 144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
'navigate-right' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M184 112l144 144-144 144"/></svg>', 'navigate-right' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m184 112 144 144-144 144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
'navigate-up' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 328l144-144 144 144"/></svg>', 'navigate-up' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m112 328 144-144 144 144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
'people' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M402 168c-2.93 40.67-33.1 72-66 72s-63.12-31.32-66-72c-3-42.31 26.37-72 66-72s69 30.46 66 72z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M336 304c-65.17 0-127.84 32.37-143.54 95.41-2.08 8.34 3.15 16.59 11.72 16.59h263.65c8.57 0 13.77-8.25 11.72-16.59C463.85 335.36 401.18 304 336 304z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M200 185.94c-2.34 32.48-26.72 58.06-53 58.06s-50.7-25.57-53-58.06C91.61 152.15 115.34 128 147 128s55.39 24.77 53 57.94z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M206 306c-18.05-8.27-37.93-11.45-59-11.45-52 0-102.1 25.85-114.65 76.2-1.65 6.66 2.53 13.25 9.37 13.25H154" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/></svg>', 'people' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M402 168c-2.93 40.67-33.1 72-66 72s-63.12-31.32-66-72c-3-42.31 26.37-72 66-72s69 30.46 66 72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M336 304c-65.17 0-127.84 32.37-143.54 95.41-2.08 8.34 3.15 16.59 11.72 16.59h263.65c8.57 0 13.77-8.25 11.72-16.59C463.85 335.36 401.18 304 336 304Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M200 185.94c-2.34 32.48-26.72 58.06-53 58.06s-50.7-25.57-53-58.06C91.61 152.15 115.34 128 147 128s55.39 24.77 53 57.94" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M206 306c-18.05-8.27-37.93-11.45-59-11.45-52 0-102.1 25.85-114.65 76.2-1.65 6.66 2.53 13.25 9.37 13.25H154" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/></svg>',
'play' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M112 111v290c0 17.44 17 28.52 31 20.16l247.9-148.37c12.12-7.25 12.12-26.33 0-33.58L143 90.84c-14-8.36-31 2.72-31 20.16z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/></svg>', 'play' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M112 111v290c0 17.44 17 28.52 31 20.16l247.9-148.37c12.12-7.25 12.12-26.33 0-33.58L143 90.84c-14-8.36-31 2.72-31 20.16Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
'radio' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><circle cx="256" cy="256.02" r="32"/><path d="M184.25 192.25a96 96 0 000 127.52M327.77 319.77a96 96 0 000-127.52M133.28 141.28a168 168 0 000 229.44M378.72 370.72a168 168 0 000-229.44M435 416a240.34 240.34 0 000-320M77 96a240.34 240.34 0 000 320" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'radio' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><circle cx="256" cy="256.02" r="32"/><path d="M184.25 192.25a96 96 0 0 0 0 127.52M327.77 319.77a96 96 0 0 0 0-127.52M133.28 141.28a168 168 0 0 0 0 229.44M378.72 370.72a168 168 0 0 0 0-229.44" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M435 416a240.34 240.34 0 0 0 0-320M77 96a240.34 240.34 0 0 0 0 320" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'save' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M380.93 57.37A32 32 0 00358.3 48H94.22A46.21 46.21 0 0048 94.22v323.56A46.21 46.21 0 0094.22 464h323.56A46.36 46.36 0 00464 417.78V153.7a32 32 0 00-9.37-22.63zM256 416a64 64 0 1164-64 63.92 63.92 0 01-64 64zm48-224H112a16 16 0 01-16-16v-64a16 16 0 0116-16h192a16 16 0 0116 16v64a16 16 0 01-16 16z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'save' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M380.93 57.37A32 32 0 0 0 358.3 48H94.22A46.21 46.21 0 0 0 48 94.22v323.56A46.21 46.21 0 0 0 94.22 464h323.56A46.36 46.36 0 0 0 464 417.78V153.7a32 32 0 0 0-9.37-22.63ZM256 416a64 64 0 1 1 64-64 63.92 63.92 0 0 1-64 64m48-224H112a16 16 0 0 1-16-16v-64a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16v64a16 16 0 0 1-16 16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'school' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M32 192L256 64l224 128-224 128L32 192z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M112 240v128l144 80 144-80V240M480 368V192M256 320v128"/></svg>', 'school' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M32 192 256 64l224 128-224 128z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M112 240v128l144 80 144-80V240M480 368V192M256 320v128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'search' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M221.09 64a157.09 157.09 0 10157.09 157.09A157.1 157.1 0 00221.09 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M338.29 338.29L448 448"/></svg>', 'search' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M221.09 64a157.09 157.09 0 1 0 157.09 157.09A157.1 157.1 0 0 0 221.09 64Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M338.29 338.29 448 448" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/></svg>',
'seeder' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M464 208L352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96"/></svg>', 'seeder' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M464 208 352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'settings' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M262.29 192.31a64 64 0 1057.4 57.4 64.13 64.13 0 00-57.4-57.4zM416.39 256a154.34 154.34 0 01-1.53 20.79l45.21 35.46a10.81 10.81 0 012.45 13.75l-42.77 74a10.81 10.81 0 01-13.14 4.59l-44.9-18.08a16.11 16.11 0 00-15.17 1.75A164.48 164.48 0 01325 400.8a15.94 15.94 0 00-8.82 12.14l-6.73 47.89a11.08 11.08 0 01-10.68 9.17h-85.54a11.11 11.11 0 01-10.69-8.87l-6.72-47.82a16.07 16.07 0 00-9-12.22 155.3 155.3 0 01-21.46-12.57 16 16 0 00-15.11-1.71l-44.89 18.07a10.81 10.81 0 01-13.14-4.58l-42.77-74a10.8 10.8 0 012.45-13.75l38.21-30a16.05 16.05 0 006-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 00-6.07-13.94l-38.19-30A10.81 10.81 0 0149.48 186l42.77-74a10.81 10.81 0 0113.14-4.59l44.9 18.08a16.11 16.11 0 0015.17-1.75A164.48 164.48 0 01187 111.2a15.94 15.94 0 008.82-12.14l6.73-47.89A11.08 11.08 0 01213.23 42h85.54a11.11 11.11 0 0110.69 8.87l6.72 47.82a16.07 16.07 0 009 12.22 155.3 155.3 0 0121.46 12.57 16 16 0 0015.11 1.71l44.89-18.07a10.81 10.81 0 0113.14 4.58l42.77 74a10.8 10.8 0 01-2.45 13.75l-38.21 30a16.05 16.05 0 00-6.05 14.08c.33 4.14.55 8.3.55 12.47z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', 'settings' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M262.29 192.31a64 64 0 1 0 57.4 57.4 64.13 64.13 0 0 0-57.4-57.4M416.39 256a154 154 0 0 1-1.53 20.79l45.21 35.46a10.81 10.81 0 0 1 2.45 13.75l-42.77 74a10.81 10.81 0 0 1-13.14 4.59l-44.9-18.08a16.11 16.11 0 0 0-15.17 1.75A164.5 164.5 0 0 1 325 400.8a15.94 15.94 0 0 0-8.82 12.14l-6.73 47.89a11.08 11.08 0 0 1-10.68 9.17h-85.54a11.11 11.11 0 0 1-10.69-8.87l-6.72-47.82a16.07 16.07 0 0 0-9-12.22 155 155 0 0 1-21.46-12.57 16 16 0 0 0-15.11-1.71l-44.89 18.07a10.81 10.81 0 0 1-13.14-4.58l-42.77-74a10.8 10.8 0 0 1 2.45-13.75l38.21-30a16.05 16.05 0 0 0 6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 0 0-6.07-13.94l-38.19-30A10.81 10.81 0 0 1 49.48 186l42.77-74a10.81 10.81 0 0 1 13.14-4.59l44.9 18.08a16.11 16.11 0 0 0 15.17-1.75A164.5 164.5 0 0 1 187 111.2a15.94 15.94 0 0 0 8.82-12.14l6.73-47.89A11.08 11.08 0 0 1 213.23 42h85.54a11.11 11.11 0 0 1 10.69 8.87l6.72 47.82a16.07 16.07 0 0 0 9 12.22 155 155 0 0 1 21.46 12.57 16 16 0 0 0 15.11 1.71l44.89-18.07a10.81 10.81 0 0 1 13.14 4.58l42.77 74a10.8 10.8 0 0 1-2.45 13.75l-38.21 30a16.05 16.05 0 0 0-6.05 14.08c.33 4.14.55 8.3.55 12.47" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
'tv' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="32" y="96" width="448" height="272" rx="32.14" ry="32.14" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M128 416h256"/></svg>', 'tv' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="448" height="272" x="32" y="96" rx="32.14" ry="32.14" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><path d="M128 416h256" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/></svg>',
'information-circle' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M248 64C146.39 64 64 146.39 64 248s82.39 184 184 184 184-82.39 184-184S349.61 64 248 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 220h32v116"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M208 340h88"/><path d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z" fill="currentColor" stroke="currentColor" stroke-miterlimit="10" stroke-width="1"/></svg>', 'information-circle' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M248 64C146.39 64 64 146.39 64 248s82.39 184 184 184 184-82.39 184-184S349.61 64 248 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 220h32v116"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M208 340h88"/><path d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z" fill="currentColor" stroke="currentColor" stroke-miterlimit="10" stroke-width="1"/></svg>',
'newspaper' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M368 415.86V72a24.07 24.07 0 00-24-24H72a24.07 24.07 0 00-24 24v352a40.12 40.12 0 0040 40h328" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path d="M416 464h0a48 48 0 01-48-48V128h72a24 24 0 0124 24v264a48 48 0 01-48 48z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path d="M240 128h64M240 192h64M112 256h192M112 320h192M112 384h192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M176 208h-64a16 16 0 01-16-16v-64a16 16 0 0116-16h64a16 16 0 0116 16v64a16 16 0 01-16 16z" fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="1"/></svg>', 'newspaper' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M368 415.86V72a24.07 24.07 0 00-24-24H72a24.07 24.07 0 00-24 24v352a40.12 40.12 0 0040 40h328" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path d="M416 464h0a48 48 0 01-48-48V128h72a24 24 0 0124 24v264a48 48 0 01-48 48z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path d="M240 128h64M240 192h64M112 256h192M112 320h192M112 384h192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M176 208h-64a16 16 0 01-16-16v-64a16 16 0 0116-16h64a16 16 0 0116 16v64a16 16 0 01-16 16z" fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="1"/></svg>',
} }

View File

@@ -25,13 +25,14 @@
# Yahya-Lando <yahya-lando@users.noreply.translate.codeberg.org>, 2025. # Yahya-Lando <yahya-lando@users.noreply.translate.codeberg.org>, 2025.
# curtwheeler <curtwheeler@users.noreply.translate.codeberg.org>, 2025. # curtwheeler <curtwheeler@users.noreply.translate.codeberg.org>, 2025.
# return42 <return42@noreply.codeberg.org>, 2025. # return42 <return42@noreply.codeberg.org>, 2025.
# DZDevelopers <dzdevelopers@noreply.codeberg.org>, 2025.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-03-29 09:21+0000\n" "POT-Creation-Date: 2025-03-29 09:21+0000\n"
"PO-Revision-Date: 2025-03-31 18:08+0000\n" "PO-Revision-Date: 2025-04-15 10:37+0000\n"
"Last-Translator: return42 <return42@noreply.codeberg.org>\n" "Last-Translator: DZDevelopers <dzdevelopers@noreply.codeberg.org>\n"
"Language-Team: Arabic <https://translate.codeberg.org/projects/searxng/" "Language-Team: Arabic <https://translate.codeberg.org/projects/searxng/"
"searxng/ar/>\n" "searxng/ar/>\n"
"Language: ar\n" "Language: ar\n"
@@ -535,15 +536,15 @@ msgstr "جودة الملف"
#: searx/plugins/ahmia_filter.py:32 #: searx/plugins/ahmia_filter.py:32
msgid "Ahmia blacklist" msgid "Ahmia blacklist"
msgstr "" msgstr "{دالة}"
#: searx/plugins/ahmia_filter.py:33 #: searx/plugins/ahmia_filter.py:33
msgid "Filter out onion results that appear in Ahmia's blacklist." msgid "Filter out onion results that appear in Ahmia's blacklist."
msgstr "" msgstr "قم بتصفية نتائج .onion التي تظهر في القائمة السوداء الخاصة بـ Ahmia."
#: searx/plugins/calculator.py:38 #: searx/plugins/calculator.py:38
msgid "Basic Calculator" msgid "Basic Calculator"
msgstr "" msgstr "آلة حاسبة بسيطة"
#: searx/plugins/calculator.py:39 #: searx/plugins/calculator.py:39
msgid "Calculate mathematical expressions via the search bar" msgid "Calculate mathematical expressions via the search bar"
@@ -639,7 +640,7 @@ msgstr ""
#: searx/plugins/unit_converter.py:49 #: searx/plugins/unit_converter.py:49
msgid "Unit converter plugin" msgid "Unit converter plugin"
msgstr "" msgstr "إضافة محول الوحدات"
#: searx/plugins/unit_converter.py:50 #: searx/plugins/unit_converter.py:50
msgid "Convert between units" msgid "Convert between units"
@@ -1190,6 +1191,8 @@ msgid ""
"A URL containing your preferences. This URL can be used to restore your " "A URL containing your preferences. This URL can be used to restore your "
"settings on a different device." "settings on a different device."
msgstr "" msgstr ""
"رابط يحتوي على تفضيلاتك. يمكن استخدام هذا الرابط لاستعادة إعداداتك على جهاز "
"مختلف."
#: searx/templates/simple/preferences/cookies.html:46 #: searx/templates/simple/preferences/cookies.html:46
msgid "Copy preferences hash" msgid "Copy preferences hash"

View File

@@ -18,21 +18,22 @@
# Kjev <Kjev@users.noreply.translate.codeberg.org>, 2025. # Kjev <Kjev@users.noreply.translate.codeberg.org>, 2025.
# KinoCineaste <kinocineaste@users.noreply.translate.codeberg.org>, 2025. # KinoCineaste <kinocineaste@users.noreply.translate.codeberg.org>, 2025.
# AlanBacker <alanbacker@users.noreply.translate.codeberg.org>, 2025. # AlanBacker <alanbacker@users.noreply.translate.codeberg.org>, 2025.
# return42 <return42@noreply.codeberg.org>, 2025.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-03-29 09:21+0000\n" "POT-Creation-Date: 2025-03-29 09:21+0000\n"
"PO-Revision-Date: 2025-02-17 13:01+0000\n" "PO-Revision-Date: 2025-05-09 07:09+0000\n"
"Last-Translator: AlanBacker " "Last-Translator: return42 <return42@noreply.codeberg.org>\n"
"<alanbacker@users.noreply.translate.codeberg.org>\n" "Language-Team: Esperanto <https://translate.codeberg.org/projects/searxng/"
"searxng/eo/>\n"
"Language: eo\n" "Language: eo\n"
"Language-Team: Esperanto "
"<https://translate.codeberg.org/projects/searxng/searxng/eo/>\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.2\n"
"Generated-By: Babel 2.17.0\n" "Generated-By: Babel 2.17.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING'] #. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -260,7 +261,7 @@ msgstr ""
#: searx/engines/duckduckgo_weather.py:81 searx/engines/wttr.py:36 #: searx/engines/duckduckgo_weather.py:81 searx/engines/wttr.py:36
#: searx/searxng.msg #: searx/searxng.msg
msgid "Sunrise" msgid "Sunrise"
msgstr "" msgstr "Sunleviĝo"
#. WEATHER_TERMS['SUNSET'] #. WEATHER_TERMS['SUNSET']
#: searx/engines/duckduckgo_weather.py:82 searx/engines/wttr.py:37 #: searx/engines/duckduckgo_weather.py:82 searx/engines/wttr.py:37
@@ -2067,4 +2068,3 @@ msgstr "kaŝi videojn"
#~ "Specifante kutimajn agordojn en la URL" #~ "Specifante kutimajn agordojn en la URL"
#~ " de preferoj povas esti uzata por " #~ " de preferoj povas esti uzata por "
#~ "sinkronigi preferojn tra aparatoj." #~ "sinkronigi preferojn tra aparatoj."

View File

@@ -43,13 +43,14 @@
# pxrb <pxrb@users.noreply.translate.codeberg.org>, 2025. # pxrb <pxrb@users.noreply.translate.codeberg.org>, 2025.
# curtwheeler <curtwheeler@users.noreply.translate.codeberg.org>, 2025. # curtwheeler <curtwheeler@users.noreply.translate.codeberg.org>, 2025.
# return42 <return42@noreply.codeberg.org>, 2025. # return42 <return42@noreply.codeberg.org>, 2025.
# Atul_Eterno <atul_eterno@noreply.codeberg.org>, 2025.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-03-29 09:21+0000\n" "POT-Creation-Date: 2025-03-29 09:21+0000\n"
"PO-Revision-Date: 2025-04-08 11:34+0000\n" "PO-Revision-Date: 2025-04-24 14:06+0000\n"
"Last-Translator: return42 <return42@noreply.codeberg.org>\n" "Last-Translator: Atul_Eterno <atul_eterno@noreply.codeberg.org>\n"
"Language-Team: Spanish <https://translate.codeberg.org/projects/searxng/" "Language-Team: Spanish <https://translate.codeberg.org/projects/searxng/"
"searxng/es/>\n" "searxng/es/>\n"
"Language: es\n" "Language: es\n"
@@ -553,7 +554,7 @@ msgstr "Calidad del archivo"
#: searx/plugins/ahmia_filter.py:32 #: searx/plugins/ahmia_filter.py:32
msgid "Ahmia blacklist" msgid "Ahmia blacklist"
msgstr "" msgstr "Lista negra de Ahmia"
#: searx/plugins/ahmia_filter.py:33 #: searx/plugins/ahmia_filter.py:33
msgid "Filter out onion results that appear in Ahmia's blacklist." msgid "Filter out onion results that appear in Ahmia's blacklist."
@@ -1210,6 +1211,8 @@ msgid ""
"A URL containing your preferences. This URL can be used to restore your " "A URL containing your preferences. This URL can be used to restore your "
"settings on a different device." "settings on a different device."
msgstr "" msgstr ""
"Una URL conteniendo sus preferencias. Esta URL puede ser usada para "
"restaurar sus ajustes en un dispositivo diferente."
#: searx/templates/simple/preferences/cookies.html:46 #: searx/templates/simple/preferences/cookies.html:46
msgid "Copy preferences hash" msgid "Copy preferences hash"

View File

@@ -23,13 +23,14 @@
# Parsa Ranjbar <parsa@users.noreply.translate.codeberg.org>, 2025. # Parsa Ranjbar <parsa@users.noreply.translate.codeberg.org>, 2025.
# arashe22 <arashe22@users.noreply.translate.codeberg.org>, 2025. # arashe22 <arashe22@users.noreply.translate.codeberg.org>, 2025.
# return42 <return42@noreply.codeberg.org>, 2025. # return42 <return42@noreply.codeberg.org>, 2025.
# ehsanrs2 <ehsanrs2@noreply.codeberg.org>, 2025.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-03-29 09:21+0000\n" "POT-Creation-Date: 2025-03-29 09:21+0000\n"
"PO-Revision-Date: 2025-03-31 18:08+0000\n" "PO-Revision-Date: 2025-05-06 12:53+0000\n"
"Last-Translator: return42 <return42@noreply.codeberg.org>\n" "Last-Translator: ehsanrs2 <ehsanrs2@noreply.codeberg.org>\n"
"Language-Team: Persian <https://translate.codeberg.org/projects/searxng/" "Language-Team: Persian <https://translate.codeberg.org/projects/searxng/"
"searxng/fa/>\n" "searxng/fa/>\n"
"Language: fa_IR\n" "Language: fa_IR\n"
@@ -533,15 +534,15 @@ msgstr "کیفیت فایل"
#: searx/plugins/ahmia_filter.py:32 #: searx/plugins/ahmia_filter.py:32
msgid "Ahmia blacklist" msgid "Ahmia blacklist"
msgstr "" msgstr "لیست سیاه Ahmia"
#: searx/plugins/ahmia_filter.py:33 #: searx/plugins/ahmia_filter.py:33
msgid "Filter out onion results that appear in Ahmia's blacklist." msgid "Filter out onion results that appear in Ahmia's blacklist."
msgstr "" msgstr "نتایج onion که در لیست سیاه Ahmia ظاهر می‌شوند را فیلتر کنید."
#: searx/plugins/calculator.py:38 #: searx/plugins/calculator.py:38
msgid "Basic Calculator" msgid "Basic Calculator"
msgstr "" msgstr "ماشین حساب اولیه"
#: searx/plugins/calculator.py:39 #: searx/plugins/calculator.py:39
msgid "Calculate mathematical expressions via the search bar" msgid "Calculate mathematical expressions via the search bar"
@@ -614,7 +615,7 @@ msgstr ""
#: searx/plugins/tor_check.py:65 #: searx/plugins/tor_check.py:65
msgid "Could not download the list of Tor exit-nodes from" msgid "Could not download the list of Tor exit-nodes from"
msgstr "دانلود لیست گره‌های خروجی تور از این مسیر ممکن نیست:" msgstr "نتوانستم لیست گره‌های خروجی Tor را از اینجا دانلود کنم"
#: searx/plugins/tor_check.py:72 #: searx/plugins/tor_check.py:72
msgid "You are using Tor and it looks like you have the external IP address" msgid "You are using Tor and it looks like you have the external IP address"
@@ -634,7 +635,7 @@ msgstr "آرگومان های ردیاب ها را از URL برگشتی حذف
#: searx/plugins/unit_converter.py:49 #: searx/plugins/unit_converter.py:49
msgid "Unit converter plugin" msgid "Unit converter plugin"
msgstr "" msgstr "افزونه تبدیل واحد"
#: searx/plugins/unit_converter.py:50 #: searx/plugins/unit_converter.py:50
msgid "Convert between units" msgid "Convert between units"
@@ -681,7 +682,7 @@ msgstr "ردیاب مشکل"
#: searx/templates/simple/base.html:70 searx/templates/simple/stats.html:18 #: searx/templates/simple/base.html:70 searx/templates/simple/stats.html:18
msgid "Engine stats" msgid "Engine stats"
msgstr "آمار موتور" msgstr "وضعیت موتور"
#: searx/templates/simple/base.html:72 #: searx/templates/simple/base.html:72
msgid "Public instances" msgid "Public instances"
@@ -693,7 +694,7 @@ msgstr "سیاست حفظ حریم خصوصی"
#: searx/templates/simple/base.html:78 #: searx/templates/simple/base.html:78
msgid "Contact instance maintainer" msgid "Contact instance maintainer"
msgstr "تماس با مسئول‌نگهداری نمونه" msgstr "تماس با نگهدارنده نمونه"
#: searx/templates/simple/categories.html:30 #: searx/templates/simple/categories.html:30
msgid "Click on the magnifier to perform search" msgid "Click on the magnifier to perform search"
@@ -1185,6 +1186,8 @@ msgid ""
"A URL containing your preferences. This URL can be used to restore your " "A URL containing your preferences. This URL can be used to restore your "
"settings on a different device." "settings on a different device."
msgstr "" msgstr ""
"یک URL حاوی تنظیمات برگزیده شما. از این URL می‌توان برای بازیابی تنظیمات شما "
"در دستگاه دیگری استفاده کرد."
#: searx/templates/simple/preferences/cookies.html:46 #: searx/templates/simple/preferences/cookies.html:46
msgid "Copy preferences hash" msgid "Copy preferences hash"
@@ -1244,7 +1247,7 @@ msgstr "زمان بیشینه"
#: searx/templates/simple/preferences/favicon.html:2 #: searx/templates/simple/preferences/favicon.html:2
msgid "Favicon Resolver" msgid "Favicon Resolver"
msgstr "" msgstr "حل کننده فاویکون"
#: searx/templates/simple/preferences/favicon.html:15 #: searx/templates/simple/preferences/favicon.html:15
msgid "Display favicons near search results" msgid "Display favicons near search results"

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