18 Commits

Author SHA1 Message Date
Ivan Gabaldon
22056605a6 [mod] ci: increase container test delay
Testing under armv7 is having trouble running before the delay ends. Increasing
to 20 seconds should help.
2026-09-05 12:37:39 +08:00
dependabot[bot]
23e7e4da00 [upd] github-actions: Bump docker/setup-qemu-action from 4.2.0 to 4.3.0 (#6644)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](96fe6ef7f3...1f40c72289)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 15:16:28 +02:00
Markus Heiser
15a91992e4 [docs] fix some minor reST markup issues in the doc-string (#6646)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-09-04 11:18:59 +02:00
Bnyro
03c439a5b9 [fix] marginalia: crashes with missing filter name 2026-09-04 10:33:42 +02:00
vojkovic
be836e614a [mod] network: migrate to curl_cffi 2026-09-04 16:16:09 +08:00
Bnyro
15b0c8ef3a [feat] engines: add JS-based s1search engine 2026-09-04 09:49:07 +02:00
Bnyro
a1144dda3e [mod] engines: migrate video engines away from get_embedded_stream_url 2026-09-03 17:28:29 +02:00
Bnyro
86008c9dd6 [feat] results: automatically set embedded stream url for video results
Currently, many engines that provide video results call the same
`get_embedded_stream_url` method for setting the `iframe_src`. There
is no value in that because this logic is engine-specific and probably
many video engines forgot to implement this.

With these changes, it's done automatically, so engine implementors
don't have to worry about setting an `iframe_src` (unless the engine
explicitly has a field for it).
2026-09-03 17:28:29 +02:00
Bnyro
8f452ee892 [fix] chatnoir: fails to extract API session parameters 2026-09-03 10:03:05 +02:00
Markus Heiser
745d5b6fc5 [fix] engine: braveapi - braveapi pagination sends an invalid offset (#6627)
Closes: https://github.com/searxng/searxng/issues/6545
2026-09-03 07:52:31 +02:00
Bnyro
05cd77f71b [mod] s1search: update mirrors to the ones that are not cloudflared 2026-09-02 16:59:05 +02:00
Om Alve
b4f616fed7 [mod] engines: migrate to setup() from init() for simple tasks 2026-09-02 16:30:00 +02:00
Markus Heiser
fd29de6c55 [fix] engine: meilisearch - AttributeError: no attribute 'about' (#6626)
- Closes: https://github.com/searxng/searxng/issues/6622

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-09-02 14:26:22 +02:00
Bnyro
eca1af4be1 [del] heexy: remove engine due to cloudflare captcha (#6625) 2026-09-02 13:43:03 +02:00
Markus Heiser
7a01f5830c [fix] hardening engine: ddg weather - get GEO location from response (#6623)
The previous implementation ran into an error if the search term contained words
other than just the location (ValueError was raised).

To test engine use search terms like:

    !ddw weather berlin germany

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-09-02 13:40:53 +02:00
Ivan Gabaldon
18af21159b [mod] py: clearurls update location (#6618)
We should opt to retrieve the lists through jsDelivr service as our first choice
and avoid hitting the GH (GitHub) and GL (GitLab) servers.
2026-09-01 21:43:50 +02:00
dependabot[bot]
248e37991c [upd] web-client (simple): Bump the minor group (#6588)
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), [svgo](https://github.com/svg/svgo) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


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

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

Updates `svgo` from 4.0.2 to 4.1.0
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v4.0.2...v4.1.0)

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

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 26.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: svgo
  dependency-version: 4.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: vite
  dependency-version: 8.2.2
  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-09-01 21:42:44 +02:00
github-actions[bot]
ff424745fd [mod] data: update searx.data - update_external_bangs.py (#6593)
Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2026-09-01 21:38:36 +02:00
162 changed files with 4615 additions and 4752 deletions

View File

@@ -62,7 +62,7 @@ jobs:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -117,7 +117,7 @@ jobs:
password: "${{ secrets.GITHUB_TOKEN }}" password: "${{ secrets.GITHUB_TOKEN }}"
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

View File

@@ -15,8 +15,8 @@
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.9", "@biomejs/biome": "2.5.10",
"@types/node": "^26.2.0", "@types/node": "^26.3.0",
"browserslist": "^4.28.8", "browserslist": "^4.28.8",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.1",
@@ -27,9 +27,9 @@
"stylelint": "^17.14.1", "stylelint": "^17.14.1",
"stylelint-config-standard-less": "^4.1.0", "stylelint-config-standard-less": "^4.1.0",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svgo": "^4.0.2", "svgo": "^4.1.0",
"typescript": "~7.0.2", "typescript": "~7.0.2",
"vite": "^8.2.1", "vite": "^8.2.2",
"vite-bundle-analyzer": "^1.3.9" "vite-bundle-analyzer": "^1.3.9"
} }
}, },
@@ -69,9 +69,9 @@
} }
}, },
"node_modules/@biomejs/biome": { "node_modules/@biomejs/biome": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.10.tgz",
"integrity": "sha512-KkgCvdHB4IhtpHpF564plA9jo6fDOwWGQ/3jvreLzgOtRLEDoPqr7QO9qejNA8jKwDsSkAKr77hqBHnyUbIw4g==", "integrity": "sha512-WRKXARA3kTuiV5sxqTpobJ/I0MVd4vk3pOL6wnp5az4LntFIhWTj1RWZq3DI9PCEN3lXcqy7p5aqUHzvq8AXyQ==",
"dev": true, "dev": true,
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"bin": { "bin": {
@@ -85,20 +85,20 @@
"url": "https://opencollective.com/biome" "url": "https://opencollective.com/biome"
}, },
"optionalDependencies": { "optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.5.9", "@biomejs/cli-darwin-arm64": "2.5.10",
"@biomejs/cli-darwin-x64": "2.5.9", "@biomejs/cli-darwin-x64": "2.5.10",
"@biomejs/cli-linux-arm64": "2.5.9", "@biomejs/cli-linux-arm64": "2.5.10",
"@biomejs/cli-linux-arm64-musl": "2.5.9", "@biomejs/cli-linux-arm64-musl": "2.5.10",
"@biomejs/cli-linux-x64": "2.5.9", "@biomejs/cli-linux-x64": "2.5.10",
"@biomejs/cli-linux-x64-musl": "2.5.9", "@biomejs/cli-linux-x64-musl": "2.5.10",
"@biomejs/cli-win32-arm64": "2.5.9", "@biomejs/cli-win32-arm64": "2.5.10",
"@biomejs/cli-win32-x64": "2.5.9" "@biomejs/cli-win32-x64": "2.5.10"
} }
}, },
"node_modules/@biomejs/cli-darwin-arm64": { "node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.10.tgz",
"integrity": "sha512-am22pX2aBqznqq1eMyIj/bZ++riF3Lk6ct7cbv+gQK0csFhr+d8O0RkOi2FF2qSgFgANbqNkIZ0/PxlnW2pLFg==", "integrity": "sha512-ItCrxKK6SXVT6flYs0qIuBd4AA3TTTl4d66Re6YI2FuGZnN85NmuYNzkiTJUyYw8qBLv69L5zTUB6uyWd++h3Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -113,9 +113,9 @@
} }
}, },
"node_modules/@biomejs/cli-darwin-x64": { "node_modules/@biomejs/cli-darwin-x64": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.10.tgz",
"integrity": "sha512-l44KWDHLDvEnD0N/XcrVs7VXb3A18xL7QS3WB0eL93wbmk529ffIG55vleGCqaunpRUjLrdnjK05Qki1dsjylg==", "integrity": "sha512-yLsPU9pAmtChXDu8vhKAzErqe+LeeYuwuUB2FZMkRitsmdodxsYRa9KHrFispsUHzzOu+9HB3nP/TQxyia+Sjw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -130,9 +130,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64": { "node_modules/@biomejs/cli-linux-arm64": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.10.tgz",
"integrity": "sha512-ICaK+IYaVZvKbBxX2rwrPT0DdUDMnE9Vm3nQGe+mltQPmUg19pONzkPWGdY4FCsoreDETWDynvdt4ysCbF5gNQ==", "integrity": "sha512-VG8uQW/86a1roLaIFvtIbEigxIdzdJ190oGyg1tV7VYeQtOS+x10sflk7WbuXgw91EtZX5DlIIIej1YqkNLlcg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -150,9 +150,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64-musl": { "node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.10.tgz",
"integrity": "sha512-7ImVPwBLCtkmpR5esd8RHhTqW94f0JLJQum6AneYcy94jRm18TaPPm7slaigGzFhfgt3QiD1Vj52LKmBAnKizA==", "integrity": "sha512-t1QAKZwQJRB4dvgJSgFiQ4BNfNPChg69BNonz854qLVxnjT3UvDzQg9mbkTJRu35ZqU0Rw10A73J8Urgbg2RPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -170,9 +170,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64": { "node_modules/@biomejs/cli-linux-x64": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.10.tgz",
"integrity": "sha512-z22Q/zFYSvbIJfW1CbfZPu4X8PddS6Qd2ORbc6h+aT6EcwAxUF3m6fA4HjNvA3TU4X0dTJRwNPB165ES3PJXzg==", "integrity": "sha512-4O6T0eq2heoHZN0a9UX+rWQoxXEBaKf+lRi2hbsGlHneUz9BWXM76nEWMK7Eeq8gzMxR1khQB6BFpAASpeXqGg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -190,9 +190,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64-musl": { "node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.10.tgz",
"integrity": "sha512-RXGaD0o1/pTTguYw1aeDJh9ad6Lfrui0fI7mBderTyGr7WuUJkBIttgLkR3XJyoxOkkgfBDspaUT8wXArTqLZw==", "integrity": "sha512-pgDDqp9JybHm2I0KRgzN6i4+lt8xu4iqxUwLzglUMmOmyRTU1AYBGKzh9sNMOtIjah7xoWvKHlLVetvyifzoiQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -210,9 +210,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-arm64": { "node_modules/@biomejs/cli-win32-arm64": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.10.tgz",
"integrity": "sha512-nHK+/HHC+D0ogAHUxomgoSTdjImb6fmNNVTKmf0tyu4eDL1DqPKIHc+i+UL8+b0RnAu8224qo8F2tCVnaT0A3w==", "integrity": "sha512-pxAbxduPO4xq/Cvgaa2lOrs9BB0hEXmmDqfMNP4ZOffGOkUrD1/QGw9UAMpFQpX2P8MqTIIRuQKcmetum4Oa6A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -227,9 +227,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-x64": { "node_modules/@biomejs/cli-win32-x64": {
"version": "2.5.9", "version": "2.5.10",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.9.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.10.tgz",
"integrity": "sha512-Yiq0H56LjXSSw/hd9YkXgSLQfzyDJzbzU2TezozxyNw+uKWAqOtqGVvBfzKRRDiaFF5avGAhHdWKx7LtDOShUw==", "integrity": "sha512-M+2dgBsl3lXRiTfgPVc2p3anS4Tocojke4rzFLScZ2Y/wmF+36dRb1iHCLiyGqOzQGyTplZH1HnEYviiAqi3nA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1577,9 +1577,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "26.2.0", "version": "26.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz",
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2284,17 +2284,17 @@
} }
}, },
"node_modules/css-select": { "node_modules/css-select": {
"version": "5.2.2", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"boolbase": "^1.0.0", "boolbase": "^1.0.0",
"css-what": "^6.1.0", "css-what": "^7.0.0",
"domhandler": "^5.0.2", "domhandler": "^5.0.3",
"domutils": "^3.0.1", "domutils": "^3.2.2",
"nth-check": "^2.0.1" "nth-check": "^2.1.1"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
@@ -2315,9 +2315,9 @@
} }
}, },
"node_modules/css-what": { "node_modules/css-what": {
"version": "6.2.2", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
@@ -4275,9 +4275,9 @@
"optional": true "optional": true
}, },
"node_modules/sax": { "node_modules/sax": {
"version": "1.6.0", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
@@ -4769,19 +4769,19 @@
"dev": true "dev": true
}, },
"node_modules/svgo": { "node_modules/svgo": {
"version": "4.0.2", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz",
"integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==", "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"commander": "^11.1.0", "commander": "^11.1.0",
"css-select": "^5.1.0", "css-select": "^6.0.0",
"css-tree": "^3.0.1", "css-tree": "^3.0.1",
"css-what": "^6.1.0", "css-what": "^7.0.0",
"csso": "^5.0.5", "csso": "^5.0.5",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"sax": "^1.5.0" "sax": "1.6.1"
}, },
"bin": { "bin": {
"svgo": "bin/svgo.js" "svgo": "bin/svgo.js"
@@ -5044,16 +5044,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.2.1", "version": "8.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.33.0", "lightningcss": "^1.33.0",
"picomatch": "^4.0.5", "picomatch": "^4.0.5",
"postcss": "^8.5.25", "postcss": "^8.5.26",
"rolldown": "~1.2.1", "rolldown": "~1.2.4",
"tinyglobby": "^0.2.17" "tinyglobby": "^0.2.17"
}, },
"bin": { "bin": {
@@ -5070,7 +5070,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0", "@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.4.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0",
"esbuild": "^0.27.0 || ^0.28.0", "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "^4.0.0", "less": "^4.0.0",

View File

@@ -29,8 +29,8 @@
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.9", "@biomejs/biome": "2.5.10",
"@types/node": "^26.2.0", "@types/node": "^26.3.0",
"browserslist": "^4.28.8", "browserslist": "^4.28.8",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.1",
@@ -41,9 +41,9 @@
"stylelint": "^17.14.1", "stylelint": "^17.14.1",
"stylelint-config-standard-less": "^4.1.0", "stylelint-config-standard-less": "^4.1.0",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svgo": "^4.0.2", "svgo": "^4.1.0",
"typescript": "~7.0.2", "typescript": "~7.0.2",
"vite": "^8.2.1", "vite": "^8.2.2",
"vite-bundle-analyzer": "^1.3.9" "vite-bundle-analyzer": "^1.3.9"
} }
} }

