32 Commits

Author SHA1 Message Date
dependabot[bot]
539c90647b [upd] web-client (simple): Bump the minor group across 1 directory with 7 updates
Bumps the minor group with 7 updates in the /client/simple directory:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.0` | `2.5.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.0` | `26.1.0` |
| [browserslist](https://github.com/browserslist/browserslist) | `4.28.2` | `4.28.4` |
| [less](https://github.com/less/less.js) | `4.6.6` | `4.6.7` |
| [sharp](https://github.com/lovell/sharp) | `0.35.1` | `0.35.3` |
| [stylelint](https://github.com/stylelint/stylelint) | `17.13.0` | `17.14.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.3` |



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

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

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

Updates `less` from 4.6.6 to 4.6.7
- [Release notes](https://github.com/less/less.js/releases)
- [Changelog](https://github.com/less/less.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/less/less.js/compare/v4.6.6...v4.6.7)

Updates `sharp` from 0.35.1 to 0.35.3
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.1...v0.35.3)

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

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

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: browserslist
  dependency-version: 4.28.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: less
  dependency-version: 4.6.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: sharp
  dependency-version: 0.35.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor
- dependency-name: stylelint
  dependency-version: 17.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: vite
  dependency-version: 8.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-03 07:24:05 +00:00
Markus Heiser
67973783de [fix] typo in utils/lib.sh
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-07-02 14:48:03 +02:00
Markus Heiser
0f9e30e3f9 [fix] docs.live (sphinx-autobuild) - watch .rst, .py, .yml and .conf (#6348)
So far, any change to any file triggered a new build, and changes to the
doc-strings (in ./searx) were not taken into account. With this patch, a build
is only triggered when files with the specified file extensions change.

The watch has been expanded to ./searx so that changes to doc strings also
trigger a new build.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-07-02 13:22:12 +02:00
Markus Heiser
640f88c9bd [fix] API docs: remove doc of not existing parameters (#6346)
TBH: Some of the parameters such as engines or plugins still theoretically exist
in SearXNG, but we will no longer support them.  They are usually accessed via
cookies and should not pollute the API / it is these supposed features that
hinder SearXNG in further development.

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

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-07-02 11:48:53 +02:00
Bnyro
c5d8d05f03 [fix] searchzee: circumvent botblocking 2026-07-01 15:35:46 +02:00
Markus Heiser
d115c61a70 [fix] clarify the mess of Engine.setup and Engine.init (#6343)
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2026-06-30 19:22:23 +02:00
Zhijie He
c5b1d066e5 [feat] engines: add sina engine
Apply suggestions from code review

Co-authored-by: Bnyro <bnyro@tutanota.com>
2026-06-30 16:59:55 +02:00
Bnyro
774616ada6 [fix] heexy: circumvent botblocking 2026-06-30 15:46:58 +02:00
Bnyro
6b2ec018e2 [fix] qwant: access denied due to mismatching query order
We're now shuffling the order of query arguments, as
it seems that Qwant actively blocks the query order of
SearXNG.

tgp seems to be short for "test group" (look at the XHR requests the
browser sends to "https://www.qwant.com/action/ui") - its actual value
doesn't matter, as long as it's sent and at the correct position in the
query params and doesn't change too frequently
2026-06-30 15:23:09 +02:00
dependabot[bot]
bfeaad6c37 [upd] pypi: Bump granian from 2.7.6 to 2.7.7 in the minor group (#6319)
Bumps the minor group with 1 update: [granian](https://github.com/emmett-framework/granian).


Updates `granian` from 2.7.6 to 2.7.7
- [Release notes](https://github.com/emmett-framework/granian/releases)
- [Commits](https://github.com/emmett-framework/granian/compare/v2.7.6...v2.7.7)

---
updated-dependencies:
- dependency-name: granian
  dependency-version: 2.7.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 08:18:49 +02:00
Sisyphus
28d3885764 [fix] naver: update HTML parsing for redesigned Naver search results page
Naver redesigned their search results page, replacing the old CSS classes
(lst_total, link_tit, total_dsc_wrap, api_txt_lines) with a new layout
using fds-web-doc-root containers and sds-comps design system.

Updated parse_general():
- Result container: fds-web-doc-root + fds-web-normal-doc-root
- Title: sds-comps-text-type-headline1 span
- URL: first external http link (with try/except guard)
- Content: sds-comps-text-type-body1 span
- Thumbnail: profile-thumbnail img src
- Added guard: skip results without title or URL
2026-06-29 20:25:23 +02:00
Hu Butui
b084194c09 [fix] engine baidu: fix image search blocked by antiFlag
Baidu image search returns antiFlag=1 when the acjson API is called
without session cookies from image.baidu.com.

Warm up cookies with a GET to the image homepage, cache them for one
hour via EngineCache, and pass them to image search requests. Raise
SearxEngineAccessDeniedException when antiFlag is still returned.

Signed-off-by: Hu Butui <hot123tea123@gmail.com>
2026-06-29 19:54:10 +02:00
Hu Butui
8c4df4cf3f [feat] bilibili engine: add time range support
Signed-off-by: Hu Butui <hot123tea123@gmail.com>
2026-06-29 19:53:20 +02:00
Bnyro
a0e594e16e [fix] neosearch: crashes when result has no url 2026-06-29 18:54:01 +02:00
Bnyro
cb4bfbe129 [chore] data.traits: update traits for neosearch 2026-06-29 13:05:01 +02:00
Bnyro
a5660bcc4f [feat] engines: add support for neosearch.org (general) 2026-06-29 13:05:01 +02:00
Bnyro
1396265774 [feat] engines: add magnific images engine 2026-06-29 10:19:19 +02:00
Bnyro
0fd40d5f29 [feat] engines: add shopify stock images engine 2026-06-29 10:02:16 +02:00
Bnyro
13a5ace8b5 [feat] engines: add stocksnap images engine 2026-06-29 10:02:04 +02:00
Bnyro
a6831797ba [feat] engines: add support for picjumbo.com 2026-06-29 10:01:43 +02:00
github-actions[bot]
0e990f78a3 [data] update searx.data - update_gsa_useragents.py (#6329) 2026-06-29 08:03:21 +02:00
github-actions[bot]
990c63b709 [data] update searx.data - update_currencies.py (#6333) 2026-06-29 07:09:12 +02:00
github-actions[bot]
e535e4c61a [data] update searx.data - update_firefox_version.py (#6332) 2026-06-29 07:08:32 +02:00
github-actions[bot]
79506441c5 [data] update searx.data - update_wikidata_units.py (#6330) 2026-06-29 07:08:02 +02:00
github-actions[bot]
6cb1c077e9 [data] update searx.data - update_ahmia_blacklist.py (#6331) 2026-06-29 07:07:28 +02:00
Bnyro
8605230eb8 [fix] resulthunter: images in search results are not clickable 2026-06-28 22:31:01 +02:00
Bnyro
a54722e692 [feat] tiger: add support for related search terms / suggestions 2026-06-28 22:30:48 +02:00
Bnyro
7e9a6c2861 [fix] tiger: captcha paths have changed 2026-06-28 22:30:48 +02:00
dependabot[bot]
357662d86d [upd] github-actions: Bump docker/scout-action from 1.21.0 to 1.22.0 (#6317)
Bumps [docker/scout-action](https://github.com/docker/scout-action) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/docker/scout-action/releases)
- [Commits](cd72f264be...7520205ff6)

---
updated-dependencies:
- dependency-name: docker/scout-action
  dependency-version: 1.22.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-06-28 09:19:29 +02:00
dependabot[bot]
bfa76e4cd9 [upd] github-actions: Bump actions/cache from 5.0.5 to 6.0.0 (#6316)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.0.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](27d5ce7f10...2c8a9bd745)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:19:14 +02:00
dependabot[bot]
a9d49a3349 [upd] github-actions: Bump actions/setup-python from 6.2.0 to 6.3.0 (#6315)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](a309ff8b42...ece7cb06ca)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.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-06-28 09:18:54 +02:00
github-actions[bot]
f8ffbf36f9 [l10n] update translations from Weblate (#6320)
6de4d96dd - 2026-06-23 - code_gremlin <code_gremlin@noreply.codeberg.org>
2026-06-26 10:24:36 +02:00
39 changed files with 7656 additions and 5059 deletions

View File

@@ -73,7 +73,7 @@ jobs:
# yamllint enable rule:line-length # yamllint enable rule:line-length
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
@@ -84,7 +84,7 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -96,7 +96,7 @@ jobs:
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
- name: Setup cache container - name: Setup cache container
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}" key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -41,7 +41,7 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
@@ -51,7 +51,7 @@ jobs:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
@@ -43,7 +43,7 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -34,7 +34,7 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
@@ -44,7 +44,7 @@ jobs:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
@@ -77,13 +77,13 @@ jobs:
node-version-file: "./.nvmrc" node-version-file: "./.nvmrc"
- name: Setup cache Node.js - name: Setup cache Node.js
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}" key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
path: "./client/simple/node_modules/" path: "./client/simple/node_modules/"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
@@ -46,7 +46,7 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -83,7 +83,7 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
@@ -94,7 +94,7 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: "false" persist-credentials: "false"
- name: Sync GHCS from Docker Scout - name: Sync GHCS from Docker Scout
uses: docker/scout-action@cd72f264beff1cd72735de31148b9d3244a0234a # v1.21.0 uses: docker/scout-action@7520205ff60037fdc436b40b6a1d1e55a839ec2d # v1.22.0
with: with:
organization: "searxng" organization: "searxng"
dockerhub-user: "${{ secrets.DOCKER_USER }}" dockerhub-user: "${{ secrets.DOCKER_USER }}"

File diff suppressed because it is too large Load Diff

View File

@@ -29,21 +29,21 @@
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.0", "@biomejs/biome": "2.5.2",
"@types/node": "^26.0.0", "@types/node": "^26.1.0",
"browserslist": "^4.28.2", "browserslist": "^4.28.4",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.1",
"less": "^4.6.6", "less": "^4.6.7",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.1", "sharp": "~0.35.3",
"sort-package-json": "^4.0.0", "sort-package-json": "^4.0.0",
"stylelint": "^17.13.0", "stylelint": "^17.14.0",
"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.1", "svgo": "^4.0.1",
"typescript": "~6.0.3", "typescript": "~6.0.3",
"vite": "^8.0.16", "vite": "^8.1.3",
"vite-bundle-analyzer": "^1.3.8" "vite-bundle-analyzer": "^1.3.8"
} }
} }

View File

@@ -4,31 +4,32 @@
Search API Search API
========== ==========
SearXNG supports querying via a simple HTTP API. SearXNG supports querying via a simple HTTP API. Two endpoints, ``/`` and
Two endpoints, ``/`` and ``/search``, are supported for both GET and POST methods. ``/search``, are supported for both GET and POST methods. The ``GET`` method
The GET method expects parameters as URL query parameters, while the POST method expects parameters as form data. expects parameters as URL query parameters, while the POST method expects
parameters as form data (``application/x-www-form-urlencoded``).
If you want to consume the results as JSON, CSV, or RSS, you need to set the If you want to consume the results as JSON, CSV, or RSS, you need to set the
``format`` parameter accordingly. Supported formats are defined in ``settings.yml``, under the ``search`` section. ``format`` parameter accordingly. Supported formats are defined in
Requesting an unset format will return a 403 Forbidden error. Be aware that many public instances have these formats disabled. ``settings.yml``, under the :ref:`settings search` section. Requesting an
unset format will return a 403 Forbidden error. Be aware that many public
instances have these formats disabled.
Endpoints: Endpoints:
``GET /`` .. code::
``GET /search``
``POST /`` GET /
``POST /search`` GET /search
POST /
POST /search
example cURL calls: example cURL calls:
.. code-block:: bash .. code:: bash
curl 'https://searx.example.org/search?q=searxng&format=json' curl 'https://searx.example.org/search?q=searxng&format=json'
curl -X POST 'https://searx.example.org/search' -d 'q=searxng&format=csv' curl -X POST 'https://searx.example.org/search' -d 'q=searxng&format=csv'
curl -L -X POST -d 'q=searxng&format=json' 'https://searx.example.org/' curl -L -X POST -d 'q=searxng&format=json' 'https://searx.example.org/'
Parameters Parameters
@@ -53,90 +54,27 @@ Parameters
Comma separated list, specifies the active search categories (see Comma separated list, specifies the active search categories (see
:ref:`configured engines`) :ref:`configured engines`)
``engines`` : optional
Comma separated list, specifies the active search engines (see
:ref:`configured engines`).
``language`` : default from :ref:`settings search` ``language`` : default from :ref:`settings search`
Code of the language. Code of the language.
``pageno`` : default ``1`` ``pageno`` : default ``1``
Search page number. Search page number.
``time_range`` : optional ``time_range`` : optional : [ ``day``, ``month``, ``year`` ]
[ ``day``, ``month``, ``year`` ]
Time range of search for engines which support it. See if an engine supports Time range of search for engines which support it. See if an engine supports
time range search in the preferences page of an instance. time range search in the preferences page of an instance.
``format`` : optional ``format`` : optional : [ ``json``, ``csv``, ``rss`` ]
[ ``json``, ``csv``, ``rss`` ]
Output format of results. Format needs to be activated in :ref:`settings Output format of results. Format needs to be activated in :ref:`settings
search`. search`.
``results_on_new_tab`` : default ``0`` ``safesearch`` : default from :ref:`settings search` : [ ``0``, ``1``, ``2`` ]
[ ``0``, ``1`` ]
Open search results on new tab.
``image_proxy`` : default from :ref:`settings server`
[ ``True``, ``False`` ]
Proxy image results through SearXNG.
``autocomplete`` : default from :ref:`settings search`
[ ``google``, ``dbpedia``, ``duckduckgo``, ``mwmbl``, ``startpage``,
``privacywall``, ``wikipedia``, ``swisscows``, ``qwant`` ]
Service which completes words as you type.
``safesearch`` : default from :ref:`settings search`
[ ``0``, ``1``, ``2`` ]
Filter search results of engines which support safe search. See if an engine Filter search results of engines which support safe search. See if an engine
supports safe search in the preferences page of an instance. supports safe search in the preferences page of an instance.
``theme`` : default ``simple`` ``theme`` : default ``simple`` : [ ``simple`` ]
[ ``simple`` ]
Theme of instance. Theme of instance.
Please note, available themes depend on an instance. It is possible that an Please note, available themes depend on an instance. It is possible that an
instance administrator deleted, created or renamed themes on their instance. instance administrator deleted, created or renamed themes on their instance.
See the available options in the preferences page of the instance. See the available options in the preferences page of the instance.
``enabled_plugins`` : optional
List of enabled plugins.
:default:
``Hash_plugin``, ``Self_Information``,
``Tracker_URL_remover``, ``Ahmia_blacklist``
:values:
.. enabled by default
``Hash_plugin``, ``Self_Information``,
``Tracker_URL_remover``, ``Ahmia_blacklist``,
.. disabled by default
``Hostnames_plugin``, ``Open_Access_DOI_rewrite``,
``Vim-like_hotkeys``, ``Tor_check_plugin``
``disabled_plugins``: optional
List of disabled plugins.
:default:
``Hostnames_plugin``, ``Open_Access_DOI_rewrite``,
``Vim-like_hotkeys``, ``Tor_check_plugin``
:values:
see values from ``enabled_plugins``
``enabled_engines`` : optional : *all* :origin:`engines <searx/engines>`
List of enabled engines.
``disabled_engines`` : optional : *all* :origin:`engines <searx/engines>`
List of disabled engines.

View File

@@ -23,6 +23,6 @@ coloredlogs==15.0.1
docutils>=0.21.2;python_version <= "3.11" docutils>=0.21.2;python_version <= "3.11"
docutils>=0.22.4; python_version > "3.11" docutils>=0.22.4; python_version > "3.11"
parameterized==0.9.0 parameterized==0.9.0
granian[reload]==2.7.6 granian[reload]==2.7.7
basedpyright==1.39.8 basedpyright==1.39.8
types-lxml==2026.2.16 types-lxml==2026.2.16

View File

@@ -1,2 +1,2 @@
granian==2.7.6 granian==2.7.7
granian[pname]==2.7.6 granian[pname]==2.7.7

File diff suppressed because it is too large Load Diff

View File

@@ -773,7 +773,7 @@
"nl": "Boliviaanse boliviano", "nl": "Boliviaanse boliviano",
"oc": "Boliviano", "oc": "Boliviano",
"pa": "ਬੋਲੀਵੀਆਨੋ", "pa": "ਬੋਲੀਵੀਆਨੋ",
"pl": "boliviano", "pl": "boliwiano",
"pt": "Boliviano", "pt": "Boliviano",
"ro": "boliviano", "ro": "boliviano",
"ru": "боливиано", "ru": "боливиано",
@@ -1107,7 +1107,7 @@
"fi": "Kongon frangi", "fi": "Kongon frangi",
"fr": "franc congolais", "fr": "franc congolais",
"ga": "franc an Chongó", "ga": "franc an Chongó",
"gl": "Franco congolés", "gl": "franco congolés",
"he": "פרנק קונגולזי", "he": "פרנק קונגולזי",
"hr": "Kongoanski franak", "hr": "Kongoanski franak",
"hu": "kongói frank", "hu": "kongói frank",
@@ -1636,7 +1636,7 @@
"fi": "Algerian dinaari", "fi": "Algerian dinaari",
"fr": "dinar algérien", "fr": "dinar algérien",
"ga": "dinar na hAilgéire", "ga": "dinar na hAilgéire",
"gl": "Dinar alxeriano", "gl": "dinar alxeriano",
"he": "דינר אלג'ירי", "he": "דינר אלג'ירי",
"hr": "Alžirski dinar", "hr": "Alžirski dinar",
"hu": "algériai dinár", "hu": "algériai dinár",
@@ -1651,7 +1651,7 @@
"pap": "dinar argelino", "pap": "dinar argelino",
"pl": "dinar algierski", "pl": "dinar algierski",
"pt": "dinar argelino", "pt": "dinar argelino",
"ro": "Dinar algerian", "ro": "dinar algerian",
"ru": "алжирский динар", "ru": "алжирский динар",
"sk": "Alžírský dinár", "sk": "Alžírský dinár",
"sl": "alžirski dinar", "sl": "alžirski dinar",
@@ -1786,7 +1786,7 @@
"cy": "Ewro", "cy": "Ewro",
"da": "Euro", "da": "Euro",
"de": "Euro", "de": "Euro",
"en": "Euro", "en": "euro",
"eo": "eŭro", "eo": "eŭro",
"es": "Euro", "es": "Euro",
"et": "Euro", "et": "Euro",
@@ -1803,7 +1803,7 @@
"it": "Euro", "it": "Euro",
"ja": "ユーロ", "ja": "ユーロ",
"ko": "유로", "ko": "유로",
"lt": "Euras", "lt": "euras",
"lv": "eiro", "lv": "eiro",
"ml": "യൂറോ", "ml": "യൂറോ",
"ms": "Euro", "ms": "Euro",
@@ -2759,7 +2759,7 @@
"pa": "ਜਪਾਨੀ ਯੈੱਨ", "pa": "ਜਪਾਨੀ ਯੈੱਨ",
"pl": "jen", "pl": "jen",
"pt": "iene", "pt": "iene",
"ro": "yeni", "ro": "yen",
"ru": "японская иена", "ru": "японская иена",
"sk": "jen", "sk": "jen",
"sl": "japonski jen", "sl": "japonski jen",
@@ -3337,6 +3337,7 @@
"fi": "Libyan dinaari", "fi": "Libyan dinaari",
"fr": "dinar libyen", "fr": "dinar libyen",
"ga": "dinar na Libia", "ga": "dinar na Libia",
"gl": "dinar libio",
"he": "דינר לובי ", "he": "דינר לובי ",
"hr": "Libijski dinar", "hr": "Libijski dinar",
"hu": "líbiai dinár", "hu": "líbiai dinár",
@@ -3539,6 +3540,7 @@
"ja": "チャット", "ja": "チャット",
"ko": "미얀마 짯", "ko": "미얀마 짯",
"lt": "Kijatas", "lt": "Kijatas",
"lv": "Kjats",
"ml": "ബർമ്മീസ് ക്യാറ്റ്", "ml": "ബർമ്മീസ് ക്യാറ്റ്",
"nl": "Myanmarese kyat", "nl": "Myanmarese kyat",
"oc": "Kyat", "oc": "Kyat",
@@ -4310,7 +4312,7 @@
"ar": "بيسو فلبيني", "ar": "بيسو فلبيني",
"bg": "Филипинско песо", "bg": "Филипинско песо",
"ca": "peso filipí", "ca": "peso filipí",
"cs": "Filipínské peso", "cs": "filipínské peso",
"de": "philippinischer Peso", "de": "philippinischer Peso",
"en": "Philippine peso", "en": "Philippine peso",
"eo": "filipina peso", "eo": "filipina peso",
@@ -4614,7 +4616,7 @@
"fi": "Serbian dinaari", "fi": "Serbian dinaari",
"fr": "dinar serbe", "fr": "dinar serbe",
"ga": "Dinar na Seirbia", "ga": "Dinar na Seirbia",
"gl": "Dinar serbio", "gl": "dinar serbio",
"he": "דינר סרבי", "he": "דינר סרבי",
"hr": "srpski dinar", "hr": "srpski dinar",
"hu": "szerb dinár", "hu": "szerb dinár",
@@ -5331,7 +5333,7 @@
"pa": "ਤਾਜਿਕਿਸਤਾਨੀ ਸੋਮੋਨੀ", "pa": "ਤਾਜਿਕਿਸਤਾਨੀ ਸੋਮੋਨੀ",
"pl": "Somoni", "pl": "Somoni",
"pt": "Somoni", "pt": "Somoni",
"ro": "Somoni tadjic", "ro": "somoni tadjic",
"ru": "таджикский сомони", "ru": "таджикский сомони",
"sk": "tadžický som", "sk": "tadžický som",
"sl": "tadžikistanski somoni", "sl": "tadžikistanski somoni",
@@ -5394,6 +5396,7 @@
"fi": "Tunisian dinaari", "fi": "Tunisian dinaari",
"fr": "dinar tunisien", "fr": "dinar tunisien",
"ga": "dinar na Túinéise", "ga": "dinar na Túinéise",
"gl": "dinar tunisiano",
"he": "דינר תוניסאי", "he": "דינר תוניסאי",
"hr": "tuniski dinar", "hr": "tuniski dinar",
"hu": "tunéziai dinár", "hu": "tunéziai dinár",
@@ -5766,7 +5769,7 @@
"fi": "Uruguayn peso", "fi": "Uruguayn peso",
"fr": "peso uruguayen", "fr": "peso uruguayen",
"ga": "peso Uragua", "ga": "peso Uragua",
"gl": "Peso uruguaio", "gl": "peso uruguaio",
"he": "פסו של אורוגוואי", "he": "פסו של אורוגוואי",
"hr": "Urugvajski pezo", "hr": "Urugvajski pezo",
"hu": "uruguayi peso", "hu": "uruguayi peso",
@@ -6147,7 +6150,7 @@
"oc": "Drechs de tiratge Especials", "oc": "Drechs de tiratge Especials",
"pl": "specjalne prawa ciągnienia", "pl": "specjalne prawa ciągnienia",
"pt": "direitos especiais de saque", "pt": "direitos especiais de saque",
"ro": "Drepturi speciale de tragere", "ro": "drepturi speciale de tragere",
"ru": "специальные права заимствования", "ru": "специальные права заимствования",
"sk": "Zvláštne práva čerpania", "sk": "Zvláštne práva čerpania",
"sl": "posebne pravice črpanja", "sl": "posebne pravice črpanja",
@@ -6225,6 +6228,7 @@
"ja": "CFPフラン", "ja": "CFPフラン",
"ko": "CFP 프랑", "ko": "CFP 프랑",
"lt": "CFP frankas", "lt": "CFP frankas",
"lv": "Klusā okeāna franks",
"ms": "Franc CFP", "ms": "Franc CFP",
"nl": "CFP-frank", "nl": "CFP-frank",
"oc": "Franc CFP", "oc": "Franc CFP",
@@ -7055,6 +7059,7 @@
"bolivjano": "BOB", "bolivjano": "BOB",
"bolivya bolivianosu": "BOB", "bolivya bolivianosu": "BOB",
"bolivya bolivyanosu": "BOB", "bolivya bolivyanosu": "BOB",
"boliwiano": "BOB",
"bolívar digital": "VED", "bolívar digital": "VED",
"bolívar soberano": "VES", "bolívar soberano": "VES",
"bolívar sobirà": "VES", "bolívar sobirà": "VES",
@@ -8425,6 +8430,7 @@
"drame arménio": "AMD", "drame arménio": "AMD",
"dramm": "AMD", "dramm": "AMD",
"drechs de tiratge especials": "XDR", "drechs de tiratge especials": "XDR",
"drept special de tragere": "XDR",
"drepturi speciale de tragere": "XDR", "drepturi speciale de tragere": "XDR",
"drets especials de gir": "XDR", "drets especials de gir": "XDR",
"droits de tirage speciaux": "XDR", "droits de tirage speciaux": "XDR",
@@ -9518,6 +9524,8 @@
"kíp lào": "LAK", "kíp lào": "LAK",
"kīp": "LAK", "kīp": "LAK",
"kjat": "MMK", "kjat": "MMK",
"kjats": "MMK",
"klusā okeāna franks": "XPF",
"km": "BAM", "km": "BAM",
"kmf": "KMF", "kmf": "KMF",
"koeweitse dinar": "KWD", "koeweitse dinar": "KWD",
@@ -10023,6 +10031,7 @@
"lire sterline": "GBP", "lire sterline": "GBP",
"lire turque": "TRY", "lire turque": "TRY",
"lisente": "LSL", "lisente": "LSL",
"list of syrian coins": "SYP",
"liura de gibartar": "GIP", "liura de gibartar": "GIP",
"liura egipciana": "EGP", "liura egipciana": "EGP",
"liura esterlina": "GBP", "liura esterlina": "GBP",
@@ -11313,7 +11322,6 @@
"riel camboxano": "KHR", "riel camboxano": "KHR",
"riel camboyano": "KHR", "riel camboyano": "KHR",
"riel campuchia": "KHR", "riel campuchia": "KHR",
"riel kambodżański": "KHR",
"riel kamboja": "KHR", "riel kamboja": "KHR",
"riel na cambóide": "KHR", "riel na cambóide": "KHR",
"rietumāfrikas franks": "XOF", "rietumāfrikas franks": "XOF",
@@ -12257,6 +12265,7 @@
"thaise baht": "THB", "thaise baht": "THB",
"thajský baht": "THB", "thajský baht": "THB",
"thb": "THB", "thb": "THB",
"the australian dollar": "AUD",
"thebe": "BWP", "thebe": "BWP",
"third belarusian ruble": "BYN", "third belarusian ruble": "BYN",
"tical": "THB", "tical": "THB",
@@ -12740,6 +12749,7 @@
"yen": "JPY", "yen": "JPY",
"yen giapponese": "JPY", "yen giapponese": "JPY",
"yen japones": "JPY", "yen japones": "JPY",
"yen japonez": "JPY",
"yen japonés": "JPY", "yen japonés": "JPY",
"yeni": "JPY", "yeni": "JPY",
"yeni i̇srail şekeli": "ILS", "yeni i̇srail şekeli": "ILS",
@@ -12806,7 +12816,6 @@
"zimbabwe zig": "ZWG", "zimbabwe zig": "ZWG",
"zimbabwean dollar": "ZWL", "zimbabwean dollar": "ZWL",
"zimbabwean gold": "ZWG", "zimbabwean gold": "ZWG",
"zimbabwean zig": "ZWG",
"zimbabwen kulta": "ZWG", "zimbabwen kulta": "ZWG",
"zimbabwiansky zlatý": "ZWG", "zimbabwiansky zlatý": "ZWG",
"zimbabwský dolar": "ZWL", "zimbabwský dolar": "ZWL",
@@ -12965,6 +12974,7 @@
"FKP", "FKP",
"EGP" "EGP"
], ],
"£S": "SYP",
"£e": "EGP", "£e": "EGP",
"£s": "SYP", "£s": "SYP",
"¥": [ "¥": [
@@ -14959,6 +14969,8 @@
"فورنت مجري": "HUF", "فورنت مجري": "HUF",
"فورينت مجري": "HUF", "فورينت مجري": "HUF",
"فِرَنْكٌ رُوَنْدِيٌّ": "RWF", "فِرَنْكٌ رُوَنْدِيٌّ": "RWF",
"قائمة النقود المعدنية السورية": "SYP",
"قائمة عملات سوريا المعدنية": "SYP",
"ك": "KWD", "ك": "KWD",
"كتزال غواتيمالي": "GTQ", "كتزال غواتيمالي": "GTQ",
"كرونة آيسلندية": "ISK", "كرونة آيسلندية": "ISK",

View File

@@ -6544,6 +6544,103 @@
"to-TO": "to" "to-TO": "to"
} }
}, },
"neosearch": {
"all_locale": null,
"custom": {
"countrycodes": [
"AE",
"AM",
"AR",
"AT",
"AU",
"AZ",
"BE",
"BG",
"BH",
"BO",
"BR",
"BY",
"CA",
"CH",
"CL",
"CN",
"CO",
"CY",
"CZ",
"DE",
"DK",
"DZ",
"EC",
"EE",
"EG",
"ES",
"FI",
"FR",
"GB",
"GE",
"GH",
"GR",
"HK",
"HR",
"HU",
"ID",
"IE",
"IL",
"IN",
"IT",
"JO",
"JP",
"KE",
"KG",
"KR",
"KW",
"KZ",
"LB",
"LT",
"LU",
"LV",
"MA",
"MD",
"MT",
"MX",
"MY",
"NG",
"NL",
"NO",
"NZ",
"OM",
"PE",
"PH",
"PL",
"PT",
"PY",
"QA",
"RO",
"RU",
"SA",
"SE",
"SG",
"SI",
"SK",
"TH",
"TJ",
"TM",
"TN",
"TR",
"TW",
"UA",
"US",
"UY",
"UZ",
"VE",
"VN",
"ZA"
]
},
"data_type": "traits_v1",
"languages": {},
"regions": {}
},
"odysee": { "odysee": {
"all_locale": null, "all_locale": null,
"custom": {}, "custom": {},
@@ -9585,4 +9682,4 @@
}, },
"regions": {} "regions": {}
} }
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3272,7 +3272,7 @@
"Q128822": { "Q128822": {
"si_name": "Q182429", "si_name": "Q182429",
"symbol": "kn", "symbol": "kn",
"to_si_factor": 0.5144444444444445 "to_si_factor": 0.514
}, },
"Q12912288": { "Q12912288": {
"si_name": null, "si_name": null,

View File

@@ -361,37 +361,43 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
https: socks5://proxy:port https: socks5://proxy:port
""" """
def setup(self, engine_settings: dict[str, t.Any]) -> bool: # pylint: disable=unused-argument def setup(self, engine_settings: dict[str, t.Any]) -> bool | None: # pylint: disable=unused-argument
"""Dynamic setup of the engine settings. """Dynamic setup of the engine settings.
With this method, the engine's setup is carried out. For example, to With this method, the engine's setup is carried out. For example, to
check or dynamically adapt the values handed over in the parameter check or dynamically adapt the values handed over in the parameter
``engine_settings``. The return value (True/False) indicates whether ``engine_settings``.
the setup was successful and the engine can be built or rejected.
The method is optional and is called synchronously as part of the Whether the initialization was successful can be indicated by the return
value ``True`` or even ``False``.
- If no return value (``None`` ) is given from this method , this is
equivalent to ``True``.
- If an exception is thrown as part of the initialization, this is
equivalent to ``False``.
The method is optional and is called **synchronously** as part of the
initialization of the service and is therefore only suitable for simple initialization of the service and is therefore only suitable for simple
(local) exams/changes at the engine setting. The :py:obj:`Engine.init` (local) exams/changes at the engine setting.
method must be used for longer tasks in which values of a remote must be
determined, for example. The :py:obj:`Engine.init` method must be used for longer tasks in which
values of a remote must be determined, for example.
""" """
return True return True
def init(self, engine_settings: dict[str, t.Any]) -> bool | None: # pylint: disable=unused-argument def init(self, engine_settings: dict[str, t.Any]) -> bool | None: # pylint: disable=unused-argument
"""Initialization of the engine. """Initialization of the engine.
The method is optional and asynchronous (in a thread). It is suitable, The method is optional and called **asynchronous** (in a thread). The
for example, for setting up a cache (for the engine) or for querying method is comparable to :py:obj:`Engine.setup`, it is suitable, for
values (required by the engine) from a remote. caching data that first needs to be requested from a remote.
Whether the initialization was successful can be indicated by the return The method is optional and runs **asynchronously** (in a thread), it is
value ``True`` or even ``False``. comparable to :py:obj:`Engine.setup`. For instance, it is suitable for
caching data that first needs to be requested from a remote source.
- If no return value is given from this init method (``None``), this is The evaluation of the return value is analogous to :py:obj:`Engine.setup`.
equivalent to ``True``.
- If an exception is thrown as part of the initialization, this is
equivalent to ``False``.
""" """
return True return True

View File

@@ -269,21 +269,27 @@ def is_engine_active(engine: "Engine | types.ModuleType"):
def call_engine_setup(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]) -> bool: def call_engine_setup(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]) -> bool:
setup_ok = False
setup_ok: bool | None = False
setup_func = getattr(engine, "setup", None) setup_func = getattr(engine, "setup", None)
if setup_func is None: if setup_func is None:
setup_ok = True setup_ok = True
elif not callable(setup_func): elif not callable(setup_func):
logger.error("engine's setup method isn't a callable (is of type: %s)", type(setup_func)) logger.error(f"engine's setup method isn't a callable (is of type: {type(setup_func)})")
else: else:
try: try:
setup_ok = engine.setup(engine_data) setup_ok = engine.setup(engine_data)
except Exception as e: # pylint: disable=broad-except except Exception as e: # pylint: disable=broad-except
logger.exception('exception : {0}'.format(e)) logger.exception(f"(PID {os.getpid()}) {engine.name}: engine SETUP failed, exception: {e}")
setup_ok = False
# The evaluation of the return value is analogous to Engine.init
if setup_ok is None:
setup_ok = True
if not setup_ok: if not setup_ok:
logger.error("%s: Engine setup was not successful, engine is set to inactive.", engine.name) logger.error(f"(PID {os.getpid()}) {engine.name}: engine setup was not successful")
return setup_ok return setup_ok
@@ -311,14 +317,16 @@ def load_engines(engine_list: list[dict[str, t.Any]]):
for engine_data in engine_list: for engine_data in engine_list:
if engine_data.get("inactive") is True: if engine_data.get("inactive") is True:
continue continue
engine = load_engine(engine_data) engine = load_engine(engine_data)
if engine: if engine:
register_engine(engine) register_engine(engine)
else: else:
# if an engine can't be loaded (if for example the engine is missing # if an engine can't be loaded (if for example the engine is missing
# tor or some other requirements) its set to inactive! # tor or some other requirements) its set to inactive!
logger.error( logger.error(
f"(PID {os.getpid()}) loading engine %s failed: set engine to inactive!", engine_data.get("name", "???") f"(PID {os.getpid()}) {engine_data.get('name', '???')}: can't register engine (loading engine failed)"
) )
engine_data["inactive"] = True engine_data["inactive"] = True
return engines return engines

View File

@@ -7,13 +7,17 @@
# There exits a https://github.com/ohblue/baidu-serp-api/ # There exits a https://github.com/ohblue/baidu-serp-api/
# but we don't use it here (may we can learn from). # but we don't use it here (may we can learn from).
import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from datetime import datetime from datetime import datetime
from html import unescape from html import unescape
import time import time
import json import json
from searx.exceptions import SearxEngineAPIException, SearxEngineCaptchaException from searx.exceptions import SearxEngineAPIException, SearxEngineCaptchaException, SearxEngineAccessDeniedException
from searx.enginelib import EngineCache
from searx.network import get as http_get
from searx.utils import html_to_text from searx.utils import html_to_text
about = { about = {
@@ -35,6 +39,31 @@ baidu_category = 'general'
time_range_support = True time_range_support = True
time_range_dict = {"day": 86400, "week": 604800, "month": 2592000, "year": 31536000} time_range_dict = {"day": 86400, "week": 604800, "month": 2592000, "year": 31536000}
image_base_url = "https://image.baidu.com/"
COOKIE_CACHE_KEY = "cookie"
COOKIE_CACHE_EXPIRATION_SECONDS = 3600
CACHE: EngineCache
"""Stores cookies from Baidu image search warmup."""
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return True
def get_image_cookies(headers: dict[str, str]) -> dict[str, str]:
cookies: dict[str, str] | None = CACHE.get(COOKIE_CACHE_KEY)
if cookies:
return cookies
warmup = http_get(image_base_url, headers=headers, timeout=10)
cookies = dict(warmup.cookies.items())
CACHE.set(key=COOKIE_CACHE_KEY, value=cookies, expire=COOKIE_CACHE_EXPIRATION_SECONDS)
return cookies
def init(_): def init(_):
if baidu_category not in ('general', 'images', 'it'): if baidu_category not in ('general', 'images', 'it'):
@@ -88,6 +117,9 @@ def request(query, params):
if baidu_category == 'it': if baidu_category == 'it':
query_params["paramList"] += f",timestamp_range={past}-{now}" query_params["paramList"] += f",timestamp_range={past}-{now}"
if baidu_category == 'images':
params["cookies"] = get_image_cookies(params["headers"])
params["url"] = f"{query_url}?{urlencode(query_params)}" params["url"] = f"{query_url}?{urlencode(query_params)}"
params["allow_redirects"] = False params["allow_redirects"] = False
return params return params
@@ -103,6 +135,8 @@ def response(resp):
# baidu's JSON encoder wrongly quotes / and ' characters by \\ and \' # baidu's JSON encoder wrongly quotes / and ' characters by \\ and \'
text = text.replace(r"\/", "/").replace(r"\'", "'") text = text.replace(r"\/", "/").replace(r"\'", "'")
data = json.loads(text, strict=False) data = json.loads(text, strict=False)
if data.get("antiFlag") == 1:
raise SearxEngineAccessDeniedException(data.get("message", "Forbid spider access"))
parsers = {'general': parse_general, 'images': parse_images, 'it': parse_it} parsers = {'general': parse_general, 'images': parse_images, 'it': parse_it}
return parsers[baidu_category](data) return parsers[baidu_category](data)

View File

@@ -8,6 +8,7 @@ import random
import string import string
from urllib.parse import urlencode from urllib.parse import urlencode
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from searx import utils from searx import utils
@@ -39,6 +40,32 @@ cookie = {
"home_feed_column": "4", "home_feed_column": "4",
} }
_CN_TZ = ZoneInfo("Asia/Shanghai")
# Calendar-day time filter (Asia/Shanghai); dict values are days to subtract from today.
time_range_support = True
time_range_dict = {"day": 0, "week": 6, "month": 29, "year": 364}
def _pubtime_range(time_range: str) -> tuple[int, int]:
"""Return ``(pubtime_begin_s, pubtime_end_s)`` for Bilibili's search API.
Time ranges follow Bilibili's website semantics: they are counted in
**calendar days** in China Standard Time (``Asia/Shanghai``), not as
sliding 24-hour windows. For example, ``day`` means from 00:00:00 to
23:59:59 of the current local day; ``week`` spans from 00:00:00 on the
calendar day six days ago through the end of today, and so on.
The returned Unix timestamps (seconds) map to Bilibili's
``pubtime_begin_s`` and ``pubtime_end_s`` query parameters.
"""
now = datetime.now(_CN_TZ)
pubtime_end_s = int(now.replace(hour=23, minute=59, second=59, microsecond=0).timestamp())
begin_day = now - timedelta(days=time_range_dict[time_range])
pubtime_begin_s = int(begin_day.replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
return pubtime_begin_s, pubtime_end_s
def request(query, params): def request(query, params):
query_params = { query_params = {
@@ -50,6 +77,11 @@ def request(query, params):
"search_type": "video", "search_type": "video",
} }
if params.get("time_range") in time_range_dict:
pubtime_begin_s, pubtime_end_s = _pubtime_range(params["time_range"])
query_params["pubtime_begin_s"] = pubtime_begin_s
query_params["pubtime_end_s"] = pubtime_end_s
params["url"] = f"{base_url}?{urlencode(query_params)}" params["url"] = f"{base_url}?{urlencode(query_params)}"
params["headers"]["Referer"] = "https://www.bilibili.com/" params["headers"]["Referer"] = "https://www.bilibili.com/"
params["headers"]["Accept"] = "application/json, text/javascript, */*; q=0.01" params["headers"]["Accept"] = "application/json, text/javascript, */*; q=0.01"

View File

@@ -13,9 +13,13 @@ It seems to use Bing internally, as the image thumbnails are loaded from Bing.
from urllib.parse import urlencode from urllib.parse import urlencode
import typing as t import typing as t
from lxml import html
from searx.exceptions import SearxEngineAccessDeniedException from searx.enginelib import EngineCache
from searx.network import get
from searx.exceptions import SearxEngineAPIException, SearxEngineAccessDeniedException
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.utils import eval_xpath, extract_text, gen_useragent
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -38,14 +42,45 @@ heexy_categ = "web"
"""Category to search in. Can be either "web" or "image".""" """Category to search in. Can be either "web" or "image"."""
base_url = "https://seapi.heexy.org" base_url = "https://heexy.org"
api_url = "https://seapi.heexy.org"
safe_search_map = {0: "off", 1: "on", 2: "on"} 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
def init(_):
if heexy_categ not in ("web", "image"): if heexy_categ not in ("web", "image"):
raise ValueError("invalid search category: %s" % heexy_categ) 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()})
if not resp.ok:
raise SearxEngineAPIException("failed to obtain request token: invalid response code")
doc = html.fromstring(resp.text)
token = extract_text(eval_xpath(doc, "//html/@data-cacheft"))
if not token:
raise SearxEngineAPIException("failed to obtain request token: no token found")
CACHE.set("token", token)
return token
def request(query: str, params: "OnlineParams") -> None: def request(query: str, params: "OnlineParams") -> None:
args = { args = {
@@ -56,8 +91,9 @@ def request(query: str, params: "OnlineParams") -> None:
if params["searxng_locale"] != "all": if params["searxng_locale"] != "all":
args["lang"] = params["searxng_locale"].split("-")[0] args["lang"] = params["searxng_locale"].split("-")[0]
params["url"] = f"{base_url}/search/{heexy_categ}?{urlencode(args)}" params["url"] = f"{api_url}/search/{heexy_categ}?{urlencode(args)}"
params["headers"]["Origin"] = base_url params["headers"]["X-Data-Cacheft"] = _get_api_token(query)
params["headers"]["Origin"] = api_url
def response(resp: "SXNG_Response"): def response(resp: "SXNG_Response"):

62
searx/engines/magnific.py Normal file
View File

@@ -0,0 +1,62 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Magnific_ is a database for images.
.. _Magnific: https://www.magnific.com
"""
from urllib.parse import urlencode
import typing as t
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.magnific.com",
"wikidata_id": "Q104211654",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://www.magnific.com"
categories = ["images"]
paging = True
free_images_only = True
"""
Whether to only load images that may be used for free, without a Magnific account.
"""
def request(query: str, params: "OnlineParams") -> None:
args = {"term": query, "filters[ai-generated][excluded]": 1, "page": params["pageno"], "locale": "en"}
if free_images_only:
args["filters[license]"] = "free"
params["headers"]["Referer"] = f"{base_url}/search"
params["url"] = f"{base_url}/api/regular/search?{urlencode(args)}"
def response(resp: "SXNG_Response"):
res = EngineResults()
result: dict[str, t.Any] # TBH: dict[str, t.Any]
for result in resp.json()["items"]:
res.add(
res.types.Image(
title=result["name"],
url=result["url"],
thumbnail_src=result["preview"]["url"],
img_src=result["preview"]["url"],
resolution=f"{result['preview']['width']}x{result['preview']['height']}",
)
)
return res

View File

@@ -99,23 +99,35 @@ def parse_general(data):
dom = html.fromstring(data) dom = html.fromstring(data)
for item in eval_xpath_list(dom, "//ul[contains(@class, 'lst_total')]/li[contains(@class, 'bx')]"): for item in eval_xpath_list(dom, "//div[contains(@class, 'fds-web-normal-doc-root')]"):
thumbnail = None thumbnail = extract_text(
eval_xpath(
item,
".//div[contains(@class, 'sds-comps-image') and not(contains(@class, 'sds-comps-image-circle'))]/img/@src",
)
)
title = extract_text(eval_xpath(item, ".//span[contains(@class, 'sds-comps-text-type-headline1')]"))
url = None
try: try:
thumbnail = eval_xpath_getindex(item, ".//div[contains(@class, 'thumb_single')]//img/@data-lazysrc", 0) url = eval_xpath_getindex(
item, ".//a[starts-with(@href, 'http') and not(contains(@href, 'keep.naver.com'))]/@href", 0
)
except (ValueError, TypeError, SearxEngineXPathException): except (ValueError, TypeError, SearxEngineXPathException):
pass pass
results.add( content = extract_text(eval_xpath(item, ".//span[contains(@class, 'sds-comps-text-type-body1')]"))
MainResult(
title=extract_text(eval_xpath(item, ".//a[contains(@class, 'link_tit')]")), if title and url:
url=eval_xpath_getindex(item, ".//a[contains(@class, 'link_tit')]/@href", 0), results.add(
content=extract_text( MainResult(
eval_xpath(item, ".//div[contains(@class, 'total_dsc_wrap')]//a[contains(@class, 'api_txt_lines')]") title=title,
), url=url,
thumbnail=thumbnail, content=content or "",
thumbnail=thumbnail or "",
)
) )
)
return results return results
@@ -173,7 +185,7 @@ def parse_news(data):
title=title, title=title,
url=url, url=url,
content=content, content=content,
thumbnail=thumbnail, thumbnail=thumbnail or "",
) )
) )
@@ -196,7 +208,7 @@ def parse_videos(data):
length = None length = None
try: try:
length = parse_duration_string(extract_text(eval_xpath(item, ".//span[contains(@class, 'time')]"))) length = parse_duration_string(extract_text(eval_xpath(item, ".//span[contains(@class, 'time')]")) or "")
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass

View File

@@ -0,0 +1,92 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Neosearch_ aims to be a privacy-first alternative to Google.
.. _Neosearch: https://neosearch.org/About
"""
from json import loads
import typing as t
from urllib.parse import urlencode
from searx.extended_types import SXNG_Response
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.enginelib.traits import EngineTraits
from searx.search.processors import OnlineParams
traits: EngineTraits
about = {
"website": "https://neosearch.org",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://neosearch.org"
categories = ["general"]
paging = False
def request(query: str, params: "OnlineParams"):
args = {"q": query, "generate": "auto"}
countrycode = params["searxng_locale"].split("-")[-1].upper()
if countrycode in traits.custom["countrycodes"]:
args["loc"] = countrycode
params["url"] = f"{base_url}/search?{urlencode(args)}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
# first line contains something like `{"location": "de"}`
# second line contains the actual results
json_resp = loads(resp.text.splitlines()[-1])
for lens in json_resp["lenses"]:
for category in lens["categories"]:
for result in category["links"]:
if not result["url"]:
continue
res.add(
res.types.MainResult(
url=result["url"],
title=result["title"],
content=result["snippet"] or result["description"],
)
)
for suggestion in json_resp.get("suggestions", []):
res.add(res.types.LegacyResult(suggestion=suggestion))
return res
def fetch_traits(engine_traits: "EngineTraits") -> None:
# pylint: disable=import-outside-toplevel
from searx.network import get
from searx.utils import extr, js_obj_str_to_python
from babel.core import get_global
resp = get(base_url)
locations_js_raw = extr(resp.text, "const LOCATIONS = ", ";")
if not locations_js_raw:
raise RuntimeError("failed to find locations in neosearch HTML response")
locations: list[dict[str, str]] = js_obj_str_to_python(locations_js_raw)
babel_reg_list = get_global("territory_languages").keys()
countrycodes: list[str] = []
for loc in locations:
_reg = loc["code"].upper()
if _reg not in babel_reg_list:
print(f"ERROR: region tag {_reg} is unknown by babel")
continue
countrycodes.append(_reg)
countrycodes.sort()
engine_traits.custom["countrycodes"] = countrycodes

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

@@ -0,0 +1,63 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Picjumbo_ provides free stock photos.
.. _Picjumbo: https://picjumbo.com
"""
from urllib.parse import urlparse, urlunparse
import typing as t
from lxml import html
from searx.result_types import EngineResults
from searx.utils import eval_xpath, eval_xpath_list, extract_text
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://picjumbo.com",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
base_url = "https://picjumbo.com"
categories = ["images"]
paging = True
def request(query: str, params: "OnlineParams") -> None:
params["url"] = f"{base_url}/search/{query}/page/{params['pageno']}"
def _get_max_res_url(url: str) -> str:
"""Get the maximum resolution of the image based on the thumbnail URL."""
parsed_url = urlparse(url)
max_res_url = parsed_url._replace(query="w=10000&quality=100")
return urlunparse(max_res_url)
def response(resp: "SXNG_Response"):
res = EngineResults()
doc = html.fromstring(resp.text)
for result in eval_xpath_list(doc, "//div[contains(@class, 'photo_query')]/div[contains(@class, 'photo_item')]"):
thumbnail = extract_text(eval_xpath(result, ".//img[contains(@class, 'image')]/@src")) or ""
res.add(
res.types.Image(
url=extract_text(eval_xpath(result, ".//a[contains(@class, 'image')]/@href")) or "",
title=extract_text(eval_xpath(result, ".//h3")) or "",
content=extract_text(eval_xpath(result, ".//meta[@itemprop='keywords']/@content")) or "",
thumbnail_src=thumbnail,
img_src=_get_max_res_url(thumbnail),
)
)
return res

View File

@@ -38,6 +38,7 @@ Implementations
""" """
import random
import typing as t import typing as t
from datetime import ( from datetime import (
@@ -89,6 +90,11 @@ qwant_categ: str = None # pyright: ignore[reportAssignmentType]
safesearch = True safesearch = True
# tgp seems to be short for "test group" - its actual value doesn't matter, as
# long as it's sent and at the correct position in the query params and doesn't
# change too frequently
test_group_value = random.randint(1, 3)
# fmt: off # fmt: off
qwant_news_locales = [ qwant_news_locales = [
"ca_ad", "ca_es", "ca_fr", "co_fr", "de_at", "de_ch", "de_de", "en_au", "ca_ad", "ca_es", "ca_fr", "co_fr", "de_at", "de_ch", "de_de", "en_au",
@@ -114,17 +120,24 @@ def request(query: str, params: "OnlineParams") -> None:
results_per_page = 10 results_per_page = 10
if qwant_categ == "images": if qwant_categ == "images":
results_per_page = 50 results_per_page = 50
args = { args = {
"q": query, "q": query,
"count": results_per_page, "count": results_per_page,
"locale": q_locale, "locale": q_locale,
"offset": (params["pageno"] - 1) * results_per_page, "offset": (params["pageno"] - 1) * results_per_page,
"tgp": test_group_value,
"device": "desktop", "device": "desktop",
"safesearch": params["safesearch"], "safesearch": params["safesearch"],
"tgp": 1,
"display": True, "display": True,
"llm": True, "llm": True,
} }
# shuffle query parameters to be harder to fingerprint
args = list(args.items())
random.shuffle(args)
args = dict(args)
params["raise_for_httperror"] = False params["raise_for_httperror"] = False
params["url"] = f"{api_url}{qwant_categ}?{urlencode(args)}" params["url"] = f"{api_url}{qwant_categ}?{urlencode(args)}"
@@ -190,7 +203,6 @@ def response(resp: "SXNG_Response") -> EngineResults:
mainline_items: list[dict[str, t.Any]] = row.get("items", []) mainline_items: list[dict[str, t.Any]] = row.get("items", [])
for item in mainline_items: for item in mainline_items:
title: str = item.get("title", "") title: str = item.get("title", "")
res_url: str = item.get("url", "") res_url: str = item.get("url", "")
pub_date: datetime | None = None pub_date: datetime | None = None

View File

@@ -101,7 +101,7 @@ def _image_results(doc: "ElementBase") -> EngineResults:
res.types.Image( res.types.Image(
url=extract_text(eval_xpath(result, "./@href")) or "", url=extract_text(eval_xpath(result, "./@href")) or "",
title=extract_text(eval_xpath(result, "./img/@alt")) or "", title=extract_text(eval_xpath(result, "./img/@alt")) or "",
thumbnail_src=extract_text(eval_xpath(result, "./img/@src")) or "", img_src=extract_text(eval_xpath(result, "./img/@src")) or "",
), ),
) )
) )

View File

@@ -0,0 +1,96 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""SearchZee is a small, indie project, web and news results pulled from
independent search infrastructure."""
import typing as t
from urllib.parse import urlencode
from searx.exceptions import SearxEngineAPIException
from searx.extended_types import SXNG_Response
from searx.network import get
from searx.result_types import EngineResults
from searx.utils import extr, html_to_text
from searx.enginelib import EngineCache
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
about = {
"website": "https://searchzee.com",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
"description": (
"SearchZee is a small, indie project, the web and news results"
" are pulled from an independent search infrastructure."
),
}
categories: list[str] = None # type: ignore[reportAssignmentType]
paging = True
SearchzeeCategType = t.Literal["web", "news"]
searchzee_categ: SearchzeeCategType = None # type: ignore[reportAssignmentType]
CACHE: EngineCache
"""Cache for storing the scraped API Token."""
base_url = "https://searchzee.com"
# only supports for news
time_range_map = {"day": "pd", "week": "pw", "month": "pm", "year": "py"}
def setup(engine_settings: dict[str, t.Any]) -> bool:
if searchzee_categ not in t.get_args(SearchzeeCategType):
raise ValueError("invalid category: %s" % searchzee_categ)
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"]) # type: ignore[reportAny]
return True
def _obtain_api_token() -> str:
token: str | None = CACHE.get("token") # type: ignore[reportAny]
if token:
return token
token_resp = get(
f"{base_url}/app.js",
)
if not token_resp.ok:
raise SearxEngineAPIException("failed to obtain api key")
token = extr(token_resp.text, "const SEARCHZEE_API_TOKEN = \"", "\";")
CACHE.set("token", token, expire=3600)
return token
def request(query: str, params: "OnlineParams"):
params["headers"]["X-SearchZee-Token"] = _obtain_api_token()
args = {"q": query, "type": searchzee_categ, "offset": params["pageno"] - 1}
if params["time_range"]:
args["freshness"] = time_range_map[params["time_range"]]
params["url"] = f"{base_url}/api/search?{urlencode(args)}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
results: list[dict[str, str]] = resp.json()["results"] # type: ignore[reportAny]
for result in results:
res.add(
res.types.MainResult(
url=result["url"],
title=html_to_text(result["title"]),
content=html_to_text(result["summary"]),
thumbnail=result.get("thumbnail") or "",
)
)
return res

View File

@@ -0,0 +1,62 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Shopify stock photos provides royalty-free images, intended for use with
Shopify.
"""
import typing as t
from urllib.parse import urlencode
from lxml import html
from searx.result_types import EngineResults
from searx.utils import eval_xpath, eval_xpath_list, extract_text
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.shopify.com/stock-photos",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
base_url = "https://www.shopify.com"
categories = ["images"]
paging = True
def request(query: str, params: "OnlineParams") -> None:
args = {"q": query, "page": params["pageno"]}
params["url"] = f"{base_url}/stock-photos/photos/search?{urlencode(args)}"
def _get_download_url(url: str) -> str:
"""Get the link to the full quality image."""
query_start = url.find("?")
return url[:query_start] + "/download?quality=premium"
def response(resp: "SXNG_Response"):
res = EngineResults()
doc = html.fromstring(resp.text)
for result in eval_xpath_list(doc, "//div[contains(@class, 'js-masonry-grid')]/div"):
url = base_url + (extract_text(eval_xpath(result, ".//a[contains(@class, 'photo-tile')]/@href")) or "")
res.add(
res.types.Image(
url=url,
title=extract_text(eval_xpath(result, ".//p[contains(@class, 'photo-tile__title')]")) or "",
thumbnail_src=extract_text(eval_xpath(result, ".//img[contains(@class, 'photo-card__image')]/@src"))
or "",
img_src=_get_download_url(url),
)
)
return res

View File

@@ -0,0 +1,55 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Stocksnap_ is a search engine for CC0-licensed images.
.. _Stocksnap: https://stocksnap.io
"""
import typing as t
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://stocksnap.io",
"wikidata_id": None,
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
# otherwise all requests get blocked, probably HTTP2 fingerprinting
enable_http2 = False
base_url = "https://stocksnap.io"
cdn_url = "https://cdn.stocksnap.io"
categories = ["images"]
paging = True
def request(query: str, params: "OnlineParams") -> None:
params["url"] = f"{base_url}/api/search-photos/{query}/relevance/desc/{params['pageno']}"
def response(resp: "SXNG_Response"):
res = EngineResults()
result: dict[str, str] # TBH: dict[str, t.Any]
for result in resp.json()["results"]:
slug = "-".join(result['keywords'][:1]) + "-" + result["img_id"]
res.add(
res.types.Image(
title=result["tags"],
url=f"{base_url}/photo/{slug}",
thumbnail_src=f"{cdn_url}/img-thumbs/280h/{result['img_id']}.jpg",
img_src=f"{cdn_url}/img-thumbs/960w/{result['img_id']}.jpg",
img_format="JPEG",
resolution=f"{result['img_width']}x{result['img_height']}",
)
)
return res

View File

@@ -73,7 +73,7 @@ def _obtain_session_code() -> str:
if cached_session: if cached_session:
return cached_session return cached_session
results_page = get(f"{base_url}/_internCode.aspx") results_page = get(f"{base_url}/checkCode.aspx")
doc = html.fromstring(results_page.text) doc = html.fromstring(results_page.text)
extra_data: dict[str, str] = {} extra_data: dict[str, str] = {}
@@ -107,7 +107,7 @@ def _obtain_session_code() -> str:
} }
challenge_response = post( challenge_response = post(
f"{base_url}/_internCode.aspx", f"{base_url}/checkCode.aspx",
cookies=results_page.cookies, cookies=results_page.cookies,
data=data, data=data,
) )
@@ -125,7 +125,9 @@ def request(query: str, params: "OnlineParams"):
code = _obtain_session_code() code = _obtain_session_code()
args = {"w": query, "page": params["pageno"]} args = {"w": query, "page": params["pageno"]}
params["url"] = f"{base_url}/{tiger_category}?{urlencode(args)}" params["url"] = f"{base_url}/{tiger_category}?{urlencode(args)}"
params["cookies"]["Tiger.ch"] = f"Code={code}" # Setting Checked=1 shows related search terms / suggestions
# Language and country could be set with Lng= and Land= in the future
params["cookies"]["Tiger.ch"] = f"Tiger.ch=&Code={code}&Checked=1"
def response(resp: "SXNG_Response") -> EngineResults: def response(resp: "SXNG_Response") -> EngineResults:
@@ -144,6 +146,9 @@ def response(resp: "SXNG_Response") -> EngineResults:
content=extract_text(eval_xpath(result, ".//*[contains(@class, 'webbodynopic')]")) or "", content=extract_text(eval_xpath(result, ".//*[contains(@class, 'webbodynopic')]")) or "",
) )
) )
for suggestion in eval_xpath_list(doc, "//a[contains(@class, 'linkAnders')]"):
res.add(res.types.LegacyResult(suggestion=extract_text(suggestion)))
elif tiger_category == "News": elif tiger_category == "News":
for result in eval_xpath_list(doc, "//div[@id='panNews']/div"): for result in eval_xpath_list(doc, "//div[@id='panNews']/div"):
publishedDate = None publishedDate = None

View File

@@ -16,6 +16,7 @@ __all__ = [
import typing as t import typing as t
import os
from searx import logger from searx import logger
from searx import engines from searx import engines
@@ -92,7 +93,9 @@ class ProcessorMap(dict[str, EngineProcessor]):
self[eng_proc.engine.name] = eng_proc self[eng_proc.engine.name] = eng_proc
# logger.debug("registered engine processor: %s", eng_proc.engine.name) # logger.debug("registered engine processor: %s", eng_proc.engine.name)
else: else:
logger.error("can't register engine processor: %s (init failed)", eng_proc.engine.name) logger.error(
f"(PID {os.getpid()}) {eng_proc.engine.name}: can't register engines processor (init engine failed)"
)
return eng_proc_ok return eng_proc_ok

View File

@@ -150,19 +150,26 @@ class EngineProcessor(ABC):
threading.Thread(target=__init_processor_thread, daemon=True).start() threading.Thread(target=__init_processor_thread, daemon=True).start()
def init_engine(self) -> bool: def init_engine(self) -> bool:
eng_setting = get_engine_from_settings(self.engine.name) eng_setting = get_engine_from_settings(self.engine.name)
init_ok: bool | None = False init_ok: bool | None = False
try: try:
init_ok = self.engine.init(eng_setting) init_ok = self.engine.init(eng_setting)
except Exception: # pylint: disable=broad-except except Exception as e: # pylint: disable=broad-except
logger.exception( logger.exception(f"(PID {os.getpid()}) {self.engine.name}: engine INIT failed, exception: {e}")
f"(PID {os.getpid()}) Init method of engine %s failed due to an exception.", self.engine.name
)
init_ok = False init_ok = False
# In older engines, None is returned from the init method, which is # In older engines, None is returned from the init method, which is
# equivalent to indicating that the initialization was successful. # equivalent to indicating that the initialization was successful
# (compare: Engine.setup).
if init_ok is None: if init_ok is None:
init_ok = True init_ok = True
if not init_ok:
logger.error(f"(PID {os.getpid()}) {self.engine.name}: engine init was not successful")
return init_ok return init_ok
def handle_exception( def handle_exception(

View File

@@ -1585,6 +1585,11 @@ engines:
disabled: true disabled: true
inactive: true inactive: true
- name: magnific
engine: magnific
shortcut: mag
disabled: true
- name: marginalia - name: marginalia
engine: marginalia engine: marginalia
shortcut: mar shortcut: mar
@@ -1857,6 +1862,11 @@ engines:
engine: photon engine: photon
shortcut: ph shortcut: ph
- name: picjumbo
engine: picjumbo
shortcut: pj
disabled: true
- name: pinterest - name: pinterest
engine: pinterest engine: pinterest
shortcut: pin shortcut: pin
@@ -2165,6 +2175,11 @@ engines:
engine: sepiasearch engine: sepiasearch
shortcut: sep shortcut: sep
- name: shopify stock
engine: shopify_stock
shortcut: shs
disabled: true
- name: sogou - name: sogou
engine: sogou engine: sogou
shortcut: sogou shortcut: sogou
@@ -2195,6 +2210,11 @@ engines:
api_site: 'stackoverflow' api_site: 'stackoverflow'
categories: [it, q&a] categories: [it, q&a]
- name: stocksnap
engine: stocksnap
shortcut: sto
disabled: true
- name: askubuntu - name: askubuntu
engine: stackexchange engine: stackexchange
shortcut: ubuntu shortcut: ubuntu
@@ -2834,6 +2854,12 @@ engines:
shortcut: nvrv shortcut: nvrv
disabled: true disabled: true
- name: neosearch
engine: neosearch
shortcut: neo
disabled: true
inactive: true
- name: rubygems - name: rubygems
shortcut: rbg shortcut: rbg
engine: xpath engine: xpath
@@ -2963,44 +2989,17 @@ engines:
results: HTML results: HTML
- name: searchzee - name: searchzee
engine: json_engine engine: searchzee
search_url: https://searchzee.com/api/search?q={query}&type=web&offset={pageno}
paging: true
first_page_num: 0
results_query: results
url_query: url
title_query: title
content_query: summary
content_html_to_text: true
categories: general categories: general
searchzee_categ: web
shortcut: sz shortcut: sz
disabled: true disabled: true
inactive: true inactive: true
about:
website: https://searchzee.com
use_official_api: false
require_api_key: false
results: JSON
- name: searchzee news - name: searchzee news
engine: json_engine engine: searchzee
search_url: https://searchzee.com/api/search?q={query}&type=news&offset={pageno}{time_range}
paging: true
first_page_num: 0
time_range_support: true
time_range_url: "&freshness={time_range_val}"
time_range_map:
day: pd
week: pw
month: pm
year: py
results_query: results
url_query: url
title_query: title
content_query: summary
thumbnail_query: thumbnail
content_html_to_text: true
categories: news categories: news
searchzee_categ: news
shortcut: sznw shortcut: sznw
disabled: true disabled: true
inactive: true inactive: true
@@ -3285,6 +3284,36 @@ engines:
base_url: https://info.searchtoday.site base_url: https://info.searchtoday.site
disabled: true disabled: true
- name: sina
engine: json_engine
shortcut: sina
categories: news
paging: true
first_page_num: 1
search_url: https://search.sina.com.cn/api/search?q={query}&tp=mix&sort=0&page={pageno}&size=10&from=search_result
results_query: data/list
url_query: url
title_query: title
content_query: intro
thumbnail_query: thumb
title_html_to_text: true
time_range_map:
day: d
week: w
month: m
year: y
time_range_support: true
time_range_url: "&from=advanced_search&time={time_range_val}"
disabled: true
inactive: true
language: zh
about:
website: https://search.sina.com.cn
wikidata_id: Q938668
use_official_api: false
require_api_key: false
results: JSON
# - name: webcrawler # - name: webcrawler
# engine: s1search # engine: s1search
# shortcut: wc # shortcut: wc

View File

@@ -74,6 +74,8 @@ sxng_locales = (
('ja-JP', '日本語', '日本', 'Japanese', '\U0001f1ef\U0001f1f5'), ('ja-JP', '日本語', '日本', 'Japanese', '\U0001f1ef\U0001f1f5'),
('ko', '한국어', '', 'Korean', '\U0001f310'), ('ko', '한국어', '', 'Korean', '\U0001f310'),
('ko-KR', '한국어', '대한민국', 'Korean', '\U0001f1f0\U0001f1f7'), ('ko-KR', '한국어', '대한민국', 'Korean', '\U0001f1f0\U0001f1f7'),
('mi', 'Māori', '', 'Māori', '\U0001f310'),
('mi-NZ', 'Māori', 'Aotearoa', 'Māori', '\U0001f1f3\U0001f1ff'),
('nb', 'Norsk Bokmål', '', 'Norwegian Bokmål', '\U0001f310'), ('nb', 'Norsk Bokmål', '', 'Norwegian Bokmål', '\U0001f310'),
('nb-NO', 'Norsk Bokmål', 'Norge', 'Norwegian Bokmål', '\U0001f1f3\U0001f1f4'), ('nb-NO', 'Norsk Bokmål', 'Norge', 'Norwegian Bokmål', '\U0001f1f3\U0001f1f4'),
('nl', 'Nederlands', '', 'Dutch', '\U0001f310'), ('nl', 'Nederlands', '', 'Dutch', '\U0001f310'),

View File

@@ -33,22 +33,24 @@
# IcewindX <icewindx@noreply.codeberg.org>, 2025. # IcewindX <icewindx@noreply.codeberg.org>, 2025.
# 0ko <0ko@noreply.codeberg.org>, 2025. # 0ko <0ko@noreply.codeberg.org>, 2025.
# greatdng <greatdng@noreply.codeberg.org>, 2026. # greatdng <greatdng@noreply.codeberg.org>, 2026.
# code_gremlin <code_gremlin@noreply.codeberg.org>, 2026.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-06-13 11:31+0000\n" "POT-Creation-Date: 2026-06-13 11:31+0000\n"
"PO-Revision-Date: 2026-05-19 12:08+0000\n" "PO-Revision-Date: 2026-06-24 10:07+0000\n"
"Last-Translator: return42 <return42@noreply.codeberg.org>\n" "Last-Translator: code_gremlin <code_gremlin@noreply.codeberg.org>\n"
"Language-Team: Russian <https://translate.codeberg.org/projects/searxng/"
"searxng/ru/>\n"
"Language: ru\n" "Language: ru\n"
"Language-Team: Russian "
"<https://translate.codeberg.org/projects/searxng/searxng/ru/>\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) "
"|| (n%100>=11 && n%100<=14)? 2 : 3);\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || "
"(n%100>=11 && n%100<=14)? 2 : 3);\n"
"X-Generator: Weblate 2026.6.1\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING'] #. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -1667,11 +1669,11 @@ msgstr "Разрешение"
#: searx/templates/simple/result_templates/images.html:55 #: searx/templates/simple/result_templates/images.html:55
msgid "Image formats" msgid "Image formats"
msgstr "" msgstr "Форматы изображений"
#: searx/templates/simple/result_templates/images.html:56 #: searx/templates/simple/result_templates/images.html:56
msgid "original format" msgid "original format"
msgstr "" msgstr "формат оригинала"
#: searx/templates/simple/result_templates/images.html:63 #: searx/templates/simple/result_templates/images.html:63
msgid "Engine" msgid "Engine"
@@ -2441,4 +2443,3 @@ msgstr "скрыть видео"
#~ msgid "Format" #~ msgid "Format"
#~ msgstr "Формат" #~ msgstr "Формат"

View File

@@ -776,8 +776,11 @@ docs.live() {
pyenv.install pyenv.install
docs.prebuild docs.prebuild
# shellcheck disable=SC2086 # shellcheck disable=SC2086
PATH="${PY_ENV_BIN}:${PATH}" pyenv.cmd sphinx-autobuild \ SPHINX_AUTOBUILD_DEBUG=1 PATH="${PY_ENV_BIN}:${PATH}" pyenv.cmd sphinx-autobuild \
${SPHINX_VERBOSE} ${SPHINXOPTS} --open-browser --host 0.0.0.0 \ ${SPHINX_VERBOSE} ${SPHINXOPTS} \
--watch ./searx \
--re-ignore '(^|/)\.#|^(?!.*\.(rst|py|yml)$).*' \
--open-browser --host 0.0.0.0 \
-b html -c ./docs -d "${DOCS_BUILD}/.doctrees" ./docs "${DOCS_DIST}" -b html -c ./docs -d "${DOCS_BUILD}/.doctrees" ./docs "${DOCS_DIST}"
dump_return $? dump_return $?
} }