107 Commits

Author SHA1 Message Date
dependabot[bot] 21205c1e61 [upd] web-client (simple): Bump sort-package-json in /client/simple
Bumps [sort-package-json](https://github.com/keithamus/sort-package-json) from 3.6.1 to 4.0.0.
- [Release notes](https://github.com/keithamus/sort-package-json/releases)
- [Commits](https://github.com/keithamus/sort-package-json/compare/v3.6.1...v4.0.0)

---
updated-dependencies:
- dependency-name: sort-package-json
  dependency-version: 4.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-12 07:23:35 +00:00
Bnyro 4dd0bf4867 [fix] fireball: all results are shown in general category 2026-06-11 17:30:46 +02:00
Bnyro 1957876dd6 [feat] engines: add dogpile (general, news, images, videos)
Add support for the Dogpile search engine, found at:

https://seirdy.one/posts/2021/03/10/search-engines-with-own-indexes/

It seems to use the same index as startpage because results are similar and they
share the ``qadf`` (Safe-Search) request parameter.
2026-06-11 16:09:13 +02:00
Bnyro ab13451086 [mod] odysee: move format_duration helper into utils.py 2026-06-11 16:09:13 +02:00
Bnyro a1490676e3 [mod] fireball: small fixup from code review (#6240)
Co-authored-by: Markus Heiser <markus.heiser@darmarIT.de>
2026-06-11 12:09:57 +02:00
Bnyro 3a382cb3f3 [chore] helix config: enable pyling and use black via pylsp 2026-06-11 11:03:38 +02:00
Ivan Gabaldon 9d9d605b15 [fix] ci: use install buildhost script (#6105) 2026-06-11 08:23:37 +02:00
Bnyro de03f4eb11 [feat] engines: add fireball engine (general, news, videos) 2026-06-10 21:00:49 +02:00
Markus Heiser 00f7c68a6f [chore] drop emacs' obsolete .dir-locals template (#6236)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-10 17:38:19 +02:00
Bnyro 41c98b3b41 [chore] devops: add languages config for helix editor
The default Helix configuration for Python is different,
so the pylint warnings aren't shown and the formatter
re-formats files by accident when you edit an existing file.

Therefore, this commit adds `python` language configuration
to ease developing SearXNG with Helix Editor [^1].

[^1]: https://helix-editor.com
2026-06-10 17:38:01 +02:00
Bnyro f4c63c8eb0 [feat] engines: add duckduckgo web engine as alternative to html.duckduckgo.com
html.duckduckgo.com captchas all my IPs very fast. I figured out that using
duckduckgo.com works even if html.duckduckgo.com is captcha-ed, hence adding
support for duckduckgo.com's general web search here.

This implementation fetches the link to the first API page
(i.e. ``links.duckduckgo.com/d.js?...``) from duckduckgo.com and uses the ``n``
parameter of the API to fetch all subsequent pages.

This also means that it's not possible to immediately search for the third
page - the first and the second page would need to be loaded first.

The reason why we can't just normally use the `vqd` value is that the API URLs
require an additional parameter `dp` which seems generated at server-side, so we
can't build it ourselves and must scrape it from the HTML pages.
2026-06-10 16:49:56 +02:00
Markus Heiser 26801e92af [fix] sqlitedb: create DB Schema (DDL) during app initialization (hardening) (#6187)
The initialization of the DB schema ("base schema") has so far been done on
demand, which causes race conditions with competing threads and processes.

The DDL statements for creating the "base schema" are now executed as part of
the initialization of the app.

Further improvements were made to harden the database applications:

- Wikidata & Radio-Browser engine perform their initialization only once (so far
  the initialization was carried out in each thread/process).

- If multiple processes try to set DB's WAL mode when opening the DB at the same
  time, this usually leads to another race condition, which is now also caught.

Related:

- https://github.com/searxng/searxng/issues/6181#issuecomment-4586705

Closes: #6181

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-10 15:48:49 +02:00
Bnyro f3fab143be [feat] engines: add tiger.ch engine
Add support for https://tiger.ch (general, news)

It is disabled and inactive by default because it's just a metasearch engine
like SearXNG is, so it's mostly useful for bypassing rate-limits on other
engines: (it has its own German index, but it's not that great) in theory it
supports different locales, but I was too lazy to implement that (I only need
German and English results anyways, which are returned by default...)
2026-06-08 13:35:13 +02:00
Bnyro 72a827ae93 [fix] yep: send Sec-Fetch headers to bypass "access denied" (#6223)
Avoids yep's botblocking by sending Sec-Fetch-* headers (as the browser does).
2026-06-08 10:55:17 +02:00
Bnyro 6ca9d3784c [feat] engines: add seek-ninja general engine (#6217)
Add support for https://seek.ninja (general)

It's very slow because the engine uses Server-side events, that incrementally
send data in their HTTP response [1].

I.e. we wait for the end of the response (7+ seconds), even though the results
data arrives within a few seconds -> it's very slow, because SearXNG wants to
get the full response body before it calls the `response(resp)` method

We could use httpx-sse [2], but I'm not sure how to integrate this into SearXNG
and if it's worth it

[1] https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/
[2] https://github.com/florimondmanca/httpx-sse
2026-06-08 07:09:06 +02:00
Bnyro 63f264220b [feat] engines: add heexy engine (general, images) (#6218) 2026-06-08 05:54:35 +02:00
Austin-Olacsi 41fcf0be4b [fix] aol engine uses wikidata id for C++ (#6221) 2026-06-08 05:32:26 +02:00
Bnyro 86903a2c66 [fix] flaticon: crash if result tag has no name (#6219) 2026-06-07 14:16:44 +02:00
Markus Heiser 70de3cc561 Revert "[fix] no such table during engine init (#6185)" (#6215)
This reverts commit 9d49a9f344.
2026-06-07 09:23:35 +02:00
Bnyro 51b6fd4f23 [del] karmasearch: remove engine (cloudflared) (#6213)
The engine is using very aggressive Cloudflare blocking for
a while now, no matter if using a normal browser like Firefox
or not.

Closes: https://github.com/searxng/searxng/issues/5976
2026-06-07 06:49:09 +02:00
Brock Vojkovic 9d49a9f344 [fix] no such table during engine init (#6185) 2026-06-07 06:04:12 +02:00
Bnyro e260a732c8 [fix] online engine processor: accept language headers doesn't get sent for 'all' language 2026-06-06 18:24:16 +02:00
Markus Heiser 0429198415 [mod] swisscows WEB: ignore video results from the first page
On the first page of the WEB search, there are, among other things, sections for
videos and news.  The video results from these sections should not be used as
results in the WEB search of SearXNG.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-06 18:04:19 +02:00
Markus Heiser e7cf57e9ae [mod] swisscows engines: add language / region support
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-06 18:04:19 +02:00
Bnyro ed369ac0ec [feat] engines: add support for swisscows general 2026-06-06 18:04:19 +02:00
Bnyro 94bdbb5c63 [feat] engines: add support for swisscows videos 2026-06-06 18:04:19 +02:00
Bnyro 465b5229c6 [feat] engines: add swisscows news engine 2026-06-06 18:04:19 +02:00
Bnyro cbf97fd262 [feat] engines: add swisscows images engine
The implementation is basically a 1:1 port of the reverse engineered
swisscows JavaScript code. (it's been obfuscated, so I've restructured it
and made the variable names idiomatic instead of obfuscated var names like "a", "o", "i")

```js
/*
e: "/v5/images/search"
t: {
	itemsCount: "50"
	locale: "de-DE"
	offset: "50"
	query: "test"
	spellcheck: "true"
}
*/
// HASH library used: https://github.com/h2non/jshashes
function generateNonceAndSignature(queryParams, urlPath) {
  // urlPath = "/v5/images/search"
  // sort keys alphabetically and join to query string
  let queryStringSorted = '?' + U().stringify(queryParams, {
    arrayFormat: 'repeat',
    allowDots: !0
  }).split('&').map(e => {
    let[key, value] = e.split('=');
    return [key, decodeURIComponent(value)]
  }).sort((e, t) => e[0].localeCompare(t[0])).map(e => e.join('=')).join('&');

  function caesarShift(str, offset = 13) {
      const alphabet = 'abcdefghijklmnopqrstuvwxyz';
      let result = [];
      for (let a = 0; a < str.length; a++) {
        let c = str[a],
        alphabetIndex = alphabet.indexOf(c.toLowerCase());
        if ( - 1 !== alphabetIndex) {
          alphabetIndex += offset;
          while (alphabetIndex >= alphabet.length) alphabetIndex -= alphabet.length;
          c = c === c.toUpperCase() ? alphabet[alphabetIndex] : alphabet[alphabetIndex].toUpperCase()
        }
        result.push(c)
      }
      return result.join('')
    }
  const r = new (sha256Instance()).SHA256;
  const random = randomString(32);
  const randomShifted = caesarShift(random);
  let to_hash = [urlPath, queryStringSorted, randomShifted].join('');
  let signature = r.b64(to_hash);
  signature = signature.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  return {
    nonce: random,
    signature: signature
  }
}

function randomString(length) {
  let t = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~',
  n = '';
  for (let r = 0; r < length; r++) n += t.charAt(Math.floor(Math.random() * t.length));
  return n
}
```
2026-06-06 18:04:19 +02:00
dependabot[bot] 37187dc2d8 [upd] web-client (simple): Bump the minor group across 1 directory with 5 updates (#6169)
Bumps the minor group with 5 updates in the /client/simple directory:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.15` | `2.4.16` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.8.0` | `25.9.1` |
| [edge.js](https://github.com/edge-js/edge) | `6.5.0` | `6.5.1` |
| [stylelint](https://github.com/stylelint/stylelint) | `17.11.1` | `17.12.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.13` | `8.0.16` |

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

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

Updates `edge.js` from 6.5.0 to 6.5.1
- [Release notes](https://github.com/edge-js/edge/releases)
- [Changelog](https://github.com/edge-js/edge/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/edge-js/edge/compare/v6.5.0...v6.5.1)

Updates `stylelint` from 17.11.1 to 17.12.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/17.11.1...17.12.0)

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

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: edge.js
  dependency-version: 6.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: stylelint
  dependency-version: 17.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: vite
  dependency-version: 8.0.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 16:26:27 +02:00
dependabot[bot] 2f049cb037 [upd] github-actions: Bump actions/checkout from 6.0.2 to 6.0.3 (#6204)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 16:17:01 +02:00
dependabot[bot] eb39bc0dc1 [upd] github-actions: Bump github/codeql-action from 4.36.0 to 4.36.2 (#6203)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.0 to 4.36.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/7211b7c8077ea37d8641b6271f6a365a22a5fbfa...8aad20d150bbac5944a9f9d289da16a4b0d87c1e)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 16:16:35 +02:00
dependabot[bot] 007a4e2155 [upd] pypi: Bump typer from 0.26.3 to 0.26.7 in the minor group (#6205)
Bumps the minor group with 1 update: [typer](https://github.com/fastapi/typer).


Updates `typer` from 0.26.3 to 0.26.7
- [Release notes](https://github.com/fastapi/typer/releases)
- [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md)
- [Commits](https://github.com/fastapi/typer/compare/0.26.3...0.26.7)
2026-06-05 11:54:28 +02:00
github-actions[bot] 13ce187e64 [l10n] update translations from Weblate (#6206)
19b2047a9 - 2026-05-30 - daemul72 <daemul72@noreply.codeberg.org>
2026-06-05 11:52:35 +02:00
Bnyro 26fa181b84 [feat] gmx: detect captchas 2026-06-05 08:07:30 +02:00
Bnyro 0f35ef7cd6 [feat] json engine: add option to not send page num on first page 2026-06-05 08:04:49 +02:00
Bnyro b1ae576b2d [fix] xpath engine: add missing send_page_num_on_first_page docstring 2026-06-05 08:04:49 +02:00
Bnyro e6559c9ad6 [fix] gabanza: result URLs are invalid 2026-06-04 08:55:19 +02:00
Bnyro 5bae05514b [feat] engines: add zapmeta general search engine 2026-06-03 22:38:59 +02:00
Bnyro 00ca5776f2 [feat] engines: add gabanza general engine 2026-06-03 22:38:23 +02:00
Bnyro 577f5f2f30 [fix] online engines: send_accept_language_header is sent even if disabled 2026-06-03 22:37:13 +02:00
Bnyro 253dc86c10 [fix] duckduckgo: image requests get blocked 2026-06-03 22:37:13 +02:00
Bnyro 3066bc19eb [fix] public domain image archive: fails to extract API url 2026-06-03 22:35:21 +02:00
Austin-Olacsi e964708c00 [fix] bilibili engine: fix Referer and add Accept HTTP header (#6189) 2026-06-02 06:06:31 +02:00
Bnyro 7159b8aed3 [feat] marginalia: add support for pagination 2026-05-31 12:54:53 +02:00
Bnyro 246f5a5499 [mod] svgrepo: remove engine
- SVGRepo uses Cloudflare for every session, no matter
if you're opening it in a browser or not
2026-05-31 12:54:32 +02:00
vojkovic 300695de5c [fix] crash when lock is omitted 2026-05-31 01:37:37 +08:00
Markus Heiser bd863f16b1 [build] /static 2026-05-30 22:43:50 +08:00
Markus Heiser 4ac822fd7f [mod] typification of the preference settings
no functional change / except the missing online doc which is now available::

    $ make docs.live
    $ xdg-open "http://127.0.0.1:8000/admin/settings/settings_preferences.html"

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-30 22:43:50 +08:00
vojkovic e1d25c5078 [mod] enable image proxy for public instances 2026-05-30 22:43:50 +08:00
dependabot[bot] 01159b82fe [upd] pypi: Bump the minor group with 3 updates (#6164)
Bumps the minor group with 3 updates: [granian](https://github.com/emmett-framework/granian), [basedpyright](https://github.com/detachhead/basedpyright) and [typer](https://github.com/fastapi/typer).


Updates `granian` from 2.7.4 to 2.7.5
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.7.4...v2.7.5)

Updates `basedpyright` from 1.39.5 to 1.39.6
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.39.5...v1.39.6)

Updates `typer` from 0.25.1 to 0.26.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.25.1...0.26.3)
2026-05-30 11:33:10 +02:00
Bnyro 780ee32564 [fix] pexels: fix engine crashes with SearxEngineAccessDeniedException 2026-05-29 22:03:22 +02:00
github-actions[bot] 217c9a1597 [l10n] update translations from Weblate (#6170)
207f98ecc - 2026-05-26 - mustafa-phd <mustafa-phd@noreply.codeberg.org>
3b51fbca7 - 2026-05-25 - Amirkhandrend-Nicest-XII <amirkhandrend-nicest-xii@noreply.codeberg.org>

Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-05-29 14:47:43 +02:00
dependabot[bot] 70e810bd7b [upd] github-actions: Bump docker/setup-qemu-action from 4.0.0 to 4.1.0 (#6166)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/ce360397dd3f832beb865e1373c09c0e9f86d70a...06116385d9baf250c9f4dcb4858b16962ea869c3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 14:42:48 +02:00
dependabot[bot] baab1c160a [upd] github-actions: Bump github/codeql-action from 4.35.5 to 4.36.0 (#6167)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.5 to 4.36.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e0d7b8d25671d64c341c19c0152d693099fb5ba...7211b7c8077ea37d8641b6271f6a365a22a5fbfa)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 14:42:28 +02:00
dependabot[bot] dd4664e03a [upd] github-actions: Bump docker/login-action from 4.1.0 to 4.2.0 (#6168)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...650006c6eb7dba73a995cc03b0b2d7f5ca915bee)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 14:41:49 +02:00
github-actions[bot] 4ea5c57a84 [data] update searx.data - update_firefox_version.py (#6161) 2026-05-29 06:25:18 +02:00
github-actions[bot] 6917395dc1 [data] update searx.data - update_wikidata_units.py (#6160) 2026-05-29 06:24:50 +02:00
github-actions[bot] 128e28fe3f [data] update searx.data - update_gsa_useragents.py (#6158) 2026-05-29 06:24:11 +02:00
github-actions[bot] fb3ed5b081 [data] update searx.data - update_ahmia_blacklist.py (#6159) 2026-05-29 06:22:55 +02:00
github-actions[bot] 4ebe6b90d6 [data] update searx.data - update_currencies.py (#6162) 2026-05-29 06:21:55 +02:00
github-actions[bot] 0657217a3e [data] update searx.data - update_engine_descriptions.py (#6163) 2026-05-29 06:20:57 +02:00
Bnyro 0037d43d87 [fix] aol: disable http2 to prevent request fingerprinting (#6149) 2026-05-26 12:15:35 +02:00
Bnyro f5be39e245 [mod] podcastindex: remove engine (#6140)
PodcastIndex.org started using a Proof-of-Work JavaScript
challenge whose results are sent as `X-Pow-*` request headers.
Although it is technically possible to re-implement the
PoW challenge in Python, it's likely impossible to maintain
because

- the actual Proof of Concept logic might change very often

- the whole idea of the Proof of Work challenge is to use
  a "big" amount of resources (about 1s on my PC); so executing the challenge
  would almost block all other work on the SearXNG instance

At first glance, the challenge looks very similar to what
Anubis does, because it also uses SHA256 hashes.
2026-05-26 11:53:20 +02:00
Bnyro 1574939441 [fix] json, xpath engine: rename safe_search_support option to safesearch (#6143) 2026-05-26 11:38:07 +02:00
Markus Heiser f1a22dec9e [fix] disable qwant engine / the rate-limits are just very strict (#6148)
Qwant is set to inactive by default due to its strict rate-limits

Related:

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

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-26 11:05:06 +02:00
Markus Heiser 3db8b424a8 [mod] engine flaticon: migrate from LegacyResult to Image (#6142)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-26 09:19:17 +02:00
Markus Heiser a16a3dedb4 [build] /static (#6142) 2026-05-26 09:19:17 +02:00
Markus Heiser c629dd4f3c [mod] typification of SearXNG: add new result type Image (#6142)
- Python class:   searx/result_types/image.py
- Jinja template: searx/templates/simple/result_templates/images.html
- CSS (less)      client/simple/src/less/result_types/image.less

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-26 09:19:17 +02:00
Markus Heiser 28ef4f7447 [mod] hardening of the Result.filter_urls() method (#6117)
Exceptions in the execution of the callback must be caught / ignored and logged
on the ERROR log.

To test, apply this patch to provoke a ValueError exception::

    diff --git a/searx/data/tracker_patterns.py b/searx/data/tracker_patterns.py
    index ed4415bce..695ed05d2 100644
    --- a/searx/data/tracker_patterns.py
    +++ b/searx/data/tracker_patterns.py
    @@ -114,6 +114,7 @@ class TrackerPatternsDB:
             Returns bool ``True`` to use URL unchanged (``False`` to ignore URL).
             If URL should be modified, the returned string is the new URL to use.
             """
    +        raise ValueError("test callback exceptions")

             new_url = url
             parsed_new_url = urlparse(url=new_url)

Start a `make run` instance and query for example `amazon` .. have a look at the
ERROR log:

    ERROR   searx.result_types: filter_urls (field 'url'): ignore ValueError('test callback exceptions') from callback searx/data/tracker_patterns.py:117

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-25 18:12:40 +02:00
Bnyro cb4b70ac50 [fix] qwant news: results don't have any descriptions (#6135)
BTW: fix some typecast issues
2026-05-25 18:04:14 +02:00
Markus Heiser e29e861e2c [fix] bing engines - geoblocking in China (#6134)
In regions like China, the domain must be adjusted to avoid a redirect.

- https://github.com/searxng/searxng/issues/5243
- https://github.com/searxng/searxng/pull/5324
- https://github.com/searxng/searxng/pull/6133

Suggested / tested by @hubutui in https://github.com/searxng/searxng/pull/6133#issuecomment-4534637069

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-25 17:05:08 +02:00
Markus Heiser 89b89a88fe [mod] engine: MyMemory Translated - typification and html to text (#6132)
The implementation is normalized, type annotations are applied, and the results
are freed from the HTML markup (which is partially present).

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-25 16:38:06 +02:00
Bnyro 46071a011a [mod] qwant: remove web lite and improve request spoofing (#6127)
- https://lite.qwant.com seems to be dead.
- The request parameters were changed to match the ones from the Qwant website.
- Qwant is now set to inactive by default due to its strict rate-limits
2026-05-25 15:46:40 +02:00
Bnyro b0d8af96bf [feat] engines: add flaticon icons engine (#6122) 2026-05-25 13:41:44 +02:00
Markus Heiser dd27fce3b7 [unbload] drop meaningless field `number_of_results_xpath` from results (#6130)
In the result-list, the ``number_of_results`` indicate the number of hits in the
Index, they do not indicate how many results are in the answer.

In the past, search engines such as google or ddg had an indication on the first
page of a search term of how many hits there were for this term in total in
their index.

This info was added up in SearXNG and delivered under ``number_of_results``.
Nowadays the search engines no longer indicate how many hits there are in the
index and so this field in SearXNG is also superfluous.

- https://github.com/searxng/searxng/issues/2457#issuecomment-2566181574
- https://github.com/searxng/searxng/issues/2987
- https://github.com/searxng/searxng/issues/5034

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-25 12:43:02 +02:00
Markus Heiser efc305b7f9 [mod] normalize variable name for the max number of results per request (#6131)
[mod] normalize variable name for the max number of results per request

In the past, we have used different names for the variable that specifies the
maximum number of hits in the outgoing request.

- ``page_size``
- ``number_of_results``
- ``nb_per_page``

Since *page_size* is the most accurate term and is also used in the XPath
engines, all other engines are adjusted accordingly within this
patch .. documentation adjusted accordingly.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-25 12:41:31 +02:00
Bnyro 323ce76004 [fix] startpage: all requests get blocked with CAPTCHA
Changes:
- Setting the "abp" query parameter causes instant blocks, it's no longer
used at Startpage
- The safesearch map changed for both the request form and the cookies. As
we were sending invalid values, that also made it easier to detect us
2026-05-23 09:43:17 +02:00
Bnyro dfc2da707b [fix] mojeek: access denied because of wrong request parameters 2026-05-23 09:43:03 +02:00
Bnyro fc90c5b09c [fix] yep: api path changed 2026-05-22 21:38:26 +02:00
dependabot[bot] c57f772ad0 [upd] github-actions: Bump github/codeql-action from 4.35.4 to 4.35.5 (#6114)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.4 to 4.35.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 14:50:44 +02:00
dependabot[bot] b8498d7891 [upd] github-actions: Bump docker/scout-action from 1.20.4 to 1.21.0 (#6112)
Bumps [docker/scout-action](https://github.com/docker/scout-action) from 1.20.4 to 1.21.0.
- [Release notes](https://github.com/docker/scout-action/releases)
- [Commits](https://github.com/docker/scout-action/compare/bacf462e8d090c09660de30a6ccc718035f961e3...cd72f264beff1cd72735de31148b9d3244a0234a)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 14:50:31 +02:00
github-actions[bot] d791a3906a [l10n] update translations from Weblate (#6116) 2026-05-22 14:29:10 +02:00
dependabot[bot] 295e0bffaf [upd] pypi: Bump the minor group with 3 updates (#6113)
Bumps the minor group with 3 updates: [certifi](https://github.com/certifi/python-certifi), [lxml](https://github.com/lxml/lxml) and [basedpyright](https://github.com/detachhead/basedpyright).


Updates `certifi` from 2026.4.22 to 2026.5.20
- [Commits](https://github.com/certifi/python-certifi/compare/2026.04.22...2026.05.20)

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

Updates `basedpyright` from 1.39.4 to 1.39.5
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.39.4...v1.39.5)
2026-05-22 14:28:09 +02:00
Bnyro b9340f50c2 [fix] preferences: opening preferences page is very slow (multiple seconds on bad hardware) (#6090)
I've been profiling the `/preferences` endpoint using werkzeug's
`ProfilerMiddleware` (i.e. just do `app.wsgi_app = ProfilerMiddleware(app.wsgi_app)`)
and look at the outputs in the terminal when doing `make run`.

It turns out that 95%+ of the time spent were inside babel's
Locale parsing (> 700ms on my machine). That's because, when opening the settings,
we loaded the full engine traits of each engine and checked if it matches
the user-defined search language. As we have 250+ engines, and babel is
very slow when parsing Locale's, this took a very long time.

By removing this feature that shows whether the selected search language
is supported by the engine, the load time went down from 800ms to 50ms
on my machine (which is still very slow, but well, that's future work on
optimizing).
2026-05-21 21:15:09 +02:00
Markus Heiser d3deacc6d4 [mod] engine fyyd: typing added, no functional change (#6103)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-21 12:06:55 +02:00
Bnyro d8f74af3d1 [mod] engine 500px: calc cursor instead of relying on pageInfo (#6091) 2026-05-21 07:31:13 +02:00
Bnyro 24b1a1b6a8 [feat] engines: add 500px.com engine (#6091) 2026-05-21 07:31:13 +02:00
Bnyro d7e8b7cd18 [feat] engines: add cara.app engine (#6092) 2026-05-17 18:39:47 +02:00
Markus Heiser f26e450778 [fix] engine: google-news - Google pushed a frontend update (#5984)
Around March 9 - 10, 2026, Google pushed a frontend update to Google News that
completely changed the HTML structure of search results.

This is a complete overhaul of the Google News engine.

- The real URL is encoded in the "jslog" attribute.
  @SeriousConcept1134: the attribute is a base64 encoded JSON
- CEID list is updated
- The typification was pushed forward

Related:

- https://github.com/searxng/searxng/issues/5852#issuecomment-4254438184
- https://github.com/searxng/searxng/issues/5852#issuecomment-4265598833

Closes: https://github.com/searxng/searxng/issues/5852
Suggested-by: SeriousConcept1134

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-17 15:27:00 +02:00
Bnyro dce3bb69bb [chore] settings.yml: set broken engines yahoo and karmasearch to inactive 2026-05-16 16:04:36 +02:00
Bnyro de49d27846 [fix] yandex images: crashes when parsing images without fallback source (#6084) 2026-05-16 15:53:23 +02:00
Bnyro 16a7537bfd [chore] engines: remove ask.com (service was discontinued) (#6083)
Source: https://www.ask.com/
2026-05-16 15:36:47 +02:00
Markus Heiser afafca93f3 [fix] engine wikidata - fails to initialize with HTTP 403 (#6081)
In order not to be further blocked, the WIKIDATA_PROPERTIES are cached, which
drastically reduces the WD-SQL request.

BTW: improve type hints

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

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-15 16:21:47 +02:00
dependabot[bot] 240f403d93 [upd] web-client (simple): Bump the minor group (#6080)
Bumps the minor group in /client/simple with 4 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [stylelint](https://github.com/stylelint/stylelint) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

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

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

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

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

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 25.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: stylelint
  dependency-version: 17.11.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: vite
  dependency-version: 8.0.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 14:19:28 +02:00
dependabot[bot] 9b30ae005b [upd] pypi: Bump the minor group with 2 updates (#6079)
Bumps the minor group with 2 updates: [selenium](https://github.com/SeleniumHQ/Selenium) and [basedpyright](https://github.com/detachhead/basedpyright).


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

Updates `basedpyright` from 1.39.3 to 1.39.4
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.39.3...v1.39.4)

---
updated-dependencies:
- dependency-name: selenium
  dependency-version: 4.44.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: basedpyright
  dependency-version: 1.39.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
2026-05-15 11:12:57 +02:00
Arnaud Jeannin 790683bbd7 [fix] google: improve CAPTCHA detection (#5922)
- Detect HTTP 302 responses (Google redirecting to /sorry/index
  without the HTTP client following the redirect)
- Detect short HTML responses (<2000 bytes) containing "/sorry/"
  links (meta-refresh or JS redirect variants)

Instances with rotating IPs can set the `suspended_times.SearxEngineCaptcha` to
0 in the search settings [1], the next request will typically use a different
outgoing IP when rotating proxies are configured

[1] https://docs.searxng.org/admin/settings/settings_search.html
2026-05-15 09:25:13 +02:00
Bnyro 52b446b4ad [make] update searx.data.traits (#6075) 2026-05-15 08:37:11 +02:00
Bnyro 6cee4b8947 [feat] yep: add support for selecting search language (#6075) 2026-05-15 08:37:11 +02:00
Markus Heiser 8e5aa9d394 [doc] Development Quickstart: documentation of the tools (dev.env)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-13 13:55:50 +02:00
Markus Heiser cf4d7e31c4 [mod] update_engine_traits.py: improve type annotations
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-13 13:55:50 +02:00
Markus Heiser 471f2b205f [feat] update_engine_traits.py: add option to update engines selective
Previously, `update_engine_traits.py` would fetch traits for all engines, which
is very slow and by side-effect touches engine data that are unrelated to the
engine you're currently working on.

To be faster with developing `update_engine_traits.py` supports now engine
arguments.

To test, jump into the developer environment and run the script::

    $ ./manage dev.env
    (dev.env)$ ./searxng_extra/update/update_engine_traits.py --help

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-05-13 13:55:50 +02:00
Markus Heiser 09829b1ccc [fix] installation instructions - launch SearXNG by python -m (#6078)
[1] https://github.com/searxng/searxng/issues/126#issuecomment-4433874986

Suggested-by: @virtadpt in [1]
2026-05-13 11:34:31 +02:00
dependabot[bot] df1f24fb7f [upd] web-client (simple): Bump the minor group (#6056)
Bumps the minor group in /client/simple with 4 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [stylelint](https://github.com/stylelint/stylelint) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

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

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

Updates `stylelint` from 17.9.1 to 17.11.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/17.9.1...17.11.0)

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

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 25.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: stylelint
  dependency-version: 17.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: vite
  dependency-version: 8.0.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 15:34:19 +02:00
dependabot[bot] 0cba32c15f [upd] pypi: Bump markdown-it-py from 4.0.0 to 4.2.0 in the minor group (#6054)
Bumps the minor group with 1 update: [markdown-it-py](https://github.com/executablebooks/markdown-it-py).


Updates `markdown-it-py` from 4.0.0 to 4.2.0
- [Release notes](https://github.com/executablebooks/markdown-it-py/releases)
- [Changelog](https://github.com/executablebooks/markdown-it-py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/markdown-it-py/compare/v4.0.0...v4.2.0)

---
updated-dependencies:
- dependency-name: markdown-it-py
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
2026-05-09 08:43:28 +02:00
Tommaso Colella 849e17e431 [fix] 360search: improve empty results set management and increase engine timeout (#6058) 2026-05-09 08:35:21 +02:00
github-actions[bot] d8ab61a9e0 [l10n] update translations from Weblate (#6057)
94e9ade46 - 2026-05-04 - Aindriú Mac Giolla Eoin <aindriu80@noreply.codeberg.org>
883cac081 - 2026-04-30 - alexgabi <alexgabi@noreply.codeberg.org>

Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-05-08 14:27:49 +02:00
dependabot[bot] 7eb130b1a8 [upd] github-actions: Bump github/codeql-action from 4.35.2 to 4.35.4 (#6055)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.2 to 4.35.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...68bde559dea0fdcac2102bfdf6230c5f70eb485e)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 14:25:27 +02:00
270 changed files with 11982 additions and 7483 deletions
-163
View File
@@ -1,163 +0,0 @@
;;; .dir-locals.el
;;
;; Per-Directory Local Variables:
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html
;;
;; For full fledge developer tools install emacs packages:
;;
;; M-x package-install ...
;;
;; magit gitconfig
;; nvm lsp-mode lsp-pyright lsp-eslint
;; pyvenv pylint pip-requirements
;; jinja2-mode
;; json-mode
;; company company-jedi company-quickhelp company-shell
;; realgud
;; sphinx-doc markdown-mode graphviz-dot-mode
;; apache-mode nginx-mode
;;
;; To setup a developer environment, build target::
;;
;; $ make node.env.dev pyenv.install
;;
;; Some buffer locals are referencing the project environment:
;;
;; - prj-root --> <repo>/
;; - nvm-dir --> <repo>/.nvm
;; - python-environment-directory --> <repo>/local
;; - python-environment-default-root-name --> py3
;; - python-shell-virtualenv-root --> <repo>/local/py3
;; When this variable is set with the path of the virtualenv to use,
;; `process-environment' and `exec-path' get proper values in order to run
;; shells inside the specified virtualenv, example::
;; (setq python-shell-virtualenv-root "/path/to/env/")
;; - python-shell-interpreter --> <repo>/local/py3/bin/python
;;
;; Python development:
;;
;; Jedi, flycheck & other python stuff should use the 'python-shell-interpreter'
;; from the local py3 environment.
;;
((nil
. ((fill-column . 80)
(indent-tabs-mode . nil)
(eval . (progn
(add-to-list 'auto-mode-alist '("\\.html\\'" . jinja2-mode))
;; project root folder is where the `.dir-locals.el' is located
(setq-local prj-root
(locate-dominating-file default-directory ".dir-locals.el"))
(setq-local python-environment-directory
(expand-file-name "./local" prj-root))
;; to get in use of NVM environment, install https://github.com/rejeep/nvm.el
(setq-local nvm-dir (expand-file-name "./.nvm" prj-root))
;; use nodejs from the (local) NVM environment (see nvm-dir)
(nvm-use-for-buffer)
(ignore-errors (require 'lsp))
(setq-local lsp-server-install-dir (car (cdr nvm-current-version)))
(setq-local lsp-enable-file-watchers nil)
;; use 'py3' environment as default
(setq-local python-environment-default-root-name
"py3")
(setq-local python-shell-virtualenv-root
(expand-file-name
python-environment-default-root-name python-environment-directory))
(setq-local python-shell-interpreter
(expand-file-name
"bin/python" python-shell-virtualenv-root))))))
(makefile-gmake-mode
. ((indent-tabs-mode . t)))
(yaml-mode
. ((eval . (progn
;; flycheck should use the local py3 environment
(setq-local flycheck-yaml-yamllint-executable
(expand-file-name "bin/yamllint" python-shell-virtualenv-root))
(setq-local flycheck-yamllintrc
(expand-file-name ".yamllint.yml" prj-root))
(flycheck-checker . yaml-yamllint)))))
(json-mode
. ((eval . (progn
(setq-local js-indent-level 4)
(flycheck-checker . json-python-json)))))
(js-mode
. ((eval . (progn
(ignore-errors (require 'lsp-eslint))
(setq-local js-indent-level 2)
;; flycheck should use the eslint checker from developer tools
(setq-local flycheck-javascript-eslint-executable
(expand-file-name "node_modules/.bin/eslint" prj-root))
;; (flycheck-mode)
(if (featurep 'lsp-eslint)
(lsp))
))))
(python-mode
. ((eval . (progn
(ignore-errors (require 'jedi-core))
(ignore-errors (require 'lsp-pyright))
(ignore-errors (sphinx-doc-mode))
(setq-local python-environment-virtualenv
(list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
;;"--system-site-packages"
"--quiet"))
(setq-local pylint-command
(expand-file-name "bin/pylint" python-shell-virtualenv-root))
(if (featurep 'lsp-pyright)
(lsp))
;; pylint will find the '.pylintrc' file next to the CWD
;; https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options
(setq-local flycheck-pylintrc
".pylintrc")
;; flycheck & other python stuff should use the local py3 environment
(setq-local flycheck-python-pylint-executable
python-shell-interpreter)
;; use 'M-x jedi:show-setup-info' and 'M-x epc:controller' to inspect jedi server
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-root -- You
;; can specify a full path instead of a name (relative path). In that case,
;; python-environment-directory is ignored and Python virtual environment
;; is created at the specified path.
(setq-local jedi:environment-root
python-shell-virtualenv-root)
;; https://tkf.github.io/emacs-jedi/latest/#jedi:server-command
(setq-local jedi:server-command
(list python-shell-interpreter
jedi:server-script))
;; jedi:environment-virtualenv --> see above 'python-environment-virtualenv'
;; is set buffer local! No need to setup jedi:environment-virtualenv:
;;
;; Virtualenv command to use. A list of string. If it is nil,
;; python-environment-virtualenv is used instead. You must set non-nil
;; value to jedi:environment-root in order to make this setting work.
;;
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-virtualenv
;;
;; (setq-local jedi:environment-virtualenv
;; (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
;; "--python"
;; "/usr/bin/python3.4"
;; ))
))))
)
+9 -9
View File
@@ -78,7 +78,7 @@ jobs:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
fetch-depth: "0"
@@ -106,10 +106,10 @@ jobs:
- if: ${{ matrix.emulation }}
name: Setup QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: "ghcr.io"
username: "${{ github.repository_owner }}"
@@ -141,16 +141,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
- if: ${{ matrix.emulation }}
name: Setup QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: "ghcr.io"
username: "${{ github.repository_owner }}"
@@ -175,19 +175,19 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: "ghcr.io"
username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: "docker.io"
username: "${{ secrets.DOCKER_USER }}"
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
+5 -2
View File
@@ -37,7 +37,7 @@ jobs:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
fetch-depth: "0"
@@ -50,11 +50,14 @@ jobs:
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/"
- name: Setup dependencies
run: sudo ./utils/searxng.sh install buildhost
- name: Setup venv
run: make V=1 install
- name: Build documentation
run: make V=1 docs.clean docs.html
run: make V=1 docs.html
- if: github.ref_name == 'master'
name: Release
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
python-version: "${{ matrix.python-version }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
@@ -67,7 +67,7 @@ jobs:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0"
@@ -88,7 +88,7 @@ jobs:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0"
+3 -3
View File
@@ -24,12 +24,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: "false"
- name: Sync GHCS from Docker Scout
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
uses: docker/scout-action@cd72f264beff1cd72735de31148b9d3244a0234a # v1.21.0
with:
organization: "searxng"
dockerhub-user: "${{ secrets.DOCKER_USER }}"
@@ -41,6 +41,6 @@ jobs:
write-comment: "false"
- name: Upload SARIFs
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: "./scout.sarif"
+11
View File
@@ -0,0 +1,11 @@
[[language]]
name = "python"
language-servers = ["basedpyright", "pylsp"]
auto-format = true
[language-server.pylsp.config.pylsp]
plugins.pylint.enabled = true
plugins.isort.enabled = true
plugins.black.enabled = true
plugins.black.skip_string_normalization = true
plugins.black.line_length = 120
+191 -202
View File
@@ -15,21 +15,21 @@
"swiped-events": "1.2.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.13",
"@types/node": "^25.6.0",
"@biomejs/biome": "2.4.16",
"@types/node": "^25.9.1",
"browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.0",
"edge.js": "^6.5.1",
"less": "^4.6.4",
"mathjs": "^15.2.0",
"sharp": "~0.34.5",
"sort-package-json": "^3.6.1",
"stylelint": "^17.9.1",
"sort-package-json": "^4.0.0",
"stylelint": "^17.12.0",
"stylelint-config-standard-less": "^4.1.0",
"stylelint-prettier": "^5.0.3",
"svgo": "^4.0.1",
"typescript": "~6.0.3",
"vite": "^8.0.10",
"vite": "^8.0.16",
"vite-bundle-analyzer": "^1.3.8"
}
},
@@ -69,9 +69,9 @@
}
},
"node_modules/@biomejs/biome": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.13.tgz",
"integrity": "sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz",
"integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@@ -85,20 +85,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.4.13",
"@biomejs/cli-darwin-x64": "2.4.13",
"@biomejs/cli-linux-arm64": "2.4.13",
"@biomejs/cli-linux-arm64-musl": "2.4.13",
"@biomejs/cli-linux-x64": "2.4.13",
"@biomejs/cli-linux-x64-musl": "2.4.13",
"@biomejs/cli-win32-arm64": "2.4.13",
"@biomejs/cli-win32-x64": "2.4.13"
"@biomejs/cli-darwin-arm64": "2.4.16",
"@biomejs/cli-darwin-x64": "2.4.16",
"@biomejs/cli-linux-arm64": "2.4.16",
"@biomejs/cli-linux-arm64-musl": "2.4.16",
"@biomejs/cli-linux-x64": "2.4.16",
"@biomejs/cli-linux-x64-musl": "2.4.16",
"@biomejs/cli-win32-arm64": "2.4.16",
"@biomejs/cli-win32-x64": "2.4.16"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.13.tgz",
"integrity": "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz",
"integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==",
"cpu": [
"arm64"
],
@@ -113,9 +113,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.13.tgz",
"integrity": "sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz",
"integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==",
"cpu": [
"x64"
],
@@ -130,9 +130,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.13.tgz",
"integrity": "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz",
"integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==",
"cpu": [
"arm64"
],
@@ -147,9 +147,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.13.tgz",
"integrity": "sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz",
"integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==",
"cpu": [
"arm64"
],
@@ -164,9 +164,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.13.tgz",
"integrity": "sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz",
"integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==",
"cpu": [
"x64"
],
@@ -181,9 +181,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.13.tgz",
"integrity": "sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz",
"integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==",
"cpu": [
"x64"
],
@@ -198,9 +198,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.13.tgz",
"integrity": "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz",
"integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==",
"cpu": [
"arm64"
],
@@ -215,9 +215,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.13.tgz",
"integrity": "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==",
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz",
"integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==",
"cpu": [
"x64"
],
@@ -232,13 +232,13 @@
}
},
"node_modules/@cacheable/memory": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz",
"integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
"integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/utils": "^2.4.0",
"@cacheable/utils": "^2.4.1",
"@keyv/bigmap": "^1.3.1",
"hookified": "^1.15.1",
"keyv": "^5.6.0"
@@ -1023,9 +1023,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.127.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1107,9 +1107,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -1124,9 +1124,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -1141,9 +1141,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -1158,9 +1158,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -1175,9 +1175,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -1192,9 +1192,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
@@ -1209,9 +1209,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
@@ -1226,9 +1226,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
@@ -1243,9 +1243,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
@@ -1260,9 +1260,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
@@ -1277,9 +1277,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
@@ -1294,9 +1294,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -1311,9 +1311,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [
"wasm32"
],
@@ -1330,9 +1330,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -1347,9 +1347,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -1364,9 +1364,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
@@ -1511,9 +1511,9 @@
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1522,13 +1522,13 @@
}
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/pluralize": {
@@ -1727,17 +1727,17 @@
}
},
"node_modules/cacheable": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.4.tgz",
"integrity": "sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==",
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
"integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/memory": "^2.0.8",
"@cacheable/utils": "^2.4.0",
"@cacheable/utils": "^2.4.1",
"hookified": "^1.15.0",
"keyv": "^5.6.0",
"qified": "^0.9.0"
"qified": "^0.10.1"
}
},
"node_modules/callsites": {
@@ -2118,48 +2118,48 @@
}
},
"node_modules/edge-lexer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/edge-lexer/-/edge-lexer-6.0.4.tgz",
"integrity": "sha512-rHlTSZUQfBu/fwnAjoaLCGGmDzpRPgUC8FEqNdJtpPEjBRCqU3a4Le7iJ8KSQfY2WvWx6NTGAwti62xj3eIz1w==",
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/edge-lexer/-/edge-lexer-6.0.5.tgz",
"integrity": "sha512-paSprHn8GRzOUWTVLapgacqDBSOpJgOY60/V5QQ8bbNLHRKUMSxp8Z2oOO0WKtcKmb5+sAlmvG3izhbFulp19A==",
"dev": true,
"license": "MIT",
"dependencies": {
"edge-error": "^4.0.2"
},
"engines": {
"node": ">=18.16.0"
"node": ">=24.0.0"
}
},
"node_modules/edge-parser": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/edge-parser/-/edge-parser-9.1.0.tgz",
"integrity": "sha512-Z7sEbRNjjGuUVch3ELHMbjgksVjQlAjUASCwUWe+1I+nJ0mVBmUD2rn6zyes/+EjLssvEGQcIWMjLMNn1ChXgQ==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/edge-parser/-/edge-parser-9.1.1.tgz",
"integrity": "sha512-kem/vInJgTzHCABdFe060WGjyGva8j23QwVbZkNFXkNAQhRz3mga5kNBuPFYQ3t73nPT2vZ8WOGV4OCS0P16Cw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.15.0",
"acorn": "^8.16.0",
"astring": "^1.9.0",
"edge-error": "^4.0.2",
"edge-lexer": "^6.0.4",
"edge-lexer": "^6.0.5",
"js-stringify": "^1.0.2"
},
"engines": {
"node": ">=18.16.0"
"node": ">=24.0.0"
}
},
"node_modules/edge.js": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/edge.js/-/edge.js-6.5.0.tgz",
"integrity": "sha512-WEXNseOSK6n5+Maf6dBPCMgsOuw4mpOqItMniXmdILVCH5PcjQ/CZDfw8IYyMwAjhshoznG+8WjsERy4+56xhA==",
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/edge.js/-/edge.js-6.5.1.tgz",
"integrity": "sha512-+NgWA8KunEdhVB247GWdmqUZ3MSIExeUCAbWNgNvA0ONH43UL+a/l6AE8GsevT4beqVsbp4Q6USwSpfbnMqBkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@poppinss/inspect": "^1.0.1",
"@poppinss/macroable": "^1.1.0",
"@poppinss/utils": "^7.0.0-next.4",
"@poppinss/macroable": "^1.1.2",
"@poppinss/utils": "^7.0.1",
"edge-error": "^4.0.2",
"edge-lexer": "^6.0.4",
"edge-parser": "^9.0.4",
"edge-lexer": "^6.0.5",
"edge-parser": "^9.1.1",
"he": "^1.2.0",
"property-information": "^7.1.0",
"stringify-attributes": "^4.0.0"
@@ -2334,13 +2334,13 @@
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz",
"integrity": "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==",
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
"integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^6.1.20"
"flat-cache": "^6.1.22"
}
},
"node_modules/fill-range": {
@@ -2434,9 +2434,9 @@
}
},
"node_modules/get-east-asian-width": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
"integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2747,16 +2747,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
@@ -3268,9 +3258,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -3475,9 +3465,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -3495,7 +3485,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -3627,9 +3617,9 @@
"optional": true
},
"node_modules/qified": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/qified/-/qified-0.9.1.tgz",
"integrity": "sha512-n7mar4T0xQ+39dE2vGTAlbxUEpndwPANH0kDef1/MYsB8Bba9wshkybIRx74qgcvKQPEWErf9AqAdYjhzY2Ilg==",
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
"integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3640,9 +3630,9 @@
}
},
"node_modules/qified/node_modules/hookified": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-2.1.1.tgz",
"integrity": "sha512-AHb76R16GB5EsPBE2J7Ko5kiEyXwviB9P5SMrAKcuAu4vJPZttViAbj9+tZeaQE5zjDme+1vcHP78Yj/WoAveA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
"integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
"dev": true,
"license": "MIT"
},
@@ -3741,14 +3731,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.127.0",
"@rolldown/pluginutils": "1.0.0-rc.17"
"@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -3757,21 +3747,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
"node_modules/run-parallel": {
@@ -3954,9 +3944,9 @@
"license": "MIT"
},
"node_modules/sort-package-json": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.6.1.tgz",
"integrity": "sha512-Chgejw1+10p2D0U2tB7au1lHtz6TkFnxmvZktyBCRyV0GgmF6nl1IxXxAsPtJVsUyg/fo+BfCMAVVFUVRkAHrQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-4.0.0.tgz",
"integrity": "sha512-6aYOlYI9AWioZ+rzu+4zKLmoFqJP0/fHDxrd7X04yqEibikY+5YVF0EYlyGn4v6X2PJY7yAUWV7oeP+i5rOm/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3972,7 +3962,7 @@
"sort-package-json": "cli.js"
},
"engines": {
"node": ">=20"
"node": ">=22"
}
},
"node_modules/sort-package-json/node_modules/semver": {
@@ -4010,9 +4000,9 @@
}
},
"node_modules/string-width": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
"integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
"integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4059,9 +4049,9 @@
}
},
"node_modules/stylelint": {
"version": "17.9.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.9.1.tgz",
"integrity": "sha512-THTmnAPJTrg/JhkTWZlSyrO+HUYMx6ELthIHeMyD2WOKqXIJUFQv2Yxn91bvUrZdbBJaW2dUuQdPST2wcQ6C3g==",
"version": "17.12.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.12.0.tgz",
"integrity": "sha512-KIlzWXMHUvgfPUR0R7TK3H80yCIi0uoivUwf+6Az4yrHJD1Q3c1qIkh/H5Z0i/K3QXgtq/UMEkWyBUSUwnpnOg==",
"dev": true,
"funding": [
{
@@ -4089,24 +4079,23 @@
"debug": "^4.4.3",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^11.1.2",
"file-entry-cache": "^11.1.3",
"global-modules": "^2.0.0",
"globby": "^16.2.0",
"globjoin": "^0.1.4",
"html-tags": "^5.1.0",
"ignore": "^7.0.5",
"import-meta-resolve": "^4.2.0",
"is-plain-object": "^5.0.0",
"mathml-tag-names": "^4.0.0",
"meow": "^14.1.0",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.5.9",
"postcss": "^8.5.14",
"postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^7.1.1",
"postcss-value-parser": "^4.2.0",
"string-width": "^8.2.0",
"string-width": "^8.2.1",
"supports-hyperlinks": "^4.4.0",
"svg-tags": "^1.0.0",
"table": "^6.9.0",
@@ -4385,9 +4374,9 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4478,9 +4467,9 @@
}
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
@@ -4545,17 +4534,17 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.10",
"rolldown": "1.0.0-rc.17",
"tinyglobby": "^0.2.16"
"postcss": "^8.5.15",
"rolldown": "1.0.3",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -4571,7 +4560,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
+6 -6
View File
@@ -29,21 +29,21 @@
"swiped-events": "1.2.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.13",
"@types/node": "^25.6.0",
"@biomejs/biome": "2.4.16",
"@types/node": "^25.9.1",
"browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.0",
"edge.js": "^6.5.1",
"less": "^4.6.4",
"mathjs": "^15.2.0",
"sharp": "~0.34.5",
"sort-package-json": "^3.6.1",
"stylelint": "^17.9.1",
"sort-package-json": "^4.0.0",
"stylelint": "^17.12.0",
"stylelint-config-standard-less": "^4.1.0",
"stylelint-prettier": "^5.0.3",
"svgo": "^4.0.1",
"typescript": "~6.0.3",
"vite": "^8.0.10",
"vite": "^8.0.16",
"vite-bundle-analyzer": "^1.3.8"
}
}
-1
View File
@@ -5,7 +5,6 @@ import type { KeyBindingLayout } from "./main/keyboard.ts";
// synced with searx/webapp.py get_client_settings
type Settings = {
plugins?: string[];
advanced_search?: boolean;
autocomplete?: string;
autocomplete_min?: number;
doi_resolver?: string;
+1 -1
View File
@@ -7,7 +7,6 @@
@import "mixins.less";
@import "toolkit.less";
@import "autocomplete.less";
@import "detail.less";
@import "animations.less";
@import "embedded.less";
@import "info.less";
@@ -1165,3 +1164,4 @@ pre code {
@import "result_types/code.less";
@import "result_types/paper.less";
@import "result_types/file.less";
@import "result_types/image.less";
+1
View File
@@ -19,6 +19,7 @@ Settings
settings_search
settings_server
settings_ui
settings_preferences
settings_redis
settings_valkey
settings_outgoing
@@ -0,0 +1,8 @@
.. _settings preferences:
================
``preferences:``
================
.. autoclass:: searx._settings.SettingsPref
:members:
+1
View File
@@ -47,6 +47,7 @@
activated:
- :py:obj:`searx.botdetection.link_token` in the :ref:`limiter`
- :ref:`image_proxy`
.. _image_proxy:
+1 -1
View File
@@ -169,7 +169,7 @@ ${fedora_build}
$ sudo -H -u ${SERVICE_USER} -i
(${SERVICE_USER})$ cd ${SEARXNG_SRC}
(${SERVICE_USER})$ export SEARXNG_SETTINGS_PATH=\"${SEARXNG_SETTINGS_PATH}\"
(${SERVICE_USER})$ python searx/webapp.py
(${SERVICE_USER})$ python -m searx.webapp
# disable debug
$ sudo -H sed -i -e \"s/debug : True/debug : False/g\" \"$SEARXNG_SETTINGS_PATH\"
+1 -1
View File
@@ -107,7 +107,7 @@ module:
======================= =========== ===========================================
base_url string base-url, can be overwritten to use same
engine on other URL
number_of_results int maximum number of results per request
page_size int maximum number of results per request
language string ISO code of language and country like en_US
api_key string api-key if required by engine
======================= =========== ===========================================
+8
View File
@@ -0,0 +1,8 @@
.. _500px engine:
=====
500px
=====
.. automodule:: searx.engines.500px
:members:
+8
View File
@@ -0,0 +1,8 @@
.. _cara engine:
===========
Cara Images
===========
.. automodule:: searx.engines.cara
:members:
-8
View File
@@ -1,8 +0,0 @@
.. _karmasearch engine:
===========
Karmasearch
===========
.. automodule:: searx.engines.karmasearch
:members:
+19 -1
View File
@@ -60,11 +60,29 @@ into the developer environment and start a python based HTTP server by::
$ ./manage dev.env
...
(dev.env)$ SEARXNG_DEBUG=1 python -m searx.webapp
(dev.env)$ SEARXNG_DEBUG=1 searxng-run
Since this is a pure Python solution, you can set breakpoints in your code with
``pdb.set_trace()`` and the debugger will wait for you in the terminal prompt.
Any other script or command line provided by SearXNG can also be used in the
same environment, here are a few examples::
# tools related to favicons
(dev.env)$ python -m searx.favicons
# tools related to DATA stored in searx/data
(dev.env)$ python -m searx.data --help
# tools related to engines
(dev.env)$ python -m searx.enginelib --help
# to test one of the update scripts
(dev.env)$ searxng_extra/update/update_engine_traits.py --help
# to test the update of the wikidata units
(dev.env)$ searxng_extra/update/update_wikidata_units.py
.. sidebar:: further read
+7
View File
@@ -0,0 +1,7 @@
.. _result_types.image:
=============
Image Results
=============
.. automodule:: searx.result_types.image
@@ -1,4 +1,8 @@
.. _result_types.mainresult:
============
Main Results
============
.. autoclass:: searx.result_types._base.MainResult
:members:
+1 -1
View File
@@ -18,13 +18,13 @@ following types have been implemented so far ..
main/code
main/paper
main/file
main/image
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that
have not yet been typed. The templates can be used as orientation until the
final typing is complete.
- :ref:`template default` / :py:obj:`Result`
- :ref:`template images`
- :ref:`template videos`
- :ref:`template torrent`
- :ref:`template map`
-47
View File
@@ -129,53 +129,6 @@ audio_src : uri,
URL of an embedded ``<audio controls>``.
.. _template images:
``images.html``
---------------
The images are displayed as small thumbnails in the main results list.
title : :py:class:`str`
Title of the image.
thumbnail_src : :py:class:`str`
URL of a preview of the image.
resolution :py:class:`str`
The resolution of the image (e.g. ``1920 x 1080`` pixel)
Image labels
~~~~~~~~~~~~
Clicking on the preview opens a gallery view in which all further metadata for
the image is displayed. Addition fields used in the :origin:`images.html
<searx/templates/simple/result_templates/images.html>`:
img_src : :py:class:`str`
URL of the full size image.
content: :py:class:`str`
Description of the image.
author: :py:class:`str`
Name of the author of the image.
img_format : :py:class:`str`
The format of the image (e.g. ``png``).
source : :py:class:`str`
Source of the image.
filesize: :py:class:`str`
Size of bytes in :py:obj:`human readable <searx.humanize_bytes>` notation
(e.g. ``MB`` for 1024 \* 1024 Bytes filesize).
url : :py:class:`str`
URL of the page from where the images comes from (source).
.. _template videos:
``videos.html``
+3 -3
View File
@@ -4,7 +4,7 @@ cov-core==1.15.0
black==25.9.0
pylint==4.0.5
splinter==0.21.0
selenium==4.43.0
selenium==4.44.0
Sphinx==8.2.3;python_version <= "3.11"
Sphinx==9.1.0; python_version > "3.11"
sphinx-issues==6.0.0
@@ -23,6 +23,6 @@ coloredlogs==15.0.1
docutils>=0.21.2;python_version <= "3.11"
docutils>=0.22.4; python_version > "3.11"
parameterized==0.9.0
granian[reload]==2.7.4
basedpyright==1.39.3
granian[reload]==2.7.5
basedpyright==1.39.6
types-lxml==2026.2.16
+2 -2
View File
@@ -1,2 +1,2 @@
granian==2.7.4
granian[pname]==2.7.4
granian==2.7.5
granian[pname]==2.7.5
+4 -4
View File
@@ -1,9 +1,9 @@
certifi==2026.4.22
certifi==2026.5.20
babel==2.18.0
flask-babel==4.0.0
flask==3.1.3
jinja2==3.1.6
lxml==6.1.0
lxml==6.1.1
pygments==2.20.0
python-dateutil==2.9.0.post0
pyyaml==6.0.3
@@ -11,9 +11,9 @@ httpx[http2]==0.28.1
httpx-socks[asyncio]==0.10.0
sniffio==1.3.1
valkey==6.1.1
markdown-it-py==4.0.0
markdown-it-py==4.2.0
msgspec==0.21.1
typer==0.25.1
typer==0.26.7
isodate==0.7.2
whitenoise==6.12.0
typing-extensions==4.15.0
+8 -1
View File
@@ -10,6 +10,7 @@ from os.path import dirname, abspath
import logging
import msgspec
from ._settings import SettingsPref
# Debug
LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s'
@@ -47,6 +48,12 @@ def init_settings():
settings.clear()
settings.update(cfg)
if get_setting("server.public_instance"):
# enable image proxy for public instances #6125
settings["server"]["image_proxy"] = True
pref: SettingsPref = get_setting("preferences")
pref.lock.add("image_proxy")
sxng_debug = get_setting("general.debug")
if sxng_debug:
_logging_config_debug()
@@ -66,7 +73,7 @@ def init_settings():
if settings['server']['public_instance']:
logger.warning(
"Be aware you have activated features intended only for public instances. "
"This force the usage of the limiter and link_token / "
"This force the usage of the limiter, link_token and image proxy / "
"see https://docs.searxng.org/admin/searx.limiter.html"
)
+42
View File
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of the :py:obj:`preference <searx.preference>` settings."""
# pylint: disable = too-few-public-methods
import typing as t
import msgspec
class SettingsPref(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
"""Options for configuring the preferences
.. code:: yaml
preferences:
lock:
- favicon_resolver
- image_proxy
- method
# ...
"""
lock: set[
t.Literal[
"categories",
"language",
"locale",
"autocomplete",
"favicon_resolver",
"image_proxy",
"method",
"safesearch",
"theme",
"results_on_new_tab",
"doi_resolver",
"simple_style",
"center_alignment",
"query_in_title",
"search_on_category_select",
]
] = set()
"""Lock arbitrary settings on the preferences page."""
+15 -6
View File
@@ -114,7 +114,13 @@ class ExpireCacheStats:
if expire:
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} ")
value_str = str(value)
if len(value_str) > 120:
value_str = f"{value_str[:120]} ..."
lines.append(
f"[{ctx_name:20s}] {valid_until} {key:12}"
f" --> ({type(value).__name__}:{len(value)}) {value_str} "
)
lines.append(f"Number of contexts: {c_ctx}")
lines.append(f"number of key/value pairs: {c_kv}")
@@ -438,12 +444,10 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
def get(self, key: str, default: typing.Any = 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.
from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists in
the table or the table not exists, the ``default`` value is returned.
"""
table = ctx
self.maintenance()
if not table:
table = self.normalize_name(self.cfg.name)
@@ -451,6 +455,9 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
if table not in self.table_names:
return default
# Before values are taken from the table, a maintenance interval may
# need to be carried out.
self.maintenance()
sql = f"SELECT value FROM {table} WHERE key = ?"
row = self.DB.execute(sql, (key,)).fetchone()
if row is None:
@@ -463,12 +470,14 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
If ``ctx`` argument is ``None`` (the default), a table name is
generated from the :py:obj:`ExpireCacheCfg.name`."""
table = ctx
self.maintenance()
if not table:
table = self.normalize_name(self.cfg.name)
if table in self.table_names:
# Before values are taken from the table, a maintenance interval may
# need to be carried out.
self.maintenance()
for row in self.DB.execute(f"SELECT key, value FROM {table}"):
yield row[0], self.deserialize(row[1])
File diff suppressed because it is too large Load Diff
+61 -23
View File
@@ -84,6 +84,7 @@
"sr": "УАЕ дирхам",
"sv": "Emiratisk dirham",
"ta": "ஐக்கிய அரபு அமீரக திர்கம்",
"te": "యూఏఈ దిర్హామ్",
"tr": "Birleşik Arap Emirlikleri dirhemi",
"tt": "БГӘ дирһәме",
"uk": "дирхам ОАЕ"
@@ -165,7 +166,7 @@
"pap": "lek albanes",
"pl": "lek",
"pt": "lek",
"ro": "Lek",
"ro": "lek albanez",
"ru": "албанский лек",
"sk": "Albánsky lek",
"sl": "albanski lek",
@@ -738,6 +739,7 @@
"sr": "брунејски долар",
"sv": "Bruneisk dollar",
"ta": "புரூணை டாலர்",
"te": "బ్రూనై డాలర్",
"th": "ดอลลาร์บรูไน",
"tr": "Brunei doları",
"tt": "Бруней доллары",
@@ -911,6 +913,7 @@
"sr": "бутански нгултрум",
"sv": "Ngultrum",
"ta": "பூட்டானின் இங்குல்ட்ரம்",
"te": "భూటానీస్ గుల్త్రమ్",
"th": "งุลตรัมภูฏาน",
"tr": "Ngultrum",
"tt": "ңгултрум",
@@ -989,7 +992,7 @@
"pa": "ਬੈਲਾਰੂਸੀ ਰੂਬਲ",
"pl": "Rubel białoruski",
"pt": "Rublo bielorrusso",
"ro": "Rublă belarusă",
"ro": "rublă belarusă",
"ru": "Белорусский рубль",
"sk": "Bieloruský rubeľ",
"sl": "beloruski rubelj",
@@ -1083,6 +1086,7 @@
"sr": "канадски долар",
"sv": "kanadensisk dollar",
"ta": "கனடா டொலர்",
"te": "కెనడియన్ డాలర్",
"th": "ดอลลาร์แคนาดา",
"tr": "Kanada doları",
"tt": "Канада дуллыры",
@@ -1231,6 +1235,7 @@
"sl": "čilski peso",
"sr": "чилеански пезос",
"sv": "Chilensk peso",
"te": "చిలీ పెసో",
"th": "เปโซชิลี",
"tr": "Şili pesosu",
"tt": "Чили песосы",
@@ -1284,6 +1289,7 @@
"sr": "ренминби",
"sv": "Renminbi",
"ta": "ரென்மின்பி",
"te": "రెన్మిన్బి",
"th": "เหรินหมินปี้",
"tr": "Renminbi",
"tt": "юән",
@@ -1702,7 +1708,7 @@
"vi": "Bảng Ai Cập"
},
"ERN": {
"ar": اكفا",
"ar": قفة",
"ca": "nakfa",
"cs": "Eritrejská nakfa",
"da": "Nakfa",
@@ -1776,11 +1782,11 @@
"bg": "евро",
"bn": "ইউরো",
"ca": "euro",
"cs": "euro",
"cs": "Euro",
"cy": "Ewro",
"da": "Euro",
"de": "Euro",
"en": "euro",
"en": "Euro",
"eo": "eŭro",
"es": "Euro",
"et": "Euro",
@@ -1794,7 +1800,7 @@
"hu": "euró",
"ia": "Euro",
"id": "Euro",
"it": "euro",
"it": "Euro",
"ja": "ユーロ",
"ko": "유로",
"lt": "Euras",
@@ -1805,11 +1811,11 @@
"oc": "Èuro",
"pa": "ਯੂਰੋ",
"pap": "Euro",
"pl": "euro",
"pl": "Euro",
"pt": "Euro",
"ro": "euro",
"ro": "Euro",
"ru": "евро",
"sk": "euro",
"sk": "Euro",
"sl": "evro",
"sr": "евро",
"sv": "Euro",
@@ -1935,7 +1941,7 @@
"sk": "libra šterlingov",
"sl": "funt šterling",
"sr": "британска фунта",
"sv": "Brittiskt pund",
"sv": "brittiskt pund",
"ta": "பிரித்தானிய பவுண்டு",
"th": "ปอนด์สเตอร์ลิง",
"tr": "İngiliz sterlini",
@@ -2410,6 +2416,7 @@
"pl": "rupia indonezyjska",
"pt": "rupia indonésia",
"ru": "индонезийская рупия",
"sk": "Indonézska rupia",
"sl": "indonezijska rupija",
"sr": "индонежанска рупија",
"sv": "Rupiah",
@@ -2463,6 +2470,7 @@
"sr": "нови израелски шекел",
"sv": "Shekel",
"ta": "புது இசுரேலிய சேக்கல்",
"te": "ఇజ్రాయెల్ షెకెల్",
"tr": "Yeni İsrail Şekeli",
"tt": "Исраил шекеле",
"uk": "ізраїльський новий шекель"
@@ -2758,6 +2766,7 @@
"sr": "јапански јен",
"sv": "yen",
"ta": "யென்",
"te": "జపనీస్ యెన్",
"th": "เยน",
"tr": "Japon yeni",
"tt": "япон иенасы",
@@ -2823,6 +2832,7 @@
"ja": "キルギス・ソム",
"ko": "키르기스스탄 솜",
"lt": "somas",
"lv": "Kirgizstānas soms",
"nl": "Kirgizische som",
"pa": "ਕਿਰਗਿਜ਼ਸਤਾਨੀ ਸੋਮ",
"pl": "som",
@@ -3236,6 +3246,7 @@
"sr": "шриланчанска рупија",
"sv": "Lankesisk rupie",
"ta": "இலங்கை ரூபாய்",
"te": "శ్రీలంక రూపాయి",
"tr": "Sri Lanka rupisi",
"tt": "Шри-Ланка рупиясе",
"uk": "ланкійська рупія",
@@ -3810,6 +3821,7 @@
"MXV": {
"de": "UNIDAD DE INVERSION",
"en": "unidad de inversión",
"eo": "Meksika unuo de investo",
"es": "Unidades de Inversión",
"ja": "メキシコ投資単位"
},
@@ -3901,7 +3913,7 @@
"de": "Namibia-Dollar",
"en": "Namibian dollar",
"eo": "namibia dolaro",
"es": "Dólar namibio",
"es": "dólar namibio",
"fi": "Namibian dollari",
"fr": "Dollar namibien",
"ga": "dollar na Namaibe",
@@ -4090,6 +4102,7 @@
"sr": "непалска рупија",
"sv": "Nepalesisk rupee",
"ta": "நேபாள ரூபாய்",
"te": "నేపాలీ రూపాయి",
"th": "รูปีเนปาล",
"tr": "Nepal rupisi",
"tt": "Непал рупиясе",
@@ -4325,6 +4338,7 @@
"sr": "филипински пезо",
"sv": "Filippinsk peso",
"ta": "பிலிப்பைன் பெசோ",
"te": "ఫిలిప్పీన్ పెసో",
"th": "เปโซฟิลิปปินส์",
"tr": "Filipinler pesosu",
"tt": "Филипин писысы",
@@ -4370,6 +4384,7 @@
"sr": "пакистанска рупија",
"sv": "Pakistansk rupee",
"ta": "பாக்கித்தானிய ரூபாய்",
"te": "పాకిస్థానీ రూపాయి",
"tr": "Pakistan rupisi",
"tt": "Пакстан рупиясе",
"uk": "пакистанська рупія",
@@ -4549,7 +4564,7 @@
"de": "rumänischer Leu",
"en": "Romanian Leu",
"eo": "rumana leo",
"es": "Leu rumano",
"es": "leu rumano",
"et": "Rumeenia leu",
"fi": "Romanian leu",
"fr": "leu roumain",
@@ -4750,6 +4765,7 @@
"sr": "саудијски ријал",
"sv": "Saudiarabisk rial",
"ta": "சவூதி ரியால்",
"te": "సౌదీ రియాల్",
"tr": "Suudi Arabistan riyali",
"tt": "Согуд риялы",
"uk": "саудівський ріал",
@@ -4948,6 +4964,7 @@
"sr": "сингапурски долар",
"sv": "Singaporiansk dollar",
"ta": "சிங்கப்பூர் வெள்ளி",
"te": "సింగపూర్ డాలర్",
"th": "ดอลลาร์สิงคโปร์",
"tr": "Singapur doları",
"tt": "Сингапур доллары",
@@ -5175,7 +5192,7 @@
"de": "syrische Lira",
"en": "Syrian pound",
"eo": "siria pundo",
"es": "Dolar sirio",
"es": "libra siria",
"fi": "Syyrian punta",
"fr": "livre syrienne",
"ga": "punt na Siria",
@@ -5977,7 +5994,7 @@
"ms": "Franc CFA Afrika Tengah",
"nl": "Central African CFA franc",
"oc": "Franc CFA d'Africa Centrala",
"pl": "środkowoafrykański frank CFA",
"pl": "frank CFA",
"pt": "franco",
"ro": "Franc CFA BEAC",
"ru": "франк КФА BEAC",
@@ -5999,6 +6016,7 @@
"fr": "argent d'investissement",
"ja": "投資対象としての銀",
"ms": "Perak sebagai pelaburan",
"pt": "Prata como investimento",
"ru": "серебро как инвестиция",
"sv": "Silver som investering",
"tr": "Yatırım olarak gümüş",
@@ -6017,6 +6035,7 @@
"lv": "zelts kā investīcija",
"ml": "സ്വർണവും സാമ്പത്തിക ശാസ്ത്രവും",
"ms": "emas sebagai pelaburan",
"pt": "Ouro como investimento",
"ru": "золото как инвестиция",
"sr": "investiciono zlato",
"sv": "investeringsguld",
@@ -7905,7 +7924,6 @@
"dolar de las islas caimán": "KYD",
"dolar de las islas salomon": "SBD",
"dolar de las islas salomón": "SBD",
"dolar de namibia": "NAD",
"dolar de nueva zelanda": "NZD",
"dolar de singapor": "SGD",
"dolar de singapur": "SGD",
@@ -7963,7 +7981,6 @@
"dolar namibia": "NAD",
"dolar namibian": "NAD",
"dolar namibijski": "NAD",
"dolar namibio": "NAD",
"dolar neocelandes": "NZD",
"dolar neocelandés": "NZD",
"dolar neozeelandez": "NZD",
@@ -7978,7 +7995,6 @@
"dolar singapura": "SGD",
"dolar singapurski": "SGD",
"dolar singapurtar": "SGD",
"dolar sirio": "SYP",
"dolar sua": "USD",
"dolar surinamdar": "SRD",
"dolar suriname": "SRD",
@@ -8179,6 +8195,7 @@
"dominicaanse peso": "DOP",
"dominican peso": "DOP",
"dominican peso oro": "DOP",
"dominički pezo": "DOP",
"dominik pesosu": "DOP",
"dominika peso": "DOP",
"dominikaanisen tasavallan peso": "DOP",
@@ -8528,7 +8545,6 @@
"etiópsky birr": "ETB",
"eua 17": "XBD",
"eua 9": "XBC",
"eur": "EUR",
"euras": "EUR",
"eurco": "XBA",
"euro": "EUR",
@@ -8859,7 +8875,6 @@
"frank kongijski": "CDF",
"frank rwandyjski": "RWF",
"frank szwajcarski": "CHF",
"frank zachodnioafrykański": "XOF",
"franko suitzar": "CHF",
"frw": "RWF",
"ft": "HUF",
@@ -9180,6 +9195,7 @@
"indonezijska rupija": "IDR",
"indonéská rupie": "IDR",
"indonéz rúpia": "IDR",
"indonézska rupia": "IDR",
"indonēziešu rūpija": "IDR",
"indonēzijas rūpija": "IDR",
"inglise nael": "GBP",
@@ -9494,6 +9510,8 @@
"kirgizia somo": "KGS",
"kirgizische som": "KGS",
"kirgizistansk som": "KGS",
"kirgizstānas soms": "KGS",
"kirgīzu soms": "KGS",
"kiwi dollar": "NZD",
"kínai jüan": "CNY",
"kíp": "LAK",
@@ -9810,7 +9828,6 @@
"lek na halbáine": "ALL",
"lek novo": "ALL",
"lekas": "ALL",
"lekă albaneză": "ALL",
"lekë": "ALL",
"leko": "ALL",
"lempira": "HNL",
@@ -9981,6 +9998,7 @@
"lira libanesa": "LBP",
"lira libanese": "LBP",
"lira na tuirce": "TRY",
"lira siria": "SYP",
"lira siriana": "SYP",
"lira síria": "SYP",
"lira thổ nhĩ kỳ": "TRY",
@@ -10291,6 +10309,7 @@
"meksički pezo": "MXN",
"meksika peso": "MXN",
"meksika pesosu": "MXN",
"meksika unuo de investo": "MXV",
"meksikaanse peso": "MXN",
"meksikas peso": "MXN",
"meksikon peso": "MXN",
@@ -10746,6 +10765,7 @@
"ouguiya mauritanien": "MRU",
"ouguiya mawritania": "MRU",
"ouguiya na máratáine": "MRU",
"ouro como investimento": "XAU",
"ouro do zimbábue": "ZWG",
"örmény dram": "AMD",
"östkaribisk dollar": "XCD",
@@ -11111,6 +11131,7 @@
"põhja korea vonn": "KPW",
"põhja korea won": "KPW",
"põhja makedoonia denaar": "MKD",
"prata como investimento": "XAG",
"pula": "BWP",
"pula botswana": "BWP",
"pula botswanais": "BWP",
@@ -11901,6 +11922,7 @@
"somoni taxico": "TJS",
"somoni tayiko": "TJS",
"somonis": "TJS",
"soms": "KGS",
"sonderziehungsrecht": "XDR",
"sos": "SOS",
"sosh": "SOS",
@@ -12102,7 +12124,6 @@
"szyling tanzański": "TZS",
"szyling ugandyjski": "UGX",
"sırp dinarı": "RSD",
"środkowoafrykański frank cfa": "XAF",
"šalamounský dolar": "SBD",
"šalomounský dolar": "SBD",
"šekel chadaš": "ILS",
@@ -14017,6 +14038,7 @@
"самоанский доллар": "WST",
"самоанська тала": "WST",
"сан томе һәм принсипи добрасы": "STN",
"саотомеанска добра": "STN",
"саотомска добра": "STN",
"саудитски риал": "SAR",
"саудијски риал": "SAR",
@@ -15350,13 +15372,28 @@
"ஹங்கேரிய போரிண்ட்": "HUF",
"ஹிருன்யா": "UAH",
"ஹொங்கொங் டொலர்": "HKD",
"అమెరికన్ డాలర్": "USD",
"ఇజ్రాయెల్ షెకెల్": "ILS",
"కెనడియన్ డాలర్": "CAD",
"చిలీ పెసో": "CLP",
"జపనీస్ యెన్": "JPY",
"డిజిటల్ రూపాయి": "INR",
"నేపాలీ రూపాయి": "NPR",
"పాకిస్థానీ రూపాయి": "PKR",
"ఫిలిప్పీన్ పెసో": "PHP",
"బ్రూనై డాలర్": "BND",
"భారత రూపాయి": "INR",
"భారతదేశ రూపాయి": "INR",
"భారతీయ రూపాయి": "INR",
"భూటానీస్ గుల్త్రమ్": "BTN",
"యునైటెడ్ స్టేట్స్ డాలర్": "USD",
"యూఏఈ దిర్హామ్": "AED",
"యూరో": "EUR",
"రూపాయి": "INR",
"రెన్మిన్బి": "CNY",
"శ్రీలంక రూపాయి": "LKR",
"సింగపూర్ డాలర్": "SGD",
"సౌదీ రియాల్": "SAR",
"స్విస్ ఫ్రాంక్": "CHF",
"അൾജീരിയൻ ദിനാർ": "DZD",
"ഇന്തോനേഷ്യൻ റുപിയ": "IDR",
@@ -15607,6 +15644,8 @@
"부탄 뉘땀": "BTN",
"부탄눌트럼": "BTN",
"북마케도니아 데나르": "MKD",
"북조선 원": "KPW",
"북한 원": "KPW",
"브라질 레알": "BRL",
"브라질 헤알": "BRL",
"브루나이 달러": "BND",
@@ -16199,7 +16238,6 @@
"香港・ドル": "HKD",
"香港元": "HKD",
"﷼": "IRR",
"﷼'": "YER",
"💶": "EUR"
"﷼'": "YER"
}
}
File diff suppressed because it is too large Load Diff
+178 -187
View File
@@ -4079,6 +4079,7 @@
"bg-BG": "BG:bg",
"bn-BD": "BD:bn",
"bn-IN": "IN:bn",
"ca-ES": "ES:ca",
"cs-CZ": "CZ:cs",
"de-AT": "AT:de",
"de-CH": "CH:de",
@@ -4110,16 +4111,15 @@
"es-CO": "CO:es-419",
"es-CU": "CU:es-419",
"es-ES": "ES:es",
"es-MX": "MX:es-419",
"es-PE": "PE:es-419",
"es-US": "US:es-419",
"es-VE": "VE:es-419",
"et-EE": "EE:et",
"fi-FI": "FI:fi",
"fr-BE": "BE:fr",
"fr-CA": "CA:fr",
"fr-CH": "CH:fr",
"fr-FR": "FR:fr",
"fr-MA": "MA:fr",
"fr-SN": "SN:fr",
"gu-IN": "IN:gu",
"he-IL": "IL:he",
"hi-IN": "IN:hi",
"hu-HU": "HU:hu",
@@ -4131,12 +4131,13 @@
"lv-LV": "LV:lv",
"ml-IN": "IN:ml",
"mr-IN": "IN:mr",
"ms-MY": "MY:ms",
"nb-NO": "NO:no",
"nl-BE": "BE:nl",
"nl-NL": "NL:nl",
"pa-IN": "IN:pa",
"pl-PL": "PL:pl",
"pt-BR": "BR:pt-419",
"pt-PT": "PT:pt-150",
"ro-RO": "RO:ro",
"ru-RU": "RU:ru",
"ru-UA": "UA:ru",
@@ -4151,8 +4152,7 @@
"uk-UA": "UA:uk",
"vi-VN": "VN:vi",
"zh-CN": "CN:zh-Hans",
"zh-HK": "HK:zh-Hant",
"zh-TW": "TW:zh-Hant"
"zh-HK": "HK:zh-Hant"
},
"supported_domains": {}
},
@@ -5740,186 +5740,6 @@
"zu-ZA": "ZA"
}
},
"karmasearch": {
"all_locale": null,
"custom": {},
"data_type": "traits_v1",
"languages": {},
"regions": {
"da-DK": "da-DK",
"de-AT": "de-AT",
"de-CH": "de-CH",
"de-DE": "de-DE",
"en-AU": "en-AU",
"en-CA": "en-CA",
"en-GB": "en-GB",
"en-ID": "en-ID",
"en-IN": "en-IN",
"en-MY": "en-MY",
"en-NZ": "en-NZ",
"en-PH": "en-PH",
"en-US": "en-US",
"en-ZA": "en-ZA",
"es-AR": "es-AR",
"es-CL": "es-CL",
"es-ES": "es-ES",
"es-MX": "es-MX",
"es-US": "es-US",
"fi-FI": "fi-FI",
"fr-BE": "fr-BE",
"fr-CA": "fr-CA",
"fr-CH": "fr-CH",
"fr-FR": "fr-FR",
"it-IT": "it-IT",
"ja-JP": "ja-JP",
"ko-KR": "ko-KR",
"nl-BE": "nl-BE",
"nl-NL": "nl-NL",
"pl-PL": "pl-PL",
"pt-BR": "pt-BR",
"ru-RU": "ru-RU",
"sv-SE": "sv-SE",
"tr-TR": "tr-TR",
"zh-CN": "zh-CN",
"zh-HK": "zh-HK",
"zh-TW": "zh-TW"
}
},
"karmasearch images": {
"all_locale": null,
"custom": {},
"data_type": "traits_v1",
"languages": {},
"regions": {
"da-DK": "da-DK",
"de-AT": "de-AT",
"de-CH": "de-CH",
"de-DE": "de-DE",
"en-AU": "en-AU",
"en-CA": "en-CA",
"en-GB": "en-GB",
"en-ID": "en-ID",
"en-IN": "en-IN",
"en-MY": "en-MY",
"en-NZ": "en-NZ",
"en-PH": "en-PH",
"en-US": "en-US",
"en-ZA": "en-ZA",
"es-AR": "es-AR",
"es-CL": "es-CL",
"es-ES": "es-ES",
"es-MX": "es-MX",
"es-US": "es-US",
"fi-FI": "fi-FI",
"fr-BE": "fr-BE",
"fr-CA": "fr-CA",
"fr-CH": "fr-CH",
"fr-FR": "fr-FR",
"it-IT": "it-IT",
"ja-JP": "ja-JP",
"ko-KR": "ko-KR",
"nl-BE": "nl-BE",
"nl-NL": "nl-NL",
"pl-PL": "pl-PL",
"pt-BR": "pt-BR",
"ru-RU": "ru-RU",
"sv-SE": "sv-SE",
"tr-TR": "tr-TR",
"zh-CN": "zh-CN",
"zh-HK": "zh-HK",
"zh-TW": "zh-TW"
}
},
"karmasearch news": {
"all_locale": null,
"custom": {},
"data_type": "traits_v1",
"languages": {},
"regions": {
"da-DK": "da-DK",
"de-AT": "de-AT",
"de-CH": "de-CH",
"de-DE": "de-DE",
"en-AU": "en-AU",
"en-CA": "en-CA",
"en-GB": "en-GB",
"en-ID": "en-ID",
"en-IN": "en-IN",
"en-MY": "en-MY",
"en-NZ": "en-NZ",
"en-PH": "en-PH",
"en-US": "en-US",
"en-ZA": "en-ZA",
"es-AR": "es-AR",
"es-CL": "es-CL",
"es-ES": "es-ES",
"es-MX": "es-MX",
"es-US": "es-US",
"fi-FI": "fi-FI",
"fr-BE": "fr-BE",
"fr-CA": "fr-CA",
"fr-CH": "fr-CH",
"fr-FR": "fr-FR",
"it-IT": "it-IT",
"ja-JP": "ja-JP",
"ko-KR": "ko-KR",
"nl-BE": "nl-BE",
"nl-NL": "nl-NL",
"pl-PL": "pl-PL",
"pt-BR": "pt-BR",
"ru-RU": "ru-RU",
"sv-SE": "sv-SE",
"tr-TR": "tr-TR",
"zh-CN": "zh-CN",
"zh-HK": "zh-HK",
"zh-TW": "zh-TW"
}
},
"karmasearch videos": {
"all_locale": null,
"custom": {},
"data_type": "traits_v1",
"languages": {},
"regions": {
"da-DK": "da-DK",
"de-AT": "de-AT",
"de-CH": "de-CH",
"de-DE": "de-DE",
"en-AU": "en-AU",
"en-CA": "en-CA",
"en-GB": "en-GB",
"en-ID": "en-ID",
"en-IN": "en-IN",
"en-MY": "en-MY",
"en-NZ": "en-NZ",
"en-PH": "en-PH",
"en-US": "en-US",
"en-ZA": "en-ZA",
"es-AR": "es-AR",
"es-CL": "es-CL",
"es-ES": "es-ES",
"es-MX": "es-MX",
"es-US": "es-US",
"fi-FI": "fi-FI",
"fr-BE": "fr-BE",
"fr-CA": "fr-CA",
"fr-CH": "fr-CH",
"fr-FR": "fr-FR",
"it-IT": "it-IT",
"ja-JP": "ja-JP",
"ko-KR": "ko-KR",
"nl-BE": "nl-BE",
"nl-NL": "nl-NL",
"pl-PL": "pl-PL",
"pt-BR": "pt-BR",
"ru-RU": "ru-RU",
"sv-SE": "sv-SE",
"tr-TR": "tr-TR",
"zh-CN": "zh-CN",
"zh-HK": "zh-HK",
"zh-TW": "zh-TW"
}
},
"mojeek": {
"all_locale": null,
"custom": {
@@ -8513,6 +8333,177 @@
"zh-classical": "zh-classical"
}
},
"yep": {
"all_locale": null,
"custom": {},
"data_type": "traits_v1",
"languages": {
"aa": "aa",
"ab": "ab",
"af": "af",
"ak": "ak",
"am": "am",
"an": "an",
"ar": "ar",
"as": "as",
"az": "az",
"ba": "ba",
"be": "be",
"bg": "bg",
"bho": "bh",
"bm": "bm",
"bn": "bn",
"bo": "bo",
"br": "br",
"bs": "bs",
"ca": "ca",
"ce": "ce",
"co": "co",
"cs": "cs",
"cu": "cu",
"cv": "cv",
"cy": "cy",
"da": "da",
"de": "de",
"dv": "dv",
"dz": "dz",
"ee": "ee",
"el": "el",
"en": "en",
"eo": "eo",
"es": "es",
"et": "et",
"eu": "eu",
"fa": "fa",
"ff": "ff",
"fi": "fi",
"fil": "tl",
"fo": "fo",
"fr": "fr",
"fy": "fy",
"ga": "ga",
"gd": "gd",
"gl": "gl",
"gn": "gn",
"gu": "gu",
"gv": "gv",
"ha": "ha",
"he": "he",
"hi": "hi",
"hr": "hr",
"ht": "ht",
"hu": "hu",
"hy": "hy",
"ia": "ia",
"id": "id",
"ie": "ie",
"ig": "ig",
"ii": "ii",
"io": "io",
"is": "is",
"it": "it",
"iu": "iu",
"ja": "ja",
"jv": "jv",
"ka": "ka",
"ki": "ki",
"kk": "kk",
"kl": "kl",
"km": "km",
"kn": "kn",
"ko": "ko",
"ks": "ks",
"ku": "ku",
"kw": "kw",
"ky": "ky",
"la": "la",
"lb": "lb",
"lg": "lg",
"ln": "ln",
"lo": "lo",
"lt": "lt",
"lu": "lu",
"lv": "lv",
"mg": "mg",
"mi": "mi",
"mk": "mk",
"ml": "ml",
"mn": "mn",
"mr": "mr",
"ms": "ms",
"mt": "mt",
"my": "my",
"nb": "nb",
"nd": "nd",
"ne": "ne",
"nl": "nl",
"nn": "nn",
"no": "no",
"nr": "nr",
"nv": "nv",
"ny": "ny",
"oc": "oc",
"om": "om",
"or": "or",
"os": "os",
"pa": "pa",
"pl": "pl",
"ps": "ps",
"pt": "pt",
"qu": "qu",
"rm": "rm",
"rn": "rn",
"ro": "ro",
"ru": "ru",
"rw": "rw",
"sa": "sa",
"sc": "sc",
"sd": "sd",
"se": "se",
"sg": "sg",
"si": "si",
"sk": "sk",
"sl": "sl",
"sn": "sn",
"so": "so",
"sq": "sq",
"sr": "sr",
"ss": "ss",
"st": "st",
"su": "su",
"sv": "sv",
"sw": "sw",
"ta": "ta",
"te": "te",
"tg": "tg",
"th": "th",
"ti": "ti",
"tk": "tk",
"tn": "tn",
"to": "to",
"tr": "tr",
"ts": "ts",
"tt": "tt",
"ug": "ug",
"uk": "uk",
"ur": "ur",
"uz": "uz",
"ve": "ve",
"vi": "vi",
"vo": "vo",
"wa": "wa",
"wo": "wo",
"xh": "xh",
"yi": "yi",
"yo": "yo",
"za": "za",
"zh": "zh",
"zh_Hans": "zh-cn",
"zh_Hant": "zh-tw",
"zu": "zu"
},
"regions": {}
},
"z-library": {
"all_locale": "",
"custom": {
+4502 -2286
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -5,7 +5,7 @@
],
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
"versions": [
"150.0",
"149.0"
"151.0",
"150.0"
]
}
+258 -143
View File
@@ -69,11 +69,6 @@
"symbol": "pk (US)",
"to_si_factor": 0.008809768
},
"Q101427917": {
"si_name": "Q25517",
"symbol": "pk (UK)",
"to_si_factor": 0.00909218
},
"Q101428098": {
"si_name": "Q44395",
"symbol": "dbar",
@@ -84,6 +79,11 @@
"symbol": "μbar",
"to_si_factor": 0.1
},
"Q101435213": {
"si_name": "Q101435195",
"symbol": "Jy s",
"to_si_factor": 1e-26
},
"Q101435332": {
"si_name": "Q44395",
"symbol": "cm Hg",
@@ -124,11 +124,6 @@
"symbol": "pm²",
"to_si_factor": 1e-24
},
"Q101463679": {
"si_name": "Q25343",
"symbol": "hm²",
"to_si_factor": 10000.0
},
"Q101464050": {
"si_name": "Q25343",
"symbol": "Mm²",
@@ -199,6 +194,11 @@
"symbol": "d⁻¹",
"to_si_factor": 1.15741e-05
},
"Q102129764": {
"si_name": "Q6137407",
"symbol": "a⁻¹",
"to_si_factor": 3.17098e-08
},
"Q102130673": {
"si_name": "Q182429",
"symbol": "ym/s",
@@ -425,9 +425,9 @@
"to_si_factor": 10.0
},
"Q1042866": {
"si_name": null,
"si_name": "Q199",
"symbol": "Zib",
"to_si_factor": null
"to_si_factor": 1.1805916207174113e+21
},
"Q104317437": {
"si_name": "Q25381181",
@@ -995,9 +995,9 @@
"to_si_factor": 1000000000000.0
},
"Q106247880": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "digit/s",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q106247940": {
"si_name": null,
@@ -1504,6 +1504,11 @@
"symbol": "m²/(s² K)",
"to_si_factor": 1.0
},
"Q106707206": {
"si_name": null,
"symbol": "HU",
"to_si_factor": null
},
"Q106707404": {
"si_name": "Q106707404",
"symbol": "kg m²/(s² K)",
@@ -2314,79 +2319,124 @@
"symbol": "cm²/(sr erg)",
"to_si_factor": 1000.0
},
"Q107710161": {
"si_name": "Q199",
"symbol": "J/bit",
"to_si_factor": 1.0
},
"Q107821494": {
"si_name": null,
"si_name": "Q21401573",
"symbol": "bit/m³",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q107822428": {
"si_name": null,
"si_name": "Q11547251",
"symbol": "bit/m",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q107824325": {
"si_name": null,
"si_name": "Q11547252",
"symbol": "bit/m²",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q107825143": {
"si_name": "Q11547251",
"symbol": "Eibit/m",
"to_si_factor": 1.152921504606847e+18
},
"Q107825584": {
"si_name": "Q11547252",
"symbol": "Eibit/m²",
"to_si_factor": 1.152921504606847e+18
},
"Q107825716": {
"si_name": "Q21401573",
"symbol": "Eibit/m³",
"to_si_factor": 1.152921504606847e+18
},
"Q107862736": {
"si_name": "Q11547251",
"symbol": "Gibit/m",
"to_si_factor": 1073741824.0
},
"Q107862746": {
"si_name": "Q11547252",
"symbol": "Gibit/m²",
"to_si_factor": 1073741824.0
},
"Q107862762": {
"si_name": "Q21401573",
"symbol": "Gibit/m³",
"to_si_factor": 1073741824.0
},
"Q107862770": {
"si_name": null,
"si_name": "Q11547251",
"symbol": "Kibit/m",
"to_si_factor": null
"to_si_factor": 1024.0
},
"Q107862783": {
"si_name": null,
"si_name": "Q11547252",
"symbol": "Kibit/m",
"to_si_factor": null
"to_si_factor": 1024.0
},
"Q107862850": {
"si_name": null,
"si_name": "Q21401573",
"symbol": "Kibit/m³",
"to_si_factor": null
"to_si_factor": 1024.0
},
"Q107862870": {
"si_name": null,
"si_name": "Q11547251",
"symbol": "Mibit/m",
"to_si_factor": null
"to_si_factor": 1048576.0
},
"Q107862884": {
"si_name": null,
"si_name": "Q11547252",
"symbol": "Mibit/m²",
"to_si_factor": null
"to_si_factor": 1048576.0
},
"Q107862898": {
"si_name": null,
"si_name": "Q21401573",
"symbol": "Mibit/m³",
"to_si_factor": null
"to_si_factor": 1048576.0
},
"Q107970215": {
"si_name": null,
"si_name": "Q11547251",
"symbol": "Pibit/m",
"to_si_factor": null
"to_si_factor": 1125899906842624.0
},
"Q107970224": {
"si_name": null,
"si_name": "Q11547252",
"symbol": "Pibit/m²",
"to_si_factor": null
"to_si_factor": 1125899906842624.0
},
"Q107970230": {
"si_name": null,
"si_name": "Q21401573",
"symbol": "Pibit/m³",
"to_si_factor": null
"to_si_factor": 1125899906842624.0
},
"Q107970235": {
"si_name": null,
"si_name": "Q11547251",
"symbol": "Tibit/m",
"to_si_factor": null
"to_si_factor": 1099511627776.0
},
"Q107970256": {
"si_name": null,
"si_name": "Q21401573",
"symbol": "Tibit/m³",
"to_si_factor": null
"to_si_factor": 1099511627776.0
},
"Q107970266": {
"si_name": null,
"si_name": "Q11547252",
"symbol": "Tibit/m²",
"to_si_factor": 1099511627776.0
},
"Q108112819": {
"si_name": null,
"symbol": "€/kWh",
"to_si_factor": null
},
"Q108112891": {
"si_name": null,
"symbol": "€/(MW h)",
"to_si_factor": null
},
"Q108270163": {
@@ -2395,9 +2445,9 @@
"to_si_factor": 3.169e-05
},
"Q1084321": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "Tb/s",
"to_si_factor": null
"to_si_factor": 1000000000000.0
},
"Q108533173": {
"si_name": "Q108533173",
@@ -2434,6 +2484,16 @@
"symbol": "GeV/c²",
"to_si_factor": 1.7826619216278976e-27
},
"Q108913970": {
"si_name": null,
"symbol": "person/km²",
"to_si_factor": null
},
"Q108914485": {
"si_name": null,
"symbol": "person/m²",
"to_si_factor": null
},
"Q108920356": {
"si_name": "Q25406",
"symbol": "esu",
@@ -2464,6 +2524,11 @@
"symbol": "e.u.",
"to_si_factor": 4.184
},
"Q109337616": {
"si_name": "Q139710667",
"symbol": "1/(100 eV)",
"to_si_factor": 6.2415e+16
},
"Q109448508": {
"si_name": null,
"symbol": "man-Sv",
@@ -2499,6 +2564,11 @@
"symbol": "nm²",
"to_si_factor": 1e-18
},
"Q11123": {
"si_name": "Q25517",
"symbol": "pt (UK)",
"to_si_factor": 0.00056826125
},
"Q111494193": {
"si_name": "Q111494193",
"symbol": "J/(Hz mol)",
@@ -2509,6 +2579,11 @@
"symbol": "%",
"to_si_factor": 0.01
},
"Q112659472": {
"si_name": null,
"symbol": "PVU",
"to_si_factor": null
},
"Q1131660": {
"si_name": "Q11570",
"symbol": "st",
@@ -2540,14 +2615,14 @@
"to_si_factor": 31688087.81402895
},
"Q1140444": {
"si_name": null,
"si_name": "Q199",
"symbol": "Zb",
"to_si_factor": null
"to_si_factor": 1e+21
},
"Q1140577": {
"si_name": null,
"si_name": "Q199",
"symbol": "Yb",
"to_si_factor": null
"to_si_factor": 0.0
},
"Q114559346": {
"si_name": null,
@@ -2560,14 +2635,14 @@
"to_si_factor": null
},
"Q1152074": {
"si_name": null,
"si_name": "Q199",
"symbol": "Pb",
"to_si_factor": null
"to_si_factor": 1000000000000000.0
},
"Q1152323": {
"si_name": null,
"si_name": "Q199",
"symbol": "Tb",
"to_si_factor": null
"to_si_factor": 1000000000000.0
},
"Q115277430": {
"si_name": null,
@@ -2810,14 +2885,14 @@
"to_si_factor": 4.4482216152605
},
"Q1194580": {
"si_name": null,
"si_name": "Q199",
"symbol": "Mib",
"to_si_factor": null
"to_si_factor": 1048576.0
},
"Q1195111": {
"si_name": null,
"si_name": "Q199",
"symbol": "Eb",
"to_si_factor": null
"to_si_factor": 1e+18
},
"Q1196837": {
"si_name": "Q1063756",
@@ -2865,15 +2940,20 @@
"to_si_factor": 0.03110348
},
"Q1204894": {
"si_name": null,
"si_name": "Q199",
"symbol": "Gib",
"to_si_factor": null
"to_si_factor": 1073741824.0
},
"Q12129": {
"si_name": "Q11573",
"symbol": "pc",
"to_si_factor": 3.085677581491367e+16
},
"Q12145303": {
"si_name": "Q11573",
"symbol": "rd",
"to_si_factor": 5.02921
},
"Q121965382": {
"si_name": "Q121965382",
"symbol": "mol/mol",
@@ -3114,6 +3194,11 @@
"symbol": "QF",
"to_si_factor": 1e+30
},
"Q12558489": {
"si_name": "Q25517",
"symbol": "pk (UK)",
"to_si_factor": 0.00909218
},
"Q125962250": {
"si_name": null,
"symbol": "Ry",
@@ -3240,14 +3325,14 @@
"to_si_factor": 1e-30
},
"Q131395783": {
"si_name": null,
"si_name": "Q199",
"symbol": "Rib",
"to_si_factor": null
"to_si_factor": 1.2379400392853803e+27
},
"Q131395793": {
"si_name": null,
"si_name": "Q199",
"symbol": "Qib",
"to_si_factor": null
"to_si_factor": 1.2676506002282294e+30
},
"Q13147228": {
"si_name": "Q844211",
@@ -3309,6 +3394,11 @@
"symbol": "1/K",
"to_si_factor": null
},
"Q133796439": {
"si_name": null,
"symbol": "T/mm²",
"to_si_factor": null
},
"Q13400897": {
"si_name": "Q1051665",
"symbol": "g",
@@ -3317,18 +3407,23 @@
"Q13479685": {
"si_name": "Q44395",
"symbol": "mm H2O",
"to_si_factor": 9.80638
"to_si_factor": 9.80665
},
"Q1351253": {
"si_name": null,
"si_name": "Q199",
"symbol": "Eib",
"to_si_factor": null
"to_si_factor": 1.152921504606847e+18
},
"Q1351334": {
"si_name": null,
"symbol": "Pib",
"to_si_factor": null
},
"Q135373020": {
"si_name": null,
"symbol": "GTexel",
"to_si_factor": null
},
"Q135415097": {
"si_name": "Q25236",
"symbol": "shp",
@@ -3380,9 +3475,9 @@
"to_si_factor": 1e-06
},
"Q136039973": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "FPS",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q1361854": {
"si_name": "Q11570",
@@ -3450,9 +3545,9 @@
"to_si_factor": 1000000000000.0
},
"Q139054848": {
"si_name": null,
"si_name": "Q117899185",
"symbol": "A·h/m²",
"to_si_factor": null
"to_si_factor": 3600.0
},
"Q139086088": {
"si_name": "Q69425409",
@@ -3642,7 +3737,7 @@
"Q1654435": {
"si_name": "Q25250",
"symbol": "IRE",
"to_si_factor": 0.007
"to_si_factor": 0.007143
},
"Q16673974": {
"si_name": null,
@@ -3820,9 +3915,9 @@
"to_si_factor": 0.01
},
"Q18434272": {
"si_name": null,
"si_name": "Q199",
"symbol": "°Balling",
"to_si_factor": null
"to_si_factor": 0.01
},
"Q185078": {
"si_name": "Q25343",
@@ -3850,9 +3945,9 @@
"to_si_factor": 1e-21
},
"Q188768": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "FLOPS",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q190095": {
"si_name": "Q190095",
@@ -3989,6 +4084,11 @@
"symbol": "ppb",
"to_si_factor": 1e-09
},
"Q206037": {
"si_name": "Q6137407",
"symbol": "r/min",
"to_si_factor": 0.0166667
},
"Q2064166": {
"si_name": "Q179836",
"symbol": "fc",
@@ -4320,9 +4420,9 @@
"to_si_factor": 10.0
},
"Q2243141": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "Gb/s",
"to_si_factor": null
"to_si_factor": 1000000000.0
},
"Q2254856": {
"si_name": "Q25343",
@@ -4335,9 +4435,9 @@
"to_si_factor": 0.00508
},
"Q2269250": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "kb/s",
"to_si_factor": null
"to_si_factor": 1000.0
},
"Q2278977": {
"si_name": null,
@@ -4535,9 +4635,9 @@
"to_si_factor": 898755178700.0
},
"Q25325238": {
"si_name": null,
"si_name": "Q106919394",
"symbol": "bhp/cm³",
"to_si_factor": null
"to_si_factor": 745700000.0
},
"Q253276": {
"si_name": "Q11573",
@@ -4545,9 +4645,9 @@
"to_si_factor": 1609.344
},
"Q2533495": {
"si_name": null,
"si_name": "Q199",
"symbol": "°P",
"to_si_factor": null
"to_si_factor": 0.01
},
"Q25343": {
"si_name": "Q25343",
@@ -4885,9 +4985,9 @@
"to_si_factor": 9.80665
},
"Q28657331": {
"si_name": null,
"si_name": "Q106688958",
"symbol": "erg/(s cm²)",
"to_si_factor": null
"to_si_factor": 0.001
},
"Q28683485": {
"si_name": "Q28683485",
@@ -4920,9 +5020,9 @@
"to_si_factor": 0.001
},
"Q29463526": {
"si_name": null,
"si_name": "Q199",
"symbol": "hr/yr",
"to_si_factor": null
"to_si_factor": 1.14155e-06
},
"Q296936": {
"si_name": "Q25269",
@@ -5155,9 +5255,9 @@
"to_si_factor": 1e-15
},
"Q3194304": {
"si_name": null,
"si_name": "Q199",
"symbol": "kb",
"to_si_factor": null
"to_si_factor": 1000.0
},
"Q3196665": {
"si_name": "Q215571",
@@ -5295,9 +5395,9 @@
"to_si_factor": 3517.0
},
"Q3332814": {
"si_name": null,
"si_name": "Q199",
"symbol": "Mb",
"to_si_factor": null
"to_si_factor": 1000000.0
},
"Q33680": {
"si_name": "Q33680",
@@ -5390,9 +5490,9 @@
"to_si_factor": 3.085677581e+22
},
"Q3815076": {
"si_name": null,
"si_name": "Q199",
"symbol": "Kib",
"to_si_factor": null
"to_si_factor": 1024.0
},
"Q3858002": {
"si_name": "Q25406",
@@ -5410,9 +5510,9 @@
"to_si_factor": 0.3048
},
"Q389062": {
"si_name": null,
"si_name": "Q199",
"symbol": "Tib",
"to_si_factor": null
"to_si_factor": 1099511627776.0
},
"Q3902688": {
"si_name": "Q25517",
@@ -5534,6 +5634,11 @@
"symbol": "D",
"to_si_factor": 3.335640951981521e-30
},
"Q4063874": {
"si_name": "Q21401573",
"symbol": "amg",
"to_si_factor": 2.6868e+25
},
"Q4068266": {
"si_name": "Q11570",
"symbol": "Ʒ",
@@ -5640,9 +5745,9 @@
"to_si_factor": null
},
"Q474533": {
"si_name": null,
"si_name": "Q25272",
"symbol": "At",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q48013": {
"si_name": "Q11570",
@@ -5705,14 +5810,14 @@
"to_si_factor": 0.01
},
"Q50094": {
"si_name": null,
"si_name": "Q199",
"symbol": "Np",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q50098": {
"si_name": null,
"si_name": "Q199",
"symbol": "B",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q50190518": {
"si_name": "Q25377184",
@@ -5755,9 +5860,9 @@
"to_si_factor": 1000.0
},
"Q524410": {
"si_name": null,
"si_name": "Q11574",
"symbol": "Ga",
"to_si_factor": null
"to_si_factor": 3.15576e+16
},
"Q531": {
"si_name": "Q11573",
@@ -5765,9 +5870,9 @@
"to_si_factor": 9460730472580800.0
},
"Q5329": {
"si_name": null,
"si_name": "Q50098",
"symbol": "dB",
"to_si_factor": null
"to_si_factor": 0.1
},
"Q53393488": {
"si_name": "Q39369",
@@ -6410,9 +6515,9 @@
"to_si_factor": 3.4262590996353905
},
"Q549389": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "b/s",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q550341": {
"si_name": "Q550341",
@@ -6440,9 +6545,9 @@
"to_si_factor": null
},
"Q5558595": {
"si_name": null,
"si_name": "Q199",
"symbol": "GFLOPS",
"to_si_factor": null
"to_si_factor": 1000000000.0
},
"Q55663153": {
"si_name": "Q55663153",
@@ -6519,20 +6624,25 @@
"symbol": "GtCO2eq",
"to_si_factor": null
},
"Q57084839": {
"si_name": null,
"symbol": "gCDE/km",
"to_si_factor": null
},
"Q57084901": {
"si_name": null,
"symbol": "KgCO2eq",
"to_si_factor": null
},
"Q57084921": {
"si_name": null,
"si_name": "Q11570",
"symbol": "gCO2eq",
"to_si_factor": null
"to_si_factor": 0.001
},
"Q5711255": {
"si_name": null,
"si_name": "Q25517",
"symbol": "aL",
"to_si_factor": null
"to_si_factor": 1e-21
},
"Q5711261": {
"si_name": "Q25517",
@@ -6674,11 +6784,6 @@
"symbol": "in",
"to_si_factor": 0.0254
},
"Q61793198": {
"si_name": "Q11573",
"symbol": "rd",
"to_si_factor": 5.02921
},
"Q61794766": {
"si_name": "Q11573",
"symbol": "ch (US survey)",
@@ -6710,9 +6815,9 @@
"to_si_factor": null
},
"Q61995006": {
"si_name": null,
"si_name": "Q106682512",
"symbol": "KWth",
"to_si_factor": null
"to_si_factor": 1000.0
},
"Q61996348": {
"si_name": "Q794261",
@@ -6755,14 +6860,14 @@
"to_si_factor": 1.1574074e-05
},
"Q64740041": {
"si_name": null,
"si_name": "Q106688958",
"symbol": "kWh/(m² yr)",
"to_si_factor": null
"to_si_factor": 0.000114
},
"Q64740314": {
"si_name": null,
"si_name": "Q106688958",
"symbol": "kWh/(m² day)",
"to_si_factor": null
"to_si_factor": 41.67
},
"Q64748817": {
"si_name": "Q80374519",
@@ -6825,9 +6930,9 @@
"to_si_factor": 1016.0469088
},
"Q66778234": {
"si_name": null,
"si_name": "Q199",
"symbol": "TFLOPS",
"to_si_factor": null
"to_si_factor": 1000000000000.0
},
"Q66778809": {
"si_name": null,
@@ -6839,6 +6944,11 @@
"symbol": "PFLOPS",
"to_si_factor": null
},
"Q66778951": {
"si_name": null,
"symbol": "ZFLOPS",
"to_si_factor": null
},
"Q67060736": {
"si_name": "Q67060736",
"symbol": "W/kg",
@@ -7225,9 +7335,9 @@
"to_si_factor": 1.0
},
"Q7350781": {
"si_name": null,
"si_name": "Q6137407",
"symbol": "Mb/s",
"to_si_factor": null
"to_si_factor": 1000000.0
},
"Q743895": {
"si_name": "Q39369",
@@ -7545,9 +7655,9 @@
"to_si_factor": 1.0
},
"Q836941": {
"si_name": null,
"si_name": "Q199",
"symbol": "°Bx",
"to_si_factor": null
"to_si_factor": 0.01
},
"Q83853845": {
"si_name": "Q83853845",
@@ -7599,6 +7709,11 @@
"symbol": "hm",
"to_si_factor": 100.0
},
"Q84451486": {
"si_name": "Q84451486",
"symbol": "K m/W",
"to_si_factor": 1.0
},
"Q844976": {
"si_name": "Q2844478",
"symbol": "Oe",
@@ -7630,9 +7745,9 @@
"to_si_factor": 1000000000.0
},
"Q855161": {
"si_name": null,
"si_name": "Q199",
"symbol": "Yib",
"to_si_factor": null
"to_si_factor": 1.2089258196146292e+24
},
"Q856240": {
"si_name": "Q794261",
@@ -7659,6 +7774,11 @@
"symbol": "abA",
"to_si_factor": 10.0
},
"Q86897783": {
"si_name": "Q86897783",
"symbol": "Pa² s",
"to_si_factor": 1.0
},
"Q87047886": {
"si_name": "Q87047886",
"symbol": "Pa s/m³",
@@ -7680,14 +7800,14 @@
"to_si_factor": 1e-12
},
"Q8799": {
"si_name": null,
"si_name": "Q199",
"symbol": "B",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q8805": {
"si_name": null,
"si_name": "Q8805",
"symbol": "bit",
"to_si_factor": null
"to_si_factor": 1.0
},
"Q88296091": {
"si_name": "Q25517",
@@ -7714,11 +7834,6 @@
"symbol": "bu (UK)",
"to_si_factor": 0.03636872
},
"Q89662131": {
"si_name": "Q25517",
"symbol": "pt (UK)",
"to_si_factor": 0.00056826125
},
"Q89992008": {
"si_name": "Q89992008",
"symbol": "F⁻¹",
@@ -7745,9 +7860,9 @@
"to_si_factor": null
},
"Q9048643": {
"si_name": null,
"si_name": "Q25517",
"symbol": "nl",
"to_si_factor": null
"to_si_factor": 1e-12
},
"Q905912": {
"si_name": "Q281096",
+9 -7
View File
@@ -8,8 +8,8 @@
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
$ ./manage dev.env
(dev.env)$ python -m searx.enginelib --help
.. hint::
@@ -46,6 +46,7 @@ ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache(
name="ENGINES_CACHE",
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
MAINTENANCE_PERIOD=60 * 60, # 2h
MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB
)
)
"""Global :py:obj:`searx.cache.ExpireCacheSQLite` instance where the cached
@@ -71,9 +72,9 @@ def state():
@app.command()
def maintenance(force: bool = True):
def maintenance(force: bool = True, truncate: bool = False):
"""Carry out maintenance on cache of the engines."""
ENGINES_CACHE.maintenance(force=force)
ENGINES_CACHE.maintenance(force=force, truncate=truncate)
class EngineCache:
@@ -111,8 +112,8 @@ class EngineCache:
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
$ ./manage dev.env
(dev.env)$ python -m searx.enginelib cache state
cache tables and key/values
===========================
@@ -159,7 +160,8 @@ class EngineCache:
def __init__(self, engine_name: str, expire: int | None = None):
self.expire: int = expire or ENGINES_CACHE.cfg.MAXHOLD_TIME
_valid = "-_." + string.ascii_letters + string.digits
self.table_name: str = "".join([c if c in _valid else "_" for c in engine_name])
# engine_name is a table and SQL table names must start with a letter
self.table_name: str = "eng_" + "".join([c if c in _valid else "_" for c in engine_name])
def set(self, key: str, value: t.Any, expire: int | None = None) -> bool:
return ENGINES_CACHE.set(
-13
View File
@@ -116,19 +116,6 @@ class EngineTraits:
return self.all_locale
return locales.get_engine_locale(searxng_locale, self.regions, default=default)
def is_locale_supported(self, searxng_locale: str) -> bool:
"""A *locale* (SearXNG's internal representation) is considered to be
supported by the engine if the *region* or the *language* is supported
by the engine.
For verification the functions :py:func:`EngineTraits.get_region` and
:py:func:`EngineTraits.get_language` are used.
"""
if self.data_type == "traits_v1":
return bool(self.get_region(searxng_locale) or self.get_language(searxng_locale))
raise TypeError("engine traits of type %s is unknown" % self.data_type)
def copy(self):
"""Create a copy of the dataclass object."""
return EngineTraits(**dataclasses.asdict(self))
+4
View File
@@ -84,6 +84,10 @@ def request(query, params):
def response(resp):
# sometimes 360search returns empty response when called from non-chinese ips
if not resp.text or not resp.text.strip():
return []
dom = html.fromstring(resp.text)
results = []
+137
View File
@@ -0,0 +1,137 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=invalid-name
"""500px_ is a online network for photographers with millions of members
worldwide. Photographers come to 500px to discover and share incredible photos,
gain meaningful exposure, compete in photo contests, and license their photos
through our exclusive distribution partners.
.. _500px: https://500px.com
"""
import typing as t
import codecs
import random
import string
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
# about
about = {
"website": "https://500px.com",
"wikidata_id": "Q354894",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://500px.com"
api_url = "https://api.500px.com"
categories = ["images"]
paging = True
results_per_page = 30
"""Number of results to return in the request.
The default was taken from the WEB UI, where the GraphQL query sets the value to
*static*: ``first: 30``.
"""
SXNG_query = """query PhotoSearchPaginationContainerQuery(
$first: Int, $cursor: String, $search: String!, $sort: PhotoSort, $filters: [PhotoSearchFilter!], $nlp: Boolean
) {
...SXNG_query
}
fragment SXNG_query on Query {
photoSearch(sort: $sort, first: $first, after: $cursor, search: $search, filters: $filters, nlp: $nlp) {
edges {
node {
id
canonicalPath
name
description
width
height
photographer: uploader {
displayName
}
images(sizes: [35, 33]) {
size
url
jpegUrl
webpUrl
id
}
}
cursor
}
}
}
"""
def setup(_) -> bool:
global SXNG_query # pylint: disable=global-statement
rand_str: str = "".join(random.choice(string.ascii_letters) for _ in range(5))
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
return True
def request(query: str, params: "OnlineParams") -> None:
# cursor is the base64 hash of the string "pos-<offset-1>", e.g. "pos-29" -> "cG9zLTI5"
offset = ((params["pageno"] - 1) * results_per_page) - 1
cursor = codecs.encode(f"pos-{offset}".encode("utf-8"), "base64").decode("utf-8")
params["url"] = f"{api_url}/graphql"
params["method"] = "POST"
params["json"] = {
"operationName": "PhotoSearchPaginationContainerQuery",
"variables": {
"first": results_per_page,
"cursor": cursor,
"search": query,
"sort": "RELEVANCE",
"filters": [],
"nlp": False,
},
"query": SXNG_query,
}
def response(resp: "SXNG_Response"):
res = EngineResults()
json_data = resp.json()["data"]["photoSearch"]
for edge in json_data["edges"]:
node = edge["node"] # pyright: ignore[reportAny]
if not node["images"]:
continue
images: list[dict[str, str]] = sorted(node["images"], key=lambda i: i["size"])
thumbnail_src = images[0]["url"]
img_src = images[-1]["url"]
res.add(
res.types.LegacyResult(
{
"template": "images.html",
"url": base_url + node["canonicalPath"],
"thumbnail_src": thumbnail_src,
"img_src": img_src,
"title": node["name"],
"content": node["description"],
"author": node["photographer"]["displayName"],
"resolution": f"{node['width']}x{node['height']}",
}
)
)
return res
+4 -1
View File
@@ -12,6 +12,7 @@ import typing as t
import sys
import copy
import os
from os.path import realpath, dirname
import types
@@ -278,6 +279,8 @@ def load_engines(engine_list: list[dict[str, t.Any]]):
else:
# if an engine can't be loaded (if for example the engine is missing
# tor or some other requirements) its set to inactive!
logger.error("loading engine %s failed: set engine to inactive!", engine_data.get("name", "???"))
logger.error(
f"(PID {os.getpid()}) loading engine %s failed: set engine to inactive!", engine_data.get("name", "???")
)
engine_data["inactive"] = True
return engines
-9
View File
@@ -39,7 +39,6 @@ url_xpath = './h4/a/@href'
title_xpath = './h4/a[1]'
content_xpath = './/p[1]'
correction_xpath = '//*[@id="didYouMean"]//a'
number_of_results_xpath = '//*[@id="totalResults"]'
name_token_xpath = '//form[@id="searchForm"]/input[@type="hidden"]/@name'
value_token_xpath = '//form[@id="searchForm"]/input[@type="hidden"]/@value'
@@ -107,14 +106,6 @@ def response(resp):
for correction in eval_xpath_list(dom, correction_xpath):
results.append({'correction': extract_text(correction)})
# get number of results
number_of_results = eval_xpath(dom, number_of_results_xpath)
if number_of_results:
try:
results.append({'number_of_results': int(extract_text(number_of_results))})
except: # pylint: disable=bare-except
pass
# Update the tokens to the newest ones
token_str = _get_tokens(dom)
CACHE.set('ahmia-tokens', token_str, expire=60 * 60)
+3 -1
View File
@@ -40,7 +40,7 @@ if t.TYPE_CHECKING:
about = {
"website": "https://www.aol.com",
"wikidata_id": "Q2407",
"wikidata_id": "Q27585",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
@@ -60,6 +60,8 @@ base_url = "https://search.aol.com"
time_range_map = {"day": "1d", "week": "1w", "month": "1m", "year": "1y"}
safesearch_map = {0: "p", 1: "r", 2: "i"}
enable_http2 = False
def init(_):
if search_type not in ("search", "image", "video"):
+2 -2
View File
@@ -21,7 +21,7 @@ about = {
categories = ['images']
paging = True
nb_per_page = 20
page_size = 20
search_api = 'https://api.artic.edu/api/v1/artworks/search?'
image_api = 'https://www.artic.edu/iiif/2/'
@@ -34,7 +34,7 @@ def request(query, params):
'q': query,
'page': params['pageno'],
'fields': 'id,title,artist_display,medium_display,image_id,date_display,dimensions,artist_titles',
'limit': nb_per_page,
'limit': page_size,
}
)
params['url'] = search_api + args
-75
View File
@@ -1,75 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Ask.com"""
from urllib.parse import urlencode
import dateutil
from lxml import html
from searx import utils
# Metadata
about = {
"website": "https://www.ask.com/",
"wikidata_id": 'Q847564',
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
# Engine Configuration
categories = ['general']
paging = True
max_page = 5
"""Ask.com has at max 5 pages."""
# Base URL
base_url = "https://www.ask.com/web"
def request(query, params):
query_params = {
"q": query,
"page": params["pageno"],
}
params["url"] = f"{base_url}?{urlencode(query_params)}"
return params
def response(resp):
start_tag = 'window.MESON.initialState = {'
end_tag = '}};'
dom = html.fromstring(resp.text)
script = utils.eval_xpath_getindex(dom, '//script', 0, default=None).text
pos = script.index(start_tag) + len(start_tag) - 1
script = script[pos:]
pos = script.index(end_tag) + len(end_tag) - 1
script = script[:pos]
json_resp = utils.js_obj_str_to_python(script)
results = []
for item in json_resp['search']['webResults']['results']:
pubdate_original = item.get('pubdate_original')
if pubdate_original:
pubdate_original = dateutil.parser.parse(pubdate_original)
metadata = [item.get(field) for field in ['category_l1', 'catsy'] if item.get(field)]
results.append(
{
"url": item['url'].split('&ueid')[0],
"title": item['title'],
"content": item['abstract'],
"publishedDate": pubdate_original,
# "thumbnail": item.get('image_url') or None, # these are not thumbs / to large
"metadata": ' | '.join(metadata),
}
)
return results
+3 -3
View File
@@ -26,7 +26,7 @@ base_url = (
# engine dependent config
paging = True
number_of_results = 10
page_size = 10
# shortcuts for advanced search
shortcut_dict = {
@@ -57,12 +57,12 @@ def request(query, params):
query = re.sub(key, val, query)
# basic search
offset = (params['pageno'] - 1) * number_of_results
offset = (params['pageno'] - 1) * page_size
string_args = {
'query': urlencode({'query': query}),
'offset': offset,
'hits': number_of_results,
'hits': page_size,
}
params['url'] = base_url.format(**string_args)
+2 -3
View File
@@ -51,11 +51,10 @@ def request(query, params):
}
params["url"] = f"{base_url}?{urlencode(query_params)}"
params["headers"]["Referer"] = "https://www.bilibili.com"
params["headers"]["Referer"] = "https://www.bilibili.com/"
params["headers"]["Accept"] = "application/json, text/javascript, */*; q=0.01"
params["cookies"] = cookie
return params
def response(resp):
search_res = resp.json()
+3 -16
View File
@@ -13,7 +13,6 @@ implementations are shared by other engines:
"""
import base64
import re
import typing as t
from urllib.parse import parse_qs, urlencode, urlparse
@@ -48,7 +47,7 @@ _safesearch_map: dict[int, str] = {
}
"""Filter results. 0: None, 1: Moderate, 2: Strict"""
base_url = "https://www.bing.com/search"
base_url = "https://www.bing.com"
"""Bing-Web search URL"""
@@ -94,7 +93,7 @@ def override_accept_language(params: "OnlineParams", engine_region: str | None)
params["headers"]["Accept-Language"] = f"{engine_region},{lang};q=0.9"
def request(query: str, params: "OnlineParams") -> "OnlineParams":
def request(query: str, params: "OnlineParams"):
"""Assemble a Bing-Web request."""
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
@@ -110,13 +109,7 @@ def request(query: str, params: "OnlineParams") -> "OnlineParams":
if locale_params:
query_params.update(locale_params)
params["url"] = f"{base_url}?{urlencode(query_params)}"
# in some regions where geoblocking is employed (e.g. China),
# www.bing.com redirects to the regional version of Bing
params["allow_redirects"] = True
return params
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]:
@@ -159,12 +152,6 @@ def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]:
results.append({"url": href, "title": title, "content": content})
if results:
result_len_container = "".join(eval_xpath(dom, '//span[@class="sb_count"]//text()'))
result_len_container = re.sub(r"[^0-9]", "", result_len_container)
if result_len_container:
results.append({"number_of_results": int(result_len_container)})
return results
+2 -4
View File
@@ -34,7 +34,7 @@ time_map = {
"year": 60 * 24 * 365,
}
base_url = "https://www.bing.com/images/async"
base_url = "https://www.bing.com"
"""Bing-Image search URL"""
@@ -64,9 +64,7 @@ def request(query, params):
if params["time_range"]:
query_params["qft"] = "filterui:age-lt%s" % time_map[params["time_range"]]
params["url"] = base_url + "?" + urlencode(query_params)
return params
params["url"] = base_url + "/images/async?" + urlencode(query_params)
def response(resp):
+2 -4
View File
@@ -44,7 +44,7 @@ time_map = {
difference of *last day* and *last week* in the result list is just marginally.
Bing does not have news range ``year`` / we use ``month`` instead."""
base_url = "https://www.bing.com/news/infinitescrollajax"
base_url = "https://www.bing.com"
"""Bing (News) search URL"""
@@ -74,9 +74,7 @@ def request(query, params):
if params["time_range"]:
query_params["qft"] = time_map.get(params["time_range"], 'interval="9"')
params["url"] = base_url + "?" + urlencode(query_params)
return params
params["url"] = base_url + "/news/infinitescrollajax?" + urlencode(query_params)
def response(resp):
+2 -2
View File
@@ -29,7 +29,7 @@ paging = True
safesearch = True
time_range_support = True
base_url = "https://www.bing.com/videos/asyncv2"
base_url = "https://www.bing.com"
"""Bing-Video search URL"""
@@ -60,7 +60,7 @@ def request(query, params):
query_params["form"] = "VRFLTR"
query_params["qft"] = " filterui:videoage-lt%s" % time_map[params["time_range"]]
params["url"] = base_url + "?" + urlencode(query_params)
params["url"] = base_url + "/videos/asyncv2?" + urlencode(query_params)
return params
+85
View File
@@ -0,0 +1,85 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=invalid-name
"""Cara_ is a social media and portfolio-sharing platform for artists and art
enthusiasts.
With the widespread use of generative AI, Cara_ decided to build a place that
filters out gen AI images so that people searching for authentic creatives and
images can do so easily.
.. _Cara: https://cara.app/about
"""
from urllib.parse import urlencode
import typing as t
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://cara.app",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://cara.app"
images_url = "https://images.cara.app"
categories = ["images"]
paging = True
results_per_page = 24
# if using HTTP2, we get blocked immediately
enable_http2 = False
def request(query: str, params: "OnlineParams") -> None:
args = {
"q": query,
"sortBy": "Top",
"take": results_per_page,
"skip": (params["pageno"] - 1) * results_per_page,
}
params["url"] = f"{base_url}/api/search/portfolio-posts?{urlencode(args)}"
def response(resp: "SXNG_Response"):
res = EngineResults()
json_data: list[dict[str, t.Any]] = resp.json()
for result in json_data:
thumbnail, img = None, None
i: dict[str, str]
for i in result["images"]:
if thumbnail is None or i["isCoverImg"]:
thumbnail = i
if img is None or not i["isCoverImg"]:
img = i
if not thumbnail or not img:
continue
res.add(
res.types.LegacyResult(
{
"template": "images.html",
"url": f"{base_url}/post/{result['id']}",
"thumbnail_src": f"{images_url}/{thumbnail['src']}?height=256",
"img_src": f"{images_url}/{img['src']}",
"title": result["title"],
"content": result["content"],
"author": result["name"],
}
)
)
return res
+2 -2
View File
@@ -16,7 +16,7 @@ about = {
paging = True
categories = []
number_of_results = 20
page_size = 20
skip_premium = True
@@ -25,7 +25,7 @@ thumbnail_format = "crop-240x300"
def request(query, params):
args = {'query': query, 'limit': number_of_results, 'offset': (params['pageno'] - 1) * number_of_results}
args = {'query': query, 'limit': page_size, 'offset': (params['pageno'] - 1) * page_size}
params['url'] = f"{base_url}/v2/search-gateway/recipes?{urlencode(args)}"
return params
+3 -3
View File
@@ -56,7 +56,7 @@ the API key in the engine :ref:`core engine config`."""
categories = ["science", "scientific publications"]
paging = True
nb_per_page = 10
page_size = 10
base_url = "https://api.core.ac.uk/v3/search/works/"
@@ -77,8 +77,8 @@ def request(query: str, params: "OnlineParams") -> None:
# API v3 uses different parameters
search_params = {
"q": query,
"offset": (params["pageno"] - 1) * nb_per_page,
"limit": nb_per_page,
"offset": (params["pageno"] - 1) * page_size,
"limit": page_size,
"sort": "relevance",
}
+2 -2
View File
@@ -38,7 +38,7 @@ about = {
# engine dependent config
categories = ["videos"]
paging = True
number_of_results = 10
page_size = 10
time_range_support = True
time_delta_dict = {
@@ -113,7 +113,7 @@ def request(query, params):
"password_protected": "false",
"private": "false",
"sort": "relevance",
"limit": number_of_results,
"limit": page_size,
"fields": ",".join(result_fields),
}
-1
View File
@@ -109,7 +109,6 @@ def search(query: str, params: "RequestParams") -> EngineResults:
kvmap=kvmap,
)
)
res.add(res.types.LegacyResult(number_of_results=count))
# cache counter value for 20sec
CACHE.set("count", count, expire=20)
-2
View File
@@ -176,6 +176,4 @@ def response(resp):
results.append(result)
results.append({'number_of_results': len(json_data['topics'])})
return results
+101
View File
@@ -0,0 +1,101 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Dogpile is a metasearch engine by the American advertising company `System1`_.
.. _System1: https://system1.com/
"""
import typing as t
from datetime import datetime, timezone
import html
from searx.utils import format_duration, html_to_text, humanize_number
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.dogpile.com",
"wikidata_id": "Q3595363",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
paging = True
safesearch = True
categories = ["general"]
dogpile_categ = "search"
"""Category to search in. Can be either "search", "images", "videos" or "news"."""
base_url = "https://www.dogpile.com"
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
def init(_):
if dogpile_categ not in ("search", "images", "videos", "news"):
raise ValueError("invalid search type: %s" % dogpile_categ)
def request(query: str, params: "OnlineParams"):
params["url"] = f"{base_url}/api/{dogpile_categ}"
params["method"] = "POST"
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
return params
def response(resp: "SXNG_Response"):
res = EngineResults()
json_resp = resp.json()
for result in json_resp["results"]:
if dogpile_categ == "search":
res.add(
res.types.MainResult(
url=result["clickUrl"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
)
)
elif dogpile_categ == "news":
res.add(
res.types.MainResult(
url=result["clickUrl"],
title=html_to_text(html.unescape(result["title"])),
content=html_to_text(html.unescape(result["description"])),
thumbnail=result["thumbnailUrl"],
publishedDate=datetime.fromtimestamp(result["date"], tz=timezone.utc),
)
)
elif dogpile_categ == "videos":
res.add(
res.types.LegacyResult(
template="videos.html",
url=result["clickUrl"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
thumbnail=result["thumbnailUrl"],
publishedDate=datetime.fromisoformat(result["publishDate"]),
length=format_duration(result["duration"]),
views=humanize_number(result["viewCount"]),
)
)
elif dogpile_categ == "images":
res.add(
res.types.Image(
url=result["altClickUrl"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
img_src=result["clickUrl"],
thumbnail_src=result["thumbnailUrl"],
resolution=f"{result['width']}x{result['height']}",
img_format=result["format"],
)
)
return res
-1
View File
@@ -21,7 +21,6 @@ about = {
# engine dependent config
categories = ['general'] # 'images', 'music', 'videos', 'files'
paging = False
number_of_results = 5
# search-url
# Doku is OpenSearch compatible
+2
View File
@@ -41,7 +41,9 @@ safesearch_cookies = {0: "-2", 1: None, 2: "1"}
safesearch_args = {0: "1", 1: None, 2: "1"}
search_path_map = {"images": "i", "videos": "v", "news": "news"}
_HTTP_User_Agent: str = gen_useragent()
send_accept_language_header = False
def init(engine_settings: dict[str, t.Any]):
+154
View File
@@ -0,0 +1,154 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""DuckDuckGo Web (general)
This implementation fetches the link to the first API page
(i.e. ``links.duckduckgo.com/d.js?...``) from duckduckgo.com and uses the ``n``
parameter of the API to fetch all subsequent pages.
This also means that it's not possible to immediately search for the third
page - the first and the second page would need to be loaded first.
The reason why we can't just normally use the `vqd` value is that the API URLs
require an additional parameter `dp` which seems generated at server-side, so we
can't build it ourselves and must scrape it from the HTML pages.
"""
import typing as t
from urllib.parse import quote_plus
from lxml import html
from searx.utils import html_to_text, gen_useragent, extract_text, eval_xpath
from searx.result_types import EngineResults
from searx.enginelib import EngineCache
from searx.network import get
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://duckduckgo.com/",
"wikidata_id": "Q12805",
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
# engine dependent config
categories = ["general"]
paging = True
_HTTP_User_Agent: str = gen_useragent()
base_url = "https://duckduckgo.com"
CACHE: EngineCache
"""Cache to store the API URLs for combinations of (query, page)."""
def setup(engine_settings: dict[str, str]):
global CACHE # pylint:disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return CACHE
def _fetch_first_page_link(
query: str,
headers: dict[str, str],
):
"""Search for a::
<link id="deep_preload_link" rel="preload" as="script"
href="https://links.duckduckgo.com/d.js?q=rust&t=D&l=us-en&s=0&a=h_&ct=DE&vqd=VQD_VALUE&bing_market=en-US&p_ent=&ex=-1&dp=LONG_TOKEN
>
This points to the first page
""" # pylint:disable=line-too-long
cache_key = _cache_key(query, 1)
cached: str | None = CACHE.get(cache_key)
if cached:
return cached
resp = get(
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
headers=headers,
timeout=2,
)
if resp.status_code != 200:
logger.error("vqd: got HTTP %s from duckduckgo.com", resp.status_code)
dom = html.fromstring(resp.text)
first_page_link = extract_text(eval_xpath(dom, "//link[@id='deep_preload_link']/@href"))
if not first_page_link:
logger.error("vqd: failed to load first page JS url from ddg response (return empty string)")
return ""
logger.debug("got link to first page from duckduckgo.com request: '%s'", first_page_link)
CACHE.set(cache_key, first_page_link, expire=7200)
return first_page_link
def _cache_key(query: str, pageno: int) -> str:
return f"nextpage_url|{query}|{pageno}"
def request(query: str, params: "OnlineParams") -> None:
if len(query) >= 500:
# DDG does not accept queries with more than 499 chars
params["url"] = None
return
headers = params["headers"]
# The vqd value is generated from the query and the UA header. To be able
# to reuse the vqd value, the UA header must be static.
headers["User-Agent"] = _HTTP_User_Agent
headers["Accept"] = "*/*"
headers["Referer"] = f"{base_url}/"
headers["Host"] = "duckduckgo.com"
# Sec-Fetch headers are required to not get blocked when sending a Firefox user agent
headers["Sec-Fetch-Dest"] = "script"
headers["Sec-Fetch-Mode"] = "no-cors"
headers["Sec-Fetch-Site"] = "same-site"
api_url = ""
if params["pageno"] > 1:
api_url = CACHE.get(_cache_key(query, params["pageno"]))
else:
api_url = _fetch_first_page_link(query, headers)
if not api_url:
params["url"] = None
return
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
def response(resp: "SXNG_Response"):
res = EngineResults()
res_json = resp.json()
for result in res_json["results"]:
if "u" not in result:
continue
res.add(res.types.MainResult(url=result["u"], title=result["t"], content=html_to_text(result["a"])))
# link to next page
next_page_path = res_json["results"][-1].get("n")
if next_page_path:
CACHE.set(
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
base_url + next_page_path,
expire=60 * 60,
)
return res
-8
View File
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Duden"""
import re
from urllib.parse import quote, urljoin
from lxml import html
from searx.utils import extract_text, eval_xpath, eval_xpath_list, eval_xpath_getindex
@@ -51,13 +50,6 @@ def response(resp):
dom = html.fromstring(resp.text)
number_of_results_element = eval_xpath_getindex(
dom, '//a[@class="active" and contains(@href,"/suchen/dudenonline")]/span/text()', 0, default=None
)
if number_of_results_element is not None:
number_of_results_string = re.sub('[^0-9]', '', number_of_results_element)
results.append({'number_of_results': int(number_of_results_string)})
for result in eval_xpath_list(dom, '//section[not(contains(@class, "essay"))]'):
url = eval_xpath_getindex(result, './/h2/a', 0).get('href')
url = urljoin(base_url, url)
+169
View File
@@ -0,0 +1,169 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Fireball_ is a Germany-based, privacy-focused search engine.
It likely doesn't have its own index, but it's unclear where its results come
from.
.. _Fireball: https://fireball.com
"""
import typing as t
from datetime import datetime
from urllib.parse import urlencode
from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException
from searx.extended_types import SXNG_Response
from searx.result_types import EngineResults
from searx.network import post
from searx.utils import html_to_text
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
about = {
"website": "https://fireball.com",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://fireball.com"
categories = ["general"]
fireball_category = "web" # values: "web", "news", "videos"
paging = False
safesearch = True
safe_search_map = {0: "off", 1: "moderate", 2: "strict"}
CACHE: EngineCache
"""Cache to store the settings cookie (contains e.g. language, safesearch, ...)."""
CACHE_VALID_DURATION = 30 * 24 * 3600 # one month, same as website
"""Duration how long settings cookies are valid."""
def init(engine_settings: dict[str, t.Any]):
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
if fireball_category not in ("web", "news", "videos"):
raise ValueError(f"Unsupported category: {fireball_category}")
def _cache_key(fireball_settings: dict[str, str]) -> str:
return f"fireball_settings_{fireball_settings['safesearch']}_{fireball_settings['market']}"
def _get_search_settings_cookie(params: 'OnlineParams') -> str:
"""Get a 'fireball' cookie for the given locale and safesearch setting set
in params."""
# the language is set by only specifying the search country on their
# website, they only list DE and US, but in fact it supports much more
# countries
country = "US"
if params["searxng_locale"] != "all":
language_parts = params["searxng_locale"].split("-")
country = language_parts[-1].upper()
fireball_settings = {
"action": "save",
"language": "en", # language is irrelevant, only changes UI language
"market": country,
"adprovider": "automatic",
"target": "_blank",
"tiles": "on",
"safesearch": safe_search_map[params["safesearch"]],
}
cache_key = _cache_key(fireball_settings)
cached_cookie = CACHE.get(cache_key)
if cached_cookie:
return cached_cookie
resp = post("https://fireball.com/settings", data=fireball_settings)
if not resp.ok:
raise SearxEngineAPIException("failed to obtain cookie for settings")
cookie = resp.cookies.get("fireball")
if not cookie:
raise SearxEngineAPIException("failed to obtain cookie for settings")
CACHE.set(cache_key, cookie, expire=CACHE_VALID_DURATION)
return cookie
def request(query: str, params: "OnlineParams"):
# no matter the category, the request is always the same, i.e. we get all
# different categories with one HTTP request
args = {
"f": "web",
"q": query,
}
params["url"] = f"{base_url}/getResults/?{urlencode(args)}"
params["cookies"]["fireball"] = _get_search_settings_cookie(params)
# referer header has to be set, otherwise the requests get blocked
params["headers"]["Referer"] = f"{base_url}/search?{urlencode(args)}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_data = resp.json()
for result in json_data.get(fireball_category, {}).get("results", []):
published_date = None
if result.get("page_age"):
published_date = datetime.fromisoformat(result["page_age"])
if fireball_category == "web":
res.add(
res.types.MainResult(
url=result["url"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
publishedDate=published_date,
)
)
elif fireball_category == "news":
thumbnail: str | None = None
if result.get("thumbnail"):
thumbnail = result["thumbnail"]["src"]
res.add(
res.types.MainResult(
url=result["url"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
thumbnail=thumbnail or "",
publishedDate=published_date,
)
)
elif fireball_category == "videos":
length = None
if result.get("video"):
length = result["video"].get("duration")
res.add(
res.types.LegacyResult(
{
"template": "videos.html",
"url": result["url"],
"title": html_to_text(result["title"]),
"content": html_to_text(result["description"]),
"thumbnail": result.get("thumbnail", {}).get("original"),
"length": length,
"publishedDate": published_date,
}
)
)
return res
+70
View File
@@ -0,0 +1,70 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Flaticon_ is a database for icons.
.. _Flaticon: https://www.flaticon.com
"""
from urllib.parse import urlencode
import typing as t
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.flaticon.com",
"wikidata_id": "Q105283791",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://www.flaticon.com"
categories = ["images", "icons"]
paging = True
def request(query: str, params: "OnlineParams") -> None:
args = {
"word": query,
}
params["headers"].update(
{
# important: query term is not URL encoded in the referer string
"Referer": f"{base_url}/search?word={query}",
"X-Requested-With": "XMLHttpRequest",
}
)
params["url"] = f"{base_url}/ajax/search/{params['pageno']}?{urlencode(args)}"
def _fix_url(url: str) -> str:
return url.replace(r"\/", "/")
def response(resp: "SXNG_Response"):
res = EngineResults()
result: dict[str, str] # TBH: dict[str, t.Any]
for result in resp.json()["items"]:
tags = [
tag_info["tag"] for tag_info in result["tags"] if tag_info["tag"] # pyright: ignore[reportArgumentType]
]
res.add(
res.types.Image(
title=result["name"],
content=", ".join(tags),
url=_fix_url(result["slug"]),
thumbnail_src=_fix_url(result["png"]),
img_src=_fix_url(result["png512"]),
author=result["team_name"],
)
)
return res
+3 -3
View File
@@ -20,7 +20,7 @@ about = {
categories = ['images']
nb_per_page = 15
page_size = 15
paging = True
api_key = None
@@ -29,7 +29,7 @@ url = (
'https://api.flickr.com/services/rest/?method=flickr.photos.search'
+ '&api_key={api_key}&{text}&sort=relevance'
+ '&extras=description%2C+owner_name%2C+url_o%2C+url_n%2C+url_z'
+ '&per_page={nb_per_page}&format=json&nojsoncallback=1&page={page}'
+ '&per_page={page_size}&format=json&nojsoncallback=1&page={page}'
)
photo_url = 'https://www.flickr.com/photos/{userid}/{photoid}'
@@ -42,7 +42,7 @@ def build_flickr_url(user_id, photo_id):
def request(query, params):
params['url'] = url.format(
text=urlencode({'text': query}), api_key=api_key, nb_per_page=nb_per_page, page=params['pageno']
text=urlencode({'text': query}), api_key=api_key, page_size=page_size, page=params['pageno']
)
return params
+31 -24
View File
@@ -1,15 +1,23 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Fyyd (podcasts)"""
import typing as t
from datetime import datetime
from urllib.parse import urlencode
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
'website': 'https://fyyd.de',
'official_api_documentation': 'https://github.com/eazyliving/fyyd-api',
'use_official_api': True,
'require_api_key': False,
'results': 'JSON',
"website": "https://fyyd.de",
"official_api_documentation": "https://github.com/eazyliving/fyyd-api",
"use_official_api": True,
"require_api_key": False,
"results": "JSON",
}
categories = []
paging = True
@@ -18,31 +26,30 @@ base_url = "https://api.fyyd.de"
page_size = 10
def request(query, params):
def request(query: str, params: "OnlineParams") -> None:
args = {
'term': query,
'count': page_size,
'page': params['pageno'] - 1,
"term": query,
"count": page_size,
"page": params["pageno"] - 1,
}
params['url'] = f"{base_url}/0.2/search/podcast?{urlencode(args)}"
return params
params["url"] = f"{base_url}/0.2/search/podcast?{urlencode(args)}"
def response(resp):
results = []
def response(resp: "SXNG_Response"):
res = EngineResults()
json_results = resp.json()['data']
json_results: list[dict[str, str]] = resp.json()["data"] # pyright: ignore[reportAny]
for result in json_results:
results.append(
{
'url': result['htmlURL'],
'title': result['title'],
'content': result['description'],
'thumbnail': result['smallImageURL'],
'publishedDate': datetime.strptime(result['status_since'], '%Y-%m-%d %H:%M:%S'),
'metadata': f"Rank: {result['rank']} || {result['episode_count']} episodes",
}
res.add(
res.types.MainResult(
url=result["htmlURL"],
title=result["title"],
content=result["description"],
thumbnail=result["smallImageURL"],
publishedDate=datetime.strptime(result["status_since"], "%Y-%m-%d %H:%M:%S"),
metadata=f"Rank: {result['rank']} || {result['episode_count']} episodes",
)
)
return results
return res
+8 -1
View File
@@ -10,10 +10,12 @@ import time
import typing as t
from urllib.parse import urlencode
from lxml import html
from searx.result_types import EngineResults
from searx.exceptions import SearxEngineCaptchaException
from searx.extended_types import SXNG_Response
from searx.utils import extr, gen_useragent, html_to_text
from searx.utils import extr, gen_useragent, html_to_text, eval_xpath
from searx.network import get
if t.TYPE_CHECKING:
@@ -40,6 +42,11 @@ time_range_map = {"day": "d", "week": "w", "month": "m", "year": "y"}
def _get_page_hash(query: str, page: int, headers: dict[str, str]) -> str:
resp = get(f"{base_url}/web/result?q={query}&page={page}", headers=headers)
# detect captcha (if any)
doc = html.fromstring(resp.text)
if eval_xpath(doc, "//*[@id='spam-messages']"):
raise SearxEngineCaptchaException()
# the text we search for looks like:
# load("/desk?lang="+eV.p.param['hl']+"&q="+eV['p']['q_encode']+"&page=5&h=aa45603&t=177582576&origin=web&comp=web_serp_pag&p=gmx-com&sp=&lr="+eV.p.param['lr0']+"&mkt="+eV.p.param['mkt0']+"&family="+eV.p.param['familyFilter']+"&fcons="+eV.p.perm.fCons,"google", "eMMO", "eMH","eMP"); # pylint: disable=line-too-long
return extr(resp.text, "&h=", "&t=")
+19 -1
View File
@@ -278,10 +278,28 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
return ret_val
def detect_google_sorry(resp):
def detect_google_sorry(resp: "SXNG_Response"):
"""Detect Google's bot-protection responses (CAPTCHA / sorry pages).
Google may block requests in several ways:
1. Redirect to sorry.google.com (standard CAPTCHA).
2. HTTP 302 redirect to ``/sorry/index?...`` on the same host -- when the
HTTP client doesn't follow the redirect, the response body is a short
HTML stub with a link to the sorry page.
3. Short HTML response (<2000 bytes) containing "/sorry/" -- a meta-refresh
or JS redirect variant.
"""
if resp.url.host == "sorry.google.com" or resp.url.path.startswith("/sorry"):
raise SearxEngineCaptchaException()
if resp.status_code == 302:
raise SearxEngineCaptchaException()
if len(resp.text) < 2000 and "/sorry/" in resp.text:
raise SearxEngineCaptchaException()
def request(query: str, params: "OnlineParams") -> None:
"""Google search request"""
+188 -159
View File
@@ -23,9 +23,11 @@ The google news API ignores some parameters from the common :ref:`google API`:
.. _num: https://developers.google.com/custom-search/docs/xml_results#numsp
.. _save: https://developers.google.com/custom-search/docs/xml_results#safesp
"""
import typing as t
from urllib.parse import urlencode
import json
import base64
from urllib.parse import urlencode
from lxml import html
import babel
@@ -44,18 +46,24 @@ from searx.engines.google import (
)
from searx.enginelib.traits import EngineTraits
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
# about
about = {
"website": 'https://news.google.com',
"wikidata_id": 'Q12020',
"official_api_documentation": 'https://developers.google.com/custom-search',
"website": "https://news.google.com",
"wikidata_id": "Q12020",
"official_api_documentation": "https://developers.google.com/custom-search",
"use_official_api": False,
"require_api_key": False,
"results": 'HTML',
"results": "HTML",
}
# engine dependent config
categories = ['news']
categories = ["news"]
paging = False
time_range_support = False
@@ -64,231 +72,252 @@ time_range_support = False
#
# safesearch : results are identical for safesearch=0 and safesearch=2
safesearch = True
base_url: str = "https://news.google.com"
def request(query, params):
def request(query: str, params: "OnlineParams") -> None:
"""Google-News search request"""
sxng_locale = params.get('searxng_locale', 'en-US')
ceid = locales.get_engine_locale(sxng_locale, traits.custom['ceid'], default='US:en')
sxng_locale = params.get("searxng_locale", "en-US")
ceid: str = locales.get_engine_locale(
sxng_locale, traits.custom["ceid"], default="US:en"
) # pyright: ignore[reportAssignmentType]
google_info = get_google_info(params, traits)
google_info['subdomain'] = 'news.google.com' # google news has only one domain
google_info["subdomain"] = "news.google.com" # google news has only one domain
ceid_region, ceid_lang = ceid.split(':')
ceid_region, ceid_lang = ceid.split(":")
ceid_lang, ceid_suffix = (
ceid_lang.split('-')
ceid_lang.split(":")
+ [
None,
"",
]
)[:2]
google_info['params']['hl'] = ceid_lang
google_info["params"]["hl"] = ceid_lang
if ceid_suffix and ceid_suffix not in ['Hans', 'Hant']:
if ceid_suffix and ceid_suffix not in ["Hans", "Hant"]:
if ceid_region.lower() == ceid_lang:
google_info['params']['hl'] = ceid_lang + '-' + ceid_region
google_info["params"]["hl"] = ceid_lang + "-" + ceid_region
else:
google_info['params']['hl'] = ceid_lang + '-' + ceid_suffix
google_info["params"]["hl"] = ceid_lang + "-" + ceid_suffix
elif ceid_region.lower() != ceid_lang:
if ceid_region in ['AT', 'BE', 'CH', 'IL', 'SA', 'IN', 'BD', 'PT']:
google_info['params']['hl'] = ceid_lang
if ceid_region in ["AT", "BE", "CH", "IL", "SA", "IN", "BD", "PT"]:
google_info["params"]["hl"] = ceid_lang
else:
google_info['params']['hl'] = ceid_lang + '-' + ceid_region
google_info["params"]["hl"] = ceid_lang + "-" + ceid_region
google_info['params']['lr'] = 'lang_' + ceid_lang.split('-')[0]
google_info['params']['gl'] = ceid_region
google_info["params"]["lr"] = "lang_" + ceid_lang.split("-")[0]
google_info["params"]["gl"] = ceid_region
query_url = (
'https://'
+ google_info['subdomain']
"https://"
+ google_info["subdomain"]
+ "/search?"
+ urlencode(
{
'q': query,
**google_info['params'],
}
{"q": query, **google_info["params"]},
)
# ceid includes a ':' character which must not be urlencoded
+ ('&ceid=%s' % ceid)
+ ("&ceid=%s" % ceid)
)
params['url'] = query_url
params['cookies'] = google_info['cookies']
params['headers'].update(google_info['headers'])
return params
params["url"] = query_url
params["cookies"] = google_info["cookies"]
params["headers"].update(google_info["headers"])
def response(resp):
def response(resp: "SXNG_Response") -> EngineResults:
"""Get response from google's search request"""
results = []
res = EngineResults()
detect_google_sorry(resp)
# convert the text to dom
dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, '//div[@class="xrnccd"]'):
for result in eval_xpath_list(dom, "//div[@jslog and @data-n-tid and @jsdata]"):
# The first <a> tag in the <article> contains the link to the article
# The href attribute of the <a> tag is a google internal link, we have
# to decode
url: str = eval_xpath_getindex(result, "./a[@target='_blank']/@href", 0, default=0)
if not url:
continue
if url.startswith("./"):
url = base_url + url[1:]
href = eval_xpath_getindex(result, './article/a/@href', 0)
href = href.split('?')[0]
href = href.split('/')[-1]
href = base64.urlsafe_b64decode(href + '====')
href = href[href.index(b'http') :].split(b'\xd2')[0]
href = href.decode()
# The real URL is often encoded in the "jslog" attribute
jslog: str | None = eval_xpath_getindex(result, "./a[@target='_blank']/@jslog", 0, default=None)
title = extract_text(eval_xpath(result, './article/h3[1]'))
# Try to extract the real URL from jslog
real_url: str | None = None
if jslog:
# jslog format is usually: "95014; 5:<base64>; track:click,vis". We
# want the second part (index 1) after splitting by ";"
parts: list[str] = jslog.split(";")
if len(parts) > 1:
b64_data: str = parts[1].split(":")[-1].strip()
# Pad base64 if necessary
b64_data += "=" * (-len(b64_data) % 4)
decoded_data: list[str | None] = json.loads(base64.b64decode(b64_data).decode("utf-8"))
# The URL is typically the last element in the decoded array
if (
isinstance(decoded_data, list)
and isinstance(decoded_data[-1], str)
and decoded_data[-1].startswith("http")
):
real_url = decoded_data[-1]
if real_url:
url = real_url
else:
logger.error(f"no real-url found: {url}")
continue
# The pub_date is mostly a string like 'yesterday', not a real
# timezone date or time. Therefore we can't use publishedDate.
pub_date = extract_text(eval_xpath(result, './article//time'))
pub_origin = extract_text(eval_xpath(result, './article//a[@data-n-tid]'))
title = extract_text(eval_xpath(result, "./h4")) or ""
content = ' / '.join([x for x in [pub_origin, pub_date] if x])
# The pub_date is mostly a string like 'yesterday', not a real timezone
# date or time. Therefore we can't use publishedDate and place the
# *pub* sting into the content.
# The image URL is located in a preceding sibling <img> tag, e.g.:
# "https://lh3.googleusercontent.com/DjhQh7DMszk.....z=-p-h100-w100"
# These URL are long but not personalized (double checked via tor).
pub_date = extract_text(eval_xpath(result, ".//time"))
pub_origin = extract_text(eval_xpath(result, ".//div[contains(@class, 'vr1PYe')]"))
content = " / ".join([x for x in [pub_origin, pub_date] if x])
thumbnail = extract_text(result.xpath('preceding-sibling::a/figure/img/@src'))
thumbnail: str = eval_xpath_getindex(result, ".//figure/img/@src", 0, default="")
if thumbnail and thumbnail.startswith("/"):
thumbnail = base_url + thumbnail
results.append(
{
'url': href,
'title': title,
'content': content,
'thumbnail': thumbnail,
}
res.add(
res.types.MainResult(
url=url,
title=title,
content=content,
thumbnail=thumbnail,
)
)
# return results
return results
return res
ceid_list = [
'AE:ar',
'AR:es-419',
'AT:de',
'AU:en',
'BD:bn',
'BE:fr',
'BE:nl',
'BG:bg',
'BR:pt-419',
'BW:en',
'CA:en',
'CA:fr',
'CH:de',
'CH:fr',
'CL:es-419',
'CN:zh-Hans',
'CO:es-419',
'CU:es-419',
'CZ:cs',
'DE:de',
'EG:ar',
'ES:es',
'ET:en',
'FR:fr',
'GB:en',
'GH:en',
'GR:el',
'HK:zh-Hant',
'HU:hu',
'ID:en',
'ID:id',
'IE:en',
'IL:en',
'IL:he',
'IN:bn',
'IN:en',
'IN:hi',
'IN:ml',
'IN:mr',
'IN:ta',
'IN:te',
'IT:it',
'JP:ja',
'KE:en',
'KR:ko',
'LB:ar',
'LT:lt',
'LV:en',
'LV:lv',
'MA:fr',
'MX:es-419',
'MY:en',
'NA:en',
'NG:en',
'NL:nl',
'NO:no',
'NZ:en',
'PE:es-419',
'PH:en',
'PK:en',
'PL:pl',
'PT:pt-150',
'RO:ro',
'RS:sr',
'RU:ru',
'SA:ar',
'SE:sv',
'SG:en',
'SI:sl',
'SK:sk',
'SN:fr',
'TH:th',
'TR:tr',
'TW:zh-Hant',
'TZ:en',
'UA:ru',
'UA:uk',
'UG:en',
'US:en',
'US:es-419',
'VE:es-419',
'VN:vi',
'ZA:en',
'ZW:en',
"AE:ar",
"AR:es-419",
"AT:de",
"AU:en",
"BD:bn",
"BE:fr",
"BE:nl",
"BG:bg",
"BR:pt-419",
"BW:en",
"CA:en",
"CA:fr",
"CH:de",
"CH:fr",
"CL:es-419",
"CN:zh-Hans",
"CO:es-419",
"CU:es-419",
"CZ:cs",
"DE:de",
"EE:et",
"EG:ar",
"ES:ca",
"ES:es",
"ET:en",
"FI:fi",
"FR:fr",
"GB:en",
"GH:en",
"GR:el",
"HK:zh-Hant",
"HU:hu",
"ID:en",
"ID:id",
"IE:en",
"IL:en",
"IL:he",
"IN:bn",
"IN:en",
"IN:gu",
"IN:hi",
"IN:ml",
"IN:mr",
"IN:pa",
"IN:ta",
"IN:te",
"IT:it",
"JP:ja",
"KE:en",
"KR:ko",
"LB:ar",
"LT:lt",
"LV:en",
"LV:lv",
"MA:fr",
"MY:en",
"MY:ms",
"NA:en",
"NG:en",
"NL:nl",
"NO:no",
"NZ:en",
"PH:en",
"PK:en",
"PL:pl",
"RO:ro",
"RS:sr",
"RU:ru",
"SA:ar",
"SE:sv",
"SG:en",
"SI:sl",
"SK:sk",
"SN:fr",
"TH:th",
"TR:tr",
"TZ:en",
"UA:ru",
"UA:uk",
"UG:en",
"US:en",
"VN:vi",
"ZA:en",
"ZW:en",
]
"""List of region/language combinations supported by Google News. Values of the
``ceid`` argument of the Google News REST API."""
_skip_values = [
'ET:en', # english (ethiopia)
'ID:en', # english (indonesia)
'LV:en', # english (latvia)
"ET:en", # english (ethiopia)
"ID:en", # english (indonesia)
"LV:en", # english (latvia)
]
_ceid_locale_map = {'NO:no': 'nb-NO'}
_ceid_locale_map = {"NO:no": "nb-NO"}
def fetch_traits(engine_traits: EngineTraits):
_fetch_traits(engine_traits, add_domains=False)
engine_traits.custom['ceid'] = {}
engine_traits.custom["ceid"] = {}
for ceid in ceid_list:
if ceid in _skip_values:
continue
region, lang = ceid.split(':')
x = lang.split('-')
region, lang = ceid.split(":")
x = lang.split("-")
if len(x) > 1:
if x[1] not in ['Hant', 'Hans']:
if x[1] not in ["Hant", "Hans"]:
lang = x[0]
sxng_locale = _ceid_locale_map.get(ceid, lang + '-' + region)
sxng_locale = _ceid_locale_map.get(ceid, lang + "-" + region)
try:
locale = babel.Locale.parse(sxng_locale, sep='-')
locale = babel.Locale.parse(sxng_locale, sep="-")
except babel.UnknownLocaleError:
print("ERROR: %s -> %s is unknown by babel" % (ceid, sxng_locale))
continue
engine_traits.custom['ceid'][locales.region_tag(locale)] = ceid
engine_traits.custom["ceid"][locales.region_tag(locale)] = ceid
+90
View File
@@ -0,0 +1,90 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Heexy_ is a minimalist search engine that focuses on privacy.
Although it also supports news and videos, these are not implemented here
because they usually return no result to very few irrelevant ones.
It seems to use Bing internally, as the image thumbnails are loaded from Bing.
.. _Heexy: https://docs.heexy.org/introduction
"""
from urllib.parse import urlencode
import typing as t
from searx.exceptions import SearxEngineAccessDeniedException
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://heexy.org",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
paging = True
safesearch = True
categories = ["general"]
heexy_categ = "web"
"""Category to search in. Can be either "web" or "image"."""
base_url = "https://seapi.heexy.org"
safe_search_map = {0: "off", 1: "on", 2: "on"}
def init(_):
if heexy_categ not in ("web", "image"):
raise ValueError("invalid search category: %s" % heexy_categ)
def request(query: str, params: "OnlineParams") -> None:
args = {
"q": query,
"page": params["pageno"],
"safe": safe_search_map[params["safesearch"]],
}
if params["searxng_locale"] != "all":
args["lang"] = params["searxng_locale"].split("-")[0]
params["url"] = f"{base_url}/search/{heexy_categ}?{urlencode(args)}"
params["headers"]["Origin"] = base_url
def response(resp: "SXNG_Response"):
res = EngineResults()
json_resp = resp.json()
if not json_resp["success"]:
raise SearxEngineAccessDeniedException()
result: dict[str, str]
for result in json_resp["results"]:
if heexy_categ == "web":
res.add(
res.types.MainResult(
url=result["url"],
title=result["title"],
content=result["description"],
)
)
elif heexy_categ == "image":
res.add(
res.types.Image(
title=result["description"],
url=result["url"],
thumbnail_src=result["image"],
img_src=result["rawImage"],
)
)
return res
+12 -5
View File
@@ -20,6 +20,7 @@ Paging:
- :py:obj:`paging`
- :py:obj:`page_size`
- :py:obj:`first_page_num`
- :py:obj:`send_page_num_on_first_page`
Time Range:
@@ -29,7 +30,7 @@ Time Range:
Safe-Search:
- :py:obj:`safe_search_support`
- :py:obj:`safesearch`
- :py:obj:`safe_search_map`
Response:
@@ -103,7 +104,7 @@ Replacements are:
``{safe_search}``:
Safe-search :py:obj:`URL parameter <safe_search_map>` if engine
:py:obj:`supports safe-search <safe_search_support>`. The ``{safe_search}``
:py:obj:`supports safe-search <safesearch>`. The ``{safe_search}``
replacement is taken from the :py:obj:`safes_search_map`. Filter results::
0: none, 1: moderate, 2:strict
@@ -169,6 +170,10 @@ number, but an offset.'''
first_page_num = 1
'''Number of the first page (usually 0 or 1).'''
send_page_num_on_first_page = True
'''Whether to include the page number in the request for the first page.
This can help if an engine blocks request that send a page number for the first page.'''
results_query = ''
'''JSON query for the list of result items.
@@ -236,7 +241,7 @@ time_range_map = {
year: 365
'''
safe_search_support = False
safesearch = False
'''Engine supports safe-search.'''
safe_search_map = {0: '&filter=none', 1: '&filter=moderate', 2: '&filter=strict'}
@@ -286,7 +291,6 @@ def do_query(data, q): # pylint: disable=invalid-name
qkey = q[0]
for key, value in iterate(data):
if len(q) == 1:
if key == qkey:
ret.append(value)
@@ -323,10 +327,13 @@ def request(query, params): # pylint: disable=redefined-outer-name
if params['safesearch']:
safe_search = safe_search_map[params['safesearch']]
pageno = ""
if send_page_num_on_first_page or params["pageno"] != 1:
pageno = (params['pageno'] - 1) * page_size + first_page_num
fp = { # pylint: disable=invalid-name
'query': urlencode({'q': query})[2:],
'lang': lang,
'pageno': (params['pageno'] - 1) * page_size + first_page_num,
'pageno': pageno,
'time_range': time_range,
'safe_search': safe_search,
}
-205
View File
@@ -1,205 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Karmasearch uses Brave's index, so the results should be the same as Brave's.
However, the advantages of this engine are:
- it has less strict rate-limits
- it has a JSON API, so it's less likely to break
"""
from datetime import datetime
from urllib.parse import urlencode
import typing as t
from dateutil import parser
from searx.enginelib.traits import EngineTraits
from searx.utils import html_to_text
from searx.result_types import EngineResults, MainResult
from searx.result_types._base import LegacyResult
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://karmasearch.org",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://api.karmasearch.org"
categories = ["web", "general"]
search_type = "web" # supported: web, images, videos, news
# all types except "images" support pagination
paging = True
safesearch = True
time_range_support = True
safe_search_map = {0: "off", 1: "moderate", 2: "strict"}
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
def init(_):
if search_type not in ("web", "images", "videos", "news"):
raise ValueError(f"invalid search type: {search_type}")
def request(query: str, params: "OnlineParams") -> None:
engine_region: str = traits.get_region(params["searxng_locale"]) or "en-US"
args: dict[str, str | int] = {
"searchTerm": query,
"adultFilter": safe_search_map[params["safesearch"]],
"pageNumber": params["pageno"],
"country": engine_region.split("-")[-1],
"userLanguage": "en", # UI language: en, es or fr / no effect on search results
"market": engine_region,
}
if params["time_range"]:
args["freshness"] = time_range_map[params["time_range"]]
# Needed to circumvent Cloudflare bot protection
params['headers']['Referer'] = "https://karmasearch.org"
params["url"] = f"{base_url}/search/{search_type}?{urlencode(args)}"
def _parse_date(date_string: str) -> datetime | None:
try:
return parser.parse(date_string)
except parser.ParserError:
return None
def _parse_general(result: dict[str, str]):
return MainResult(
url=result["url"],
title=result["title"],
content=html_to_text(result["description"]),
thumbnail=result.get("thumbnail", ""),
)
def _parse_news(result: dict[str, str]) -> LegacyResult:
return LegacyResult(
{
"url": result["url"],
"title": result["title"],
"content": html_to_text(result["description"]),
"thumbnail": result.get("thumbnail"),
"publishedDate": _parse_date(result.get("age", "")),
}
)
def _parse_videos(result: dict[str, t.Any]) -> LegacyResult:
return LegacyResult(
{
"template": "videos.html",
"url": result["url"],
"title": result["title"],
"content": html_to_text(result["description"]),
"thumbnail": result.get("thumbnail"),
"publishedDate": _parse_date(result.get("age", "")),
"length": result.get("video", {}).get("duration"),
}
)
def _parse_images(result: dict[str, t.Any]) -> LegacyResult:
return LegacyResult(
{
"template": "images.html",
"url": result["url"],
"title": result["title"],
"content": "",
"img_src": result.get("properties", {}).get("url"),
"thumbnail_src": result.get("thumbnail", {}).get("src"),
}
)
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_resp: dict[str, t.Any] = resp.json()
if not isinstance(json_resp, dict):
return res # pyright: ignore[reportUnreachable]
for result in json_resp["results"]:
# hide sponsored results
if result.get("sponsored", False):
continue
if "videos" in result:
for videos_result in result["videos"]:
res.add(_parse_videos(videos_result))
continue
if "news" in result:
for news_result in result["news"]:
res.add(_parse_news(news_result))
continue
if search_type == "news":
res.add(_parse_news(result))
elif search_type == "videos":
res.add(_parse_videos(result))
elif search_type == "images":
res.add(_parse_images(result))
else:
res.add(_parse_general(result))
return res
def fetch_traits(engine_traits: EngineTraits):
"""Fetch :ref:`languages <brave languages>` and :ref:`regions <brave
regions>` from Brave."""
# pylint: disable=import-outside-toplevel, too-many-branches
from lxml import html
import babel
from searx.locales import region_tag
from searx.network import get # see https://github.com/searxng/searxng/issues/762
# from searx.engines.xpath import extract_text
from searx.utils import gen_useragent
headers = {
"Accept-Encoding": "gzip, deflate",
"Cache-Control": "no-cache",
"DNT": "1",
"Connection": "keep-alive",
"Accept-Language": "en,en-US;q=0.7,en;q=0.3",
"User-Agent": gen_useragent(),
}
resp = get("https://karmasearch.org/settings", headers=headers, timeout=5)
if not resp.ok:
raise RuntimeError("Response from Brave languages is not OK.")
dom = html.fromstring(resp.text)
for option in dom.xpath("//select[@name='country']/option"):
country_tag: str = option.get("value", "")
try:
sxng_tag = region_tag(babel.Locale.parse(country_tag, sep="-"))
except babel.UnknownLocaleError:
# silently ignore unknown languages
continue
# print("%-20s: %s <-- %s" % (extract_text(option), country_tag, sxng_tag))
conflict = engine_traits.regions.get(sxng_tag)
if conflict:
if conflict != country_tag:
print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, country_tag))
continue
engine_traits.regions[sxng_tag] = country_tag
+7 -2
View File
@@ -45,7 +45,7 @@ about = {
base_url = "https://api2.marginalia-search.com"
safesearch = True
categories = ["general"]
paging = False
paging = True
results_per_page = 20
api_key = None
"""To get an API key, please follow the instructions from `Key and license`_
@@ -85,7 +85,12 @@ class ApiSearchResults(t.TypedDict):
def request(query: str, params: dict[str, t.Any]):
query_params = {"count": results_per_page, "nsfw": min(params["safesearch"], 1), "query": query}
query_params = {
"page": params["pageno"],
"count": results_per_page,
"nsfw": min(params["safesearch"], 1),
"query": query,
}
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
params["headers"]["User-Agent"] = searxng_useragent()
+3 -3
View File
@@ -51,7 +51,7 @@ about = {
# engine dependent config
categories = ['general']
paging = True
number_of_results = 5
page_size = 5
search_type: str = 'nearmatch'
"""Which type of search to perform. One of the following values: ``nearmatch``,
@@ -110,7 +110,7 @@ def request(query, params):
params['language'] = params['language'].split('-')[0]
api_url = f"{base_url.rstrip('/')}/{api_path}?".format(language=params['language'])
offset = (params['pageno'] - 1) * number_of_results
offset = (params['pageno'] - 1) * page_size
args = {
'action': 'query',
@@ -118,7 +118,7 @@ def request(query, params):
'format': 'json',
'srsearch': query,
'sroffset': offset,
'srlimit': number_of_results,
'srlimit': page_size,
'srwhat': search_type,
'srprop': srprop,
'srsort': srsort,
+3 -3
View File
@@ -14,7 +14,7 @@ about = {
}
# engine dependent config
number_of_results = 20 # Don't put this over 5000
page_size = 20 # Don't put this over 5000
categories = ["it", "packages"]
disabled = True
shortcut = "cpan"
@@ -43,7 +43,7 @@ query_data_template = {
{"date": {"order": "desc"}},
],
'_source': ['documentation', "abstract"],
'size': number_of_results,
'size': page_size,
}
search_url = urlunparse(["https", "fastapi.metacpan.org", "/v1/file/_search", "", "", ""])
@@ -53,7 +53,7 @@ def request(query, params):
params["method"] = "POST"
query_data = query_data_template
query_data["query"]["multi_match"]["query"] = query
query_data["from"] = (params["pageno"] - 1) * number_of_results
query_data["from"] = (params["pageno"] - 1) * page_size
params["json"] = query_data
return params
+4 -2
View File
@@ -59,8 +59,6 @@ def request(query, params):
args = {
"q": query,
"safe": min(params["safesearch"], 1),
language_param: traits.get_language(params["searxng_locale"], traits.custom["language_all"]),
region_param: traits.get_region(params["searxng_locale"], traits.custom["region_all"]),
}
if search_type:
@@ -76,6 +74,10 @@ def request(query, params):
logger.debug(args["since"])
params["url"] = f"{base_url}/search?{urlencode(args)}"
params["cookies"] = {
language_param: traits.get_language(params["searxng_locale"], traits.custom["language_all"]),
region_param: traits.get_region(params["searxng_locale"], traits.custom["region_all"]),
}
return params
-1
View File
@@ -93,7 +93,6 @@ def search(query, params) -> EngineResults:
query = _client.find({key: q}).skip((params['pageno'] - 1) * results_per_page).limit(results_per_page)
res.add(res.types.LegacyResult(number_of_results=query.count()))
for row in query:
del row['_id']
kvmap = {str(k): str(v) for k, v in row.items()}
+1 -10
View File
@@ -4,7 +4,6 @@
.. _Odysee: https://github.com/OdyseeTeam/odysee-frontend
"""
import time
from datetime import datetime
from urllib.parse import urlencode
@@ -12,6 +11,7 @@ import babel
from searx.enginelib.traits import EngineTraits
from searx.locales import language_tag
from searx.utils import format_duration
# Engine metadata
about = {
@@ -61,15 +61,6 @@ def request(query, params):
return params
# Format the video duration
def format_duration(duration):
seconds = int(duration)
length = time.gmtime(seconds)
if length.tm_hour:
return time.strftime("%H:%M:%S", length)
return time.strftime("%M:%S", length)
def response(resp):
data = resp.json()
results = []
+3 -3
View File
@@ -21,15 +21,15 @@ about = {
categories = ['images']
paging = True
nb_per_page = 20
page_size = 20
base_url = 'https://api.openverse.org/v1/images/'
search_string = '?page={page}&page_size={nb_per_page}&format=json&{query}'
search_string = '?page={page}&page_size={page_size}&format=json&{query}'
def request(query, params):
search_path = search_string.format(query=urlencode({'q': query}), nb_per_page=nb_per_page, page=params['pageno'])
search_path = search_string.format(query=urlencode({'q': query}), page_size=page_size, page=params['pageno'])
params['url'] = base_url + search_path
+4 -2
View File
@@ -9,7 +9,7 @@ from lxml import html
from searx.result_types import EngineResults
from searx.utils import eval_xpath_list, gen_useragent
from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException
from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException
from searx.network import get
@@ -58,6 +58,8 @@ def _get_secret_key():
# circumvents Cloudflare bot protections
"User-Agent": gen_useragent(),
"Referer": base_url,
"Sec-GPC": "1",
"Connection": "keep-alive",
},
)
@@ -95,7 +97,7 @@ def request(query, params):
try:
secret_key = _get_secret_key()
CACHE.set(SECRET_KEY_DB_KEY, secret_key)
except SearxEngineAPIException as e:
except (SearxEngineAPIException, SearxEngineAccessDeniedException) as e:
logger.debug("failed to extract API key %s" % e)
secret_key = api_key
+2 -2
View File
@@ -20,7 +20,7 @@ about = {
# engine dependent config
categories = ['map']
paging = False
number_of_results = 10
page_size = 10
# search-url
base_url = 'https://photon.komoot.io/'
@@ -33,7 +33,7 @@ supported_languages = ['de', 'en', 'fr', 'it']
# do search-request
def request(query, params):
params['url'] = base_url + search_string.format(query=urlencode({'q': query}), limit=number_of_results)
params['url'] = base_url + search_string.format(query=urlencode({'q': query}), limit=page_size)
if params['language'] != 'all':
language = params['language'].split('_')[0]
-41
View File
@@ -1,41 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Podcast Index"""
from urllib.parse import quote_plus
from datetime import datetime
about = {
'website': 'https://podcastindex.org',
'official_api_documentation': None, # requires an account
'use_official_api': False,
'require_api_key': False,
'results': 'JSON',
}
categories = []
base_url = "https://podcastindex.org"
def request(query, params):
params['url'] = f"{base_url}/api/search/byterm?q={quote_plus(query)}"
return params
def response(resp):
results = []
json = resp.json()
for result in json['feeds']:
results.append(
{
'url': result['link'],
'title': result['title'],
'content': result['description'],
'thumbnail': result['image'],
'publishedDate': datetime.fromtimestamp(result['newestItemPubdate']),
'metadata': f"{result['author']}, {result['episodeCount']} episodes",
}
)
return results
+8 -3
View File
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Public domain image archive"""
import re
from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl
from json import dumps
@@ -49,6 +51,8 @@ paging = True
__CACHED_API_URL = None
_API_URL_RE = re.compile(r"\"(https://.*?/search-proxy)\"")
def _clean_url(url):
parsed = urlparse(url)
@@ -74,11 +78,12 @@ def _get_algolia_api_url():
if resp.status_code != 200:
raise LookupError("Failed to obtain AWS api url for PDImageArchive")
api_url = extr(resp.text, 'const r="', '"', default=None)
if api_url is None:
api_url_match = _API_URL_RE.search(resp.text)
if api_url_match is None:
raise LookupError("Couldn't obtain AWS api url for PDImageArchive")
api_url = api_url_match.group(1)
__CACHED_API_URL = api_url
return api_url
+3 -3
View File
@@ -57,7 +57,7 @@ categories = ["science", "scientific publications"]
eutils_api = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
# engine dependent config
number_of_results = 10
page_size = 10
pubmed_url = "https://www.ncbi.nlm.nih.gov/pubmed/"
@@ -67,8 +67,8 @@ def request(query: str, params: "OnlineParams") -> None:
{
"db": "pubmed",
"term": query,
"retstart": (params["pageno"] - 1) * number_of_results,
"hits": number_of_results,
"retstart": (params["pageno"] - 1) * page_size,
"hits": page_size,
}
)
esearch_url = f"{eutils_api}/esearch.fcgi?{args}"
+112 -148
View File
@@ -6,7 +6,6 @@ engineered by reading the network log of https://www.qwant.com/ queries.
For Qwant's *web-search* two alternatives are implemented:
- ``web``: uses the :py:obj:`api_url` which returns a JSON structure
- ``web-lite``: uses the :py:obj:`web_lite_url` which returns a HTML page
Configuration
@@ -22,7 +21,7 @@ This implementation is used by different qwant engines in the :ref:`settings.yml
.. code:: yaml
- name: qwant
qwant_categ: web-lite # alternatively use 'web'
qwant_categ: web
...
- name: qwant news
qwant_categ: news
@@ -39,6 +38,8 @@ Implementations
"""
import typing as t
from datetime import (
datetime,
timedelta,
@@ -47,8 +48,7 @@ from json import loads
from urllib.parse import urlencode
import babel
import lxml
from flask_babel import gettext
from flask_babel import gettext # pyright: ignore[reportUnknownVariableType]
from searx.enginelib.traits import EngineTraits
from searx.exceptions import (
@@ -59,11 +59,13 @@ from searx.exceptions import (
)
from searx.network import raise_for_httperror
from searx.utils import (
eval_xpath,
eval_xpath_list,
extract_text,
get_embeded_stream_url,
)
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
from searx.extended_types import SXNG_Response
# about
about = {
@@ -82,113 +84,66 @@ max_page = 5
"""5 pages maximum (``&p=5``): Trying to do more just results in an improper
redirect"""
qwant_categ = None
"""One of ``web-lite`` (or ``web``), ``news``, ``images`` or ``videos``"""
qwant_categ: str = None # pyright: ignore[reportAssignmentType]
"""One of ``web``, ``news``, ``images`` or ``videos``"""
safesearch = True
# safe_search_map = {0: '&safesearch=0', 1: '&safesearch=1', 2: '&safesearch=2'}
# fmt: off
qwant_news_locales = [
'ca_ad', 'ca_es', 'ca_fr', 'co_fr', 'de_at', 'de_ch', 'de_de', 'en_au',
'en_ca', 'en_gb', 'en_ie', 'en_my', 'en_nz', 'en_us', 'es_ad', 'es_ar',
'es_cl', 'es_co', 'es_es', 'es_mx', 'es_pe', 'eu_es', 'eu_fr', 'fc_ca',
'fr_ad', 'fr_be', 'fr_ca', 'fr_ch', 'fr_fr', 'it_ch', 'it_it', 'nl_be',
'nl_nl', 'pt_ad', 'pt_pt',
"ca_ad", "ca_es", "ca_fr", "co_fr", "de_at", "de_ch", "de_de", "en_au",
"en_ca", "en_gb", "en_ie", "en_my", "en_nz", "en_us", "es_ad", "es_ar",
"es_cl", "es_co", "es_es", "es_mx", "es_pe", "eu_es", "eu_fr", "fc_ca",
"fr_ad", "fr_be", "fr_ca", "fr_ch", "fr_fr", "it_ch", "it_it", "nl_be",
"nl_nl", "pt_ad", "pt_pt",
]
# fmt: on
# search-url
api_url = "https://api.qwant.com/v3/search/"
"""URL of Qwant's API (JSON)"""
web_lite_url = "https://lite.qwant.com/"
"""URL of Qwant-Lite (HTML)"""
def request(query, params):
def request(query: str, params: "OnlineParams") -> None:
"""Qwant search request"""
if not query:
return None
return
q_locale = traits.get_region(params["searxng_locale"], default="en_US")
url = api_url + f"{qwant_categ}?"
args = {"q": query}
results_per_page = 10
if qwant_categ == "images":
results_per_page = 50
args = {
"q": query,
"count": results_per_page,
"locale": q_locale,
"offset": (params["pageno"] - 1) * results_per_page,
"device": "desktop",
"safesearch": params["safesearch"],
"tgp": 1,
"display": True,
"llm": True,
}
params["raise_for_httperror"] = False
if qwant_categ == "web-lite":
url = web_lite_url + "?"
args["locale"] = q_locale.lower()
args["l"] = q_locale.split("_")[0]
args["s"] = params["safesearch"]
args["p"] = params["pageno"]
params["raise_for_httperror"] = True
elif qwant_categ == "images":
args["count"] = 50
args["locale"] = q_locale
args["safesearch"] = params["safesearch"]
args["tgp"] = 3
args["offset"] = (params["pageno"] - 1) * args["count"]
else: # web, news, videos
args["count"] = 10
args["locale"] = q_locale
args["safesearch"] = params["safesearch"]
args["llm"] = "false"
args["tgp"] = 3
args["offset"] = (params["pageno"] - 1) * args["count"]
params["url"] = url + urlencode(args)
return params
params["url"] = f"{api_url}{qwant_categ}?{urlencode(args)}"
def response(resp):
if qwant_categ == "web-lite":
return parse_web_lite(resp)
return parse_web_api(resp)
def parse_web_lite(resp):
"""Parse results from Qwant-Lite"""
results = []
dom = lxml.html.fromstring(resp.text)
for item in eval_xpath_list(dom, "//section/article"):
if eval_xpath(item, "./span[contains(@class, 'tooltip')]"):
# ignore randomly interspersed advertising adds
continue
results.append(
{
"url": extract_text(eval_xpath(item, "./span[contains(@class, 'url partner')]")),
"title": extract_text(eval_xpath(item, "./h2/a")),
"content": extract_text(eval_xpath(item, "./p")),
}
)
return results
def parse_web_api(resp):
def response(resp: "SXNG_Response") -> EngineResults:
"""Parse results from Qwant's API"""
# pylint: disable=too-many-locals, too-many-branches, too-many-statements
results = []
res = EngineResults()
# Try to load JSON result
search_results: dict[str, t.Any] = {}
try:
search_results = loads(resp.text)
search_results = resp.json()
except ValueError:
search_results = {}
pass
data = search_results.get("data", {})
data: dict[str, t.Any] = search_results.get("data", {}) # pyright: ignore[reportAny]
# check for an API error
if search_results.get("status") != "success":
@@ -207,13 +162,13 @@ def parse_web_api(resp):
if qwant_categ == "web":
# The WEB query contains a list named 'mainline'. This list can contain
# different result types (e.g. mainline[0]['type'] returns type of the
# result items in mainline[0]['items']
# different result types (e.g. mainline[0]["type"] returns type of the
# result items in mainline[0]["items"]
mainline = data.get("result", {}).get("items", {}).get("mainline", {})
else:
# Queries on News, Images and Videos do not have a list named 'mainline'
# in the response. The result items are directly in the list
# result['items'].
# result["items"].
mainline = data.get("result", {}).get("items", [])
mainline = [
{"type": qwant_categ, "items": mainline},
@@ -221,8 +176,9 @@ def parse_web_api(resp):
# return empty array if there are no results
if not mainline:
return []
return res
row: dict[str, t.Any]
for row in mainline:
mainline_type = row.get("type", "web")
if mainline_type != qwant_categ:
@@ -232,90 +188,98 @@ def parse_web_api(resp):
# ignore adds
continue
mainline_items = row.get("items", [])
mainline_items: list[dict[str, t.Any]] = row.get("items", [])
for item in mainline_items:
title = item.get("title", None)
res_url = item.get("url", None)
title: str = item.get("title", "")
res_url: str = item.get("url", "")
pub_date: datetime | None = None
thumbnail: str = ""
content: str = item.get("desc", "")
_date: float | None = item.get("date")
if _date:
try:
pub_date = datetime.fromtimestamp(_date)
except ValueError:
# news' date value milli seconds
pub_date = datetime.fromtimestamp(_date / 1000)
if mainline_type == "web":
content = item["desc"]
results.append(
{
"title": title,
"url": res_url,
"content": content,
}
res.add(
res.types.MainResult(
title=title,
url=res_url,
content=content,
)
)
elif mainline_type == "news":
pub_date = item["date"]
if pub_date is not None:
pub_date = datetime.fromtimestamp(pub_date)
news_media = item.get("media", [])
thumbnail = None
if news_media:
thumbnail = news_media[0].get("pict", {}).get("url", None)
results.append(
{
"title": title,
"url": res_url,
"publishedDate": pub_date,
"thumbnail": thumbnail,
}
thumbnail = news_media[0].get("pict", {}).get("url", "")
res.add(
res.types.MainResult(
title=title,
content=content,
url=res_url,
publishedDate=pub_date,
thumbnail=thumbnail,
)
)
elif mainline_type == "images":
thumbnail = item["thumbnail"]
img_src = item["media"]
results.append(
{
"title": title,
"url": res_url,
"template": "images.html",
"thumbnail_src": thumbnail,
"img_src": img_src,
"resolution": f"{item['width']} x {item['height']}",
"img_format": item.get("thumb_type"),
}
res.add(
res.types.LegacyResult(
title=title,
url=res_url,
template="images.html",
thumbnail_src=item["thumbnail"] or "",
img_src=item["media"] or "",
resolution=f"{item['width']} x {item['height']}",
img_format=item.get("thumb_type"),
)
)
elif mainline_type == "videos":
# some videos do not have a description: while qwant-video
# returns an empty string, such video from a qwant-web query
# miss the 'desc' key.
d, s, c = item.get("desc"), item.get("source"), item.get("channel")
content_parts = []
d: str = item.get("desc", "")
s: str = item.get("source", "")
c: str = item.get("channel", "")
content_parts: list[str] = []
if d:
content_parts.append(d)
content_parts.append(f"{d}")
if s:
content_parts.append("%s: %s " % (gettext("Source"), s))
content_parts.append(f"{gettext('Source')}: {s} ")
if c:
content_parts.append("%s: %s " % (gettext("Channel"), c))
content_parts.append(f"{gettext('Channel')}: {c} ")
content = " // ".join(content_parts)
length = item["duration"]
if length is not None:
length = timedelta(milliseconds=length)
pub_date = item["date"]
if pub_date is not None:
pub_date = datetime.fromtimestamp(pub_date)
thumbnail = item["thumbnail"]
length = timedelta(milliseconds=(item["duration"] or 0))
thumbnail = item["thumbnail"] or ""
# from some locations (DE and others?) the s2 link do
# response a 'Please wait ..' but does not deliver the thumbnail
thumbnail = thumbnail.replace("https://s2.qwant.com", "https://s1.qwant.com", 1)
results.append(
{
"title": title,
"url": res_url,
"content": content,
"iframe_src": get_embeded_stream_url(res_url),
"publishedDate": pub_date,
"thumbnail": thumbnail,
"template": "videos.html",
"length": length,
}
res.add(
res.types.LegacyResult(
title=title,
url=res_url,
content=content,
iframe_src=get_embeded_stream_url(res_url),
publishedDate=pub_date,
thumbnail=thumbnail,
template="videos.html",
length=length,
)
)
return results
return res
def fetch_traits(engine_traits: EngineTraits):
@@ -326,7 +290,7 @@ def fetch_traits(engine_traits: EngineTraits):
from searx.utils import extr
resp = get(
about["website"],
about["website"], # pyright: ignore[reportArgumentType]
timeout=5,
)
if not resp.ok:
@@ -336,7 +300,7 @@ def fetch_traits(engine_traits: EngineTraits):
q_initial_props = loads(json_string)
q_locales = q_initial_props.get("locales")
eng_tag_list = set()
eng_tag_list: set[str] = set()
for country, v in q_locales.items():
for lang in v["langs"]:
+17 -4
View File
@@ -6,6 +6,7 @@
"""
import os
import random
import socket
from urllib.parse import urlencode
@@ -28,7 +29,7 @@ about = {
paging = True
categories = ["music", "radio"]
number_of_results = 10
page_size = 10
station_filters = [] # ['countrycode', 'language']
"""A list of filters to be applied to the search of radio stations. By default
@@ -59,7 +60,19 @@ seconds."""
def init(_):
global CACHE # pylint: disable=global-statement
CACHE = EngineCache("radio_browser")
server_list()
# In an environment with competing processes, the initial loading of the
# cache is required only once.
eng_state: str | None = CACHE.get("eng_state")
if not eng_state or not eng_state.startswith("STATE:"):
CACHE.set("eng_state", f"STATE: being initialized by PID {os.getpid()}")
try:
server_list()
except Exception:
CACHE.set("eng_state", f"ERROR: initialization by PID {os.getpid()} failed.")
raise
else:
logger.debug(eng_state)
def server_list() -> list[str]:
@@ -100,8 +113,8 @@ def request(query, params):
args = {
"name": query,
"order": "votes",
"offset": (params["pageno"] - 1) * number_of_results,
"limit": number_of_results,
"offset": (params["pageno"] - 1) * page_size,
"limit": page_size,
"hidebroken": "true",
"reverse": "true",
}
-2
View File
@@ -54,6 +54,4 @@ def response(resp):
results.extend({'suggestion': s} for s in response_json['suggestions'])
results.append({'number_of_results': response_json['number_of_results']})
return results
+113
View File
@@ -0,0 +1,113 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Seek ninja (general)"""
from json import loads
from hashlib import sha256
from urllib.parse import urlencode, quote_plus
import typing as t
from searx.extended_types import SXNG_Response
from searx.network import get
from searx.result_types import EngineResults
from searx.utils import extr, html_to_text
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
about = {
"website": "https://seek.ninja",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
safesearch = True
base_url = "https://seek.ninja"
categories = ["general"]
safe_search_map = {0: "off", 1: "moderate", 2: "strict"}
PowChallenge = dict[str, t.Any]
def _get_challenge(query: str) -> PowChallenge:
"""Extract the challenge parameters (i.e. nonce, difficulty, ...) from the
search website."""
resp = get(f"{base_url}/s?q={quote_plus(query)}")
challenge_raw_json = "{" + extr(resp.text, "pow: {", "},") + "}"
return loads(challenge_raw_json)
def _solve_pow(challenge: PowChallenge) -> list[int]:
"""Solves a Proof of Work SHA256 challenges. This is a 1:1 port of the
site's JS code.
On a high-level, it tries to ``k`` amount of solutions, where its sha256
hash begins with: ``leading`` 0s, i.e.
.. code: js
sha256(nonce || solution).startswith("0" * leading)
"""
nonce = challenge["nonce"]
k = int(challenge["k"])
indifficulty = float(challenge["indifficulty"])
leading = int(indifficulty)
frac = indifficulty - leading
prefix = "".join("0" for _ in range(0, leading))
maxNib = 15 - int(frac * 16) if frac else 15
solutions: list[int] = []
ans = 0
while len(solutions) < k:
h = sha256(f"{nonce}{ans}".encode()).hexdigest()
if h.startswith(prefix) and (not frac or int(h[leading], base=16) <= maxNib):
solutions.append(ans)
ans += 1
return solutions
def request(query: str, params: 'OnlineParams') -> None:
challenge = _get_challenge(query)
solution = _solve_pow(challenge)
args = {
"q": query,
"panswers": ",".join(str(s) for s in solution),
"pid": challenge["challengeId"],
"adult": safe_search_map[params["safesearch"]],
}
params["url"] = f"{base_url}/search-sse?{urlencode(args)}"
def response(resp: 'SXNG_Response') -> EngineResults:
res = EngineResults()
# The response is a stream of server-side events,
# so it is split into `event: <type>` and `data: {"results": ...}`
# see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/
events = resp.text.split("\n\n")
for event in events:
event_parts = event.split("\n", maxsplit=2)
if len(event_parts) != 2:
continue
event_name, data = event_parts
if not event_name.endswith("resultsUpdate"):
continue
json_data = loads(data.removeprefix("data: "))
for result in json_data["results"]:
res.add(
res.types.MainResult(
url=result["url"],
title=result["title"],
content=html_to_text(result["blurb"]),
)
)
return res
+3 -3
View File
@@ -74,7 +74,7 @@ about = {
categories = ["science", "scientific publications"]
paging = True
nb_per_page = 10
page_size = 10
"""Number of results to return in the request, see `Pagination and Limits`_ for
more details.
@@ -109,8 +109,8 @@ def request(query: str, params: "OnlineParams") -> None:
args = {
"api_key": api_key,
"q": query,
"s": nb_per_page * (params["pageno"] - 1),
"p": nb_per_page,
"s": page_size * (params["pageno"] - 1),
"p": page_size,
}
params["url"] = f"{base_url}?{urlencode(args)}"
# For example, the ``year:`` filter requires a *Premium Plan* subscription.
+3 -2
View File
@@ -134,7 +134,7 @@ time_range_support = True
safesearch = True
time_range_dict = {"day": "d", "week": "w", "month": "m", "year": "y"}
safesearch_dict = {0: "1", 1: "0", 2: "0"}
safesearch_dict = {0: "none", 1: "moderate", 2: "heavy"}
# search-url
base_url = "https://www.startpage.com"
@@ -251,9 +251,10 @@ def request(query, params):
"t": "device",
"sc": get_sc_code(params),
"with_date": time_range_dict.get(params["time_range"], ""),
"abp": "1",
"abd": "1",
"abe": "1",
"qsr": "all",
"qadf": safesearch_dict[params["safesearch"]],
}
if engine_language:
-44
View File
@@ -1,44 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Svgrepo (icons)"""
from lxml import html
from searx.utils import extract_text, eval_xpath, eval_xpath_list
about = {
"website": 'https://www.svgrepo.com',
"official_api_documentation": 'https://svgapi.com',
"use_official_api": False,
"require_api_key": False,
"results": 'HTML',
}
paging = True
categories = ['images', 'icons']
base_url = "https://www.svgrepo.com"
results_xpath = "//div[@class='style_nodeListing__7Nmro']/div"
url_xpath = ".//a/@href"
title_xpath = ".//a/@title"
img_src_xpath = ".//img/@src"
def request(query, params):
params['url'] = f"{base_url}/vectors/{query}/{params['pageno']}/"
return params
def response(resp):
results = []
dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, results_xpath):
results.append(
{
'template': 'images.html',
'url': base_url + extract_text(eval_xpath(result, url_xpath)),
'title': extract_text(eval_xpath(result, title_xpath)).replace(" SVG File", "").replace("Show ", ""),
'img_src': extract_text(eval_xpath(result, img_src_xpath)),
}
)
return results
+287
View File
@@ -0,0 +1,287 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=invalid-name
"""Swisscows (general, images, videos)"""
import typing as t
import base64
import codecs
import hashlib
import json
import random
from datetime import datetime
from urllib.parse import urlencode
from babel.core import get_global
from searx.result_types import EngineResults, LegacyResult # pyright: ignore[reportPrivateLocalImportUsage]
from searx.utils import humanize_number, html_to_text
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://swisscows.com",
"wikidata_id": "Q22937452",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
categories = ["general"]
swisscows_category = "web" # possible: "web", "videos", "images"
results_per_page = 50
time_range_support = True
paging = True
base_url = "https://api.swisscows.com"
CAESAR_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NONCE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
# fmt: off
swisscows_regions: list[str] = [
"AR", "AU", "AT", "BE", "BR", "CA", "CL", "CN", "DK", "FI",
"FR", "DE", "HK", "HU", "IN", "ID", "IT", "JP", "KR", "LV",
"MY", "MX", "NL", "NZ", "NO", "PH", "PL", "PT", "RU", "SA",
"ZA", "ES", "SE", "CH", "TW", "TR", "UA", "GB", "US"
]
"""Regions supported by swisscows."""
# fmt: on
# swisscows_languages = [
# "GB", "DE", "ES", "FR", "IT", "LV", "HU", "NL", "PT", "RU", "UA"
# ]
def appropriate_locale(searxng_locale: str, regions: list[str], default: str) -> str:
"""Returns the appropriate swisscows locale for the region or language
selected by the user. If no value is determined, ``default`` is returned
"""
_locale = searxng_locale.split("-")
if _locale[0] == "all":
return default
if len(_locale) == 1 or _locale[1] in regions:
return searxng_locale
sxng_lang = _locale[0]
if sxng_lang.upper() in regions:
return f"{sxng_lang}-{sxng_lang.upper()}"
likely_subtag: str | None = get_global("likely_subtags").get(sxng_lang)
if likely_subtag:
_tag: list[str] = likely_subtag.split("_")
if _tag[-1] in regions:
return f"{_tag[0]}-{_tag[-1]}"
return default
def generate_nonce(length: int = 32) -> str:
"""
Generate a random char sequence with the given length.
"""
return "".join([random.choice(NONCE_ALPHABET) for _ in range(length)])
def caesar_shift_with_switch_case(s: str, offset: int = 13) -> str:
"""
Caesar shift by :py:obj:`offset` that additionally inverts the casing of all letters
(i.e. from lowercase to uppercase and vice versa).
"""
out = ""
for c in s:
if c.upper() in CAESAR_ALPHABET:
alphabet_index = ord(c.upper()) - ord("A")
shifted = CAESAR_ALPHABET[(alphabet_index + offset) % len(CAESAR_ALPHABET)]
case_switched = shifted.lower() if c.isupper() else shifted.upper()
out += case_switched
else:
out += c
return out
def sha256_hash_b64_url(s: str) -> str:
"""
Calculate the SHA256 hash and base64 URL-encodes it.
"""
hasher = hashlib.sha256()
hasher.update(s.encode())
hashed_bytes = hasher.digest()
# hashlib generates a byte digest, but since we need to convert it to base64, we
# need to do that by hand
hash_base64 = codecs.encode(hashed_bytes, "base64").decode("utf-8").rstrip('\n')
hash_base64_url_encoded = hash_base64.replace("=", "").replace("+", '-').replace("/", '_')
return hash_base64_url_encoded
def generate_nonce_and_signature(base_path: str, args: dict[str, t.Any]) -> tuple[str, str]:
"""
Generate "X-Request-Nonce" and "X-Request-Signature" which are required for accessing
Swisscows images (reverse engineered from their official website).
"""
nonce = generate_nonce()
nonce_shifted = caesar_shift_with_switch_case(nonce, 13)
# in the path, all keys must be sorted in alphabetic order,
# otherwise the generated signature won't be accepted!
# additionally, the values may not be URL encoded, they have to be plain text
# hence we don't use urlencode here
args_sorted = sorted(args.items(), key=lambda arg: arg[0])
query_string = "&".join(f"{key}={value}" for (key, value) in args_sorted)
full_path = f"{base_path}?{query_string}"
signature = sha256_hash_b64_url(full_path + nonce_shifted)
return (nonce, signature)
maximum_page_size = {"web": 20, "images": 50, "videos": 10}
def init(_):
if swisscows_category not in ("web", "images", "videos"):
raise ValueError("illegal swisscows category: %s" % swisscows_category)
if results_per_page > maximum_page_size[swisscows_category]:
raise ValueError(
"results_per_page for swisscows %s can be at most %d"
% (swisscows_category, maximum_page_size[swisscows_category])
)
def request(query: str, params: "OnlineParams") -> None:
# swisscows images only supports 2 pages
if swisscows_category == "images" and params["pageno"] > 2:
params["url"] = None
return
locale = appropriate_locale(params["searxng_locale"], swisscows_regions, "en-US")
base_path = ""
args = dict[str, t.Any]
if swisscows_category == "web":
freshness = "All"
if params["time_range"]:
freshness = time_range_map[params["time_range"]]
args = {
"freshness": freshness,
"itemsCount": results_per_page,
"locale": locale,
"offset": (params["pageno"] - 1) * results_per_page,
"query": query,
"spellcheck": True,
}
base_path = "/v5/web/search"
elif swisscows_category == "images":
args = {
"itemsCount": results_per_page,
"locale": locale,
"offset": (params["pageno"] - 1) * results_per_page,
"query": query,
"spellcheck": True,
}
base_path = "/v5/images/search"
else:
args = {
"itemsCount": results_per_page,
"offset": (params["pageno"] - 1) * results_per_page,
"query": query,
"region": locale,
"spellcheck": True,
}
base_path = "/v2/videos/search"
nonce, signature = generate_nonce_and_signature(base_path, args)
params["headers"].update(
{
"X-Request-Nonce": nonce,
"X-Request-Signature": signature,
}
)
params["url"] = f"{base_url}{base_path}?{urlencode(args)}"
def _video_result(result: dict[str, str]) -> LegacyResult:
published_date = None
if result.get("datePublished"):
published_date = datetime.fromisoformat(result["datePublished"])
view_count = None
if result.get("viewCount"):
view_count = humanize_number(result["viewCount"]) # pyright: ignore[reportArgumentType]
return LegacyResult(
{
"template": "videos.html",
"url": result["url"],
"title": html_to_text(result.get("title") or result["name"]),
"content": result["description"],
"thumbnail": result.get("thumbnailUrl")
or result.get("thumbnail", {}).get("url"), # pyright: ignore[reportAttributeAccessIssue]
"length": result.get("duration"),
"iframe_src": result.get("embedUrl"),
"publishedDate": published_date,
"views": view_count,
}
)
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_data = resp.json()
# the payload encoding is only used for general and images,
# for videos the data gets returned directly as a normal JSON response
# payload is encoded as a JSON web token -> 3 parts, separated by "."
# the actual data is in the center of the encoded string
if "payload" in json_data:
payload = json_data["payload"].split(".")[1]
# pad with '=' to be valid base64
payload = payload + '=' * (4 - len(payload) % 4)
decoded = base64.urlsafe_b64decode(payload)
json_data = json.loads(decoded.decode())
result: dict[str, t.Any]
for result in json_data["items"]:
if result["type"] == "WebPage":
res.add(
res.types.MainResult(
url=result["url"],
title=result["name"],
content=html_to_text(result["description"]),
thumbnail=result.get("thumbnail", {}).get("url"),
)
)
elif swisscows_category == "videos" and result["type"] == "VideoCollection":
for video in result["hasPart"]:
res.add(_video_result(video))
elif result["type"] == "ImageObject":
res.add(
res.types.LegacyResult(
{
"template": "images.html",
"url": result["url"],
"thumbnail_src": result["thumbnail"]["url"],
"img_src": result["contentUrl"],
"title": result["name"],
}
)
)
elif result["type"] == "video":
res.add(_video_result(result))
return res
+83
View File
@@ -0,0 +1,83 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=invalid-name
"""Swisscows news"""
from datetime import datetime
from urllib.parse import urlencode
import typing as t
from searx.utils import html_to_text
from searx.result_types import EngineResults
from searx.engines.swisscows import appropriate_locale
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://swisscows.com",
"wikidata_id": "Q22937452",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
categories = ["news"]
results_per_page = 20
time_range_support = True
paging = True
base_url = "https://api.swisscows.com"
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
swisscows_regions: list[str] = ["DE"]
"""Regions supported by swisscows News."""
def request(query: str, params: "OnlineParams") -> None:
sxng_locale = params["searxng_locale"].split("-", maxsplit=1)[0]
locale: str = appropriate_locale(sxng_locale, swisscows_regions, default="de-DE")
if not locale:
return
freshness = "All"
if params["time_range"]:
freshness = time_range_map[params["time_range"]]
args = {
"query": query,
"itemsCount": results_per_page,
"region": locale,
"language": locale.split("-", maxsplit=1)[0],
"offset": (params["pageno"] - 1) * results_per_page,
"freshness": freshness,
"sortOrder": "Desc",
"sortBy": "Created",
}
url_path = f"/news/search?{urlencode(args)}"
params["url"] = base_url + url_path
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
result: dict[str, str]
for result in resp.json()["items"]: # pyright: ignore[reportAny]
res.add(
res.types.MainResult(
url=result["uri"],
title=html_to_text(result["title"]),
content=result["description"],
publishedDate=datetime.fromisoformat(result["created"]),
thumbnail=result.get("og:image") or "",
)
)
return res
+167
View File
@@ -0,0 +1,167 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Tiger_ is a Swiss meta search engine.
.. _Tiger: https://tiger.ch
"""
from json import loads
import random
from urllib.parse import urlencode
import typing as t
from dateutil import parser
from lxml import html
from searx.exceptions import SearxEngineAPIException
from searx.extended_types import SXNG_Response
from searx.network import get, post
from searx.result_types import EngineResults
from searx.utils import extr, eval_xpath_list, eval_xpath, extract_text
from searx.enginelib import EngineCache
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
about = {
"website": "https://tiger.ch",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
paging = True
base_url = "https://tiger.ch"
categories = []
tiger_category = "Websuche"
"""
Possible values: "Websuche", "News".
"""
CACHE: EngineCache
"""Cache to store session codes (result of solved CAPTCHA)."""
def init(_):
if tiger_category not in ("Websuche", "News"):
raise ValueError("invalid search category: %s" % tiger_category)
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return True
def _obtain_session_code() -> str:
"""The challenge works like this:
- We first generate 3 random numbers.
- Then we send them to /Human.svc/Make to get the operands (+, -) for the
math challenge (i.e. a simple calculation)
- Based on the operands, we calculate a result (usually done by the user by
hand)
- We send the result of the math calculation to the server to obtain a
session "code" that has to be sent as cookie parameter for all searches
E.g., challenges look like ``19-3+5``.
"""
cached_session = CACHE.get("session")
if cached_session:
return cached_session
results_page = get(f"{base_url}/_internCode.aspx")
doc = html.fromstring(results_page.text)
extra_data: dict[str, str] = {}
for extra_param in ("__VIEWSTATE", "__VIEWSTATEGENERATOR", "__EVENTVALIDATION"):
extra_data[extra_param] = doc.xpath(f"//input[@name='{extra_param}']/@value")[0]
# var z1 = Math.floor((Math.random() * 8) + 11);
# var z2 = Math.floor((Math.random() * 8) + 1);
# var z3 = Math.floor((Math.random() * 8) + 1);
num1 = random.randint(11, 19)
num2 = random.randint(1, 9)
num3 = random.randint(1, 9)
challenge = get(f"{base_url}/Services/Human.svc/Make?M1={num1}&M2={num2}&M3={num3}", cookies=results_page.cookies)
signs = loads(challenge.json()["d"])[0]
sign1 = signs["Z1"]
sign2 = signs["Z2"]
result = num1
for num, sign in [(num2, sign1), (num3, sign2)]:
if sign == "+":
result += num
else:
result -= num
logger.debug(f"got challenge: {num1} {sign1} {num2} {sign2} {num3} = {result}")
data = {
**extra_data,
"txtM": str(result),
"btnHuman": "OK",
}
challenge_response = post(
f"{base_url}/_internCode.aspx",
cookies=results_page.cookies,
data=data,
)
cookie = challenge_response.cookies["Tiger.ch"]
code = extr(cookie, "Code=", "&")
if not code:
raise SearxEngineAPIException("failed to obtain session code")
CACHE.set("session", code, expire=60 * 24 * 60) # cookie is valid for two months
return code
def request(query: str, params: "OnlineParams"):
code = _obtain_session_code()
args = {"w": query, "page": params["pageno"]}
params["url"] = f"{base_url}/{tiger_category}?{urlencode(args)}"
params["cookies"]["Tiger.ch"] = f"Code={code}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
doc = html.fromstring(resp.text)
if tiger_category == "Websuche":
for result in eval_xpath_list(doc, "//div[@id='mainContainer']//table/tr"):
res.add(
res.types.MainResult(
url=extract_text(eval_xpath(result, ".//a[contains(@class, 'weblink')]/@href")),
title=extract_text(eval_xpath(result, ".//a[contains(@class, 'weblink')]")) or "",
content=extract_text(eval_xpath(result, ".//*[contains(@class, 'webbodynopic')]")) or "",
)
)
elif tiger_category == "News":
for result in eval_xpath_list(doc, "//div[@id='panNews']/div"):
publishedDate = None
try:
date_str = extract_text(eval_xpath(result, ".//span[contains(@class, 'help')]/span")) or ""
date_str = date_str.strip().removeprefix("-").strip()
publishedDate = parser.parse(date_str)
except parser.ParserError:
pass
thumbnail = extract_text(eval_xpath(result, "./img/@src"))
if thumbnail:
thumbnail = base_url + thumbnail
res.add(
res.types.MainResult(
url=extract_text(eval_xpath(result, ".//a[contains(@class, 'webLink')]/@href")),
title=extract_text(eval_xpath(result, ".//a[contains(@class, 'webLink')]")) or "",
thumbnail=thumbnail or "",
publishedDate=publishedDate,
)
)
return res
-4
View File
@@ -211,8 +211,4 @@ def response(resp) -> EngineResults:
# append number of results
number_of_results = json_data.get('num_matches')
if number_of_results:
results.append({'number_of_results': number_of_results})
return results
+35 -18
View File
@@ -1,56 +1,73 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""MyMemory Translated"""
import typing as t
import urllib.parse
from searx.utils import html_to_text
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineDictParams
#
# about
about = {
"website": 'https://mymemory.translated.net/',
"website": "https://mymemory.translated.net/",
"wikidata_id": None,
"official_api_documentation": 'https://mymemory.translated.net/doc/spec.php',
"official_api_documentation": "https://mymemory.translated.net/doc/spec.php",
"use_official_api": True,
"require_api_key": False,
"results": 'JSON',
"results": "JSON",
}
engine_type = 'online_dictionary'
categories = ['general', 'translate']
engine_type = "online_dictionary"
categories = ["general", "translate"]
api_url = "https://api.mymemory.translated.net"
web_url = "https://mymemory.translated.net"
weight = 100
api_key = ''
api_key = ""
def request(query, params): # pylint: disable=unused-argument
def request(_: str, params: "OnlineDictParams") -> None:
args = {"q": params["query"], "langpair": f"{params['from_lang'][1]}|{params['to_lang'][1]}"}
args = {
"q": params["query"],
"langpair": f"{params['from_lang'][1]}|{params['to_lang'][1]}",
}
if api_key:
args["key"] = api_key
params['url'] = f"{api_url}/get?{urllib.parse.urlencode(args)}"
return params
def response(resp) -> EngineResults:
def response(resp: "SXNG_Response") -> EngineResults:
results = EngineResults()
data = resp.json()
data: dict[str, t.Any] = resp.json()
params: "OnlineDictParams" = resp.search_params # pyright: ignore[reportAssignmentType]
args = {
"q": resp.search_params["query"],
"lang": resp.search_params.get("searxng_locale", "en"), # ui language
"sl": resp.search_params['from_lang'][1],
"tl": resp.search_params['to_lang'][1],
"q": params["query"],
"lang": params.get("searxng_locale", "en"), # ui language
"sl": params["from_lang"][1],
"tl": params["to_lang"][1],
}
link = f"{web_url}/search.php?{urllib.parse.urlencode(args)}"
text = data['responseData']['translatedText']
text: str = html_to_text(data["responseData"]["translatedText"])
examples = [f"{m['segment']} : {m['translation']}" for m in data['matches'] if m['translation'] != text]
examples: set[str] = set()
match: dict[str, str]
for match in data["matches"]:
_text = html_to_text(match["translation"])
if _text != text:
_seg = html_to_text(match["segment"])
examples.add(f"{_seg} : {_text}")
item = results.types.Translations.Item(text=text, examples=examples)
item = results.types.Translations.Item(text=text, examples=list(examples))
results.add(results.types.Translations(translations=[item], url=link))
return results
+3 -3
View File
@@ -62,7 +62,7 @@ about = {
categories: list[str] = []
paging = True
number_of_results = 10
page_size = 10
wc_api_url = "https://commons.wikimedia.org/w/api.php"
wc_search_type: str = ""
@@ -107,8 +107,8 @@ def request(query: str, params: "OnlineParams") -> None:
"generator": "search",
"gsrnamespace": "6", # https://www.mediawiki.org/wiki/Help:Namespaces#Renaming_namespaces
"gsrprop": "snippet",
"gsrlimit": number_of_results,
"gsroffset": number_of_results * (params["pageno"] - 1),
"gsrlimit": page_size,
"gsroffset": page_size * (params["pageno"] - 1),
"gsrsearch": f"filetype:{filetype} {query}",
# imageinfo: https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bimageinfo
"iiprop": "url|size|mime",

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