View File

@@ -58,10 +58,9 @@ engine is shown. Most of the options have a default value or even are optional.
# overwrite values from section 'outgoing:' # overwrite values from section 'outgoing:'
enable_http2: false enable_http2: false
enable_http3: false
retries: 1 retries: 1
max_connections: 100 max_connections: 100
max_keepalive_connections: 10
keepalive_expiry: 5.0
using_tor_proxy: false using_tor_proxy: false
proxies: proxies:
http: http:
@@ -163,6 +162,16 @@ engine is shown. Most of the options have a default value or even are optional.
``enable_http`` : optional ``enable_http`` : optional
Enable HTTP for this engine (by default only HTTPS is enabled). Enable HTTP for this engine (by default only HTTPS is enabled).
``enable_http3`` : optional
Use HTTP/3 (falls back to HTTP/2). Default ``false``.
Ignored when a proxy is set.
.. hint::
HTTP/3 places demands on the IP infrastructure that are not met in every
environment. Enable this option only if you are aware of these requirements
and the extent to which they are met.
``retry_on_http_error`` : optional ``retry_on_http_error`` : optional
Retry request on some HTTP status code. Retry request on some HTTP status code.
@@ -179,20 +188,12 @@ engine is shown. Most of the options have a default value or even are optional.
Using tor proxy (``true``) or not (``false``) for this engine. The default is Using tor proxy (``true``) or not (``false``) for this engine. The default is
taken from ``using_tor_proxy`` of the :ref:`settings outgoing`. taken from ``using_tor_proxy`` of the :ref:`settings outgoing`.
.. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration .. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
``max_keepalive_connection#s`` :
`Pool limit configuration`_, overwrites value ``pool_maxsize`` from
:ref:`settings outgoing` for this engine.
``max_connections`` : ``max_connections`` :
`Pool limit configuration`_, overwrites value ``pool_connections`` from `Pool limit configuration`_, overwrites value ``pool_connections`` from
:ref:`settings outgoing` for this engine. :ref:`settings outgoing` for this engine.
``keepalive_expiry`` :
`Pool limit configuration`_, overwrites value ``keepalive_expiry`` from
:ref:`settings outgoing` for this engine.
.. _private engines: .. _private engines:

View File

