1 Commits

Author SHA1 Message Date
searxng-bot
b1fc49e357 [l10n] update translations from Weblate
493c1d2a7 - 2026-07-07 - lukisko <lukisko@noreply.codeberg.org>
a67b467e5 - 2026-07-06 - marc-lopez <marc-lopez@noreply.codeberg.org>
2026-07-10 08:12:43 +00:00
402 changed files with 10519 additions and 34029 deletions

View File

@@ -1,39 +0,0 @@
// Closes issues and prs whose authors/agents don't accept the ai policy
// https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
module.exports = async ({ github, context }) => {
const item = context.payload.pull_request || context.payload.issue;
const body = item.body || '';
const kind = context.payload.pull_request ? 'pull request' : 'issue';
// https://github.com/searxng/searxng/pull/6476#discussion_r3683782481
const hasBox = /\[[Xx]\].*AI Policy/.test(body);
const hasRef = /\[AI Policy\]:\s*https:\/\/github\.com\/searxng\/searxng\/.*AI_POLICY/.test(body);
if (hasBox && hasRef) {
return;
}
const { owner, repo } = context.repo;
await github.rest.issues.createComment({
owner,
repo,
issue_number: item.number,
body:
'Hello! Thank you for your contribution.\n\n' +
`Unfortunately your ${kind} was closed as the AI Policy has not been accepted.\n\n` +
`Please open a new ${kind} after confirming your contribution aligns with our AI Policy.`,
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: item.number,
labels: ['invalid:slop'],
});
await github.rest.issues.update({
owner,
repo,
issue_number: item.number,
state: 'closed',
state_reason: 'not_planned',
});
};

View File

@@ -1,38 +0,0 @@
---
# yamllint disable rule:line-length
name: AI Policy
# Closes any new issues and PRs from people (or agents) who don't accept the AI Policy
# yamllint disable-line rule:truthy
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
check:
name: Check AI Policy
# for issues with an author who has not contributed before
if: >-
github.event.sender.type != 'Bot' &&
contains(fromJSON('["NONE","FIRST_TIMER","FIRST_TIME_CONTRIBUTOR"]'),
github.event.issue.author_association ||
github.event.pull_request.author_association)
runs-on: ubuntu-26.04-arm
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: "false"
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const script = require('./.github/scripts/ai_policy.cjs');
await script({ github, context });

View File

@@ -25,21 +25,25 @@ env:
jobs: jobs:
build: build:
if: | if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
github.event_name == 'workflow_dispatch'
|| (github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success')
name: Build (${{ matrix.arch }}) name: Build (${{ matrix.arch }})
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- runner: ubuntu-26.04 - arch: amd64
arch: amd64 march: amd64
- runner: ubuntu-26.04-arm os: ubuntu-24.04
arch: arm64 emulation: false
- runner: ubuntu-26.04-arm - arch: arm64
arch: armv7 march: arm64
os: ubuntu-24.04-arm
emulation: false
- arch: armv7
march: arm64
os: ubuntu-24.04-arm
emulation: true
permissions: permissions:
packages: write packages: write
@@ -49,25 +53,33 @@ jobs:
git_url: ${{ steps.build.outputs.git_url }} git_url: ${{ steps.build.outputs.git_url }}
steps: steps:
- name: Login to GHCR # yamllint disable rule:line-length
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - name: Setup podman
with: env:
registry: "ghcr.io" PODMAN_VERSION: "v5.7.1"
username: "${{ github.repository_owner }}" run: |
password: "${{ secrets.GITHUB_TOKEN }}" sudo apt-get purge -y podman runc crun conmon
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz"
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz.asc"
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 0CCF102C4F95D89E583FF1D4F8B5AF50344BB503
gpg --batch --verify "podman-linux-${{ matrix.march }}.tar.gz.asc" "podman-linux-${{ matrix.march }}.tar.gz"
tar -xzf "podman-linux-${{ matrix.march }}.tar.gz"
sudo cp -rfv ./podman-linux-${{ matrix.march }}/etc/. /etc/
sudo cp -rfv ./podman-linux-${{ matrix.march }}/usr/. /usr/
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
# yamllint enable rule:line-length
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup QEMU
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
@@ -79,53 +91,71 @@ jobs:
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}- python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Get date
id: date
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
- name: Setup cache container - name: Setup cache container
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
key: "container-${{ matrix.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-
container-${{ matrix.arch }}- container-${{ matrix.arch }}-
path: "/var/tmp/buildah-cache-*/*" path: "/var/tmp/buildah-cache-*/*"
- name: Build - if: ${{ matrix.emulation }}
id: build name: Setup QEMU
env: uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
OVERRIDE_ARCH: "${{ matrix.arch }}"
run: make container.build
test:
name: Test (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
needs: build
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-26.04
arch: amd64
- runner: ubuntu-26.04-arm
arch: arm64
# FIXME: https://github.com/searxng/searxng/pull/6655#issuecomment-5550293085
# - runner: ubuntu-26.04-arm
# arch: armv7
steps:
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}" password: "${{ secrets.GITHUB_TOKEN }}"
- name: Setup QEMU - name: Build
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 id: build
env:
OVERRIDE_ARCH: "${{ matrix.arch }}"
run: make podman.build
test:
name: Test (${{ matrix.arch }})
runs-on: ${{ matrix.os }}
needs: build
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
os: ubuntu-24.04
emulation: false
- arch: arm64
os: ubuntu-24.04-arm
emulation: false
- arch: armv7
os: ubuntu-24.04-arm
emulation: true
steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"
- if: ${{ matrix.emulation }}
name: Setup QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Login to GHCR
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
with:
registry: "ghcr.io"
username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}"
- name: Test - name: Test
env: env:
OVERRIDE_ARCH: "${{ matrix.arch }}" OVERRIDE_ARCH: "${{ matrix.arch }}"
@@ -135,7 +165,7 @@ jobs:
release: release:
if: github.repository_owner == 'searxng' && github.ref_name == 'master' if: github.repository_owner == 'searxng' && github.ref_name == 'master'
name: Release name: Release
runs-on: ubuntu-26.04-arm runs-on: ubuntu-24.04-arm
needs: needs:
- build - build
- test - test
@@ -144,25 +174,24 @@ jobs:
packages: write packages: write
steps: steps:
- name: Login to Docker Hub - name: Checkout
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
registry: "docker.io" persist-credentials: "false"
username: "${{ secrets.DOCKER_USER }}"
password: "${{ secrets.DOCKER_TOKEN }}"
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}" password: "${{ secrets.GITHUB_TOKEN }}"
- name: Checkout - name: Login to Docker Hub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}" registry: "docker.io"
persist-credentials: "false" username: "${{ secrets.DOCKER_USER }}"
password: "${{ secrets.DOCKER_TOKEN }}"
- name: Release - name: Release
env: env:

View File

@@ -21,7 +21,7 @@ jobs:
data: data:
if: github.repository_owner == 'searxng' if: github.repository_owner == 'searxng'
name: ${{ matrix.fetch }} name: ${{ matrix.fetch }}
runs-on: ubuntu-26.04-arm runs-on: ubuntu-24.04-arm
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -31,7 +31,7 @@ jobs:
- update_external_bangs.py - update_external_bangs.py
- update_firefox_version.py - update_firefox_version.py
- update_engine_traits.py - update_engine_traits.py
- update_wikidata.py - update_wikidata_units.py
- update_engine_descriptions.py - update_engine_descriptions.py
permissions: permissions:
@@ -40,12 +40,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
@@ -64,17 +64,23 @@ jobs:
run: V=1 ./manage pyenv.cmd python "./searxng_extra/update/${{ matrix.fetch }}" run: V=1 ./manage pyenv.cmd python "./searxng_extra/update/${{ matrix.fetch }}"
- name: Create PR - name: Create PR
id: cpr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with: with:
author: "searxng-bot <searxng-bot@users.noreply.github.com>" author: "searxng-bot <searxng-bot@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>" committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
title: "[mod] data: update searx.data - ${{ matrix.fetch }}" title: "[data] update searx.data - ${{ matrix.fetch }}"
commit-message: "[mod] data: update searx.data - ${{ matrix.fetch }}" commit-message: "[data] update searx.data - ${{ matrix.fetch }}"
branch: "ci-data-${{ matrix.fetch }}" branch: "update_data_${{ matrix.fetch }}"
delete-branch: "true" delete-branch: "true"
draft: "false" draft: "false"
signoff: "false" signoff: "false"
body: | body: |
Update searx.data - ${{ matrix.fetch }} [data] update searx.data - ${{ matrix.fetch }}
labels: | labels: |
data data
- name: Display information
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

View File

@@ -25,19 +25,19 @@ jobs:
release: release:
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch' if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
name: Release name: Release
runs-on: ubuntu-26.04-arm runs-on: ubuntu-24.04-arm
permissions: permissions:
# for JamesIves/github-pages-deploy-action to push # for JamesIves/github-pages-deploy-action to push
contents: write contents: write
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
@@ -61,11 +61,11 @@ jobs:
- if: github.ref_name == 'master' - if: github.ref_name == 'master'
name: Release name: Release
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4.9.0 uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
with: with:
folder: "dist/docs" folder: "dist/docs"
branch: "gh-pages" branch: "gh-pages"
commit-message: "[mod] docs: build from commit ${{ github.sha }}" commit-message: "[doc] build from commit ${{ github.sha }}"
# Automatically remove deleted files from the deploy branch # Automatically remove deleted files from the deploy branch
clean: "true" clean: "true"
single-commit: "true" single-commit: "true"

View File

@@ -23,7 +23,7 @@ env:
jobs: jobs:
test: test:
name: Python ${{ matrix.python-version }} name: Python ${{ matrix.python-version }}
runs-on: ubuntu-26.04 runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
python-version: python-version:
@@ -34,12 +34,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
@@ -59,24 +59,29 @@ jobs:
theme: theme:
name: Theme name: Theme
runs-on: ubuntu-26.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "26"
check-latest: "true"
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: "./.nvmrc"
- name: Setup cache Node.js
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
path: "./client/simple/node_modules/"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
@@ -85,14 +90,6 @@ jobs:
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}- python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup cache Node.js
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}"
restore-keys: |
nodejs-${{ runner.arch }}-
path: "./client/simple/node_modules/"
- name: Setup venv - name: Setup venv
run: make V=1 install run: make V=1 install

View File