@@ -12,20 +12,12 @@ Communication with search engines.
request_timeout: 2.0 # default timeout in seconds, can be override by engine request_timeout: 2.0 # default timeout in seconds, can be override by engine
max_request_timeout: 10.0 # the maximum timeout in seconds max_request_timeout: 10.0 # the maximum timeout in seconds
useragent_suffix: "" # information like an email address to the administrator useragent_suffix: "" # information like an email address to the administrator
pool_connections: 100 # Maximum number of allowable connections, or null pool_connections: 100 # Maximum number of concurrent connections (default: 100)
# for no limits. The default is 100. enable_http2: true # Enables the use of HTTP2
pool_maxsize: 10 # Number of allowable keep-alive connections, or null
# to always allow. The default is 10.
enable_http2: true # See https://www.python-httpx.org/http2/
# uncomment below section if you want to use a custom server certificate # uncomment below section if you want to use a custom server certificate
# see https://www.python-httpx.org/advanced/#changing-the-verification-defaults
# and https://www.python-httpx.org/compatibility/#ssl-configuration
# verify: ~/.mitmproxy/mitmproxy-ca-cert.cer # verify: ~/.mitmproxy/mitmproxy-ca-cert.cer
# #
# uncomment below section if you want to use a proxyq see: SOCKS proxies # uncomment below section if you want to use a proxy
# https://2.python-requests.org/en/latest/user/advanced/#proxies
# are also supported: see
# https://2.python-requests.org/en/latest/user/advanced/#socks
# #
# proxies: # proxies:
# all://: # all://:
@@ -46,30 +38,26 @@ Communication with search engines.
timeout to load). Can be override by ``timeout`` in the :ref:`settings engines`. timeout to load). Can be override by ``timeout`` in the :ref:`settings engines`.
``useragent_suffix`` : ``useragent_suffix`` :
Suffix to the user-agent SearXNG uses to send requests to others engines. If an Suffix to add when an engine's User-Agent is set via searxng_useragent().
engine wish to block you, a contact info here may be useful to avoid that. Contact info here may be useful to avoid an engine blocking you.
.. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration .. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
``pool_maxsize``:
Number of allowable keep-alive connections, or ``null`` to always allow. The
default is 10. See ``max_keepalive_connections`` `Pool limit configuration`_.
``pool_connections`` : ``pool_connections`` :
Maximum number of allowable connections, or ``null`` # for no limits. The Maximum number of concurrent connections. The default is 100.
default is 100. See ``max_connections`` `Pool limit configuration`_. See ``max_clients`` `Pool limit configuration`_.
``keepalive_expiry`` : .. _curl_cffi proxies: https://curl-cffi.readthedocs.io/en/latest/quick_start.html
Number of seconds to keep a connection in the pool. By default 5.0 seconds.
See ``keepalive_expiry`` `Pool limit configuration`_.
.. _httpx proxies: https://www.python-httpx.org/advanced/#http-proxying
``proxies`` : ``proxies`` :
Define one or more proxies you wish to use, see `httpx proxies`_. Define one or more proxies you wish to use, see `curl_cffi proxies`_.
If there are more than one proxy for one protocol (http, https), If there are more than one proxy for one protocol (http, https),
requests to the engines are distributed in a round-robin fashion. requests to the engines are distributed in a round-robin fashion.
HTTP, HTTPS, SOCKS4, SOCKS5 and SOCKS5h proxies are supported
(``http://``, ``https://``, ``socks4://``, ``socks5://``, ``socks5h://``). You should
use ``socks5h://`` when using Tor so hostnames are resolved by the proxy.
``source_ips`` : ``source_ips`` :
If you use multiple network interfaces, define from which IP the requests must If you use multiple network interfaces, define from which IP the requests must
be made. Example: be made. Example:
@@ -87,18 +75,15 @@ Communication with search engines.
different proxy and source ip. different proxy and source ip.
``enable_http2`` : ``enable_http2`` :
Enable by default. Set to ``false`` to disable HTTP/2. Enable by default (HTTP/2). Set to ``false`` to force HTTP/1.1.
HTTP/3 is opt-in per engine (``enable_http3``).
.. _httpx verification defaults: https://www.python-httpx.org/advanced/#changing-the-verification-defaults
.. _httpx ssl configuration: https://www.python-httpx.org/compatibility/#ssl-configuration
``verify``: : ``$SSL_CERT_FILE``, ``$SSL_CERT_DIR`` ``verify``: : ``$SSL_CERT_FILE``, ``$SSL_CERT_DIR``
Allow to specify a path to certificate. HTTPS verification uses the OS's trust store by default.
see `httpx verification defaults`_. Set a path to use a custom CA file.
In addition to ``verify``, SearXNG supports the ``$SSL_CERT_FILE`` (for a file) and In addition to ``verify``, SearXNG supports the ``$SSL_CERT_FILE`` (for a file) and
``$SSL_CERT_DIR`` (for a directory) OpenSSL variables. ``$SSL_CERT_DIR`` (for a directory) OpenSSL variables.
see `httpx ssl configuration`_.
``max_redirects`` : ``max_redirects`` :
30 by default. Maximum redirect before it is an error. 30 by default. Maximum redirect before it is an error.

View File

@@ -143,7 +143,7 @@ parameters with default value can be redefined for special purposes.
data dict ``{}`` data dict ``{}``
cookies dict ``{}`` cookies dict ``{}``
verify bool ``True`` verify bool ``True``
headers.User-Agent str a random User-Agent headers.User-Agent str ``''``
category str current category, like ``'general'`` category str current category, like ``'general'``
safesearch int ``0``, between ``0`` and ``2`` (normal, moderate, strict) safesearch int ``0``, between ``0`` and ``2`` (normal, moderate, strict)
time_range Optional[str] ``None``, can be ``day``, ``week``, ``month``, ``year`` time_range Optional[str] ``None``, can be ``day``, ``week``, ``month``, ``year``
@@ -229,6 +229,8 @@ following parameters can be used to specify a search request:
max_redirects int maximum redirects, hard limit max_redirects int maximum redirects, hard limit
soft_max_redirects int maximum redirects, soft limit. Record an error but don't stop the engine soft_max_redirects int maximum redirects, soft limit. Record an error but don't stop the engine
raise_for_httperror bool True by default: raise an exception if the HTTP code of response is >= 300 raise_for_httperror bool True by default: raise an exception if the HTTP code of response is >= 300
impersonate str curl_cffi impersonate target (default: chrome, none to disable)
curl_options dict Any extra libcurl options for the request
=================== =========== ========================================================================== =================== =========== ==========================================================================

View File

@@ -286,7 +286,7 @@ content becomes smart.
files & folders origin :origin:`docs/dev/reST.rst` ``:origin:`docs/dev/reST.rst``` files & folders origin :origin:`docs/dev/reST.rst` ``:origin:`docs/dev/reST.rst```
pull request :pull:`4` ``:pull:`4``` pull request :pull:`4` ``:pull:`4```
patch :patch:`af2cae6` ``:patch:`af2cae6``` patch :patch:`af2cae6` ``:patch:`af2cae6```
PyPi package :pypi:`httpx` ``:pypi:`httpx``` PyPi package :pypi:`curl_cffi` ``:pypi:`curl_cffi```
manual page man :man:`bash` ``:man:`bash``` manual page man :man:`bash` ``:man:`bash```
intersphinx_ intersphinx_
-------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------

View File

@@ -1,7 +1,7 @@
mock==5.2.0 mock==5.2.0
nose2[coverage_plugin]==0.16.0 nose2[coverage_plugin]==0.16.0
cov-core==1.15.0 cov-core==1.15.0
black==26.5.1 black==25.9.0
pylint==4.0.7 pylint==4.0.7
splinter==0.21.0 splinter==0.21.0
selenium==4.47.0 selenium==4.47.0

View File

@@ -7,9 +7,7 @@ lxml==6.1.2
pygments==2.21.0 pygments==2.21.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pyyaml==6.0.3 pyyaml==6.0.3
httpx[http2]==0.28.1 curl_cffi==0.16.1
httpx-socks[asyncio]==0.13.1
sniffio==1.3.1
valkey==6.1.1 valkey==6.1.1
markdown-it-py==4.2.0 markdown-it-py==4.2.0
msgspec==0.21.1 msgspec==0.21.1

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ from urllib.parse import urlencode
import lxml.etree import lxml.etree
import lxml.html import lxml.html
from httpx import HTTPError from curl_cffi.requests.exceptions import RequestException
from searx import settings from searx import settings
from searx.engines import ( from searx.engines import (
@@ -63,7 +63,7 @@ def bing(query: str, _sxng_locale: str) -> list[str]:
base_url = "https://www.bing.com/AS/Suggestions?" base_url = "https://www.bing.com/AS/Suggestions?"
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters # cvid has to be a 32 character long string consisting of numbers and uppsercase characters
cvid = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32)) cvid = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32))
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid})) response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}), enable_http3=True)
results: list[str] = [] results: list[str] = []
if response.ok: if response.ok:
@@ -83,7 +83,7 @@ def brave(query: str, _sxng_locale: str) -> list[str]:
url = 'https://search.brave.com/api/suggest?' url = 'https://search.brave.com/api/suggest?'
url += urlencode({'q': query}) url += urlencode({'q': query})
country = 'all' country = 'all'
kwargs = {'cookies': {'country': country}} kwargs = {'cookies': {'country': country}, 'enable_http3': True}
resp = get(url, **kwargs) resp = get(url, **kwargs)
results: list[str] = [] results: list[str] = []
@@ -147,7 +147,7 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
) )
results: list[str] = [] results: list[str] = []
resp = get('https://www.google.com/complete/search?' + args) resp = get('https://www.google.com/complete/search?' + args, enable_http3=True)
if resp and resp.ok: if resp and resp.ok:
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1] json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
data = json.loads(json_txt) data = json.loads(json_txt)
@@ -418,5 +418,5 @@ def search_autocomplete(backend_name: str, query: str, sxng_locale: str) -> list
return [] return []
try: try:
return backend(query, sxng_locale) return backend(query, sxng_locale)
except (HTTPError, SearxEngineResponseException): except (RequestException, SearxEngineResponseException):
return [] return []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of a middleware to determine the real IP of an HTTP request """Implementation of a middleware to determine the real IP of an HTTP request
(:py:obj:`flask.request.remote_addr`) behind a proxy chain.""" (:py:obj:`flask.request.remote_addr`) behind a proxy chain."""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches

View File

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

View File

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

View File

@@ -465,7 +465,7 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
# Check if value is expired. It's possible that it's expired but has not # Check if value is expired. It's possible that it's expired but has not
# yet been automatically deleted by the periodic maintenance # yet been automatically deleted by the periodic maintenance
value, expire = row (value, expire) = row
now = time.time() now = time.time()
if expire < now: if expire < now:
# The record is deleted during the maintenance interval. Deleting # The record is deleted during the maintenance interval. Deleting

View File

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

View File

@@ -4,7 +4,6 @@
make data.all make data.all
""" """
# pylint: disable=invalid-name # pylint: disable=invalid-name
__all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"] __all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"]

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Simple implementation to store TrackerPatterns data in a SQL database.""" """Simple implementation to store TrackerPatterns data in a SQL database."""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
import typing as t import typing as t
@@ -11,7 +10,7 @@ import re
from collections.abc import Iterator from collections.abc import Iterator
from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
from httpx import HTTPError from curl_cffi.requests.exceptions import RequestException
from searx.data.core import get_cache, log from searx.data.core import get_cache, log
from searx.network import get as http_get from searx.network import get as http_get
@@ -29,11 +28,11 @@ class TrackerPatternsDB:
ctx_name = "data_tracker_patterns" ctx_name = "data_tracker_patterns"
# ClearURL rule lists, the first one that responds HTTP 200 is used
CLEAR_LIST_URL = [ CLEAR_LIST_URL = [
# ClearURL rule lists, the first one that responds HTTP 200 is used "https://cdn.jsdelivr.net/gh/clearurls/rules@refs/heads/gh-pages/data.minify.json",
"https://rules1.clearurls.xyz/data.minify.json",
"https://rules2.clearurls.xyz/data.minify.json", "https://rules2.clearurls.xyz/data.minify.json",
"https://raw.githubusercontent.com/ClearURLs/Rules/refs/heads/master/data.min.json", "https://rules1.clearurls.xyz/data.minify.json",
] ]
class Fields: class Fields:
@@ -88,8 +87,8 @@ class TrackerPatternsDB:
try: try:
resp = http_get(url, timeout=3) resp = http_get(url, timeout=3)
except HTTPError as exc: except RequestException as exc:
log.warning("TRACKER_PATTERNS: HTTPError (%s) occured while fetching %s", url, exc) log.warning("TRACKER_PATTERNS: RequestException while fetching %s: %s", url, exc)
continue continue
if resp.status_code != 200: if resp.status_code != 200:

View File

@@ -305,7 +305,7 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
region: str = "" region: str = ""
"""For an engine, when there is ``region: ...`` in the YAML settings the engine """For an engine, when there is ``region: ...`` in the YAML settings the engine
does support only this one region:: does support only this one region:
.. code:: yaml .. code:: yaml
@@ -317,6 +317,9 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
enable_http: bool enable_http: bool
"""Enable HTTP (by default only HTTPS is enabled).""" """Enable HTTP (by default only HTTPS is enabled)."""
enable_http3: bool = False
"""Enables the use of HTTP/3 if available"""
shortcut: str shortcut: str
"""Code used to execute bang requests (``!foo``)""" """Code used to execute bang requests (``!foo``)"""

View File

@@ -6,7 +6,8 @@ from urllib.parse import urlencode
from datetime import datetime from datetime import datetime
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.utils import html_to_text, get_embeded_stream_url from searx.result_types import EngineResults
from searx.utils import html_to_text
about = { about = {
"website": "https://tv.360kan.com/", "website": "https://tv.360kan.com/",
@@ -29,12 +30,12 @@ def request(query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
try: try:
data = resp.json() data = resp.json()
except Exception as e: except Exception as e:
raise SearxEngineAPIException(f"Invalid response: {e}") from e raise SearxEngineAPIException(f"Invalid response: {e}") from e
results = [] res = EngineResults()
if "data" not in data or "result" not in data["data"]: if "data" not in data or "result" not in data["data"]:
raise SearxEngineAPIException("Invalid response") raise SearxEngineAPIException("Invalid response")
@@ -50,16 +51,15 @@ def response(resp):
except (ValueError, TypeError): except (ValueError, TypeError):
published_date = None published_date = None
results.append( res.add(
{ res.types.LegacyResult(
'url': entry["play_url"], url=entry["play_url"],
'title': html_to_text(entry["title"]), title=html_to_text(entry["title"]),
'content': html_to_text(entry["description"]), content=html_to_text(entry["description"]),
'template': 'videos.html', template='videos.html',
'publishedDate': published_date, publishedDate=published_date,
'thumbnail': entry["cover_img"], thumbnail=entry["cover_img"],
"iframe_src": get_embeded_stream_url(entry["play_url"]), )
}
) )
return results return res

View File

@@ -26,6 +26,7 @@ categories: list[str]
disabled: bool disabled: bool
display_error_messages: bool display_error_messages: bool
enable_http: bool enable_http: bool
enable_http3: bool
engine_type: str engine_type: str
inactive: bool inactive: bool
max_page: int max_page: int

View File

@@ -187,8 +187,9 @@ def set_loggers(engine: "Engine|types.ModuleType", engine_name: str):
def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]): def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# set engine attributes from engine_data # set / update engine attributes from engine_data
kvargs: dict[str, t.Any] kvargs: dict[str, t.Any]
engine.about = getattr(engine, "about", EngineAbout())
if isinstance(engine.about, EngineAbout): if isinstance(engine.about, EngineAbout):
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})} kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
else: else:

View File