@@ -26,21 +26,21 @@ env:
jobs: jobs:
update: update:
if: github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'searxng' if: github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success'
name: Update name: Update
runs-on: ubuntu-26.04-arm runs-on: ubuntu-24.04-arm
permissions: permissions:
# For "make V=1 weblate.push.translations" # For "make V=1 weblate.push.translations"
contents: write contents: write
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
@@ -59,7 +59,7 @@ jobs:
- name: Setup Weblate - name: Setup Weblate
run: | run: |
mkdir -p ~/.config mkdir -p ~/.config
echo "${{ secrets.WEBLATE_CONFIG }}" >~/.config/weblate echo "${{ secrets.WEBLATE_CONFIG }}" > ~/.config/weblate
- name: Setup Git - name: Setup Git
run: | run: |
@@ -74,7 +74,7 @@ jobs:
github.repository_owner == 'searxng' github.repository_owner == 'searxng'
&& (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule')
name: Pull Request name: Pull Request
runs-on: ubuntu-26.04-arm runs-on: ubuntu-24.04-arm
permissions: permissions:
# For "make V=1 weblate.translations.commit" # For "make V=1 weblate.translations.commit"
contents: write contents: write
@@ -83,12 +83,12 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
@@ -107,7 +107,7 @@ jobs:
- name: Setup Weblate - name: Setup Weblate
run: | run: |
mkdir -p ~/.config mkdir -p ~/.config
echo "${{ secrets.WEBLATE_CONFIG }}" >~/.config/weblate echo "${{ secrets.WEBLATE_CONFIG }}" > ~/.config/weblate
- name: Setup Git - name: Setup Git
run: | run: |
@@ -118,17 +118,23 @@ jobs:
run: make V=1 weblate.translations.commit run: make V=1 weblate.translations.commit
- name: Create PR - name: Create PR
id: cpr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with: with:
author: "searxng-bot <searxng-bot@users.noreply.github.com>" author: "searxng-bot <searxng-bot@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>" committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
title: "[mod] i18n: update translations from Weblate" title: "[l10n] update translations from Weblate"
commit-message: "[mod] i18n: update translations from Weblate" commit-message: "[l10n] update translations from Weblate"
branch: "translations_update" branch: "translations_update"
delete-branch: "true" delete-branch: "true"
draft: "false" draft: "false"
signoff: "false" signoff: "false"
body: | body: |
Update translations from Weblate [l10n] update translations from Weblate
labels: | labels: |
area:i18n area:i18n
- name: Display information
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

46
.github/workflows/security.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
---
name: Security
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule:
- cron: "42 05 * * *"
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions:
contents: read
jobs:
container:
if: github.repository_owner == 'searxng'
name: Container
runs-on: ubuntu-24.04-arm
permissions:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: "false"
- name: Sync GHCS from Docker Scout
uses: docker/scout-action@ce97ec1bb85613e8eb35d086fad0c77a6cedf983 # v1.23.0
with:
organization: "searxng"
dockerhub-user: "${{ secrets.DOCKER_USER }}"
dockerhub-password: "${{ secrets.DOCKER_TOKEN }}"
image: "registry://ghcr.io/searxng/searxng:latest"
command: "cves"
sarif-file: "./scout.sarif"
exit-code: "false"
write-comment: "false"
- name: Upload SARIFs
uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
sarif_file: "./scout.sarif"

View File

@@ -2,7 +2,7 @@
/* /*
this file is generated automatically by searxng_extra/update/update_pygments.py this file is generated automatically by searxng_extra/update/update_pygments.py
using pygments version 2.21.0: using pygments version 2.20.0:
./manage templates.simple.pygments ./manage templates.simple.pygments
*/ */
@@ -114,14 +114,14 @@
.gd { color: #FF4689 } /* Generic.Deleted */ .gd { color: #FF4689 } /* Generic.Deleted */
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */ .ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ .ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.gr { color: #FF4689 } /* Generic.Error */ .gr { color: #F8F8F2 } /* Generic.Error */
.gh { color: #F8F8F2 } /* Generic.Heading */ .gh { color: #F8F8F2 } /* Generic.Heading */
.gi { color: #A6E22E } /* Generic.Inserted */ .gi { color: #A6E22E } /* Generic.Inserted */
.go { color: #66D9EF } /* Generic.Output */ .go { color: #66D9EF } /* Generic.Output */
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */ .gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */ .gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
.gu { color: #959077 } /* Generic.Subheading */ .gu { color: #959077 } /* Generic.Subheading */
.gt { color: #66D9EF } /* Generic.Traceback */ .gt { color: #F8F8F2 } /* Generic.Traceback */
.kc { color: #66D9EF } /* Keyword.Constant */ .kc { color: #66D9EF } /* Keyword.Constant */
.kd { color: #66D9EF } /* Keyword.Declaration */ .kd { color: #66D9EF } /* Keyword.Declaration */
.kn { color: #FF4689 } /* Keyword.Namespace */ .kn { color: #FF4689 } /* Keyword.Namespace */
@@ -132,7 +132,7 @@
.m { color: #AE81FF } /* Literal.Number */ .m { color: #AE81FF } /* Literal.Number */
.s { color: #E6DB74 } /* Literal.String */ .s { color: #E6DB74 } /* Literal.String */
.na { color: #A6E22E } /* Name.Attribute */ .na { color: #A6E22E } /* Name.Attribute */
.nb { color: #A6E22E } /* Name.Builtin */ .nb { color: #F8F8F2 } /* Name.Builtin */
.nc { color: #A6E22E } /* Name.Class */ .nc { color: #A6E22E } /* Name.Class */
.no { color: #66D9EF } /* Name.Constant */ .no { color: #66D9EF } /* Name.Constant */
.nd { color: #A6E22E } /* Name.Decorator */ .nd { color: #A6E22E } /* Name.Decorator */
@@ -166,7 +166,7 @@
.sr { color: #E6DB74 } /* Literal.String.Regex */ .sr { color: #E6DB74 } /* Literal.String.Regex */
.s1 { color: #E6DB74 } /* Literal.String.Single */ .s1 { color: #E6DB74 } /* Literal.String.Single */
.ss { color: #E6DB74 } /* Literal.String.Symbol */ .ss { color: #E6DB74 } /* Literal.String.Symbol */
.bp { color: #A6E22E } /* Name.Builtin.Pseudo */ .bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */
.fm { color: #A6E22E } /* Name.Function.Magic */ .fm { color: #A6E22E } /* Name.Function.Magic */
.vc { color: #F8F8F2 } /* Name.Variable.Class */ .vc { color: #F8F8F2 } /* Name.Variable.Class */
.vg { color: #F8F8F2 } /* Name.Variable.Global */ .vg { color: #F8F8F2 } /* Name.Variable.Global */

File diff suppressed because it is too large Load Diff

View File

@@ -23,27 +23,27 @@
"not dead" "not dead"
], ],
"dependencies": { "dependencies": {
"ionicons": "^8.1.0", "ionicons": "^8.0.13",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"ol": "^10.10.0", "ol": "^10.9.0",
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.12", "@biomejs/biome": "2.5.2",
"@types/node": "^26.5.0", "@types/node": "^26.1.0",
"browserslist": "^4.28.8", "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.9.0", "less": "^4.6.7",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.4", "sharp": "~0.35.3",
"sort-package-json": "^4.0.0", "sort-package-json": "^4.0.0",
"stylelint": "^17.14.1", "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.1.0", "svgo": "^4.0.1",
"typescript": "~7.0.2", "typescript": "~6.0.3",
"vite": "^8.2.2", "vite": "^8.1.3",
"vite-bundle-analyzer": "^1.3.9" "vite-bundle-analyzer": "^1.3.8"
} }
} }

View File

@@ -5,7 +5,13 @@ import { assertElement } from "../util/assertElement.ts";
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => { const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
try { try {
const res = await http("GET", `./autocompleter?q=${query}`); let res: Response;
if (settings.method === "GET") {
res = await http("GET", `./autocompleter?q=${query}`);
} else {
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
}
const results = await res.json(); const results = await res.json();

View File

@@ -80,12 +80,7 @@ export default class Calculator extends Plugin {
try { try {
const node = Calculator.math.parse(searchInput.value); const node = Calculator.math.parse(searchInput.value);
const value = node.evaluate(); return `${node.toString()} = ${node.evaluate()}`;
if (typeof value !== "number") {
return;
}
return `${node.toString()} = ${value}`;
} catch { } catch {
// not a compatible math expression // not a compatible math expression
return; return;

View File

@@ -23,7 +23,7 @@ export const appendAnswerElement = (element: HTMLElement | string | number): voi
if (!(element instanceof HTMLElement)) { if (!(element instanceof HTMLElement)) {
const span = document.createElement("span"); const span = document.createElement("span");
span.textContent = element.toString(); span.innerHTML = element.toString();
// biome-ignore lint/style/noParameterAssign: TODO // biome-ignore lint/style/noParameterAssign: TODO
element = span; element = span;
} }

View File

@@ -112,15 +112,6 @@ if [ "$(id -u)" -eq 0 ]; then
fi fi
# ENVs aliases # ENVs aliases
# https://github.com/searxng/searxng/issues/5934 export GRANIAN_PORT="${SEARXNG_PORT:-$GRANIAN_PORT}"
case "${SEARXNG_PORT:-}" in
'') ;;
*[!0-9]*)
unset SEARXNG_PORT
;;
*)
export GRANIAN_PORT="$SEARXNG_PORT"
;;
esac
exec /usr/local/searxng/.venv/bin/granian searx.webapp:app exec /usr/local/searxng/.venv/bin/granian searx.webapp:app

View File

@@ -29,11 +29,10 @@ By default and without any extensions, SearXNG serves these resolvers:
- ``duckduckgo`` - ``duckduckgo``
- ``allesedv`` - ``allesedv``
- ``google`` - ``google``
- ``kagi``
- ``yandex`` - ``yandex``
With the above setting favicons are displayed, the user has the option to With the above setting favicons are displayed, the user has the option to
deactivate this feature in their settings. If the user is to have the option of deactivate this feature in his settings. If the user is to have the option of
selecting from several *resolvers*, a further setting is required / but this selecting from several *resolvers*, a further setting is required / but this
setting will be discussed :ref:`later <register resolvers>` in this article, setting will be discussed :ref:`later <register resolvers>` in this article,
first we have to setup the favicons cache. first we have to setup the favicons cache.
@@ -209,7 +208,6 @@ choose from, the following configuration could be used:
"duckduckgo" = "searx.favicons.resolvers.duckduckgo" "duckduckgo" = "searx.favicons.resolvers.duckduckgo"
"allesedv" = "searx.favicons.resolvers.allesedv" "allesedv" = "searx.favicons.resolvers.allesedv"
# "google" = "searx.favicons.resolvers.google" # "google" = "searx.favicons.resolvers.google"
# "kagi" = "searx.favicons.resolvers.kagi"
# "yandex" = "searx.favicons.resolvers.yandex" # "yandex" = "searx.favicons.resolvers.yandex"
.. note:: .. note::
@@ -228,7 +226,6 @@ into the *proxy*:
- :py:obj:`searx.favicons.resolvers.duckduckgo` - :py:obj:`searx.favicons.resolvers.duckduckgo`
- :py:obj:`searx.favicons.resolvers.allesedv` - :py:obj:`searx.favicons.resolvers.allesedv`
- :py:obj:`searx.favicons.resolvers.google` - :py:obj:`searx.favicons.resolvers.google`
- :py:obj:`searx.favicons.resolvers.kagi`
- :py:obj:`searx.favicons.resolvers.yandex` - :py:obj:`searx.favicons.resolvers.yandex`

View File

@@ -58,9 +58,10 @@ 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:
@@ -162,16 +163,6 @@ 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.
@@ -188,12 +179,20 @@ 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://curl-cffi.readthedocs.io/en/latest/api.html#sessions .. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration
``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,12 +12,20 @@ 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 concurrent connections (default: 100) pool_connections: 100 # Maximum number of allowable connections, or null
enable_http2: true # Enables the use of HTTP2 # for no limits. The default is 100.
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 proxy # uncomment below section if you want to use a proxyq see: SOCKS proxies
# 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://:
@@ -38,26 +46,30 @@ 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 add when an engine's User-Agent is set via searxng_useragent(). Suffix to the user-agent SearXNG uses to send requests to others engines. If an
Contact info here may be useful to avoid an engine blocking you. engine wish to block you, a contact info here may be useful to avoid that.
.. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions .. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration
``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 concurrent connections. The default is 100. Maximum number of allowable connections, or ``null`` # for no limits. The
See ``max_clients`` `Pool limit configuration`_. default is 100. See ``max_connections`` `Pool limit configuration`_.
.. _curl_cffi proxies: https://curl-cffi.readthedocs.io/en/latest/quick_start.html ``keepalive_expiry`` :
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 `curl_cffi proxies`_. Define one or more proxies you wish to use, see `httpx 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:
@@ -75,15 +87,18 @@ Communication with search engines.
different proxy and source ip. different proxy and source ip.
``enable_http2`` : ``enable_http2`` :
Enable by default (HTTP/2). Set to ``false`` to force HTTP/1.1. Enable by default. Set to ``false`` to disable HTTP/2.
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``
HTTPS verification uses the OS's trust store by default. Allow to specify a path to certificate.
Set a path to use a custom CA file. see `httpx verification defaults`_.
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

@@ -8,7 +8,7 @@
search: search:
safe_search: 0 safe_search: 0
autocomplete: "duckduckgo" autocomplete: ""
favicon_resolver: "" favicon_resolver: ""
default_lang: "" default_lang: ""
ban_time_on_fail: 5 ban_time_on_fail: 5
@@ -32,7 +32,7 @@
- ``2``: Strict - ``2``: Strict
``autocomplete``: ``autocomplete``:
Existing autocomplete backends, set blank to turn it off. Existing autocomplete backends, leave blank to turn it off.
- ``360search`` - ``360search``
- ``baidu`` - ``baidu``
@@ -41,7 +41,6 @@
- ``dbpedia`` - ``dbpedia``
- ``duckduckgo`` - ``duckduckgo``
- ``google`` - ``google``
- ``kagi``
- ``mwmbl`` - ``mwmbl``
- ``naver`` - ``naver``
- ``privacywall`` - ``privacywall``

View File

@@ -14,7 +14,7 @@
limiter: false limiter: false
public_instance: false public_instance: false
image_proxy: false image_proxy: false
method: "GET" method: "POST"
default_http_headers: default_http_headers:
X-Content-Type-Options : nosniff X-Content-Type-Options : nosniff
X-Download-Options : noopen X-Download-Options : noopen
@@ -58,8 +58,8 @@
``method`` : ``GET`` | ``POST`` ``method`` : ``GET`` | ``POST``
HTTP method. By default, ``GET`` is used / The ``POST`` method has the HTTP method. By defaults ``POST`` is used / The ``POST`` method has the
advantage with some browsers that the history is not saved, but advantage with some WEB browsers that the history is not easy to read, but
there are also various disadvantages that sometimes **severely restrict the there are also various disadvantages that sometimes **severely restrict the
ease of use for the end user** (e.g. back button to jump back to the previous ease of use for the end user** (e.g. back button to jump back to the previous
search page and drag & drop of search term to new tabs do not work as search page and drag & drop of search term to new tabs do not work as

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 ``''`` headers.User-Agent str a random User-Agent
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,8 +229,6 @@ 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

@@ -0,0 +1,8 @@
.. _cara engine:
===========
Cara Images
===========
.. automodule:: searx.engines.cara
:members:

View File

@@ -1,8 +0,0 @@
.. _europepmc engine:
==========
Europe PMC
==========
.. automodule:: searx.engines.europepmc
:members:

View File

@@ -1,8 +0,0 @@
.. _exaapi engine:
==============
Exa API Engine
==============
.. automodule:: searx.engines.exaapi
:members:

View File

@@ -1,8 +0,0 @@
.. _jina engine:
===========
Jina Engine
===========
.. automodule:: searx.engines.jina
:members:

View File

@@ -0,0 +1,8 @@
.. _engine presearch:
================
Presearch Engine
================
.. automodule:: searx.engines.presearch
:members:

View File

@@ -1,8 +0,0 @@
.. _yandex api engine:
=================
Yandex Search API
=================
.. automodule:: searx.engines.yandex_api
:members:

View File

@@ -80,8 +80,8 @@ same environment, here are a few examples::
# to test one of the update scripts # to test one of the update scripts
(dev.env)$ searxng_extra/update/update_engine_traits.py --help (dev.env)$ searxng_extra/update/update_engine_traits.py --help
# to test the update of the wikidata units and property names # to test the update of the wikidata units
(dev.env)$ searxng_extra/update/update_wikidata.py (dev.env)$ searxng_extra/update/update_wikidata_units.py
.. sidebar:: further read .. sidebar:: further read

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:`curl_cffi` ``:pypi:`curl_cffi``` PyPi package :pypi:`httpx` ``:pypi:`httpx```
manual page man :man:`bash` ``:man:`bash``` manual page man :man:`bash` ``:man:`bash```
intersphinx_ intersphinx_
-------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------

View File

@@ -90,10 +90,10 @@ Scripts to update static data in :origin:`searx/data/`
:members: :members:
``update_wikidata.py`` ``update_wikidata_units.py``
============================ ============================
:origin:`[source] <searxng_extra/update/update_wikidata.py>` :origin:`[source] <searxng_extra/update/update_wikidata_units.py>`
.. automodule:: searxng_extra.update.update_wikidata .. automodule:: searxng_extra.update.update_wikidata_units
:members: :members:

View File

@@ -20,11 +20,15 @@ If you don't trust anyone, you can set up your own, see :ref:`installation`.
- :ref:`self hosted <installation>` - :ref:`self hosted <installation>`
- :ref:`no user tracking / no profiling <SearXNG protect privacy>` - :ref:`no user tracking / no profiling <SearXNG protect privacy>`
- javascript & cookies are optional - script & cookies are optional
- secure, encrypted connections
- :ref:`{{engines | length}} search engines <configured engines>` - :ref:`{{engines | length}} search engines <configured engines>`
- `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_ - `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_
- about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_ - about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_
- :ref:`easy integration of search engines <demo online engine>` - :ref:`easy integration of search engines <demo online engine>`
- professional development: `CI <https://github.com/searxng/searxng/actions>`_,
`quality assurance <https://dev.searxng.org/>`_ &
`automated tested UI <https://dev.searxng.org/screenshots.html>`_
.. sidebar:: be a part .. sidebar:: be a part

2
manage
View File

@@ -48,7 +48,7 @@ PATH="${PY_ENV}/bin:${REPO_ROOT}/node_modules/.bin:${GOROOT}/bin:${GOPATH}/bin:$
PYOBJECTS="searx" PYOBJECTS="searx"
PY_SETUP_EXTRAS='[test]' PY_SETUP_EXTRAS='[test]'
GECKODRIVER_VERSION="v0.37.0" GECKODRIVER_VERSION="v0.36.0"
# SPHINXOPTS= # SPHINXOPTS=
BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization") BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization")
BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests") BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests")

View File

@@ -1,10 +1,10 @@
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.8 pylint==4.0.6
splinter==0.21.0 splinter==0.21.0
selenium==4.48.0 selenium==4.45.0
Sphinx==8.2.3;python_version <= "3.11" Sphinx==8.2.3;python_version <= "3.11"
Sphinx==9.1.0; python_version > "3.11" Sphinx==9.1.0; python_version > "3.11"
sphinx-issues==6.0.0 sphinx-issues==6.0.0
@@ -18,11 +18,11 @@ myst-parser==5.0.0
linuxdoc==20260504 linuxdoc==20260504
aiounittest==1.5.0 aiounittest==1.5.0
yamllint==1.38.0 yamllint==1.38.0
wlc==2.1.1 wlc==2.1.0
coloredlogs==15.0.1 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.8.2 granian[reload]==2.7.8
basedpyright==1.39.10 basedpyright==1.39.9
types-lxml==2026.2.16 types-lxml==2026.2.16

View File

@@ -1,2 +1,2 @@
granian==2.8.2 granian==2.7.8
granian[pname]==2.8.2 granian[pname]==2.7.8

View File

@@ -1,17 +1,19 @@
certifi==2026.7.22 certifi==2026.6.17
babel==2.18.0 babel==2.18.0
flask-babel==4.0.0 flask-babel==4.0.0
flask==3.1.3 flask==3.1.3
jinja2==3.1.6 jinja2==3.1.6
lxml==6.1.2 lxml==6.1.1
pygments==2.21.0 pygments==2.20.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pyyaml==6.0.3 pyyaml==6.0.3
curl_cffi==0.16.1 httpx[http2]==0.28.1
httpx-socks[asyncio]==0.10.0
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
typer==0.27.2 typer==0.26.8
isodate==0.7.2 isodate==0.7.2
whitenoise==6.12.0 whitenoise==6.12.0
typing-extensions==4.16.0 typing-extensions==4.16.0

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

@@ -16,7 +16,7 @@ from . import Answerer, AnswererInfo
def random_characters(): def random_characters():
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
return random.choices(random_string_letters, k=random.randint(8, 32)) return [random.choice(random_string_letters) for _ in range(random.randint(8, 32))]
def random_string(): def random_string():

View File

@@ -11,7 +11,7 @@ from urllib.parse import urlencode
import lxml.etree import lxml.etree
import lxml.html import lxml.html
from curl_cffi.requests.exceptions import RequestException from httpx import HTTPError
from searx import settings from searx import settings
from searx.engines import ( from searx.engines import (
@@ -62,8 +62,8 @@ def bing(query: str, _sxng_locale: str) -> list[str]:
# bing search autocompleter # bing search autocompleter
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.choice(string.ascii_uppercase + string.digits) for _ in range(32))
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}), enable_http3=True) response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}))
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}, 'enable_http3': True} kwargs = {'cookies': {'country': country}}
resp = get(url, **kwargs) resp = get(url, **kwargs)
results: list[str] = [] results: list[str] = []
@@ -127,17 +127,18 @@ def duckduckgo(query: str, sxng_locale: str) -> list[str]:
def google_complete(query: str, sxng_locale: str) -> list[str]: def google_complete(query: str, sxng_locale: str) -> list[str]:
"""Autocomplete from Google. Supports Google's languages """Autocomplete from Google. Supports Google's languages and subdomains
(:py:obj:`searx.engines.google.get_google_info`) by using the async REST (:py:obj:`searx.engines.google.get_google_info`) by using the async REST
API:: API::
https://www.google.com/complete/search?{args} https://{subdomain}/complete/search?{args}
""" """
data = ENGINE_TRAITS.get("google") or {} data = ENGINE_TRAITS.get("google") or {}
traits = EngineTraits(**data) traits = EngineTraits(**data)
google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, traits) google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, traits)
url = 'https://{subdomain}/complete/search?{args}'
args = urlencode( args = urlencode(
{ {
'q': query, 'q': query,
@@ -147,7 +148,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, enable_http3=True) resp = get(url.format(subdomain=google_info['subdomain'], args=args))
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)
@@ -156,24 +157,6 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
return results return results
def kagi(query: str, sxng_locale: str) -> list[str]:
"""Autocomplete from Kagi."""
args: dict[str, str] = {'q': query}
if '-' in sxng_locale:
args['r'] = sxng_locale.split('-')[1].lower()
resp = get("https://kagisuggest.com/api/autosuggest?" + urlencode(args))
results: list[str] = []
if resp.ok:
data = resp.json()
if len(data) > 1:
results = data[1]
return results
def mwmbl(query: str, _sxng_locale: str) -> list[str]: def mwmbl(query: str, _sxng_locale: str) -> list[str]:
"""Autocomplete from Mwmbl_.""" """Autocomplete from Mwmbl_."""
@@ -397,7 +380,6 @@ backends: dict[str, t.Callable[[str, str], list[str]]] = {
'dbpedia': dbpedia, 'dbpedia': dbpedia,
'duckduckgo': duckduckgo, 'duckduckgo': duckduckgo,
'google': google_complete, 'google': google_complete,
'kagi': kagi,
'mwmbl': mwmbl, 'mwmbl': mwmbl,
'naver': naver, 'naver': naver,
'privacywall': privacywall, 'privacywall': privacywall,
@@ -418,5 +400,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 (RequestException, SearxEngineResponseException): except (HTTPError, 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

@@ -151,6 +151,6 @@ def get_token() -> str:
if token: if token:
token = token.decode('UTF-8') # type: ignore token = token.decode('UTF-8') # type: ignore
else: else:
token = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16)) token = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME) valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
return token return token

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
@@ -64,20 +63,6 @@ class ProxyFix:
proxy_list: list[str] = cfg.get("botdetection.trusted_proxies", default=[]) proxy_list: list[str] = cfg.get("botdetection.trusted_proxies", default=[])
return [ip_network(net, strict=False) for net in proxy_list] return [ip_network(net, strict=False) for net in proxy_list]
def is_trusted_proxy(
self,
addr: IPv4Address | IPv6Address | None,
trusted_proxies: list[IPv4Network | IPv6Network],
) -> bool:
if addr is None:
return False
for net in trusted_proxies:
if addr.version == net.version and addr in net:
return True
return False
def trusted_remote_addr( def trusted_remote_addr(
self, self,
x_forwarded_for: list[IPv4Address | IPv6Address], x_forwarded_for: list[IPv4Address | IPv6Address],
@@ -85,8 +70,16 @@ class ProxyFix:
) -> str: ) -> str:
# always rtl # always rtl
for addr in reversed(x_forwarded_for): for addr in reversed(x_forwarded_for):
if not self.is_trusted_proxy(addr, trusted_proxies): trust: bool = False
logger.debug("client address from X-Forwarded-For: %s", addr)
for net in trusted_proxies:
if addr.version == net.version and addr in net:
logger.debug("trust proxy %s (member of %s)", addr, net)
trust = True
break
# client address
if not trust:
return addr.compressed return addr.compressed
# fallback to first address # fallback to first address
@@ -102,21 +95,19 @@ class ProxyFix:
# in this function! # in this function!
orig_remote_addr: str | None = environ.pop("REMOTE_ADDR") orig_remote_addr: str | None = environ.pop("REMOTE_ADDR")
orig_remote_ip: IPv4Address | IPv6Address | None = None
# Validate the IPs involved in this game and delete all invalid ones # Validate the IPs involved in this game and delete all invalid ones
# from the WSGI environment. # from the WSGI environment.
if orig_remote_addr: if orig_remote_addr:
try: try:
orig_remote_ip = ip_address(orig_remote_addr) addr = ip_address(orig_remote_addr)
if orig_remote_ip.version == 6 and orig_remote_ip.ipv4_mapped: if addr.version == 6 and addr.ipv4_mapped:
orig_remote_ip = orig_remote_ip.ipv4_mapped addr = addr.ipv4_mapped
orig_remote_addr = orig_remote_ip.compressed orig_remote_addr = addr.compressed
except ValueError as exc: except ValueError as exc:
logger.error("REMOTE_ADDR: %s / discard REMOTE_ADDR from WSGI environment", exc) logger.error("REMOTE_ADDR: %s / discard REMOTE_ADDR from WSGI environment", exc)
orig_remote_addr = None orig_remote_addr = None
orig_remote_ip = None
x_real_ip: str | None = environ.get("HTTP_X_REAL_IP") x_real_ip: str | None = environ.get("HTTP_X_REAL_IP")
if x_real_ip: if x_real_ip:
@@ -150,13 +141,11 @@ class ProxyFix:
if not x_forwarded_for and not x_real_ip: if not x_forwarded_for and not x_real_ip:
log_error_only_once("X-Forwarded-For nor X-Real-IP header is set!") log_error_only_once("X-Forwarded-For nor X-Real-IP header is set!")
if x_forwarded_for or x_real_ip: if x_forwarded_for and not trusted_proxies:
if not trusted_proxies: log_error_only_once("missing botdetection.trusted_proxies config")
log_error_only_once("missing botdetection.trusted_proxies config") # without trusted_proxies, this variable is useless for determining
# the real IP
if not self.is_trusted_proxy(orig_remote_ip, trusted_proxies): x_forwarded_for = []
x_forwarded_for = []
x_real_ip = None
# securing the WSGI environment variables that are adjusted # securing the WSGI environment variables that are adjusted

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

@@ -48,7 +48,7 @@ class ExpireCacheCfg(msgspec.Struct): # pylint: disable=too-few-public-methods
MAXHOLD_TIME: int = 60 * 60 * 24 * 7 # 7 days MAXHOLD_TIME: int = 60 * 60 * 24 * 7 # 7 days
"""Hold time (default in sec.), after which a value is removed from the cache.""" """Hold time (default in sec.), after which a value is removed from the cache."""
MAINTENANCE_PERIOD: int = 60 * 60 # 1h MAINTENANCE_PERIOD: int = 60 * 60 # 2h
"""Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to """Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to
``auto``.""" ``auto``."""
@@ -458,22 +458,12 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
# Before values are taken from the table, a maintenance interval may # Before values are taken from the table, a maintenance interval may
# need to be carried out. # need to be carried out.
self.maintenance() self.maintenance()
sql = f"SELECT value, expire FROM {table} WHERE key = ?" sql = f"SELECT value FROM {table} WHERE key = ?"
row = self.DB.execute(sql, (key,)).fetchone() row = self.DB.execute(sql, (key,)).fetchone()
if row is None: if row is None:
return default return default
# Check if value is expired. It's possible that it's expired but has not return self.deserialize(row[0])
# yet been automatically deleted by the periodic maintenance
value, expire = row
now = time.time()
if expire < now:
# The record is deleted during the maintenance interval. Deleting
# the record at this point offers no advantage, as a SELECT
# statement must be executed for every cache.get request anyways.
return default
return self.deserialize(value)
def pairs(self, ctx: str) -> Iterator[tuple[str, typing.Any]]: def pairs(self, ctx: str) -> Iterator[tuple[str, typing.Any]]:
"""Iterate over key/value pairs from table given by argument ``ctx``. """Iterate over key/value pairs from table given by argument ``ctx``.

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"]
@@ -33,13 +32,6 @@ class WikiDataUnitType(t.TypedDict):
to_si_factor: float to_si_factor: float
WikiDataPropertyNameType = str | dict[str, str]
"""Name of a Wikidata property. Can be either the plain name or a dictionary of
language code to property name, e.g. ``{"en": "Date of birth"}``."""
WikiDataPropertiesType = dict[str, WikiDataPropertyNameType]
"""Dictionary from wikidata property ID to property name."""
class LocalesType(t.TypedDict): class LocalesType(t.TypedDict):
"""Data structure of an item in ``locales.json``""" """Data structure of an item in ``locales.json``"""
@@ -49,7 +41,6 @@ class LocalesType(t.TypedDict):
USER_AGENTS: UserAgentType USER_AGENTS: UserAgentType
WIKIDATA_UNITS: dict[str, WikiDataUnitType] WIKIDATA_UNITS: dict[str, WikiDataUnitType]
WIKIDATA_PROPERTIES: WikiDataPropertiesType
TRACKER_PATTERNS: TrackerPatternsDB TRACKER_PATTERNS: TrackerPatternsDB
LOCALES: LocalesType LOCALES: LocalesType
CURRENCIES: CurrenciesDB CURRENCIES: CurrenciesDB
@@ -61,12 +52,11 @@ ENGINE_DESCRIPTIONS: dict[str, dict[str, t.Any]]
ENGINE_TRAITS: dict[str, dict[str, t.Any]] ENGINE_TRAITS: dict[str, dict[str, t.Any]]
lazy_globals: dict[str, t.Any] = { lazy_globals = {
"CURRENCIES": CurrenciesDB(), "CURRENCIES": CurrenciesDB(),
"USER_AGENTS": None, "USER_AGENTS": None,
"EXTERNAL_URLS": None, "EXTERNAL_URLS": None,
"WIKIDATA_UNITS": None, "WIKIDATA_UNITS": None,
"WIKIDATA_PROPERTIES": None,
"EXTERNAL_BANGS": None, "EXTERNAL_BANGS": None,
"OSM_KEYS_TAGS": None, "OSM_KEYS_TAGS": None,
"ENGINE_DESCRIPTIONS": None, "ENGINE_DESCRIPTIONS": None,
@@ -79,7 +69,6 @@ data_json_files = {
"USER_AGENTS": "useragents.json", "USER_AGENTS": "useragents.json",
"EXTERNAL_URLS": "external_urls.json", "EXTERNAL_URLS": "external_urls.json",
"WIKIDATA_UNITS": "wikidata_units.json", "WIKIDATA_UNITS": "wikidata_units.json",
"WIKIDATA_PROPERTIES": "wikidata_properties.json",
"EXTERNAL_BANGS": "external_bangs.json", "EXTERNAL_BANGS": "external_bangs.json",
"OSM_KEYS_TAGS": "osm_keys_tags.json", "OSM_KEYS_TAGS": "osm_keys_tags.json",
"ENGINE_DESCRIPTIONS": "engine_descriptions.json", "ENGINE_DESCRIPTIONS": "engine_descriptions.json",

File diff suppressed because it is too large Load Diff

View File

@@ -288,7 +288,7 @@
"oc": "Kwanza", "oc": "Kwanza",
"pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ", "pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ",
"pl": "Kwanza", "pl": "Kwanza",
"pt": "kwanza", "pt": "Kwanza",
"ru": "ангольская кванза", "ru": "ангольская кванза",
"si": "ක්වන්සා", "si": "ක්වන්සා",
"sr": "анголска кванза", "sr": "анголска кванза",
@@ -334,7 +334,6 @@
"ro": "Peso argentinian", "ro": "Peso argentinian",
"ru": "аргентинское песо", "ru": "аргентинское песо",
"sk": "Argentinské peso", "sk": "Argentinské peso",
"sl": "argentinski peso",
"sr": "аргентински пезос", "sr": "аргентински пезос",
"sv": "Argentinsk peso", "sv": "Argentinsk peso",
"ta": "ஆர்ஜென்டின பீசோ", "ta": "ஆர்ஜென்டின பீசோ",
@@ -2003,7 +2002,7 @@
"eo": "ganaa cedio", "eo": "ganaa cedio",
"es": "cedi", "es": "cedi",
"fi": "Cedi", "fi": "Cedi",
"fr": "cedi", "fr": "Cedi",
"ga": "cedi", "ga": "cedi",
"gl": "Cedi", "gl": "Cedi",
"he": "סדי גאני", "he": "סדי גאני",
@@ -2953,7 +2952,7 @@
"pap": "won nortkoreano", "pap": "won nortkoreano",
"pl": "won północnokoreański", "pl": "won północnokoreański",
"pt": "won norte-coreano", "pt": "won norte-coreano",
"ro": "won nord-coreean", "ro": "Won nord-coreean",
"ru": "вона КНДР", "ru": "вона КНДР",
"sk": "severokorejsky won", "sk": "severokorejsky won",
"sl": "severnokorejski von", "sl": "severnokorejski von",
@@ -3095,7 +3094,6 @@
"ca": "tenge", "ca": "tenge",
"cs": "Tenge", "cs": "Tenge",
"cy": "tenge Casachstan", "cy": "tenge Casachstan",
"da": "Tenge",
"de": "Tenge", "de": "Tenge",
"en": "Kazakhstani tenge", "en": "Kazakhstani tenge",
"eo": "kazaĥa tengo", "eo": "kazaĥa tengo",
@@ -4836,7 +4834,6 @@
"nl": "Seychelse roepie", "nl": "Seychelse roepie",
"pl": "Rupia seszelska", "pl": "Rupia seszelska",
"pt": "rupia das Seicheles", "pt": "rupia das Seicheles",
"ro": "rupie seychelloză",
"ru": "сейшельская рупия", "ru": "сейшельская рупия",
"sk": "Seychelská rupia", "sk": "Seychelská rupia",
"sl": "sejšelska rupija", "sl": "sejšelska rupija",
@@ -5067,7 +5064,6 @@
"nl": "Somalische shilling", "nl": "Somalische shilling",
"pl": "Szyling somalijski", "pl": "Szyling somalijski",
"pt": "xelim somaliano", "pt": "xelim somaliano",
"ro": "șiling somalez",
"ru": "сомалийский шиллинг", "ru": "сомалийский шиллинг",
"sk": "Somálsky šiling", "sk": "Somálsky šiling",
"sl": "somalski šiling", "sl": "somalski šiling",
@@ -5887,7 +5883,6 @@
"ja": "ドン", "ja": "ドン",
"ko": "베트남 동", "ko": "베트남 동",
"lt": "Vietnamo dongas", "lt": "Vietnamo dongas",
"ms": "Dồng Vietnam",
"nl": "Vietnamese dong", "nl": "Vietnamese dong",
"oc": "Dong", "oc": "Dong",
"pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ", "pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ",
@@ -6127,8 +6122,7 @@
"ro": "Gulden caraibian", "ro": "Gulden caraibian",
"ru": "Карибский гульден", "ru": "Карибский гульден",
"sk": "Karibský gulden", "sk": "Karibský gulden",
"sl": "karibski goldinar", "sl": "karibski goldinar"
"sv": "Karibisk gulden"
}, },
"XDR": { "XDR": {
"ar": "حقوق السحب الخاصة", "ar": "حقوق السحب الخاصة",
@@ -6730,8 +6724,6 @@
"antilliaanse gulden": "ANG", "antilliaanse gulden": "ANG",
"antilski gulden": "ANG", "antilski gulden": "ANG",
"aoa": "AOA", "aoa": "AOA",
"apvienotās karalistes ekonomika": "GBP",
"apvienotās karalistes saimniecība": "GBP",
"apvienotās karalistes sterliņu mārciņa": "GBP", "apvienotās karalistes sterliņu mārciņa": "GBP",
"ar": "MGA", "ar": "MGA",
"arab accounting dinar": "XAD", "arab accounting dinar": "XAD",
@@ -6844,7 +6836,6 @@
"avustralya doları": "AUD", "avustralya doları": "AUD",
"awg": "AWG", "awg": "AWG",
"az arany mint befektetés": "XAU", "az arany mint befektetés": "XAU",
"az egyesült királyság gazdasága": "GBP",
"azerbaidžanin manat": "AZN", "azerbaidžanin manat": "AZN",
"azerbaidžano manatas": "AZN", "azerbaidžano manatas": "AZN",
"azerbaidžānas manats": "AZN", "azerbaidžānas manats": "AZN",
@@ -7028,7 +7019,6 @@
"bir etíope": "ETB", "bir etíope": "ETB",
"biras": "ETB", "biras": "ETB",
"birleşik arap emirlikleri dirhemi": "AED", "birleşik arap emirlikleri dirhemi": "AED",
"birleşik krallık ekonomisi": "GBP",
"birma kjato": "MMK", "birma kjato": "MMK",
"birr": "ETB", "birr": "ETB",
"birr da etiópia": "ETB", "birr da etiópia": "ETB",
@@ -7116,19 +7106,15 @@
"brit font": "GBP", "brit font": "GBP",
"brita pundo": "GBP", "brita pundo": "GBP",
"britaj pundoj": "GBP", "britaj pundoj": "GBP",
"britannian talous": "GBP",
"britanska funta": "GBP", "britanska funta": "GBP",
"britanski funt": "GBP", "britanski funt": "GBP",
"britische wirtschaft": "GBP",
"britisches pfund": "GBP", "britisches pfund": "GBP",
"british economy": "GBP",
"british pound": "GBP", "british pound": "GBP",
"britisk pund": "GBP", "britisk pund": "GBP",
"britiske pund": "GBP", "britiske pund": "GBP",
"brits pond": "GBP", "brits pond": "GBP",
"britse pond": "GBP", "britse pond": "GBP",
"britská libra": "GBP", "britská libra": "GBP",
"brittisk ekonomi": "GBP",
"brittiska pund": "GBP", "brittiska pund": "GBP",
"brittiskt pund": "GBP", "brittiskt pund": "GBP",
"brunei doları": "BND", "brunei doları": "BND",
@@ -7212,7 +7198,6 @@
"cedi du ghana": "GHS", "cedi du ghana": "GHS",
"cedi ghana": "GHS", "cedi ghana": "GHS",
"cedi ghanese": "GHS", "cedi ghanese": "GHS",
"cedi ghanéen": "GHS",
"centr afrika franko": "XAF", "centr afrika franko": "XAF",
"central african cfa franc": "XAF", "central african cfa franc": "XAF",
"centralafrikansk cfa franc": "XAF", "centralafrikansk cfa franc": "XAF",
@@ -7315,6 +7300,7 @@
"colón costa ricense": "CRC", "colón costa ricense": "CRC",
"colón costa riquenho": "CRC", "colón costa riquenho": "CRC",
"colón costa riquense": "CRC", "colón costa riquense": "CRC",
"colón costa riqueny": "CRC",
"colón costaricain": "CRC", "colón costaricain": "CRC",
"colón costaricano": "CRC", "colón costaricano": "CRC",
"colón costaricien": "CRC", "colón costaricien": "CRC",
@@ -8427,7 +8413,6 @@
"dólares canadenses": "CAD", "dólares canadenses": "CAD",
"dólares estadounidenses": "USD", "dólares estadounidenses": "USD",
"dólares neozelandeses": "NZD", "dólares neozelandeses": "NZD",
"dồng vietnam": "VND",
"dram": "AMD", "dram": "AMD",
"dram armean": "AMD", "dram armean": "AMD",
"dram armenia": "AMD", "dram armenia": "AMD",
@@ -8451,7 +8436,6 @@
"droits de tirage speciaux": "XDR", "droits de tirage speciaux": "XDR",
"droits de tirage spéciaux": "XDR", "droits de tirage spéciaux": "XDR",
"dschibuti franc": "DJF", "dschibuti franc": "DJF",
"dvn": "VND",
"dzd": "DZD", "dzd": "DZD",
"dzsibuti frank": "DJF", "dzsibuti frank": "DJF",
"džibučio frankas": "DJF", "džibučio frankas": "DJF",
@@ -8465,21 +8449,6 @@
"eastern caribbean currency union": "XCD", "eastern caribbean currency union": "XCD",
"eastern caribbean dollar": "XCD", "eastern caribbean dollar": "XCD",
"ec$": "XCD", "ec$": "XCD",
"economi'r deyrnas unedig": "GBP",
"economia": "GBP",
"economia del regne unit": "GBP",
"economia del regno unito": "GBP",
"economia del reialme unit": "GBP",
"economia del reino unido": "GBP",
"economia do reino unido": "GBP",
"economia regatului unit": "GBP",
"economie du royaume uni": "GBP",
"economie van het verenigd koninkrijk": "GBP",
"economía del reino unido": "GBP",
"economía do reino unido": "GBP",
"economy": "GBP",
"economy of the uk": "GBP",
"economy of the united kingdom": "GBP",
"egipatska funta": "EGP", "egipatska funta": "EGP",
"egipta pundo": "EGP", "egipta pundo": "EGP",
"egipto svaras": "EGP", "egipto svaras": "EGP",
@@ -8499,12 +8468,6 @@
"einr": "INR", "einr": "INR",
"eiro": "EUR", "eiro": "EUR",
"ekialdeko karibeko dolar": "XCD", "ekialdeko karibeko dolar": "XCD",
"ekonomi britania raya": "GBP",
"ekonomi united kingdom": "GBP",
"ekonomie van die verenigde koninkryk": "GBP",
"ekonomika spojeného království": "GBP",
"ekonomika v spojenom kráľovstve": "GBP",
"ekonomio de britujo": "GBP",
"el peso": "GTQ", "el peso": "GTQ",
"emalangeni": "SZL", "emalangeni": "SZL",
"emas sebagai pelaburan": "XAU", "emas sebagai pelaburan": "XAU",
@@ -8536,7 +8499,6 @@
"ermenistan dramı": "AMD", "ermenistan dramı": "AMD",
"ern": "ERN", "ern": "ERN",
"erreal brasildar": "BRL", "erreal brasildar": "BRL",
"erresuma batuko ekonomia": "GBP",
"errublo": "RUB", "errublo": "RUB",
"errublo errusiar": "RUB", "errublo errusiar": "RUB",
"errupia indiar": "INR", "errupia indiar": "INR",
@@ -8607,8 +8569,6 @@
"eyrir": "ISK", "eyrir": "ISK",
"e£": "EGP", "e£": "EGP",
"èuro": "EUR", "èuro": "EUR",
"économie britannique": "GBP",
"économie du royaume uni": "GBP",
"észak ír font": "GBP", "észak ír font": "GBP",
"észak koreai von": "KPW", "észak koreai von": "KPW",
"e₹": "INR", "e₹": "INR",
@@ -8742,9 +8702,6 @@
"forintti": "HUF", "forintti": "HUF",
"forinți": "HUF", "forinți": "HUF",
"fòrint": "HUF", "fòrint": "HUF",
"förenade konungariket storbritannien och irlands ekonomi": "GBP",
"förenade konungariket storbritannien och nordirlands ekonomi": "GBP",
"förenade kungarikets ekonomi": "GBP",
"franak cfp": "XPF", "franak cfp": "XPF",
"franc": [ "franc": [
"XPF", "XPF",
@@ -8997,9 +8954,6 @@
"gold als kapitalanlage": "XAU", "gold als kapitalanlage": "XAU",
"gold as an investment": "XAU", "gold as an investment": "XAU",
"gold as currency": "XAU", "gold as currency": "XAU",
"gospodarka wielkiej brytanii": "GBP",
"gospodarstvo ujedinjenog kraljevstva": "GBP",
"gospodarstvo združenega kraljestva": "GBP",
"gourde": "HTG", "gourde": "HTG",
"gourde haiti": "HTG", "gourde haiti": "HTG",
"gourde haitiano": "HTG", "gourde haitiano": "HTG",
@@ -9419,7 +9373,6 @@
"juaņs": "CNY", "juaņs": "CNY",
"juhokoréjsky won": "KRW", "juhokoréjsky won": "KRW",
"juhosudánska libra": "SSP", "juhosudánska libra": "SSP",
"jungtinės karalystės ekonomika": "GBP",
"jungtinių arabų emyratų dirhamas": "AED", "jungtinių arabų emyratų dirhamas": "AED",
"jungtinių valstijų doleris": "USD", "jungtinių valstijų doleris": "USD",
"južnoafrički rand": "ZAR", "južnoafrički rand": "ZAR",
@@ -9484,7 +9437,6 @@
"karibi forint": "XCG", "karibi forint": "XCG",
"karibia guldeno": "XCG", "karibia guldeno": "XCG",
"karibischer gulden": "XCG", "karibischer gulden": "XCG",
"karibisk gulden": "XCG",
"karibski goldinar": "XCG", "karibski goldinar": "XCG",
"karibský gulden": "XCG", "karibský gulden": "XCG",
"karipski gulden": "XCG", "karipski gulden": "XCG",
@@ -9545,9 +9497,6 @@
"kina papua nugini": "PGK", "kina papua nugini": "PGK",
"kina papuana": "PGK", "kina papuana": "PGK",
"kina papuásia": "PGK", "kina papuásia": "PGK",
"kinh tế anh": "GBP",
"kinh tế vương quốc anh": "GBP",
"kinh tế vương quốc liên hiệp anh và bắc ireland": "GBP",
"kip": "LAK", "kip": "LAK",
"kip laos": "LAK", "kip laos": "LAK",
"kip laosiano": "LAK", "kip laosiano": "LAK",
@@ -11192,7 +11141,6 @@
"põhja korea won": "KPW", "põhja korea won": "KPW",
"põhja makedoonia denaar": "MKD", "põhja makedoonia denaar": "MKD",
"prata como investimento": "XAG", "prata como investimento": "XAG",
"produits agricole de l'angleterre": "GBP",
"pula": "BWP", "pula": "BWP",
"pula botswana": "BWP", "pula botswana": "BWP",
"pula botswanais": "BWP", "pula botswanais": "BWP",
@@ -11243,7 +11191,6 @@
"qatarisk rial": "QAR", "qatarisk rial": "QAR",
"qäpik": "AZN", "qäpik": "AZN",
"qindarka": "ALL", "qindarka": "ALL",
"quanza": "AOA",
"quetzal": "GTQ", "quetzal": "GTQ",
"quetzal guatemala": "GTQ", "quetzal guatemala": "GTQ",
"quetzal guatemalteco": "GTQ", "quetzal guatemalteco": "GTQ",
@@ -11569,7 +11516,6 @@
"rupia del pakistan": "PKR", "rupia del pakistan": "PKR",
"rupia dell'india": "INR", "rupia dell'india": "INR",
"rupia delle seychelles": "SCR", "rupia delle seychelles": "SCR",
"rupia din seychelles": "SCR",
"rupia do nepal": "NPR", "rupia do nepal": "NPR",
"rupia do paquistão": "PKR", "rupia do paquistão": "PKR",
"rupia do seri lanca": "LKR", "rupia do seri lanca": "LKR",
@@ -11625,7 +11571,6 @@
], ],
"rupie indiană": "INR", "rupie indiană": "INR",
"rupie indiane": "INR", "rupie indiane": "INR",
"rupie seychelloză": "SCR",
"rupies índies": "INR", "rupies índies": "INR",
"rupija": [ "rupija": [
"NPR", "NPR",
@@ -12055,10 +12000,6 @@
"sterliņu mārciņa": "GBP", "sterliņu mārciņa": "GBP",
"stērliņu mārciņa": "GBP", "stērliņu mārciņa": "GBP",
"stn": "STN", "stn": "STN",
"storbritannien och irlands ekonomi": "GBP",
"storbritannien och nordirlands ekonomi": "GBP",
"storbritanniens ekonomi": "GBP",
"storbritanniens økonomi": "GBP",
"stredoafrický frank": "XAF", "stredoafrický frank": "XAF",
"středoafrický frank": "XAF", "středoafrický frank": "XAF",
"sucre": "XSU", "sucre": "XSU",
@@ -12108,7 +12049,6 @@
"suriye lirası": "SYP", "suriye lirası": "SYP",
"suudi arabistan riyali": "SAR", "suudi arabistan riyali": "SAR",
"suudi riyali": "SAR", "suudi riyali": "SAR",
"suurbritannia majandus": "GBP",
"suurbritannia nael": "GBP", "suurbritannia nael": "GBP",
"suurbritannia naelsterling": "GBP", "suurbritannia naelsterling": "GBP",
"suvereni bolivar": "VES", "suvereni bolivar": "VES",
@@ -12215,7 +12155,6 @@
"švicarski frank": "CHF", "švicarski frank": "CHF",
"švýcarský frank": "CHF", "švýcarský frank": "CHF",
"șekel nou": "ILS", "șekel nou": "ILS",
"șiling somalez": "SOS",
"şekel": "ILS", "şekel": "ILS",
"şili pesosu": "CLP", "şili pesosu": "CLP",
"s₣": "CHF", "s₣": "CHF",
@@ -12558,8 +12497,6 @@
"uguiya": "MRU", "uguiya": "MRU",
"ugx": "UGX", "ugx": "UGX",
"ui": "UYI", "ui": "UYI",
"uk economy": "GBP",
"uk's economy": "GBP",
"ukl": "GBP", "ukl": "GBP",
"ukraina grivna": "UAH", "ukraina grivna": "UAH",
"ukraina hrivno": "UAH", "ukraina hrivno": "UAH",
@@ -12600,8 +12537,6 @@
"unidades de inversion": "MXV", "unidades de inversion": "MXV",
"unidades de inversión": "MXV", "unidades de inversión": "MXV",
"united arab emirates dirham": "AED", "united arab emirates dirham": "AED",
"united kingdom economy": "GBP",
"united kingdom's economy": "GBP",
"united states dollar": [ "united states dollar": [
"USN", "USN",
"USD" "USD"
@@ -12703,7 +12638,6 @@
"venemaa rubla": "RUB", "venemaa rubla": "RUB",
"venezuelai bolívar": "VES", "venezuelai bolívar": "VES",
"venezuelan digital bolívar": "VED", "venezuelan digital bolívar": "VED",
"verenigd koninkrijk economie": "GBP",
"verenigde arabiese emirate dirham": "AED", "verenigde arabiese emirate dirham": "AED",
"verenigde arabische emiraten dirham": "AED", "verenigde arabische emiraten dirham": "AED",
"ves": "VES", "ves": "VES",
@@ -12735,12 +12669,6 @@
"wir euro": "CHE", "wir euro": "CHE",
"wir franc": "CHW", "wir franc": "CHW",
"wir franken": "CHW", "wir franken": "CHW",
"wirtschaft": "GBP",
"wirtschaft des vereinigten königreichs": "GBP",
"wirtschaft im vereinigten königreich": "GBP",
"wirtschaft in dem vereinigten königreich": "GBP",
"wirtschaft vom vereinigten königreich": "GBP",
"wirtschaft von dem vereinigten königreich": "GBP",
"wit russische roebel": "BYN", "wit russische roebel": "BYN",
"won": "KRW", "won": "KRW",
"won bắc triều tiên": "KPW", "won bắc triều tiên": "KPW",
@@ -12834,7 +12762,6 @@
"yeşil burun adaları eskudosu": "CVE", "yeşil burun adaları eskudosu": "CVE",
"yên nhật": "JPY", "yên nhật": "JPY",
"yhdistyneen kuningaskunnan punta": "GBP", "yhdistyneen kuningaskunnan punta": "GBP",
"yhdistyneen kuningaskunnan talous": "GBP",
"yhdistyneiden arabiemiraattien dirhami": "AED", "yhdistyneiden arabiemiraattien dirhami": "AED",
"yhdysvaltain dollari": "USD", "yhdysvaltain dollari": "USD",
"ytl": "TRY", "ytl": "TRY",
@@ -13584,8 +13511,6 @@
"египетский фунт": "EGP", "египетский фунт": "EGP",
"единая система региональных взаиморасчётов": "XSU", "единая система региональных взаиморасчётов": "XSU",
"единая система региональных взаиморасчетов": "XSU", "единая система региональных взаиморасчетов": "XSU",
"економіка великобританії": "GBP",
"економіка великої британії": "GBP",
"енглеска фунта": "GBP", "енглеска фунта": "GBP",
"еритрейська накфа": "ERN", "еритрейська накфа": "ERN",
"еритрејска накфа": "ERN", "еритрејска накфа": "ERN",
@@ -13642,8 +13567,6 @@
"израелски шекел": "ILS", "израелски шекел": "ILS",
"израильский новый шекель": "ILS", "израильский новый шекель": "ILS",
"източнокарибски долар": "XCD", "източнокарибски долар": "XCD",
"икономика на великобритания": "GBP",
"икономика на обединеното кралство": "GBP",
"индийска рупия": "INR", "индийска рупия": "INR",
"индийская рупия": "INR", "индийская рупия": "INR",
"индијска рупија": "INR", "индијска рупија": "INR",
@@ -14057,7 +13980,6 @@
"PLZ", "PLZ",
"PLN" "PLN"
], ],
"привреда уједињеног краљевства": "GBP",
"пула": "BWP", "пула": "BWP",
"південно африканський ранд": "ZAR", "південно африканський ранд": "ZAR",
"південнокорейська вона": "KRW", "південнокорейська вона": "KRW",
@@ -14145,7 +14067,6 @@
"севернокорејски вон": "KPW", "севернокорејски вон": "KPW",
"северо корейская вона": "KPW", "северо корейская вона": "KPW",
"северокорейская вона": "KPW", "северокорейская вона": "KPW",
"седі": "GHS",
"сейшел рупиясе": "SCR", "сейшел рупиясе": "SCR",
"сейшелска рупия": "SCR", "сейшелска рупия": "SCR",
"сейшельская рупия": "SCR", "сейшельская рупия": "SCR",
@@ -14198,8 +14119,6 @@
"старый румынский лей": "RON", "старый румынский лей": "RON",
"стерлинг фунты": "GBP", "стерлинг фунты": "GBP",
"стерлиң фунты": "GBP", "стерлиң фунты": "GBP",
"стопанство на великобритания": "GBP",
"стопанство на обединеното кралство": "GBP",
"суверен боливар": "VES", "суверен боливар": "VES",
"суверенний болівар": "VES", "суверенний болівар": "VES",
"суверенный боливар": "VES", "суверенный боливар": "VES",
@@ -14450,7 +14369,6 @@
"шриланкийска рупия": "LKR", "шриланкийска рупия": "LKR",
"шриланчанска рупија": "LKR", "шриланчанска рупија": "LKR",
"щатски долар": "USD", "щатски долар": "USD",
"экономика великобритании": "GBP",
"эритрейская накфа": "ERN", "эритрейская накфа": "ERN",
"эритрея накфасы": "ERN", "эритрея накфасы": "ERN",
"эсватини лилангение": "SZL", "эсватини лилангение": "SZL",
@@ -14600,8 +14518,6 @@
"יואן סיני": "CNY", "יואן סיני": "CNY",
"ין יפני": "JPY", "ין יפני": "JPY",
"כארתולי לארי": "GEL", "כארתולי לארי": "GEL",
"כלכלת בריטניה": "GBP",
"כלכלת הממלכה המאוחדת": "GBP",
"כתר דני": "DKK", "כתר דני": "DKK",
"כתר נורבגי": "NOK", "כתר נורבגי": "NOK",
"כתר נורווגי": "NOK", "כתר נורווגי": "NOK",
@@ -14749,7 +14665,6 @@
"استثمار البلاتين": "XPT", "استثمار البلاتين": "XPT",
"استثمار الذهب": "XAU", "استثمار الذهب": "XAU",
"استثمار الفضة": "XAG", "استثمار الفضة": "XAG",
"اقتصاد المملكة المتحدة": "GBP",
"الاستثمار في الذهب": "XAU", "الاستثمار في الذهب": "XAU",
"الأوقية الموريتانية": "MRU", "الأوقية الموريتانية": "MRU",
"البات": "THB", "البات": "THB",
@@ -14803,7 +14718,6 @@
"أوقية": "MRU", "أوقية": "MRU",
"أوقية موريتانية": "MRU", "أوقية موريتانية": "MRU",
"أوقيه موريتانيه": "MRU", "أوقيه موريتانيه": "MRU",
"إقتصاد بريطانى": "GBP",
"إيسكودو جزر الرأس الأخضر": "CVE", "إيسكودو جزر الرأس الأخضر": "CVE",
"بات": "THB", "بات": "THB",
"بات تايلاندي": "THB", "بات تايلاندي": "THB",
@@ -15186,7 +15100,6 @@
"মালদ্বীপীয় রুফিয়াহ": "MVR", "মালদ্বীপীয় রুফিয়াহ": "MVR",
"মিয়ানমার ক্যত": "MMK", "মিয়ানমার ক্যত": "MMK",
"মিশরীয় পাউন্ড": "EGP", "মিশরীয় পাউন্ড": "EGP",
"যুক্তরাজ্যের অর্থনীতি": "GBP",
"রুশ রুবল": "RUB", "রুশ রুবল": "RUB",
"রেনমিনবি": "CNY", "রেনমিনবি": "CNY",
"রেন্মিন্বি": "CNY", "রেন্মিন্বি": "CNY",
@@ -15818,7 +15731,6 @@
"엔": "JPY", "엔": "JPY",
"엔화": "JPY", "엔화": "JPY",
"영국 파운드": "GBP", "영국 파운드": "GBP",
"영국의 경제": "GBP",
"예멘 리알": "YER", "예멘 리알": "YER",
"예멘 리얄": "YER", "예멘 리얄": "YER",
"예멘리얄": "YER", "예멘리얄": "YER",
@@ -16026,11 +15938,9 @@
"イエメン・リアル": "YER", "イエメン・リアル": "YER",
"イエメン・リヤル": "YER", "イエメン・リヤル": "YER",
"イエメン・リヤール": "YER", "イエメン・リヤール": "YER",
"イギリスの経済": "GBP",
"イギリスの通貨": "GBP", "イギリスの通貨": "GBP",
"イギリスポンド": "GBP", "イギリスポンド": "GBP",
"イギリス・ポンド": "GBP", "イギリス・ポンド": "GBP",
"イギリス経済": "GBP",
"イラクの通貨": "IQD", "イラクの通貨": "IQD",
"イラク・ディナール": "IQD", "イラク・ディナール": "IQD",
"イランの通貨": "IRR", "イランの通貨": "IRR",
@@ -16332,7 +16242,6 @@
"英ポンド": "GBP", "英ポンド": "GBP",
"西アフリカcfaフラン": "XOF", "西アフリカcfaフラン": "XOF",
"豪ドル": "AUD", "豪ドル": "AUD",
"財政・経済政策": "GBP",
"越南銅": "VND", "越南銅": "VND",
"金投資": "XAU", "金投資": "XAU",
"韓国ウォン": "KRW", "韓国ウォン": "KRW",

File diff suppressed because it is too large Load Diff

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 curl_cffi.requests.exceptions import RequestException from httpx import HTTPError
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 = [
"https://cdn.jsdelivr.net/gh/clearurls/rules@refs/heads/gh-pages/data.minify.json", # ClearURL rule lists, the first one that responds HTTP 200 is used
"https://rules2.clearurls.xyz/data.minify.json",
"https://rules1.clearurls.xyz/data.minify.json", "https://rules1.clearurls.xyz/data.minify.json",
"https://rules2.clearurls.xyz/data.minify.json",
"https://raw.githubusercontent.com/ClearURLs/Rules/refs/heads/master/data.min.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 RequestException as exc: except HTTPError as exc:
log.warning("TRACKER_PATTERNS: RequestException while fetching %s: %s", url, exc) log.warning("TRACKER_PATTERNS: HTTPError (%s) occured while fetching %s", url, exc)
continue continue
if resp.status_code != 200: if resp.status_code != 200:

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": [
"154.0", "152.0",
"153.0" "151.0"
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3474,6 +3474,11 @@
"symbol": "mm⁻²", "symbol": "mm⁻²",
"to_si_factor": 1e-06 "to_si_factor": 1e-06
}, },
"Q136039973": {
"si_name": "Q6137407",
"symbol": "FPS",
"to_si_factor": 1.0
},
"Q1361854": { "Q1361854": {
"si_name": "Q11570", "si_name": "Q11570",
"symbol": "dwt", "symbol": "dwt",
@@ -3516,7 +3521,7 @@
}, },
"Q1377741": { "Q1377741": {
"si_name": "Q25250", "si_name": "Q25250",
"symbol": "V<sub>P</sub>", "symbol": "V_P",
"to_si_factor": 1.0429e+27 "to_si_factor": 1.0429e+27
}, },
"Q1386162": { "Q1386162": {
@@ -3689,11 +3694,6 @@
"symbol": "apc", "symbol": "apc",
"to_si_factor": 0.0308568 "to_si_factor": 0.0308568
}, },
"Q16068": {
"si_name": null,
"symbol": "DM",
"to_si_factor": null
},
"Q160857": { "Q160857": {
"si_name": "Q25236", "si_name": "Q25236",
"symbol": "hp", "symbol": "hp",
@@ -3872,11 +3872,11 @@
"Q180892": { "Q180892": {
"si_name": "Q11570", "si_name": "Q11570",
"symbol": "M☉", "symbol": "M☉",
"to_si_factor": 1.988416e+30 "to_si_factor": 1.9884e+30
}, },
"Q1811": { "Q1811": {
"si_name": "Q11573", "si_name": "Q11573",
"symbol": "au", "symbol": "AU",
"to_si_factor": 149597870700.0 "to_si_factor": 149597870700.0
}, },
"Q1815100": { "Q1815100": {
@@ -4454,11 +4454,6 @@
"symbol": "ng", "symbol": "ng",
"to_si_factor": 1e-12 "to_si_factor": 1e-12
}, },
"Q2285395": {
"si_name": null,
"symbol": "dBW",
"to_si_factor": null
},
"Q22934083": { "Q22934083": {
"si_name": "Q25406", "si_name": "Q25406",
"symbol": "nC", "symbol": "nC",
@@ -5249,11 +5244,6 @@
"symbol": "μA", "symbol": "μA",
"to_si_factor": 1e-06 "to_si_factor": 1e-06
}, },
"Q31274648": {
"si_name": "Q6137407",
"symbol": "FPS",
"to_si_factor": 1.0
},
"Q3186734": { "Q3186734": {
"si_name": "Q3186734", "si_name": "Q3186734",
"symbol": "J/(m³ K)", "symbol": "J/(m³ K)",
@@ -6326,7 +6316,7 @@
}, },
"Q536785": { "Q536785": {
"si_name": "Q844211", "si_name": "Q844211",
"symbol": "ρ<sub>P</sub>", "symbol": "ρ_P",
"to_si_factor": 5.155e+96 "to_si_factor": 5.155e+96
}, },
"Q53679433": { "Q53679433": {
@@ -6981,7 +6971,7 @@
}, },
"Q685662": { "Q685662": {
"si_name": "Q44395", "si_name": "Q44395",
"symbol": "p<sub>P</sub>", "symbol": "p_P",
"to_si_factor": 4.633e+113 "to_si_factor": 4.633e+113
}, },
"Q686163": { "Q686163": {

View File

@@ -47,7 +47,7 @@ ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache(
ExpireCacheCfg( ExpireCacheCfg(
name="ENGINES_CACHE", name="ENGINES_CACHE",
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
MAINTENANCE_PERIOD=60 * 60, # 1h MAINTENANCE_PERIOD=60 * 60, # 2h
MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB
) )
) )
@@ -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,9 +317,6 @@ 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,8 +6,7 @@ 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.result_types import EngineResults from searx.utils import html_to_text, get_embeded_stream_url
from searx.utils import html_to_text
about = { about = {
"website": "https://tv.360kan.com/", "website": "https://tv.360kan.com/",
@@ -30,12 +29,12 @@ def request(query, params):
return params return params
def response(resp) -> EngineResults: def response(resp):
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
res = EngineResults() results = []
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")
@@ -51,15 +50,16 @@ def response(resp) -> EngineResults:
except (ValueError, TypeError): except (ValueError, TypeError):
published_date = None published_date = None
res.add( results.append(
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 res return results

View File

@@ -82,7 +82,7 @@ fragment SXNG_query on Query {
def setup(_) -> bool: def setup(_) -> bool:
global SXNG_query # pylint: disable=global-statement global SXNG_query # pylint: disable=global-statement
rand_str: str = "".join(random.choices(string.ascii_letters, k=5)) rand_str: str = "".join(random.choice(string.ascii_letters) for _ in range(5))
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str) SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
return True return True

View File

@@ -26,7 +26,6 @@ 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,9 +187,8 @@ 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 / update engine attributes from engine_data # set 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

@@ -83,7 +83,7 @@ def extract_video_data(video_block):
published_date = None published_date = None
if create_time: if create_time:
try: try:
published_date = datetime.fromisoformat(create_time.strip()) published_date = datetime.strptime(create_time.strip(), "%Y-%m-%d")
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass

View File

@@ -36,7 +36,6 @@ 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
@@ -86,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 setup(_: dict[str, t.Any]) -> bool | None: def init(_):
if not categories: if not categories:
raise ValueError("adobe_stock engine: categories is unset") raise ValueError("adobe_stock engine: categories is unset")
@@ -101,9 +100,9 @@ def setup(_: dict[str, t.Any]) -> bool | None:
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 content_type in adobe_content_types: for t in adobe_content_types:
if content_type not in ADOBE_VALID_TYPES: if t not in ADOBE_VALID_TYPES:
raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % content_type) raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % t)
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

@@ -109,7 +109,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
comments_elements = eval_xpath_getindex(entry, xpath_comment, 0, default=None) comments_elements = eval_xpath_getindex(entry, xpath_comment, 0, default=None)
comments: str = "" if comments_elements is None else comments_elements.text comments: str = "" if comments_elements is None else comments_elements.text
publishedDate = datetime.fromisoformat(eval_xpath_getindex(entry, xpath_published, 0).text.rstrip("Z")) publishedDate = datetime.strptime(eval_xpath_getindex(entry, xpath_published, 0).text, "%Y-%m-%dT%H:%M:%SZ")
res.add( res.add(
res.types.Paper( res.types.Paper(

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,9 +49,6 @@ 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
@@ -68,6 +65,11 @@ 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"]
@@ -184,7 +186,7 @@ def parse_images(data):
img_date = item.get("bdImgnewsDate") img_date = item.get("bdImgnewsDate")
publishedDate = None publishedDate = None
if img_date: if img_date:
publishedDate = datetime.fromisoformat(img_date) publishedDate = datetime.strptime(img_date, "%Y-%m-%d %H:%M")
results.append( results.append(
{ {
"template": "images.html", "template": "images.html",

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

@@ -32,7 +32,7 @@ base_url = "https://api.bilibili.com/x/web-interface/search/type"
cookie = { cookie = {
"innersign": "0", "innersign": "0",
"buvid3": "".join(random.choices(string.hexdigits, k=16)) + "infoc", "buvid3": "".join(random.choice(string.hexdigits) for _ in range(16)) + "infoc",
"i-wanna-go-back": "-1", "i-wanna-go-back": "-1",
"b_ut": "7", "b_ut": "7",
"FEED_LIVE_VERSION": "V8", "FEED_LIVE_VERSION": "V8",

View File

@@ -40,7 +40,6 @@ 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",
@@ -62,8 +61,8 @@ def get_locale_params(engine_region: str | None) -> dict[str, str] | None:
The ``mkt`` parameter takes a full ``<language>-<country>`` code. The ``mkt`` parameter takes a full ``<language>-<country>`` code.
This function is shared with :py:mod:`searx.engines.bing_news`, and This function is shared with :py:mod:`searx.engines.bing_images`,
:py:mod:`searx.engines.bing_videos`. :py:mod:`searx.engines.bing_news`, and :py:mod:`searx.engines.bing_videos`.
""" """
if not engine_region or engine_region == "clear": if not engine_region or engine_region == "clear":
@@ -72,21 +71,43 @@ def get_locale_params(engine_region: str | None) -> dict[str, str] | None:
return {"mkt": engine_region} return {"mkt": engine_region}
def override_accept_language(params: "OnlineParams", engine_region: str | None) -> None:
"""Override the ``Accept-Language`` header.
The default header built by :py:class:`~searx.search.processors.online.OnlineProcessor`
appends ``en;q=0.3`` as a fallback language::
Accept-Language: de,de-DE;q=0.7,en;q=0.3
Bing seems to better select the results locale based on the
``Accept-Language`` value header.
This function is shared with :py:mod:`searx.engines.bing_images`,
:py:mod:`searx.engines.bing_news`, and :py:mod:`searx.engines.bing_videos`.
"""
if not engine_region or engine_region == "clear":
return
lang = engine_region.split("-")[0]
params["headers"]["Accept-Language"] = f"{engine_region},{lang};q=0.9"
def request(query: str, params: "OnlineParams"): def request(query: str, params: "OnlineParams"):
"""Assemble a Bing-Web request.""" """Assemble a Bing-Web request."""
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
query_params: dict[str, str | int] = { query_params: dict[str, str | int] = {
"q": query, "q": query,
"adlt": _safesearch_map.get(params.get("safesearch", 0), "off"), "adlt": _safesearch_map.get(params.get("safesearch", 0), "off"),
} }
if engine_region and engine_region != "clear": locale_params = get_locale_params(engine_region)
lang, _, cc = engine_region.partition("-") if locale_params:
query_params["setlang"] = lang query_params.update(locale_params)
if cc and cc not in ("us", "cn", "ru"): # bing just sends junk for these
query_params["cc"] = cc
params["url"] = f"{base_url}/search?{urlencode(query_params)}" params["url"] = f"{base_url}/search?{urlencode(query_params)}"

View File

@@ -1,20 +1,18 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Bing-Images: description see :py:obj:`searx.engines.bing`.""" """Bing-Images: description see :py:obj:`searx.engines.bing`."""
import typing as t
import json import json
from urllib.parse import urlencode from urllib.parse import urlencode
from lxml import html from lxml import html
from searx.engines.bing import fetch_traits # pylint: disable=unused-import from searx.engines.bing import ( # pylint: disable=unused-import
from searx.result_types import EngineResults fetch_traits,
get_locale_params,
if t.TYPE_CHECKING: override_accept_language,
from searx.extended_types import SXNG_Response )
from searx.search.processors import OnlineParams
# about
about = { about = {
"website": "https://www.bing.com/images", "website": "https://www.bing.com/images",
"wikidata_id": "Q182496", "wikidata_id": "Q182496",
@@ -24,9 +22,9 @@ about = {
"results": "HTML", "results": "HTML",
} }
# 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 = {
@@ -40,27 +38,26 @@ base_url = "https://www.bing.com"
"""Bing-Image search URL""" """Bing-Image search URL"""
def request(query: str, params: "OnlineParams"): def request(query, params):
"""Assemble a Bing-Image request.""" """Assemble a Bing-Image request."""
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
# build URL query / example: override_accept_language(params, engine_region)
# https://www.bing.com/images/async?q=foo&mmasync=1&first=1&count=35
# build URL query
# - example: https://www.bing.com/images/async?q=foo&async=1&first=1&count=35
query_params = { query_params = {
"q": query, "q": query,
"mmasync": "1", "async": "1",
# to simplify the page count lets use the default of 35 images per page # to simplify the page count lets use the default of 35 images per page
"first": (int(params.get("pageno", 1)) - 1) * 35 + 1, "first": (int(params.get("pageno", 1)) - 1) * 35 + 1,
"count": 35, "count": 35,
} }
if engine_region and engine_region != "clear": locale_params = get_locale_params(engine_region)
lang, _, cc = engine_region.partition("-") if locale_params:
query_params["setlang"] = lang query_params.update(locale_params)
if cc:
query_params["cc"] = cc
# time range # time range
# - example: one year (525600 minutes) 'qft=filterui:age-lt525600' # - example: one year (525600 minutes) 'qft=filterui:age-lt525600'
@@ -70,10 +67,10 @@ def request(query: str, params: "OnlineParams"):
params["url"] = base_url + "/images/async?" + urlencode(query_params) params["url"] = base_url + "/images/async?" + urlencode(query_params)
def response(resp: "SXNG_Response") -> EngineResults: def response(resp):
"""Get response from Bing-Image""" """Get response from Bing-Image"""
res = EngineResults() results = []
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
@@ -84,22 +81,19 @@ def response(resp: "SXNG_Response") -> EngineResults:
metadata = json.loads(result.xpath('.//a[@class="iusc"]/@m')[0]) metadata = json.loads(result.xpath('.//a[@class="iusc"]/@m')[0])
title = " ".join(result.xpath('.//div[@class="infnmpt"]//a/text()')).strip() title = " ".join(result.xpath('.//div[@class="infnmpt"]//a/text()')).strip()
if not title:
title = result.xpath('.//div[@class="infnmpt"]//a/@title')[0]
img_format = " ".join(result.xpath('.//div[@class="imgpt"]/div/span/text()')).strip().split(" · ") img_format = " ".join(result.xpath('.//div[@class="imgpt"]/div/span/text()')).strip().split(" · ")
source = " ".join(result.xpath('.//div[@class="imgpt"]//div[@class="lnkw"]//a/text()')).strip() source = " ".join(result.xpath('.//div[@class="imgpt"]//div[@class="lnkw"]//a/text()')).strip()
results.append(
res.add( {
res.types.Image( "template": "images.html",
title=title, "url": metadata["purl"],
url=metadata["purl"], "thumbnail_src": metadata["turl"],
thumbnail_src=metadata["turl"], "img_src": metadata["murl"],
img_src=metadata["murl"], "content": metadata.get("desc"),
content=metadata.get("desc"), "title": title,
source=source, "source": source,
resolution=img_format[0], "resolution": img_format[0],
img_format=img_format[1] if len(img_format) >= 2 else "", "img_format": img_format[1] if len(img_format) >= 2 else None,
) }
) )
return res return results

View File

@@ -12,7 +12,10 @@ from urllib.parse import urlencode
from lxml import html from lxml import html
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.engines.bing import get_locale_params from searx.engines.bing import (
get_locale_params,
override_accept_language,
)
from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text
# about # about
@@ -30,7 +33,6 @@ 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 = {
@@ -51,6 +53,8 @@ def request(query, params):
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
# build URL query # build URL query
# - example: https://www.bing.com/news/infinitescrollajax?q=london&first=1 # - example: https://www.bing.com/news/infinitescrollajax?q=london&first=1
page = int(params.get("pageno", 1)) - 1 page = int(params.get("pageno", 1)) - 1

View File

@@ -9,6 +9,7 @@ from lxml import html
from searx.engines.bing import ( # pylint: disable=unused-import from searx.engines.bing import ( # pylint: disable=unused-import
fetch_traits, fetch_traits,
get_locale_params, get_locale_params,
override_accept_language,
) )
from searx.engines.bing_images import time_map from searx.engines.bing_images import time_map
from searx.utils import eval_xpath, eval_xpath_getindex from searx.utils import eval_xpath, eval_xpath_getindex
@@ -25,7 +26,6 @@ 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
@@ -38,6 +38,8 @@ def request(query, params):
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale) engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
# build URL query # build URL query
# - example: https://www.bing.com/videos/asyncv2?q=foo&async=content&first=1&count=35 # - example: https://www.bing.com/videos/asyncv2?q=foo&async=content&first=1&count=35
query_params = { query_params = {

View File

@@ -44,7 +44,7 @@ def response(resp):
"url": 'https://www.bitchute.com/video/' + item['video_id'], "url": 'https://www.bitchute.com/video/' + item['video_id'],
"content": html_to_text(item['description']), "content": html_to_text(item['description']),
"author": item['channel']['channel_name'], "author": item['channel']['channel_name'],
"publishedDate": datetime.fromisoformat(item["date_published"].rstrip("Z")), "publishedDate": datetime.strptime(item["date_published"], "%Y-%m-%dT%H:%M:%S.%fZ"),
"length": item['duration'], "length": item['duration'],
"views": item['view_count'], "views": item['view_count'],
"thumbnail": item['thumbnail_url'], "thumbnail": item['thumbnail_url'],

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 setup(engine_settings: dict[str, t.Any]) -> bool: def init(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
@@ -104,7 +104,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
title=_remove_keyword_marker(result["Subject"]), title=_remove_keyword_marker(result["Subject"]),
content=_remove_keyword_marker(result["Text"]), content=_remove_keyword_marker(result["Text"]),
url=result["Url"], url=result["Url"],
publishedDate=datetime.fromisoformat(result["Published"]), publishedDate=datetime.strptime(result["Published"], "%Y-%m-%d %H:%M:%S"),
metadata=gettext.gettext("Posted by {author}").format(author=result["Author"]), metadata=gettext.gettext("Posted by {author}").format(author=result["Author"]),
) )
) )

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_embedded_stream_url, get_embeded_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,7 +151,6 @@ 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.
@@ -248,13 +247,13 @@ def extract_json_data(text: str) -> dict[str, t.Any]:
# node_ids: [0, 19], # node_ids: [0, 19],
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}] # data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# form: null, text = text[text.index("<script") : text.index("</script")]
# error: null if not text:
# }); raise ValueError("can't find JS/JSON data in the given text")
start = text.index("data: [{") start = text.index("data: [{")
newline = text.index("\n", start) end = text.rindex("}}]")
end = text.rindex("}}]", start, newline) js_obj_str = text[start:end]
js_obj_str = "{" + text[start:end] + "}}]}" js_obj_str = "{" + js_obj_str + "}}]}"
# js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for &nbsp; # js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for &nbsp;
# js_obj_str = js_obj_str.replace(r"\u003C", "<").replace(r"\u003c", "<") # fix broken HTML tags in strings # js_obj_str = js_obj_str.replace(r"\u003C", "<").replace(r"\u003c", "<") # fix broken HTML tags in strings
json_str = js_obj_str_to_json_str(js_obj_str) json_str = js_obj_str_to_json_str(js_obj_str)
@@ -339,7 +338,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_embedded_stream_url(url) iframe_src = get_embeded_stream_url(url)
if iframe_src: if iframe_src:
item["iframe_src"] = iframe_src item["iframe_src"] = iframe_src
item["template"] = "videos.html" item["template"] = "videos.html"
@@ -354,14 +353,14 @@ def _parse_news(resp: SXNG_Response) -> EngineResults:
res = EngineResults() res = EngineResults()
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, "//div[@data-type='news']"): for result in eval_xpath_list(dom, "//div[contains(@class, 'results')]//div[@data-type='news']"):
url = eval_xpath_getindex(result, ".//a/@href", 0, default=None) url = eval_xpath_getindex(result, ".//a[contains(@class, 'result-header')]/@href", 0, default=None)
if url is None: if url is None:
continue continue
title = eval_xpath_list(result, ".//div[contains(@class, 'title')]") title = eval_xpath_list(result, ".//span[contains(@class, 'snippet-title')]")
content = eval_xpath_list(result, ".//div[contains(@class, 'description')]") content = eval_xpath_list(result, ".//p[contains(@class, 'desc')]")
thumbnail = eval_xpath_getindex(result, ".//a[contains(@class, 'thumbnail')]//img/@src", 0, default="") thumbnail = eval_xpath_getindex(result, ".//div[contains(@class, 'image-wrapper')]//img/@src", 0, default="")
item = res.types.LegacyResult( item = res.types.LegacyResult(
template="default.html", template="default.html",
@@ -407,6 +406,9 @@ 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/api-reference/web/search/get", "official_api_documentation": "https://api-dashboard.search.brave.com/documentation",
"use_official_api": True, "use_official_api": True,
"require_api_key": True, "require_api_key": True,
"results": "JSON", "results": "JSON",
@@ -63,10 +63,8 @@ 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")
@@ -77,7 +75,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, "offset": (params["pageno"] - 1) * results_per_page,
"text_decorations": False, "text_decorations": False,
} }
@@ -91,7 +89,6 @@ def request(query: str, params: "OnlineParams") -> None:
params["url"] = f"{base_url}?{urlencode(search_args)}" params["url"] = f"{base_url}?{urlencode(search_args)}"
params["headers"]["X-Subscription-Token"] = api_key params["headers"]["X-Subscription-Token"] = api_key
params["headers"]["Accept"] = "application/json"
def _extract_published_date(published_date_raw: str): def _extract_published_date(published_date_raw: str):

85
searx/engines/cara.py Normal file
View File

@@ -0,0 +1,85 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=invalid-name
"""Cara_ is a social media and portfolio-sharing platform for artists and art
enthusiasts.
With the widespread use of generative AI, Cara_ decided to build a place that
filters out gen AI images so that people searching for authentic creatives and
images can do so easily.
.. _Cara: https://cara.app/about
"""
from urllib.parse import urlencode
import typing as t
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://cara.app",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://cara.app"
images_url = "https://images.cara.app"
categories = ["images"]
paging = True
results_per_page = 24
# if using HTTP2, we get blocked immediately
enable_http2 = False
def request(query: str, params: "OnlineParams") -> None:
args = {
"q": query,
"sortBy": "Top",
"take": results_per_page,
"skip": (params["pageno"] - 1) * results_per_page,
}
params["url"] = f"{base_url}/api/search/portfolio-posts?{urlencode(args)}"
def response(resp: "SXNG_Response"):
res = EngineResults()
json_data: list[dict[str, t.Any]] = resp.json()
for result in json_data:
thumbnail, img = None, None
i: dict[str, str]
for i in result["images"]:
if thumbnail is None or i["isCoverImg"]:
thumbnail = i
if img is None or not i["isCoverImg"]:
img = i
if not thumbnail or not img:
continue
res.add(
res.types.LegacyResult(
{
"template": "images.html",
"url": f"{base_url}/post/{result['id']}",
"thumbnail_src": f"{images_url}/{thumbnail['src']}?height=256",
"img_src": f"{images_url}/{img['src']}",
"title": result["title"],
"content": result["content"],
"author": result["name"],
}
)
)
return res

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]: def _obtain_api_key() -> tuple[str, 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,9 +58,10 @@ def _obtain_api_key() -> tuple[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, scraped_api_key return csrf_token, session_id, scraped_api_key
def request(query: str, params: "OnlineParams"): def request(query: str, params: "OnlineParams"):
@@ -72,7 +73,7 @@ def request(query: str, params: "OnlineParams"):
params["headers"].update(headers) params["headers"].update(headers)
else: else:
csrf_token, scraped_api_key = _obtain_api_key() csrf_token, session_id, scraped_api_key = _obtain_api_key()
headers = { headers = {
"Authorization": f"Bearer {scraped_api_key}", "Authorization": f"Bearer {scraped_api_key}",
@@ -80,11 +81,10 @@ def request(query: str, params: "OnlineParams"):
} }
params["headers"].update(headers) params["headers"].update(headers)
params["cookies"] = {"csrftoken": csrf_token} params["cookies"] = {"csrftoken": session_id, "sessionid": session_id}
params["url"] = f"{base_url}/api/v1/_search" params["url"] = f"{base_url}/api/v1/_search"
params["method"] = "POST" params["method"] = "POST"
params["impersonate"] = "none"
json_data = { json_data = {
"query": query, "query": query,

View File

@@ -43,7 +43,7 @@ def response(resp):
publishedDate = None publishedDate = None
if recipe['submissionDate']: if recipe['submissionDate']:
publishedDate = datetime.fromisoformat(result['recipe']['submissionDate'][:19]) publishedDate = datetime.strptime(result['recipe']['submissionDate'][:19], "%Y-%m-%dT%H:%M:%S")
content = [ content = [
f"Schwierigkeitsstufe (1-3): {recipe['difficulty']}", f"Schwierigkeitsstufe (1-3): {recipe['difficulty']}",

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 setup(_: dict[str, t.Any]) -> bool | None: def init(_):
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,7 +74,6 @@ 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
@@ -84,6 +83,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 = []
@@ -100,7 +100,7 @@ _command_logger = logger.getChild('command')
_compiled_parse_regex = {} _compiled_parse_regex = {}
def setup(engine_settings: dict[str, t.Any]) -> bool | None: def init(engine_settings):
check_parsing_options(engine_settings) check_parsing_options(engine_settings)
if 'command' not in engine_settings: if 'command' not in engine_settings:

View File

@@ -141,13 +141,12 @@ def response(resp: "SXNG_Response") -> EngineResults:
if name: if name:
authors.add(name) authors.add(name)
tag = result.get("fieldOfStudy")
res.add( res.add(
res.types.Paper( res.types.Paper(
title=result.get("title"), title=result.get("title"),
url=url, url=url,
content=result.get("fullText", "") or "", content=result.get("fullText", "") or "",
tags=[tag] if tag else [], tags=result.get("fieldOfStudy", []),
publishedDate=published_date, publishedDate=published_date,
type=result.get("documentType", "") or "", type=result.get("documentType", "") or "",
authors=authors, authors=authors,

View File

@@ -1,18 +1,11 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Deviantart (Images)""" """Deviantart (Images)"""
import typing as t
import urllib.parse import urllib.parse
from lxml import html from lxml import html
from searx.result_types import EngineResults
from searx.utils import extract_text, eval_xpath, eval_xpath_list from searx.utils import extract_text, eval_xpath, eval_xpath_list
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
# about # about
about = { about = {
"website": 'https://www.deviantart.com/', "website": 'https://www.deviantart.com/',
@@ -30,62 +23,63 @@ paging = True
# search-url # search-url
base_url = 'https://www.deviantart.com' base_url = 'https://www.deviantart.com'
results_xpath = '//div[@data-testid="content_row"]//a[.//*[@data-testid="thumb"]]' results_xpath = '//div[@class="V_S0t_"]/div/div/a'
img_src_xpath = './/img/@srcset' url_xpath = './@href'
thumbnail_src_xpath = './/img/@src' thumbnail_src_xpath = './div/img/@src'
author_xpath = './/*[@property="schema:name"]/@content' img_src_xpath = './div/img/@srcset'
cursor_xpath = '//a[contains(@href, "cursor=") and contains(., "Next")]/@href' title_xpath = './@aria-label'
premium_xpath = '../div/div/div/text()'
premium_keytext = 'Watch the artist to view this deviation'
cursor_xpath = '(//a[@class="vQ2brP"]/@href)[last()]'
def request(query: str, params: "OnlineParams"): def request(query, params):
# https://www.deviantart.com/search?q=foo # https://www.deviantart.com/search?q=foo
args = {'q': query} nextpage_url = params['engine_data'].get('nextpage')
if params['pageno'] > 1: # don't use nextpage when user selected to jump back to page 1
cursor = params['engine_data'].get('cursor') if params['pageno'] > 1 and nextpage_url is not None:
if cursor: params['url'] = nextpage_url
args['cursor'] = cursor else:
params['url'] = f"{base_url}/search?{urllib.parse.urlencode({'q': query})}"
params['url'] = f"{base_url}/search?{urllib.parse.urlencode(args)}" return params
def response(resp: "SXNG_Response") -> EngineResults: def response(resp):
res = EngineResults() results = []
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, results_xpath): for result in eval_xpath_list(dom, results_xpath):
thumbnail_src = extract_text(eval_xpath(result, thumbnail_src_xpath)) # skip images that are blurred
img_src = extract_text(eval_xpath(result, img_src_xpath)) _text = extract_text(eval_xpath(result, premium_xpath))
# mature locked thumbs have blur transform (blur_15, blur_30 etc..) if _text and premium_keytext in _text:
if ',blur_' in f'{thumbnail_src}{img_src}':
continue continue
img_src = extract_text(eval_xpath(result, img_src_xpath))
if img_src: if img_src:
img_src = img_src.split(' ')[0] img_src = img_src.split(' ')[0]
parsed_url = urllib.parse.urlparse(img_src) parsed_url = urllib.parse.urlparse(img_src)
img_src = parsed_url._replace(path=parsed_url.path.split('/v1')[0]).geturl() img_src = parsed_url._replace(path=parsed_url.path.split('/v1')[0]).geturl()
author = extract_text(eval_xpath(result, author_xpath)) results.append(
{
res.add( 'template': 'images.html',
res.types.Image( 'url': extract_text(eval_xpath(result, url_xpath)),
template='images.html', 'img_src': img_src,
url=result.get('href'), 'thumbnail_src': extract_text(eval_xpath(result, thumbnail_src_xpath)),
img_src=img_src or "", 'title': extract_text(eval_xpath(result, title_xpath)),
thumbnail_src=thumbnail_src or "", }
title=result.get('aria-label'),
author=author or "",
)
) )
nextpage_url = extract_text(eval_xpath(dom, cursor_xpath)) nextpage_url = extract_text(eval_xpath(dom, cursor_xpath))
cursor = urllib.parse.parse_qs(urllib.parse.urlparse(nextpage_url or '').query).get('cursor', [None])[0] if nextpage_url:
if cursor: results.append(
res.add( {
res.types.LegacyResult( 'engine_data': nextpage_url.replace("http://", "https://"),
engine_data=cursor, 'key': 'nextpage',
key='cursor', }
)
) )
return res return results

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

@@ -8,9 +8,6 @@ import typing as t
from datetime import datetime, timezone from datetime import datetime, timezone
import html import html
from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException
from searx.network import post
from searx.utils import format_duration, html_to_text, humanize_number from searx.utils import format_duration, html_to_text, humanize_number
from searx.result_types import EngineResults from searx.result_types import EngineResults
@@ -38,38 +35,17 @@ dogpile_categ = "search"
base_url = "https://www.dogpile.com" base_url = "https://www.dogpile.com"
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"} safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
CACHE: EngineCache
"""Cache for the API token from dogpile"""
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)
global CACHE # pylint: disable=global-statement
CACHE = EngineCache("dogpile") # one token for images/videos/news
return True
def _obtain_token() -> str:
token = CACHE.get("token")
if token:
return token
resp = post(f"{base_url}/api/token/refresh", headers={"Origin": base_url}, cookies={"dp_api_token": "1"})
if not resp.ok:
raise SearxEngineAPIException("failed to obtain dogpile token")
token = resp.json()["token"]
CACHE.set("token", token, expire=240) # 300s ttl
return token
def request(query: str, params: "OnlineParams"): def request(query: str, params: "OnlineParams"):
params["url"] = f"{base_url}/api/{dogpile_categ}" params["url"] = f"{base_url}/api/{dogpile_categ}"
params["headers"]["Origin"] = base_url
params["cookies"]["dp_api_token"] = "1"
params["headers"]["x-dogpile-token"] = _obtain_token()
params["method"] = "POST" params["method"] = "POST"
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]} params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
return params
def response(resp: "SXNG_Response"): def response(resp: "SXNG_Response"):

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,8 +10,7 @@ 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.result_types import EngineResults, MainResult, LegacyResult, Image from searx.utils import get_embeded_stream_url, html_to_text, gen_useragent, extr
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
@@ -48,7 +47,7 @@ _HTTP_User_Agent: str = gen_useragent()
send_accept_language_header = False send_accept_language_header = False
def setup(engine_settings: dict[str, t.Any]) -> bool | None: def init(engine_settings: dict[str, t.Any]):
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']}")
@@ -98,7 +97,6 @@ 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"] = "*/*"
@@ -150,51 +148,54 @@ def request(query: str, params: "OnlineParams") -> None:
def _image_result(result): def _image_result(result):
return Image( return {
url=result['url'], 'template': 'images.html',
title=result['title'], 'url': result['url'],
content='', 'title': result['title'],
thumbnail_src=result['thumbnail'], 'content': '',
img_src=result['image'], 'thumbnail_src': result['thumbnail'],
resolution='%s x %s' % (result['width'], result['height']), 'img_src': result['image'],
source=result['source'], 'resolution': '%s x %s' % (result['width'], result['height']),
) 'source': result['source'],
}
def _video_result(result): def _video_result(result):
return LegacyResult( return {
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'),
source=result['provider'], 'iframe_src': get_embeded_stream_url(result['content']),
length=result['duration'], 'source': result['provider'],
metadata=result.get('uploader'), 'length': result['duration'],
) 'metadata': result.get('uploader'),
}
def _news_result(result): def _news_result(result):
return MainResult( return {
url=result['url'], 'url': result['url'],
title=result['title'], 'title': result['title'],
content=html_to_text(result['excerpt']), 'content': html_to_text(result['excerpt']),
publishedDate=datetime.fromtimestamp(result['date']), 'source': result['source'],
) 'publishedDate': datetime.fromtimestamp(result['date']),
}
def response(resp: "SXNG_Response") -> EngineResults: def response(resp):
res = EngineResults() results = []
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':
res.add(_image_result(result)) results.append(_image_result(result))
elif ddg_category == 'videos': elif ddg_category == 'videos':
res.add(_video_result(result)) results.append(_video_result(result))
elif ddg_category == 'news': elif ddg_category == 'news':
res.add(_news_result(result)) results.append(_news_result(result))
else: else:
raise ValueError(f"Invalid duckduckgo category: {ddg_category}") raise ValueError(f"Invalid duckduckgo category: {ddg_category}")
return res return results

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,19 +109,7 @@ 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])
location = json_data.get("location") geoloc = weather.GeoLocation.by_query(resp.search_params["query"])
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

@@ -14,12 +14,11 @@ can't build it ourselves and must scrape it from the HTML pages.
""" """
import typing as t import typing as t
import re
from urllib.parse import quote_plus, urljoin from urllib.parse import quote_plus
from lxml import html from lxml import html
from searx.utils import html_to_text, extract_text, eval_xpath from searx.utils import html_to_text, gen_useragent, extract_text, eval_xpath
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineCache from searx.enginelib import EngineCache
from searx.network import get from searx.network import get
@@ -39,6 +38,7 @@ about = {
# engine dependent config # engine dependent config
categories = ["general"] categories = ["general"]
paging = True paging = True
_HTTP_User_Agent: str = gen_useragent()
base_url = "https://duckduckgo.com" base_url = "https://duckduckgo.com"
@@ -73,8 +73,6 @@ def _fetch_first_page_link(
resp = get( resp = get(
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web", url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
headers=headers, headers=headers,
impersonate="firefox",
default_headers=False,
timeout=2, timeout=2,
) )
@@ -98,43 +96,6 @@ def _cache_key(query: str, pageno: int) -> str:
return f"nextpage_url|{query}|{pageno}" return f"nextpage_url|{query}|{pageno}"
def _solve_jsa(resp: "SXNG_Response") -> "SXNG_Response":
"""Duckduckgo sometimes issues a challenge instead of json."""
# length that a real browser would report for where the broken snippet is
html_len = {
"<p><div></p><p></div": 32,
"<li><div></li><li></div": 29,
"<div><div></div><div></div": 33,
"<br><div></br><br></div": 23,
}
js = resp.text or ""
jsa_match = re.search(r"let jsa = (\d+);.*?DDG\.deep\.initialize\('([^']+)'", js, re.S)
if not jsa_match:
return resp
js_functions = dict(re.findall(r"let (\w+) = function\(num\) \{([^}]*)\};", js))
jsa = int(jsa_match.group(1))
try:
for name in re.findall(r"jsa = (\w+)\(jsa\);", js):
body = js_functions[name]
mul = re.search(r"num \* (\d+)", body)
jsa = jsa * int(mul.group(1)) if mul else jsa + html_len[re.search(r"`([^`]+)`", body).group(1)]
except (KeyError, AttributeError):
return resp
params = resp.search_params
follow = get(
urljoin("https://links.duckduckgo.com", jsa_match.group(2) + str(jsa)),
headers=params["headers"],
impersonate="firefox",
default_headers=False,
)
follow.search_params = params
return follow
def request(query: str, params: "OnlineParams") -> None: def request(query: str, params: "OnlineParams") -> None:
if len(query) >= 500: if len(query) >= 500:
@@ -142,15 +103,25 @@ def request(query: str, params: "OnlineParams") -> None:
params["url"] = None params["url"] = None
return return
# firefox TLS only headers = params["headers"]
params["impersonate"] = "firefox"
params["default_headers"] = False # The vqd value is generated from the query and the UA header. To be able
# to reuse the vqd value, the UA header must be static.
headers["User-Agent"] = _HTTP_User_Agent
headers["Accept"] = "*/*"
headers["Referer"] = f"{base_url}/"
headers["Host"] = "duckduckgo.com"
# Sec-Fetch headers are required to not get blocked when sending a Firefox user agent
headers["Sec-Fetch-Dest"] = "script"
headers["Sec-Fetch-Mode"] = "no-cors"
headers["Sec-Fetch-Site"] = "same-site"
api_url = "" api_url = ""
if params["pageno"] > 1: if params["pageno"] > 1:
api_url = CACHE.get(_cache_key(query, params["pageno"])) api_url = CACHE.get(_cache_key(query, params["pageno"]))
else: else:
api_url = _fetch_first_page_link(query, params["headers"]) api_url = _fetch_first_page_link(query, headers)
if not api_url: if not api_url:
params["url"] = None params["url"] = None
@@ -158,27 +129,14 @@ def request(query: str, params: "OnlineParams") -> None:
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&") params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
# loads as a script
headers = params["headers"]
headers["Accept"] = "*/*"
headers["Sec-Fetch-Dest"] = "script"
headers["Sec-Fetch-Mode"] = "no-cors"
headers["Sec-Fetch-Site"] = "same-site"
headers["Referer"] = f"{base_url}/"
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme # TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
def response(resp: "SXNG_Response"): def response(resp: "SXNG_Response"):
res = EngineResults() res = EngineResults()
res_json = resp.json()
# check if ddg returns a challenge for result in res_json["results"]:
# e.g. 'site:github.com searxng'
if "let jsa =" in (resp.text or ""):
resp = _solve_jsa(resp)
results = resp.json()["results"]
for result in results:
if "u" not in result: if "u" not in result:
continue continue
@@ -186,13 +144,13 @@ def response(resp: "SXNG_Response"):
res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"])) res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"]))
) )
if results: # link to next page
next_page_path = results[-1].get("n") next_page_path = res_json["results"][-1].get("n")
if next_page_path: if next_page_path:
CACHE.set( CACHE.set(
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1), _cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
base_url + next_page_path, base_url + next_page_path,
expire=60 * 60, expire=60 * 60,
) )
return res return res

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,

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