@@ -35,7 +35,7 @@ Implementation
============== ==============
""" """
import typing as t
from datetime import datetime, timedelta from datetime import datetime, timedelta
from urllib.parse import urlencode from urllib.parse import urlencode
@@ -85,7 +85,7 @@ Additional subcategories:
# Do we need support for "free_collection" and "include_stock_enterprise"? # Do we need support for "free_collection" and "include_stock_enterprise"?
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if not categories: if not categories:
raise ValueError("adobe_stock engine: categories is unset") raise ValueError("adobe_stock engine: categories is unset")
@@ -100,9 +100,9 @@ def init(_):
raise ValueError("adobe_stock engine: adobe_content_types is unset") raise ValueError("adobe_stock engine: adobe_content_types is unset")
if isinstance(adobe_content_types, list): if isinstance(adobe_content_types, list):
for t in adobe_content_types: for content_type in adobe_content_types:
if t not in ADOBE_VALID_TYPES: if content_type not in ADOBE_VALID_TYPES:
raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % t) raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % content_type)
else: else:
raise ValueError( raise ValueError(
"adobe_stock engine: adobe_content_types must be a list of strings not %s" % type(adobe_content_types) "adobe_stock engine: adobe_content_types must be a list of strings not %s" % type(adobe_content_types)

View File

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

View File

@@ -49,6 +49,9 @@ CACHE: EngineCache
def setup(engine_settings: dict[str, t.Any]) -> bool: def setup(engine_settings: dict[str, t.Any]) -> bool:
if baidu_category not in ('general', 'images', 'it'):
raise SearxEngineAPIException(f"Unsupported category: {baidu_category}")
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"]) CACHE = EngineCache(engine_settings["name"])
return True return True
@@ -65,11 +68,6 @@ def get_image_cookies(headers: dict[str, str]) -> dict[str, str]:
return cookies return cookies
def init(_):
if baidu_category not in ('general', 'images', 'it'):
raise SearxEngineAPIException(f"Unsupported category: {baidu_category}")
def request(query, params): def request(query, params):
page_num = params["pageno"] page_num = params["pageno"]

View File

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

View File

@@ -40,6 +40,7 @@ about: dict[str, t.Any] = {
# engine dependent config # engine dependent config
categories = ["general", "web"] categories = ["general", "web"]
safesearch = True safesearch = True
enable_http3 = True
_safesearch_map: dict[int, str] = { _safesearch_map: dict[int, str] = {
0: "off", 0: "off",
1: "moderate", 1: "moderate",

View File

@@ -25,6 +25,7 @@ about = {
# engine dependent config # engine dependent config
categories = ["images", "web"] categories = ["images", "web"]
paging = True paging = True
enable_http3 = True
safesearch = True safesearch = True
time_range_support = True time_range_support = True
time_map = { time_map = {

View File

@@ -33,6 +33,7 @@ categories = ["news"]
paging = True paging = True
"""If go through the pages and there are actually no new results for another """If go through the pages and there are actually no new results for another
page, then bing returns the results from the last page again.""" page, then bing returns the results from the last page again."""
enable_http3 = True
time_range_support = True time_range_support = True
time_map = { time_map = {

View File

@@ -26,6 +26,7 @@ about = {
# engine dependent config # engine dependent config
categories = ["videos", "web"] categories = ["videos", "web"]
paging = True paging = True
enable_http3 = True
safesearch = True safesearch = True
time_range_support = True time_range_support = True

View File

@@ -45,7 +45,7 @@ CACHE_SESSION_ID_KEY = "session_id_key"
KEYWORD_RE = re.compile(r"\[\/?Keyword\]") KEYWORD_RE = re.compile(r"\[\/?Keyword\]")
def init(engine_settings: dict[str, t.Any]) -> bool: def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_name=engine_settings["name"]) CACHE = EngineCache(engine_name=engine_settings["name"])
return True return True

View File

@@ -135,7 +135,7 @@ from searx.utils import (
eval_xpath_getindex, eval_xpath_getindex,
eval_xpath_list, eval_xpath_list,
extract_text, extract_text,
get_embeded_stream_url, get_embedded_stream_url,
js_obj_str_to_json_str, js_obj_str_to_json_str,
js_obj_str_to_python, js_obj_str_to_python,
) )
@@ -151,6 +151,7 @@ about = {
base_url = "https://search.brave.com/" base_url = "https://search.brave.com/"
categories = [] categories = []
enable_http3 = True
brave_category: t.Literal["search", "videos", "images", "news", "goggles"] = "search" brave_category: t.Literal["search", "videos", "images", "news", "goggles"] = "search"
"""Brave supports common web-search, videos, images, news, and goggles search. """Brave supports common web-search, videos, images, news, and goggles search.
@@ -338,7 +339,7 @@ def _parse_search(resp: SXNG_Response) -> EngineResults:
if len(video_tag): if len(video_tag):
# In my tests a video tag in the WEB search was most often not a # In my tests a video tag in the WEB search was most often not a
# video, except the ones from youtube .. # video, except the ones from youtube ..
iframe_src = get_embeded_stream_url(url) iframe_src = get_embedded_stream_url(url)
if iframe_src: if iframe_src:
item["iframe_src"] = iframe_src item["iframe_src"] = iframe_src
item["template"] = "videos.html" item["template"] = "videos.html"
@@ -406,9 +407,6 @@ def _parse_videos(json_resp: dict[str, t.Any]) -> EngineResults:
) )
if result["thumbnail"] is not None: if result["thumbnail"] is not None:
item["thumbnail"] = result["thumbnail"]["src"] item["thumbnail"] = result["thumbnail"]["src"]
iframe_src = get_embeded_stream_url(result["url"])
if iframe_src:
item["iframe_src"] = iframe_src
res.add(item) res.add(item)

View File

@@ -40,7 +40,7 @@ if t.TYPE_CHECKING:
about = { about = {
"website": "https://api.search.brave.com/", "website": "https://api.search.brave.com/",
"wikidata_id": None, "wikidata_id": None,
"official_api_documentation": "https://api-dashboard.search.brave.com/documentation", "official_api_documentation": "https://api-dashboard.search.brave.com/api-reference/web/search/get",
"use_official_api": True, "use_official_api": True,
"require_api_key": True, "require_api_key": True,
"results": "JSON", "results": "JSON",
@@ -63,8 +63,10 @@ base_url = "https://api.search.brave.com/res/v1/web/search"
time_range_map = {"day": "past_day", "week": "past_week", "month": "past_month", "year": "past_year"} time_range_map = {"day": "past_day", "week": "past_week", "month": "past_month", "year": "past_year"}
"""Mapping of SearXNG time ranges to Brave API time ranges.""" """Mapping of SearXNG time ranges to Brave API time ranges."""
max_page = 10
def init(_):
def setup(_: dict[str, t.Any]) -> bool | None:
"""Initialize the engine.""" """Initialize the engine."""
if not api_key: if not api_key:
raise SearxEngineAPIException("No API key provided") raise SearxEngineAPIException("No API key provided")
@@ -75,7 +77,7 @@ def request(query: str, params: "OnlineParams") -> None:
search_args: dict[str, str | int | None] = { search_args: dict[str, str | int | None] = {
"q": query, "q": query,
"count": results_per_page, "count": results_per_page,
"offset": (params["pageno"] - 1) * results_per_page, "offset": params["pageno"] - 1,
"text_decorations": False, "text_decorations": False,
} }

View File

@@ -41,7 +41,7 @@ search_index = "cw22"
<https://www.chatnoir.eu/docs/api-general>`_ for a full list.""" <https://www.chatnoir.eu/docs/api-general>`_ for a full list."""
def _obtain_api_key() -> tuple[str, str, str]: def _obtain_api_key() -> tuple[str, str]:
home_resp = get(base_url) home_resp = get(base_url)
if not home_resp.ok: if not home_resp.ok:
raise SearxEngineAPIException("failed to obtain api key") raise SearxEngineAPIException("failed to obtain api key")
@@ -58,10 +58,9 @@ def _obtain_api_key() -> tuple[str, str, str]:
) )
if not token_resp.ok: if not token_resp.ok:
raise SearxEngineAPIException("failed to obtain api key") raise SearxEngineAPIException("failed to obtain api key")
session_id = token_resp.cookies["sessionid"]
scraped_api_key = token_resp.json()["token"]["token"] scraped_api_key = token_resp.json()["token"]["token"]
return csrf_token, session_id, scraped_api_key return csrf_token, scraped_api_key
def request(query: str, params: "OnlineParams"): def request(query: str, params: "OnlineParams"):
@@ -73,7 +72,7 @@ def request(query: str, params: "OnlineParams"):
params["headers"].update(headers) params["headers"].update(headers)
else: else:
csrf_token, session_id, scraped_api_key = _obtain_api_key() csrf_token, scraped_api_key = _obtain_api_key()
headers = { headers = {
"Authorization": f"Bearer {scraped_api_key}", "Authorization": f"Bearer {scraped_api_key}",
@@ -81,7 +80,7 @@ def request(query: str, params: "OnlineParams"):
} }
params["headers"].update(headers) params["headers"].update(headers)
params["cookies"] = {"csrftoken": session_id, "sessionid": session_id} params["cookies"] = {"csrftoken": csrf_token}
params["url"] = f"{base_url}/api/v1/_search" params["url"] = f"{base_url}/api/v1/_search"
params["method"] = "POST" params["method"] = "POST"

View File

@@ -78,7 +78,7 @@ time_range_dict = {'day': '24h', 'week': '1w', 'month': '1m', 'year': '1y'}
base_url = "https://www.chinaso.com" base_url = "https://www.chinaso.com"
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if chinaso_news_source not in t.get_args(ChinasoNewsSourceType): if chinaso_news_source not in t.get_args(ChinasoNewsSourceType):
raise ValueError(f"Unsupported news source: {chinaso_news_source}") raise ValueError(f"Unsupported news source: {chinaso_news_source}")

View File

@@ -74,6 +74,7 @@ Implementations
=============== ===============
""" """
import typing as t
import re import re
from os.path import expanduser, isabs, realpath, commonprefix from os.path import expanduser, isabs, realpath, commonprefix
from shlex import split as shlex_split from shlex import split as shlex_split
@@ -83,6 +84,7 @@ from threading import Thread
from searx import logger from searx import logger
from searx.result_types import EngineResults from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
paging = True paging = True
command = [] command = []
@@ -99,7 +101,7 @@ _command_logger = logger.getChild('command')
_compiled_parse_regex = {} _compiled_parse_regex = {}
def init(engine_settings): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
check_parsing_options(engine_settings) check_parsing_options(engine_settings)
if 'command' not in engine_settings: if 'command' not in engine_settings:

View File

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

View File

@@ -36,7 +36,7 @@ base_url = "https://www.dogpile.com"
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"} safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if dogpile_categ not in ("search", "images", "videos", "news"): if dogpile_categ not in ("search", "images", "videos", "news"):
raise ValueError("invalid search type: %s" % dogpile_categ) raise ValueError("invalid search type: %s" % dogpile_categ)

View File

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

View File

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

View File

@@ -10,7 +10,8 @@ from datetime import datetime
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.parse import quote_plus from urllib.parse import quote_plus
from searx.utils import get_embeded_stream_url, html_to_text, gen_useragent, extr from searx.result_types import EngineResults, MainResult, LegacyResult, Image
from searx.utils import html_to_text, gen_useragent, extr
from searx.network import get # see https://github.com/searxng/searxng/issues/762 from searx.network import get # see https://github.com/searxng/searxng/issues/762
from searx.engines.duckduckgo import fetch_traits # pylint: disable=unused-import from searx.engines.duckduckgo import fetch_traits # pylint: disable=unused-import
@@ -47,7 +48,7 @@ _HTTP_User_Agent: str = gen_useragent()
send_accept_language_header = False send_accept_language_header = False
def init(engine_settings: dict[str, t.Any]): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
if engine_settings["ddg_category"] not in ["images", "videos", "news"]: if engine_settings["ddg_category"] not in ["images", "videos", "news"]:
raise ValueError(f"Unsupported DuckDuckGo category: {engine_settings['ddg_category']}") raise ValueError(f"Unsupported DuckDuckGo category: {engine_settings['ddg_category']}")
@@ -97,6 +98,7 @@ def request(query: str, params: "OnlineParams") -> None:
# The vqd value is generated from the query and the UA header. To be able to # 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. # reuse the vqd value, the UA header must be static.
headers["User-Agent"] = _HTTP_User_Agent headers["User-Agent"] = _HTTP_User_Agent
params["impersonate"] = "none"
vqd = get_vqd(query=query, params=params) or fetch_vqd(query=query, params=params) vqd = get_vqd(query=query, params=params) or fetch_vqd(query=query, params=params)
headers["Accept"] = "*/*" headers["Accept"] = "*/*"
@@ -148,54 +150,51 @@ def request(query: str, params: "OnlineParams") -> None:
def _image_result(result): def _image_result(result):
return { return Image(
'template': 'images.html', url=result['url'],
'url': result['url'], title=result['title'],
'title': result['title'], content='',
'content': '', thumbnail_src=result['thumbnail'],
'thumbnail_src': result['thumbnail'], img_src=result['image'],
'img_src': result['image'], resolution='%s x %s' % (result['width'], result['height']),
'resolution': '%s x %s' % (result['width'], result['height']), source=result['source'],
'source': result['source'], )
}
def _video_result(result): def _video_result(result):
return { return LegacyResult(
'template': 'videos.html', template='videos.html',
'url': result['content'], url=result['content'],
'title': result['title'], title=result['title'],
'content': result['description'], content=result['description'],
'thumbnail': result['images'].get('small') or result['images'].get('medium'), thumbnail=result['images'].get('small') or result['images'].get('medium'),
'iframe_src': get_embeded_stream_url(result['content']), source=result['provider'],
'source': result['provider'], length=result['duration'],
'length': result['duration'], metadata=result.get('uploader'),
'metadata': result.get('uploader'), )
}
def _news_result(result): def _news_result(result):
return { return MainResult(
'url': result['url'], url=result['url'],
'title': result['title'], title=result['title'],
'content': html_to_text(result['excerpt']), content=html_to_text(result['excerpt']),
'source': result['source'], publishedDate=datetime.fromtimestamp(result['date']),
'publishedDate': datetime.fromtimestamp(result['date']), )
}
def response(resp): def response(resp: "SXNG_Response") -> EngineResults:
results = [] res = EngineResults()
res_json = resp.json() res_json = resp.json()
for result in res_json['results']: for result in res_json['results']:
if ddg_category == 'images': if ddg_category == 'images':
results.append(_image_result(result)) res.add(_image_result(result))
elif ddg_category == 'videos': elif ddg_category == 'videos':
results.append(_video_result(result)) res.add(_video_result(result))
elif ddg_category == 'news': elif ddg_category == 'news':
results.append(_news_result(result)) res.add(_news_result(result))
else: else:
raise ValueError(f"Invalid duckduckgo category: {ddg_category}") raise ValueError(f"Invalid duckduckgo category: {ddg_category}")
return results return res

View File

@@ -17,6 +17,7 @@ from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
from searx import weather from searx import weather
about = { about = {
"website": 'https://duckduckgo.com/', "website": 'https://duckduckgo.com/',
"wikidata_id": 'Q12805', "wikidata_id": 'Q12805',
@@ -108,7 +109,19 @@ def response(resp: SXNG_Response):
json_data = loads(resp.text[resp.text.find('\n') + 1 : resp.text.rfind('\n') - 2]) json_data = loads(resp.text[resp.text.find('\n') + 1 : resp.text.rfind('\n') - 2])
geoloc = weather.GeoLocation.by_query(resp.search_params["query"]) location = json_data.get("location")
if not location:
return res
metadata = json_data.get("weatherAlerts", {}).get("metadata", {})
geoloc = weather.GeoLocation(
name=location,
latitude=metadata.get("latitude"),
longitude=metadata.get("longitude"),
elevation=0,
country_code=metadata.get("language").split("-")[-1],
timezone=json_data.get("location"),
)
weather_answer = EngineResults.types.WeatherAnswer( weather_answer = EngineResults.types.WeatherAnswer(
current=_weather_data(geoloc, json_data["currentWeather"]), current=_weather_data(geoloc, json_data["currentWeather"]),

View File

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

View File

@@ -41,6 +41,7 @@ authentication configured to read from ``my-index`` index.
""" """
import typing as t
from json import loads, dumps from json import loads, dumps
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults from searx.result_types import EngineResults
@@ -68,7 +69,7 @@ show_metadata = False
page_size = 10 page_size = 10
def init(engine_settings): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
if 'query_type' in engine_settings and engine_settings['query_type'] not in _available_query_types: if 'query_type' in engine_settings and engine_settings['query_type'] not in _available_query_types:
raise ValueError('unsupported query type', engine_settings['query_type']) raise ValueError('unsupported query type', engine_settings['query_type'])

View File

@@ -78,7 +78,7 @@ content_max_characters: int = 500
"""Maximum characters for the requested content.""" """Maximum characters for the requested content."""
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if not api_key: if not api_key:
raise SearxEngineAPIException("No API key provided") raise SearxEngineAPIException("No API key provided")
if not 1 <= results_per_page <= 100: if not 1 <= results_per_page <= 100:

View File

@@ -47,7 +47,7 @@ CACHE_VALID_DURATION = 30 * 24 * 3600 # one month, same as website
"""Duration how long settings cookies are valid.""" """Duration how long settings cookies are valid."""
def init(engine_settings: dict[str, t.Any]): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"]) CACHE = EngineCache(engine_settings["name"])

View File

@@ -38,6 +38,7 @@ Implementation
""" """
import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from dateutil import parser from dateutil import parser
@@ -77,7 +78,7 @@ page_size: int = 10
"""Maximum number of results per page (default 10).""" """Maximum number of results per page (default 10)."""
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if not base_url: if not base_url:
raise ValueError('gitea engine: base_url is unset') raise ValueError('gitea engine: base_url is unset')

View File

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

View File

@@ -327,6 +327,7 @@ def google_request(
params["url"] = f"https://www.google.com/wml/search?{urlencode(args)}" params["url"] = f"https://www.google.com/wml/search?{urlencode(args)}"
params["headers"]["User-Agent"] = random.choice(nokia_useragents) params["headers"]["User-Agent"] = random.choice(nokia_useragents)
params["impersonate"] = "chrome99_android"
def request(query: str, params: "OnlineParams") -> None: def request(query: str, params: "OnlineParams") -> None:

View File

@@ -30,6 +30,7 @@ about = {
categories = ["general", "web"] categories = ["general", "web"]
paging = True paging = True
enable_http3 = True
max_page = 5 max_page = 5
page_size = 20 page_size = 20
time_range_support = True time_range_support = True

View File

@@ -26,6 +26,7 @@ about = {
# engine dependent config # engine dependent config
categories = ["images", "web"] categories = ["images", "web"]
paging = True paging = True
enable_http3 = True
max_page = 50 max_page = 50
"""Google supports up to 50 pages of results, see the `Google max_page discussion`_. """Google supports up to 50 pages of results, see the `Google max_page discussion`_.

View File

@@ -20,6 +20,7 @@ about = {
} }
play_categ = None # apps|movies play_categ = None # apps|movies
enable_http3 = True
base_url = 'https://play.google.com' base_url = 'https://play.google.com'
search_url = base_url + "/store/search?{query}&c={play_categ}" search_url = base_url + "/store/search?{query}&c={play_categ}"

View File

@@ -27,7 +27,7 @@ import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from datetime import datetime from datetime import datetime
from lxml import html from lxml import html
import httpx from curl_cffi.requests.exceptions import TooManyRedirects
from searx.utils import ( from searx.utils import (
eval_xpath, eval_xpath,
@@ -63,6 +63,7 @@ about = {
# engine dependent config # engine dependent config
categories = ["science", "scientific publications"] categories = ["science", "scientific publications"]
paging = True paging = True
enable_http3 = True
max_page = 50 max_page = 50
"""`Google max 50 pages`_ """`Google max 50 pages`_
@@ -102,7 +103,7 @@ def response(resp: "SXNG_Response") -> EngineResults: # pylint: disable=too-man
raise SearxEngineAccessDeniedException( raise SearxEngineAccessDeniedException(
message="google_scholar: unusual traffic detected", message="google_scholar: unusual traffic detected",
) )
raise httpx.TooManyRedirects(f"location {resp.headers['Location'].split('?')[0]}") raise TooManyRedirects(f"location {resp.headers['Location'].split('?')[0]}")
res = EngineResults() res = EngineResults()
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)

View File

@@ -10,7 +10,6 @@ from searx.utils import (
eval_xpath_getindex, eval_xpath_getindex,
eval_xpath_list, eval_xpath_list,
extract_text, extract_text,
get_embeded_stream_url,
parse_duration_string, parse_duration_string,
) )
@@ -79,7 +78,6 @@ def response(resp: "SXNG_Response") -> EngineResults:
title=title, title=title,
thumbnail=thumbnail, thumbnail=thumbnail,
length=length, length=length,
iframe_src=get_embeded_stream_url(url) or "",
template="videos.html", template="videos.html",
) )
) )

View File

@@ -1,127 +0,0 @@
# 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.enginelib import EngineCache
from searx.network import get
from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException
from searx.result_types import EngineResults
from searx.utils import gen_useragent
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://heexy.org"
api_url = "https://seapi.heexy.org"
safe_search_map = {0: "off", 1: "on", 2: "on"}
CACHE: EngineCache
"""Cache for storing the ``X-Data-Cacheft`` token (acts like an API key)."""
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement
if heexy_categ not in ("web", "image"):
raise ValueError("invalid search category: %s" % heexy_categ)
CACHE = EngineCache(engine_settings["name"])
return True
def _get_api_token(query: str) -> str:
"""The API token is independent of the search query. We just need any query
to obtain it initially, and don't hardcode it here to decrease chances of
getting blocked. The token must be passed as ``X-Data-Cacheft`` header."""
cached_token: str = CACHE.get("token")
if cached_token:
return cached_token
resp = get(
f"{base_url}/search?q={query}", headers={"User-Agent": gen_useragent(), "Accept-Language": "en-US,en:q=0.9"}
)
if not resp.ok:
raise SearxEngineAPIException("failed to obtain request token: invalid response code")
token = resp.cookies["cacheft"]
if not token:
raise SearxEngineAPIException("failed to obtain request token: no token found")
CACHE.set("token", token, expire=3 * 60)
return token
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"{api_url}/search/{heexy_categ}?{urlencode(args)}"
params["headers"]["Origin"] = api_url
params["cookies"]["cacheft"] = _get_api_token(query)
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

View File

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

View File

@@ -33,6 +33,7 @@ Implementations
""" """
import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from datetime import datetime from datetime import datetime
@@ -62,7 +63,7 @@ huggingface_endpoint = 'models'
""" """
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if huggingface_endpoint not in ('datasets', 'models', 'spaces'): if huggingface_endpoint not in ('datasets', 'models', 'spaces'):
raise SearxEngineAPIException(f"Unsupported Hugging Face endpoint: {huggingface_endpoint}") raise SearxEngineAPIException(f"Unsupported Hugging Face endpoint: {huggingface_endpoint}")

View File

@@ -8,6 +8,7 @@ No public instance offer a public API now
""" """
import typing as t
import time import time
import random import random
from urllib.parse import quote_plus, urlparse from urllib.parse import quote_plus, urlparse
@@ -34,7 +35,7 @@ time_range_support = True
base_url: list[str] | str = [] base_url: list[str] | str = []
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if not base_url: if not base_url:
raise ValueError("missing invidious base_url") raise ValueError("missing invidious base_url")

View File

@@ -108,12 +108,14 @@ def get_infobox(alt_forms, result_url, definitions):
infobox_content.append(f'<p><i>Other forms:</i> {", ".join(alt_forms[1:])}</p>') infobox_content.append(f'<p><i>Other forms:</i> {", ".join(alt_forms[1:])}</p>')
# definitions # definitions
infobox_content.append(''' infobox_content.append(
'''
<small><a href="https://www.edrdg.org/wiki/index.php/JMdict-EDICT_Dictionary_Project">JMdict</a> <small><a href="https://www.edrdg.org/wiki/index.php/JMdict-EDICT_Dictionary_Project">JMdict</a>
and <a href="https://www.edrdg.org/enamdict/enamdict_doc.html">JMnedict</a> and <a href="https://www.edrdg.org/enamdict/enamdict_doc.html">JMnedict</a>
by <a href="https://www.edrdg.org/edrdg/licence.html">EDRDG</a>, CC BY-SA 3.0.</small> by <a href="https://www.edrdg.org/edrdg/licence.html">EDRDG</a>, CC BY-SA 3.0.</small>
<ul> <ul>
''') '''
)
for pos, engdef, extra in definitions: for pos, engdef, extra in definitions:
if pos == 'Wikipedia definition': if pos == 'Wikipedia definition':
infobox_content.append('</ul><small>Wikipedia, CC BY-SA 3.0.</small><ul>') infobox_content.append('</ul><small>Wikipedia, CC BY-SA 3.0.</small><ul>')

View File

@@ -87,7 +87,7 @@ api_key = ""
"""Kagi API key. Required for using this engine.""" """Kagi API key. Required for using this engine."""
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if not api_key: if not api_key:
raise ValueError("api_key is required for using kagi") raise ValueError("api_key is required for using kagi")

View File

@@ -51,7 +51,7 @@ enable_http2 = False
safe_search_map = {0: "Off", 1: "Moderate", 2: "Strict"} safe_search_map = {0: "Off", 1: "Moderate", 2: "Strict"}
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if luxxle_categ not in ("search", "images", "videos", "news"): if luxxle_categ not in ("search", "images", "videos", "news"):
raise ValueError("invalid luxxle category: %s" % luxxle_categ) raise ValueError("invalid luxxle category: %s" % luxxle_categ)

View File

@@ -10,7 +10,7 @@ Lofgren .
.. _marginalia filters: .. _marginalia filters:
Marginalia Filters Marginalia Filters
================= ==================
Custom filters enable server-side customization of Marginalia search results. Custom filters enable server-side customization of Marginalia search results.
Filter definitions are written in XML and scoped to an API key. Filters can Filter definitions are written in XML and scoped to an API key. Filters can
@@ -82,7 +82,7 @@ api_key = None
https://about.marginalia-search.com/article/api/ https://about.marginalia-search.com/article/api/
""" """
filter_name: str | None = None filter_name: str = ""
"""The name of the custom filter to apply to each search.""" """The name of the custom filter to apply to each search."""
@@ -164,7 +164,7 @@ def response(resp: SXNG_Response):
return res return res
def init(_: dict[str, t.Any]): def setup(_: dict[str, t.Any]) -> bool:
if not api_key: if not api_key:
logger.error("missing api_key: see https://about.marginalia-search.com/article/api") logger.error("missing api_key: see https://about.marginalia-search.com/article/api")

View File

@@ -26,6 +26,8 @@ Implementations
""" """
import typing as t
try: try:
import mariadb # pyright: ignore [reportMissingImports] import mariadb # pyright: ignore [reportMissingImports]
except ImportError: except ImportError:
@@ -60,15 +62,17 @@ paging = True
_connection = None _connection = None
def init(engine_settings): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
global _connection # pylint: disable=global-statement
if 'query_str' not in engine_settings: if 'query_str' not in engine_settings:
raise ValueError('query_str cannot be empty') raise ValueError('query_str cannot be empty')
if not engine_settings['query_str'].lower().startswith('select '): if not engine_settings['query_str'].lower().startswith('select '):
raise ValueError('only SELECT query is supported') raise ValueError('only SELECT query is supported')
def init(_):
global _connection # pylint: disable=global-statement
_connection = mariadb.connect(database=database, user=username, password=password, host=host, port=port) _connection = mariadb.connect(database=database, user=username, password=password, host=host, port=port)

View File

@@ -35,7 +35,7 @@ Here is a simple example to query a Meilisearch instance:
""" """
# pylint: disable=global-statement # pylint: disable=global-statement
import typing as t
from json import dumps from json import dumps
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -49,7 +49,7 @@ categories = ['general']
paging = True paging = True
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if index == '': if index == '':
raise ValueError('index cannot be empty') raise ValueError('index cannot be empty')

View File

@@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Mojeek (general, images, news)""" """Mojeek (general, images, news)"""
import typing as t
from datetime import datetime from datetime import datetime
from urllib.parse import urlencode from urllib.parse import urlencode
@@ -51,7 +52,7 @@ region_param = "arc"
_delta_kwargs = {"day": "days", "week": "weeks", "month": "months", "year": "years"} _delta_kwargs = {"day": "days", "week": "weeks", "month": "months", "year": "years"}
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if search_type not in ("", "images", "news"): if search_type not in ("", "images", "news"):
raise ValueError(f"Invalid search type {search_type}") raise ValueError(f"Invalid search type {search_type}")

View File

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

View File

@@ -19,6 +19,7 @@ Implementation
============== ==============
""" """
import typing as t
from urllib.parse import quote_plus from urllib.parse import quote_plus
about = { about = {
@@ -37,7 +38,7 @@ matrix_url = "https://matrix.to"
page_size = 20 page_size = 20
def init(engine_settings): # pylint: disable=unused-argument def setup(_: dict[str, t.Any]) -> bool | None:
"""The ``base_url`` must be set in the configuration, if ``base_url`` is not """The ``base_url`` must be set in the configuration, if ``base_url`` is not
set, a :py:obj:`ValueError` is raised during initialization. set, a :py:obj:`ValueError` is raised during initialization.

View File

@@ -25,6 +25,8 @@ Implementations
""" """
import typing as t
from searx.result_types import EngineResults from searx.result_types import EngineResults
try: try:
@@ -60,15 +62,17 @@ paging = True
_connection = None _connection = None
def init(engine_settings): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
global _connection # pylint: disable=global-statement
if 'query_str' not in engine_settings: if 'query_str' not in engine_settings:
raise ValueError('query_str cannot be empty') raise ValueError('query_str cannot be empty')
if not engine_settings['query_str'].lower().startswith('select '): if not engine_settings['query_str'].lower().startswith('select '):
raise ValueError('only SELECT query is supported') raise ValueError('only SELECT query is supported')
def init(_):
global _connection # pylint: disable=global-statement
_connection = mysql.connector.connect( _connection = mysql.connector.connect(
database=database, database=database,
user=username, user=username,

View File

@@ -2,6 +2,8 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long
"""Naver for SearXNG""" """Naver for SearXNG"""
import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from lxml import html from lxml import html
@@ -16,7 +18,6 @@ from searx.utils import (
html_to_text, html_to_text,
parse_duration_string, parse_duration_string,
js_obj_str_to_python, js_obj_str_to_python,
get_embeded_stream_url,
) )
# engine metadata # engine metadata
@@ -67,7 +68,7 @@ naver_category_dict = {
} }
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if naver_category not in ('general', 'images', 'news', 'videos'): if naver_category not in ('general', 'images', 'news', 'videos'):
raise SearxEngineAPIException(f"Unsupported category: {naver_category}") raise SearxEngineAPIException(f"Unsupported category: {naver_category}")
@@ -193,7 +194,7 @@ def parse_news(data):
def parse_videos(data): def parse_videos(data):
results = [] res = EngineResults()
dom = html.fromstring(data) dom = html.fromstring(data)
@@ -212,15 +213,14 @@ def parse_videos(data):
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
results.append( res.add(
{ res.types.LegacyResult(
"template": "videos.html", template="videos.html",
"title": extract_text(eval_xpath(item, ".//a[contains(@class, 'info_title')]")), title=extract_text(eval_xpath(item, ".//a[contains(@class, 'info_title')]")),
"url": url, url=url,
"thumbnail": thumbnail, thumbnail=thumbnail,
"length": length, length=length,
"iframe_src": get_embeded_stream_url(url), )
}
) )
return results return res

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
"""Pexels (images)""" """Pexels (images)"""
import re import re
import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from lxml import html from lxml import html
@@ -12,6 +13,7 @@ from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException
from searx.network import get from searx.network import get
# about # about
about = { about = {
"website": 'https://www.pexels.com', "website": 'https://www.pexels.com',
@@ -45,9 +47,10 @@ CACHE: EngineCache
enable_http2 = False enable_http2 = False
def init(engine_settings): def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"]) CACHE = EngineCache(engine_settings["name"])
return True
def _get_secret_key(): def _get_secret_key():

View File

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

View File

@@ -21,6 +21,8 @@ Implementations
""" """
import typing as t
try: try:
import psycopg2 # type: ignore import psycopg2 # type: ignore
except ImportError: except ImportError:
@@ -55,15 +57,17 @@ paging = True
_connection = None _connection = None
def init(engine_settings): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
global _connection # pylint: disable=global-statement
if 'query_str' not in engine_settings: if 'query_str' not in engine_settings:
raise ValueError('query_str cannot be empty') raise ValueError('query_str cannot be empty')
if not engine_settings['query_str'].lower().startswith('select '): if not engine_settings['query_str'].lower().startswith('select '):
raise ValueError('only SELECT query is supported') raise ValueError('only SELECT query is supported')
def init(_):
global _connection # pylint: disable=global-statement
_connection = psycopg2.connect( _connection = psycopg2.connect(
database=database, database=database,
user=username, user=username,

View File

@@ -14,10 +14,11 @@ from lxml import html
import babel import babel
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.utils import eval_xpath_list, eval_xpath, extract_text, get_embeded_stream_url, extr from searx.utils import eval_xpath_list, eval_xpath, extract_text, extr
from searx.locales import region_tag from searx.locales import region_tag
from searx.result_types import EngineResults from searx.result_types import EngineResults
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from lxml.etree import ElementBase from lxml.etree import ElementBase
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -60,7 +61,7 @@ video_page_map = {
} }
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if privacywall_category not in ("general", "images", "videos"): if privacywall_category not in ("general", "images", "videos"):
raise ValueError("invalid category: %s" % privacywall_category) raise ValueError("invalid category: %s" % privacywall_category)
@@ -153,7 +154,6 @@ def _video_results(doc: "ElementBase") -> EngineResults:
title=extract_text(eval_xpath(result, ".//h2[contains(@class, 'video-card-title')]")) or "", title=extract_text(eval_xpath(result, ".//h2[contains(@class, 'video-card-title')]")) or "",
content=extract_text(eval_xpath(result, ".//p")) or "", content=extract_text(eval_xpath(result, ".//p")) or "",
thumbnail=thumbnail or "", thumbnail=thumbnail or "",
iframe_src=get_embeded_stream_url(url) or "",
) )
) )

View File

@@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Quark (Shenma) search engine for searxng""" """Quark (Shenma) search engine for searxng"""
import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from datetime import datetime from datetime import datetime
import re import re
@@ -43,7 +44,7 @@ def is_alibaba_captcha(html):
return bool(re.search(CAPTCHA_PATTERN, html)) return bool(re.search(CAPTCHA_PATTERN, html))
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if quark_category not in ('general', 'images'): if quark_category not in ('general', 'images'):
raise SearxEngineAPIException(f"Unsupported category: {quark_category}") raise SearxEngineAPIException(f"Unsupported category: {quark_category}")

View File

@@ -60,9 +60,6 @@ from searx.exceptions import (
SearxEngineTooManyRequestsException, SearxEngineTooManyRequestsException,
) )
from searx.network import raise_for_httperror from searx.network import raise_for_httperror
from searx.utils import (
get_embeded_stream_url,
)
from searx.result_types import EngineResults from searx.result_types import EngineResults
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
@@ -299,7 +296,6 @@ def response(resp: "SXNG_Response") -> EngineResults:
title=title, title=title,
url=res_url, url=res_url,
content=content, content=content,
iframe_src=get_embeded_stream_url(res_url),
publishedDate=pub_date, publishedDate=pub_date,
thumbnail=thumbnail, thumbnail=thumbnail,
template="videos.html", template="videos.html",

View File

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

View File

@@ -42,7 +42,7 @@ resulthunter_categ = "web"
"""Supported categories are ``web`` and ``images``.""" """Supported categories are ``web`` and ``images``."""
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if resulthunter_categ not in ("web", "images"): if resulthunter_categ not in ("web", "images"):
raise ValueError("invalid category: %s" % resulthunter_categ) raise ValueError("invalid category: %s" % resulthunter_categ)

View File

@@ -38,12 +38,10 @@ CACHE: EngineCache
"""Cache to store verification tokens for pagination.""" """Cache to store verification tokens for pagination."""
def init(_): def setup(engine_settings: dict[str, t.Any]) -> bool:
if not base_url: if not base_url:
raise ValueError("base_url must be set") raise ValueError("base_url must be set")
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"]) CACHE = EngineCache(engine_settings["name"])
return True return True
@@ -82,7 +80,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
res.types.MainResult( res.types.MainResult(
url=extract_text(eval_xpath(result, ".//a[contains(@class, 'title')]/@href")), url=extract_text(eval_xpath(result, ".//a[contains(@class, 'title')]/@href")),
title=extract_text(eval_xpath(result, ".//a[contains(@class, 'title')]")), title=extract_text(eval_xpath(result, ".//a[contains(@class, 'title')]")),
content=extract_text(eval_xpath(result, ".//span[contains(@class, 'description') or @class='']")), content=extract_text(eval_xpath(result, ".//span[contains(@class, 'description') or not(@class)]")),
) )
) )

View File

@@ -0,0 +1,63 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""JavaScript-based s1search implementation. See :ref:`s1search engine`.
Works for all s1search sites that contain the ``__RAMPJS__`` JavaScript variable.
"""
import json
import typing as t
from urllib.parse import urlencode
from searx.result_types import EngineResults
from searx.utils import extr, html_to_text
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
from searx.extended_types import SXNG_Response
about = {
"website": "https://s1search.co",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
categories = ["general"]
paging = True
base_url = "https://search.answers.com"
# other working base URLs:
# - https://search.nation.online
# - https://search.activebeat.com
# - https://search.legalboulevard.com
# - https://search.walletgenius.com
# - https://search.legalboulevard.com
def request(query: str, params: "OnlineParams"):
args = {"q": query, "page": params["pageno"]}
params["url"] = f"{base_url}/?{urlencode(args)}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
data_raw = extr(resp.text, "response: ", " };")
data = json.loads(data_raw)
mainline = [s for s in data["search"]["regions"] if s["name"] == "mainline"][0]
for group in mainline["groups"]:
for result in group["results"]:
if not ("url" in result or "clickUrl" in result):
continue
res.add(
res.types.MainResult(
url=result.get("url") or result.get("clickUrl"),
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
)
)
return res

View File

@@ -28,12 +28,13 @@ This is an example configuration for searching in the collection
""" """
# pylint: disable=global-statement # pylint: disable=global-statement
import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
base_url = 'http://localhost:8983' base_url = 'http://localhost:8983'
collection = '' collection = ''
rows = 10 rows = 10
@@ -45,7 +46,7 @@ _search_url = ''
paging = True paging = True
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if collection == '': if collection == '':
raise ValueError('collection cannot be empty') raise ValueError('collection cannot be empty')

View File

@@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""SoundCloud is a German audio streaming service.""" """SoundCloud is a German audio streaming service."""
import typing as t
import re import re
import datetime import datetime
@@ -115,9 +116,10 @@ def response(resp):
return results return results
def init(engine_settings): # pylint: disable=unused-argument def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"]) # type: ignore CACHE = EngineCache(engine_settings["name"]) # type:ignore
return True
def get_client_id() -> str | None: def get_client_id() -> str | None:

View File

@@ -44,7 +44,6 @@ Implementations
=============== ===============
""" """
import typing as t import typing as t
import sqlite3 import sqlite3
import contextlib import contextlib
@@ -67,7 +66,7 @@ limit = 10
paging = True paging = True
def init(engine_settings): def setup(engine_settings: dict[str, t.Any]) -> bool | None:
if 'query_str' not in engine_settings: if 'query_str' not in engine_settings:
raise ValueError('query_str cannot be empty') raise ValueError('query_str cannot be empty')

View File

@@ -82,7 +82,6 @@ Startpage's category (for Web-search, News, Videos, ..) is set by
Supported categories are ``web``, ``news`` and ``images``. Supported categories are ``web``, ``news`` and ``images``.
""" """
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
import re import re
@@ -166,7 +165,7 @@ CACHE: EngineCache
seconds.""" seconds."""
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
global CACHE # pylint: disable=global-statement global CACHE # pylint: disable=global-statement
# hint: all three startpage engines (WEB, Images & News) can/should use the # hint: all three startpage engines (WEB, Images & News) can/should use the

View File

@@ -39,7 +39,7 @@ page_size = 10
api_url = "https://search.kompas.services" api_url = "https://search.kompas.services"
def init(_): def setup(_: dict[str, t.Any]) -> bool | None:
if startpagina_categ not in ("web", "images", "videos", "news"): if startpagina_categ not in ("web", "images", "videos", "news"):
raise ValueError("invalid search type: %s" % startpagina_categ) raise ValueError("invalid search type: %s" % startpagina_categ)

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