1 Commits

Author SHA1 Message Date
searxng-bot
218f69a833 [l10n] update translations from Weblate
94e9ade46 - 2026-05-04 - Aindriú Mac Giolla Eoin <aindriu80@noreply.codeberg.org>
883cac081 - 2026-04-30 - alexgabi <alexgabi@noreply.codeberg.org>
2026-05-08 07:40:15 +00:00
378 changed files with 12697 additions and 19922 deletions

163
.dir-locals-template.el Normal file
View File

@@ -0,0 +1,163 @@
;;; .dir-locals.el
;;
;; Per-Directory Local Variables:
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html
;;
;; For full fledge developer tools install emacs packages:
;;
;; M-x package-install ...
;;
;; magit gitconfig
;; nvm lsp-mode lsp-pyright lsp-eslint
;; pyvenv pylint pip-requirements
;; jinja2-mode
;; json-mode
;; company company-jedi company-quickhelp company-shell
;; realgud
;; sphinx-doc markdown-mode graphviz-dot-mode
;; apache-mode nginx-mode
;;
;; To setup a developer environment, build target::
;;
;; $ make node.env.dev pyenv.install
;;
;; Some buffer locals are referencing the project environment:
;;
;; - prj-root --> <repo>/
;; - nvm-dir --> <repo>/.nvm
;; - python-environment-directory --> <repo>/local
;; - python-environment-default-root-name --> py3
;; - python-shell-virtualenv-root --> <repo>/local/py3
;; When this variable is set with the path of the virtualenv to use,
;; `process-environment' and `exec-path' get proper values in order to run
;; shells inside the specified virtualenv, example::
;; (setq python-shell-virtualenv-root "/path/to/env/")
;; - python-shell-interpreter --> <repo>/local/py3/bin/python
;;
;; Python development:
;;
;; Jedi, flycheck & other python stuff should use the 'python-shell-interpreter'
;; from the local py3 environment.
;;
((nil
. ((fill-column . 80)
(indent-tabs-mode . nil)
(eval . (progn
(add-to-list 'auto-mode-alist '("\\.html\\'" . jinja2-mode))
;; project root folder is where the `.dir-locals.el' is located
(setq-local prj-root
(locate-dominating-file default-directory ".dir-locals.el"))
(setq-local python-environment-directory
(expand-file-name "./local" prj-root))
;; to get in use of NVM environment, install https://github.com/rejeep/nvm.el
(setq-local nvm-dir (expand-file-name "./.nvm" prj-root))
;; use nodejs from the (local) NVM environment (see nvm-dir)
(nvm-use-for-buffer)
(ignore-errors (require 'lsp))
(setq-local lsp-server-install-dir (car (cdr nvm-current-version)))
(setq-local lsp-enable-file-watchers nil)
;; use 'py3' environment as default
(setq-local python-environment-default-root-name
"py3")
(setq-local python-shell-virtualenv-root
(expand-file-name
python-environment-default-root-name python-environment-directory))
(setq-local python-shell-interpreter
(expand-file-name
"bin/python" python-shell-virtualenv-root))))))
(makefile-gmake-mode
. ((indent-tabs-mode . t)))
(yaml-mode
. ((eval . (progn
;; flycheck should use the local py3 environment
(setq-local flycheck-yaml-yamllint-executable
(expand-file-name "bin/yamllint" python-shell-virtualenv-root))
(setq-local flycheck-yamllintrc
(expand-file-name ".yamllint.yml" prj-root))
(flycheck-checker . yaml-yamllint)))))
(json-mode
. ((eval . (progn
(setq-local js-indent-level 4)
(flycheck-checker . json-python-json)))))
(js-mode
. ((eval . (progn
(ignore-errors (require 'lsp-eslint))
(setq-local js-indent-level 2)
;; flycheck should use the eslint checker from developer tools
(setq-local flycheck-javascript-eslint-executable
(expand-file-name "node_modules/.bin/eslint" prj-root))
;; (flycheck-mode)
(if (featurep 'lsp-eslint)
(lsp))
))))
(python-mode
. ((eval . (progn
(ignore-errors (require 'jedi-core))
(ignore-errors (require 'lsp-pyright))
(ignore-errors (sphinx-doc-mode))
(setq-local python-environment-virtualenv
(list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
;;"--system-site-packages"
"--quiet"))
(setq-local pylint-command
(expand-file-name "bin/pylint" python-shell-virtualenv-root))
(if (featurep 'lsp-pyright)
(lsp))
;; pylint will find the '.pylintrc' file next to the CWD
;; https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options
(setq-local flycheck-pylintrc
".pylintrc")
;; flycheck & other python stuff should use the local py3 environment
(setq-local flycheck-python-pylint-executable
python-shell-interpreter)
;; use 'M-x jedi:show-setup-info' and 'M-x epc:controller' to inspect jedi server
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-root -- You
;; can specify a full path instead of a name (relative path). In that case,
;; python-environment-directory is ignored and Python virtual environment
;; is created at the specified path.
(setq-local jedi:environment-root
python-shell-virtualenv-root)
;; https://tkf.github.io/emacs-jedi/latest/#jedi:server-command
(setq-local jedi:server-command
(list python-shell-interpreter
jedi:server-script))
;; jedi:environment-virtualenv --> see above 'python-environment-virtualenv'
;; is set buffer local! No need to setup jedi:environment-virtualenv:
;;
;; Virtualenv command to use. A list of string. If it is nil,
;; python-environment-virtualenv is used instead. You must set non-nil
;; value to jedi:environment-root in order to make this setting work.
;;
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-virtualenv
;;
;; (setq-local jedi:environment-virtualenv
;; (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
;; "--python"
;; "/usr/bin/python3.4"
;; ))
))))
)

View File

@@ -1,6 +1,5 @@
* *
!container/*.template.*
!container/entrypoint.sh !container/entrypoint.sh
!searx/** !searx/**
!requirements*.txt !requirements*.txt

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,82 +53,109 @@ 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@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.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
- runner: ubuntu-26.04-arm
arch: armv7
steps:
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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 }}"
@@ -134,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
@@ -143,25 +174,24 @@ jobs:
packages: write packages: write
steps: steps:
- name: Login to Docker Hub - name: Checkout
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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:
@@ -33,6 +33,7 @@ jobs:
- update_engine_traits.py - update_engine_traits.py
- update_wikidata_units.py - update_wikidata_units.py
- update_engine_descriptions.py - update_engine_descriptions.py
- update_gsa_useragents.py
permissions: permissions:
contents: write contents: write
@@ -40,17 +41,17 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -64,17 +65,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,39 +25,36 @@ 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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}- python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup dependencies
run: sudo ./utils/searxng.sh install buildhost
- name: Setup venv - name: Setup venv
run: make V=1 install run: make V=1 install
- name: Build documentation - name: Build documentation
run: make V=1 docs.html run: make V=1 docs.clean docs.html
- if: github.ref_name == 'master' - if: github.ref_name == 'master'
name: Release name: Release
@@ -65,7 +62,7 @@ jobs:
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,17 +34,17 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -59,40 +59,37 @@ 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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
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,27 +26,27 @@ 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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -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,18 +83,18 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: "false"
- name: Sync GHCS from Docker Scout
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
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@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
sarif_file: "./scout.sarif"

View File

@@ -1,11 +0,0 @@
[[language]]
name = "python"
language-servers = ["basedpyright", "pylsp"]
auto-format = true
[language-server.pylsp.config.pylsp]
plugins.pylint.enabled = true
plugins.isort.enabled = true
plugins.black.enabled = true
plugins.black.skip_string_normalization = true
plugins.black.line_length = 120

View File

@@ -63,7 +63,7 @@ format: format.python format.shell
# wrap ./manage script # wrap ./manage script
MANAGE += weblate.translations.commit weblate.push.translations MANAGE += weblate.translations.commit weblate.push.translations
MANAGE += data.all data.traits data.useragents data.locales data.currencies MANAGE += data.all data.traits data.useragents data.gsa_useragents data.locales data.currencies
MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean
MANAGE += podman.build MANAGE += podman.build
MANAGE += docker.build docker.buildx MANAGE += docker.build docker.buildx

View File

@@ -2,12 +2,12 @@
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": { "files": {
"ignoreUnknown": true, "ignoreUnknown": true,
"includes": ["**", "!node_modules", "!src/brand", "!src/svg"] "includes": ["**", "!node_modules"]
}, },
"assist": { "assist": {
"enabled": true, "enabled": true,
"actions": { "actions": {
"preset": "recommended", "recommended": true,
"source": { "source": {
"useSortedAttributes": "on", "useSortedAttributes": "on",
"useSortedProperties": "on" "useSortedProperties": "on"
@@ -27,14 +27,12 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"preset": "recommended", "recommended": true,
"complexity": { "complexity": {
"noForEach": "error", "noForEach": "error",
"noImplicitCoercions": "error", "noImplicitCoercions": "error",
"noRedundantDefaultExport": "error",
"noUselessCatchBinding": "error", "noUselessCatchBinding": "error",
"noUselessUndefined": "error", "noUselessUndefined": "error",
"useArrayFind": "error",
"useSimplifiedLogicExpression": "error" "useSimplifiedLogicExpression": "error"
}, },
"correctness": { "correctness": {
@@ -44,11 +42,25 @@
"useSingleJsDocAsterisk": "error" "useSingleJsDocAsterisk": "error"
}, },
"nursery": { "nursery": {
"noContinue": "warn",
"noEqualsToNull": "warn",
"noFloatingPromises": "warn", "noFloatingPromises": "warn",
"noForIn": "warn",
"noIncrementDecrement": "warn",
"noMisusedPromises": "warn", "noMisusedPromises": "warn",
"noMultiAssign": "warn",
"noMultiStr": "warn",
"noNestedPromises": "warn",
"noParametersOnlyUsedInRecursion": "warn",
"noRedundantDefaultExport": "warn",
"noReturnAssign": "warn",
"noUselessReturn": "off",
"useAwaitThenable": "off", "useAwaitThenable": "off",
"useConsistentEnumValueType": "warn",
"useDestructuring": "warn",
"useExhaustiveSwitchCases": "warn", "useExhaustiveSwitchCases": "warn",
"useExplicitType": "off", "useExplicitType": "off",
"useFind": "warn",
"useRegexpExec": "warn" "useRegexpExec": "warn"
}, },
"performance": { "performance": {
@@ -63,15 +75,23 @@
"noCommonJs": "error", "noCommonJs": "error",
"noEnum": "error", "noEnum": "error",
"noImplicitBoolean": "error", "noImplicitBoolean": "error",
"noIncrementDecrement": "error",
"noInferrableTypes": "error", "noInferrableTypes": "error",
"noMultiAssign": "error",
"noMultilineString": "error",
"noNamespace": "error", "noNamespace": "error",
"noNegationElse": "error", "noNegationElse": "error",
"noNestedTernary": "error", "noNestedTernary": "error",
"noParameterAssign": "error", "noParameterAssign": "error",
"noParameterProperties": "error", "noParameterProperties": "error",
"noRestrictedTypes": {
"level": "error",
"options": {
"types": {
"Element": {
"message": "Element is too generic",
"use": "HTMLElement"
}
}
}
},
"noSubstr": "error", "noSubstr": "error",
"noUnusedTemplateLiteral": "error", "noUnusedTemplateLiteral": "error",
"noUselessElse": "error", "noUselessElse": "error",
@@ -87,7 +107,6 @@
} }
}, },
"useConsistentBuiltinInstantiation": "error", "useConsistentBuiltinInstantiation": "error",
"useConsistentEnumValueType": "error",
"useConsistentMemberAccessibility": { "useConsistentMemberAccessibility": {
"level": "error", "level": "error",
"options": { "options": {
@@ -107,7 +126,6 @@
} }
}, },
"useDefaultSwitchClause": "error", "useDefaultSwitchClause": "error",
"useDestructuring": "error",
"useExplicitLengthCheck": "error", "useExplicitLengthCheck": "error",
"useForOf": "error", "useForOf": "error",
"useGroupedAccessorPairs": "error", "useGroupedAccessorPairs": "error",
@@ -124,17 +142,13 @@
"useUnifiedTypeSignatures": "error" "useUnifiedTypeSignatures": "error"
}, },
"suspicious": { "suspicious": {
"noAlert": "error",
"noBitwiseOperators": "error", "noBitwiseOperators": "error",
"noConstantBinaryExpressions": "error", "noConstantBinaryExpressions": "error",
"noDeprecatedImports": "error", "noDeprecatedImports": "error",
"noEmptyBlockStatements": "error", "noEmptyBlockStatements": "error",
"noEqualsToNull": "error",
"noEvolvingTypes": "error", "noEvolvingTypes": "error",
"noForIn": "error",
"noImportCycles": "error", "noImportCycles": "error",
"noNestedPromises": "error",
"noParametersOnlyUsedInRecursion": "error",
"noReturnAssign": "error",
"noUnassignedVariables": "error", "noUnassignedVariables": "error",
"noVar": "error", "noVar": "error",
"useNumberToFixedDigitsArgument": "error", "useNumberToFixedDigitsArgument": "error",

File diff suppressed because it is too large Load Diff

View File

@@ -29,21 +29,21 @@
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.5", "@biomejs/biome": "2.4.13",
"@types/node": "^26.1.1", "@types/node": "^25.6.0",
"browserslist": "^4.28.7", "browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"edge.js": "^6.5.1", "edge.js": "^6.5.0",
"less": "^4.8.0", "less": "^4.6.4",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.3", "sharp": "~0.34.5",
"sort-package-json": "^4.0.0", "sort-package-json": "^3.6.1",
"stylelint": "^17.14.1", "stylelint": "^17.9.1",
"stylelint-config-standard-less": "^4.1.0", "stylelint-config-standard-less": "^4.1.0",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svgo": "^4.0.2", "svgo": "^4.0.1",
"typescript": "~7.0.2", "typescript": "~6.0.3",
"vite": "^8.1.5", "vite": "^8.0.10",
"vite-bundle-analyzer": "^1.3.9" "vite-bundle-analyzer": "^1.3.8"
} }
} }

View File

@@ -35,9 +35,8 @@ const imageLoader = (resultElement: HTMLElement): void => {
}, 1000) as unknown as number; }, 1000) as unknown as number;
}; };
const imageThumbnails: NodeListOf<HTMLImageElement> = document.querySelectorAll<HTMLImageElement>( const imageThumbnails: NodeListOf<HTMLImageElement> =
"#urls img.image_thumbnail, img.thumbnail" document.querySelectorAll<HTMLImageElement>("#urls img.image_thumbnail");
);
for (const thumbnail of imageThumbnails) { for (const thumbnail of imageThumbnails) {
if (thumbnail.complete && thumbnail.naturalWidth === 0) { if (thumbnail.complete && thumbnail.naturalWidth === 0) {
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`; thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;

View File

@@ -77,9 +77,9 @@ export default class Calculator extends Plugin {
protected async run(): Promise<string | undefined> { protected async run(): Promise<string | undefined> {
const searchInput = getElement<HTMLInputElement>("q"); const searchInput = getElement<HTMLInputElement>("q");
const node = Calculator.math.parse(searchInput.value);
try { try {
const node = Calculator.math.parse(searchInput.value);
return `${node.toString()} = ${node.evaluate()}`; return `${node.toString()} = ${node.evaluate()}`;
} catch { } catch {
// not a compatible math expression // not a compatible math expression

View File

@@ -5,6 +5,7 @@ import type { KeyBindingLayout } from "./main/keyboard.ts";
// synced with searx/webapp.py get_client_settings // synced with searx/webapp.py get_client_settings
type Settings = { type Settings = {
plugins?: string[]; plugins?: string[];
advanced_search?: boolean;
autocomplete?: string; autocomplete?: string;
autocomplete_min?: number; autocomplete_min?: number;
doi_resolver?: string; doi_resolver?: string;

View File

@@ -7,6 +7,7 @@
@import "mixins.less"; @import "mixins.less";
@import "toolkit.less"; @import "toolkit.less";
@import "autocomplete.less"; @import "autocomplete.less";
@import "detail.less";
@import "animations.less"; @import "animations.less";
@import "embedded.less"; @import "embedded.less";
@import "info.less"; @import "info.less";
@@ -1164,4 +1165,3 @@ pre code {
@import "result_types/code.less"; @import "result_types/code.less";
@import "result_types/paper.less"; @import "result_types/paper.less";
@import "result_types/file.less"; @import "result_types/file.less";
@import "result_types/image.less";

View File

@@ -1,4 +1,4 @@
FROM docker.io/searxng/base:searxng-builder AS builder FROM ghcr.io/searxng/base:searxng-builder AS builder
COPY ./requirements.txt ./requirements-server.txt ./ COPY ./requirements.txt ./requirements-server.txt ./
@@ -21,6 +21,8 @@ RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/ COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
ARG TIMESTAMP_SETTINGS="0"
RUN set -eux -o pipefail; \ RUN set -eux -o pipefail; \
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \ python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \
find ./searx/static/ -type f \ find ./searx/static/ -type f \
@@ -28,4 +30,5 @@ RUN set -eux -o pipefail; \
-exec gzip -9 -k {} + \ -exec gzip -9 -k {} + \
-exec brotli -9 -k {} + \ -exec brotli -9 -k {} + \
-exec gzip --test {}.gz + \ -exec gzip --test {}.gz + \
-exec brotli --test {}.br + -exec brotli --test {}.br +; \
touch -c --date="@$TIMESTAMP_SETTINGS" ./searx/settings.yml

View File

@@ -2,7 +2,7 @@ ARG CONTAINER_IMAGE_ORGANIZATION="searxng"
ARG CONTAINER_IMAGE_NAME="searxng" ARG CONTAINER_IMAGE_NAME="searxng"
FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder
FROM docker.io/searxng/base:searxng AS dist FROM ghcr.io/searxng/base:searxng AS dist
COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/ COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/
COPY --chown=977:977 --from=builder /usr/local/searxng/searx/ ./searx/ COPY --chown=977:977 --from=builder /usr/local/searxng/searx/ ./searx/

View File

@@ -77,23 +77,43 @@ volume_handler() {
setup_ownership "$target" "directory" setup_ownership "$target" "directory"
} }
setup() { # Handle configuration file updates
local template_settings="/usr/local/searxng/settings.template.yml" config_handler() {
local target_settings="$__SEARXNG_CONFIG_PATH/settings.yml" local target="$1"
local template="$2"
local new_template_target="$target.new"
if [ ! -f "$target_settings" ]; then # Create/Update the configuration file
if [ -f "$target" ]; then
setup_ownership "$target" "file"
if [ "$template" -nt "$target" ]; then
cp -pfT "$template" "$new_template_target"
cat <<EOF
...
... INFORMATION
... Update available for "$target"
... It is recommended to update the configuration file to ensure proper functionality
...
... New version placed at "$new_template_target"
... Please review and merge changes
...
EOF
fi
else
cat <<EOF cat <<EOF
... ...
... INFORMATION ... INFORMATION
... "$target_settings" does not exist, creating from template... ... "$target" does not exist, creating from template...
... ...
EOF EOF
cp -pfT "$template_settings" "$target_settings" cp -pfT "$template" "$target"
sed -i "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" "$target_settings" sed -i "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" "$target"
fi fi
check_file "$target_settings" check_file "$target"
} }
cat <<EOF cat <<EOF
@@ -104,7 +124,8 @@ EOF
volume_handler "$__SEARXNG_CONFIG_PATH" volume_handler "$__SEARXNG_CONFIG_PATH"
volume_handler "$__SEARXNG_DATA_PATH" volume_handler "$__SEARXNG_DATA_PATH"
setup # Check for files
config_handler "$__SEARXNG_SETTINGS_PATH" "/usr/local/searxng/searx/settings.yml"
# root only features # root only features
if [ "$(id -u)" -eq 0 ]; then if [ "$(id -u)" -eq 0 ]; then
@@ -112,15 +133,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

@@ -1,8 +0,0 @@
# Read the documentation before extending the defaults:
# https://docs.searxng.org/admin/settings/
use_default_settings: true
server:
secret_key: "ultrasecretkey"
image_proxy: true

View File

@@ -283,12 +283,12 @@ container images are not officially supported):
$ make container $ make container
$ docker images $ docker images
REPOSITORY TAG IMAGE ID SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/searxng/searxng 2026.6.19-93f66bfb4 ... 265 MB localhost/searxng/searxng 2025.8.1-3d96414 ... About a minute ago 183 MB
localhost/searxng/searxng latest ... 265 MB localhost/searxng/searxng latest ... About a minute ago 183 MB
localhost/searxng/searxng builder ... 687 MB localhost/searxng/searxng builder ... About a minute ago 524 MB
docker.io/searxng/base searxng-builder ... 565 MB ghcr.io/searxng/base searxng-builder ... 2 days ago 378 MB
docker.io/searxng/base searxng ... 143 MB ghcr.io/searxng/base searxng ... 2 days ago 42.2 MB
Migrate from ``searxng-docker`` Migrate from ``searxng-docker``
=============================== ===============================

View File

@@ -32,7 +32,7 @@ By default and without any extensions, SearXNG serves these resolvers:
- ``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.

View File

@@ -19,7 +19,6 @@ Settings
settings_search settings_search
settings_server settings_server
settings_ui settings_ui
settings_preferences
settings_redis settings_redis
settings_valkey settings_valkey
settings_outgoing settings_outgoing

View File

@@ -1,8 +0,0 @@
.. _settings preferences:
================
``preferences:``
================
.. autoclass:: searx._settings.SettingsPref
:members:

View File

@@ -43,7 +43,6 @@
- ``google`` - ``google``
- ``mwmbl`` - ``mwmbl``
- ``naver`` - ``naver``
- ``privacywall``
- ``quark`` - ``quark``
- ``qwant`` - ``qwant``
- ``seznam`` - ``seznam``

View File

@@ -47,7 +47,6 @@
activated: activated:
- :py:obj:`searx.botdetection.link_token` in the :ref:`limiter` - :py:obj:`searx.botdetection.link_token` in the :ref:`limiter`
- :ref:`image_proxy`
.. _image_proxy: .. _image_proxy:

View File

@@ -169,7 +169,7 @@ ${fedora_build}
$ sudo -H -u ${SERVICE_USER} -i $ sudo -H -u ${SERVICE_USER} -i
(${SERVICE_USER})$ cd ${SEARXNG_SRC} (${SERVICE_USER})$ cd ${SEARXNG_SRC}
(${SERVICE_USER})$ export SEARXNG_SETTINGS_PATH=\"${SEARXNG_SETTINGS_PATH}\" (${SERVICE_USER})$ export SEARXNG_SETTINGS_PATH=\"${SEARXNG_SETTINGS_PATH}\"
(${SERVICE_USER})$ python -m searx.webapp (${SERVICE_USER})$ python searx/webapp.py
# disable debug # disable debug
$ sudo -H sed -i -e \"s/debug : True/debug : False/g\" \"$SEARXNG_SETTINGS_PATH\" $ sudo -H sed -i -e \"s/debug : True/debug : False/g\" \"$SEARXNG_SETTINGS_PATH\"

View File

@@ -107,7 +107,7 @@ module:
======================= =========== =========================================== ======================= =========== ===========================================
base_url string base-url, can be overwritten to use same base_url string base-url, can be overwritten to use same
engine on other URL engine on other URL
page_size int maximum number of results per request number_of_results int maximum number of results per request
language string ISO code of language and country like en_US language string ISO code of language and country like en_US
api_key string api-key if required by engine api_key string api-key if required by engine
======================= =========== =========================================== ======================= =========== ===========================================

View File

@@ -1,8 +0,0 @@
.. _500px engine:
=====
500px
=====
.. automodule:: searx.engines.500px
:members:

View File

@@ -0,0 +1,8 @@
.. _aol engine:
===
AOL
===
.. automodule:: searx.engines.aol
:members:

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
.. _kagi engines:
============
Kagi Engines
============
.. automodule:: searx.engines.kagi
:members:

View File

@@ -0,0 +1,8 @@
.. _karmasearch engine:
===========
Karmasearch
===========
.. automodule:: searx.engines.karmasearch
:members:

View File

@@ -60,29 +60,11 @@ into the developer environment and start a python based HTTP server by::
$ ./manage dev.env $ ./manage dev.env
... ...
(dev.env)$ SEARXNG_DEBUG=1 searxng-run (dev.env)$ SEARXNG_DEBUG=1 python -m searx.webapp
Since this is a pure Python solution, you can set breakpoints in your code with Since this is a pure Python solution, you can set breakpoints in your code with
``pdb.set_trace()`` and the debugger will wait for you in the terminal prompt. ``pdb.set_trace()`` and the debugger will wait for you in the terminal prompt.
Any other script or command line provided by SearXNG can also be used in the
same environment, here are a few examples::
# tools related to favicons
(dev.env)$ python -m searx.favicons
# tools related to DATA stored in searx/data
(dev.env)$ python -m searx.data --help
# tools related to engines
(dev.env)$ python -m searx.enginelib --help
# to test one of the update scripts
(dev.env)$ searxng_extra/update/update_engine_traits.py --help
# to test the update of the wikidata units
(dev.env)$ searxng_extra/update/update_wikidata_units.py
.. sidebar:: further read .. sidebar:: further read

View File

@@ -1,7 +0,0 @@
.. _result_types.image:
=============
Image Results
=============
.. automodule:: searx.result_types.image

View File

@@ -1,8 +1,4 @@
.. _result_types.mainresult: .. _result_types.mainresult:
============
Main Results
============
.. autoclass:: searx.result_types._base.MainResult .. autoclass:: searx.result_types._base.MainResult
:members: :members:

View File

@@ -18,13 +18,13 @@ following types have been implemented so far ..
main/code main/code
main/paper main/paper
main/file main/file
main/image
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that The :ref:`LegacyResult <LegacyResult>` is used internally for the results that
have not yet been typed. The templates can be used as orientation until the have not yet been typed. The templates can be used as orientation until the
final typing is complete. final typing is complete.
- :ref:`template default` / :py:obj:`Result` - :ref:`template default` / :py:obj:`Result`
- :ref:`template images`
- :ref:`template videos` - :ref:`template videos`
- :ref:`template torrent` - :ref:`template torrent`
- :ref:`template map` - :ref:`template map`

View File

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

View File

@@ -129,6 +129,53 @@ audio_src : uri,
URL of an embedded ``<audio controls>``. URL of an embedded ``<audio controls>``.
.. _template images:
``images.html``
---------------
The images are displayed as small thumbnails in the main results list.
title : :py:class:`str`
Title of the image.
thumbnail_src : :py:class:`str`
URL of a preview of the image.
resolution :py:class:`str`
The resolution of the image (e.g. ``1920 x 1080`` pixel)
Image labels
~~~~~~~~~~~~
Clicking on the preview opens a gallery view in which all further metadata for
the image is displayed. Addition fields used in the :origin:`images.html
<searx/templates/simple/result_templates/images.html>`:
img_src : :py:class:`str`
URL of the full size image.
content: :py:class:`str`
Description of the image.
author: :py:class:`str`
Name of the author of the image.
img_format : :py:class:`str`
The format of the image (e.g. ``png``).
source : :py:class:`str`
Source of the image.
filesize: :py:class:`str`
Size of bytes in :py:obj:`human readable <searx.humanize_bytes>` notation
(e.g. ``MB`` for 1024 \* 1024 Bytes filesize).
url : :py:class:`str`
URL of the page from where the images comes from (source).
.. _template videos: .. _template videos:
``videos.html`` ``videos.html``

View File

@@ -58,8 +58,8 @@ Configured Engines
{% for mod in engines %} {% for mod in engines %}
* - `{{mod.name}} <{{mod.about and mod.about.website}}>`_ * - `{{mod.name}} <{{mod.about and mod.about.website}}>`_
{%- if mod.language %} {%- if mod.about and mod.about.language %}
({{mod.language | upper}}) ({{mod.about.language | upper}})
{%- endif %} {%- endif %}
- ``!{{mod.shortcut}}`` - ``!{{mod.shortcut}}``
- {%- if 'searx.engines.' + mod.__name__ in documented_modules %} - {%- if 'searx.engines.' + mod.__name__ in documented_modules %}

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

@@ -2,27 +2,27 @@ 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==25.9.0 black==25.9.0
pylint==4.0.6 pylint==4.0.5
splinter==0.21.0 splinter==0.21.0
selenium==4.46.0 selenium==4.43.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
sphinx-jinja==2.0.2 sphinx-jinja==2.0.2
sphinx-tabs==3.5.0 sphinx-tabs==3.5.0
furo==2025.12.19 furo==2025.12.19
sphinxcontrib-programoutput==0.20 sphinxcontrib-programoutput==0.19
sphinx-autobuild==2025.8.25 sphinx-autobuild==2025.8.25
sphinx-notfound-page==1.1.0 sphinx-notfound-page==1.1.0
myst-parser==5.0.0 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.0 wlc==2.0.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.7.9 granian[reload]==2.7.4
basedpyright==1.39.9 basedpyright==1.39.3
types-lxml==2026.2.16 types-lxml==2026.2.16

View File

@@ -1,2 +1,2 @@
granian==2.7.9 granian==2.7.4
granian[pname]==2.7.9 granian[pname]==2.7.4

View File

@@ -1,9 +1,9 @@
certifi==2026.7.22 certifi==2026.4.22
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.1 lxml==6.1.0
pygments==2.20.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
@@ -11,9 +11,9 @@ httpx[http2]==0.28.1
httpx-socks[asyncio]==0.10.0 httpx-socks[asyncio]==0.10.0
sniffio==1.3.1 sniffio==1.3.1
valkey==6.1.1 valkey==6.1.1
markdown-it-py==4.2.0 markdown-it-py==4.0.0
msgspec==0.21.1 msgspec==0.21.1
typer==0.27.0 typer==0.25.1
isodate==0.7.2 isodate==0.7.2
whitenoise==6.12.0 whitenoise==6.12.0
typing-extensions==4.16.0 typing-extensions==4.15.0

View File

@@ -10,7 +10,6 @@ from os.path import dirname, abspath
import logging import logging
import msgspec import msgspec
from ._settings import SettingsPref
# Debug # Debug
LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s' LOG_FORMAT_DEBUG: str = '%(levelname)-7s %(name)-30.30s: %(message)s'
@@ -48,12 +47,6 @@ def init_settings():
settings.clear() settings.clear()
settings.update(cfg) settings.update(cfg)
if get_setting("server.public_instance"):
# enable image proxy for public instances #6125
settings["server"]["image_proxy"] = True
pref: SettingsPref = get_setting("preferences")
pref.lock.add("image_proxy")
sxng_debug = get_setting("general.debug") sxng_debug = get_setting("general.debug")
if sxng_debug: if sxng_debug:
_logging_config_debug() _logging_config_debug()
@@ -73,7 +66,7 @@ def init_settings():
if settings['server']['public_instance']: if settings['server']['public_instance']:
logger.warning( logger.warning(
"Be aware you have activated features intended only for public instances. " "Be aware you have activated features intended only for public instances. "
"This force the usage of the limiter, link_token and image proxy / " "This force the usage of the limiter and link_token / "
"see https://docs.searxng.org/admin/searx.limiter.html" "see https://docs.searxng.org/admin/searx.limiter.html"
) )

View File

@@ -1,42 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of the :py:obj:`preference <searx.preference>` settings."""
# pylint: disable = too-few-public-methods
import typing as t
import msgspec
class SettingsPref(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
"""Options for configuring the preferences
.. code:: yaml
preferences:
lock:
- favicon_resolver
- image_proxy
- method
# ...
"""
lock: set[
t.Literal[
"categories",
"language",
"locale",
"autocomplete",
"favicon_resolver",
"image_proxy",
"method",
"safesearch",
"theme",
"results_on_new_tab",
"doi_resolver",
"simple_style",
"center_alignment",
"query_in_title",
"search_on_category_select",
]
] = set()
"""Lock arbitrary settings on the preferences page."""

View File

@@ -21,8 +21,6 @@ from searx.engines import (
from searx.network import get as http_get, post as http_post from searx.network import get as http_get, post as http_post
from searx.exceptions import SearxEngineResponseException from searx.exceptions import SearxEngineResponseException
from searx.utils import extr, gen_useragent from searx.utils import extr, gen_useragent
from searx.data import ENGINE_TRAITS
from searx.enginelib.traits import EngineTraits
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -135,9 +133,7 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
""" """
data = ENGINE_TRAITS.get("google") or {} google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, engines['google'].traits)
traits = EngineTraits(**data)
google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, traits)
url = 'https://{subdomain}/complete/search?{args}' url = 'https://{subdomain}/complete/search?{args}'
args = urlencode( args = urlencode(
{ {
@@ -183,23 +179,6 @@ def naver(query: str, _sxng_locale: str) -> list[str]:
return results return results
def privacywall(query: str, sxng_locale: str) -> list[str]:
# Privacywall search autocompleter
country = None
if "-" in sxng_locale:
country = sxng_locale.split("-")[1]
args = {'q': query, 'cc': country}
url = f"https://www.privacywall.org/search/secure/suggestions.php?{urlencode(args)}"
response = get(url)
if not response.ok:
return []
data: list[list[str]] = response.json()
return data[1]
def qihu360search(query: str, _sxng_locale: str) -> list[str]: def qihu360search(query: str, _sxng_locale: str) -> list[str]:
# 360Search search autocompleter # 360Search search autocompleter
url = f"https://sug.so.360.cn/suggest?{urlencode({'format': 'json', 'word': query})}" url = f"https://sug.so.360.cn/suggest?{urlencode({'format': 'json', 'word': query})}"
@@ -382,7 +361,6 @@ backends: dict[str, t.Callable[[str, str], list[str]]] = {
'google': google_complete, 'google': google_complete,
'mwmbl': mwmbl, 'mwmbl': mwmbl,
'naver': naver, 'naver': naver,
'privacywall': privacywall,
'quark': quark, 'quark': quark,
'qwant': qwant, 'qwant': qwant,
'seznam': seznam, 'seznam': seznam,

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``."""
@@ -114,13 +114,7 @@ class ExpireCacheStats:
if expire: if expire:
valid_until = datetime.datetime.fromtimestamp(expire).strftime("%Y-%m-%d %H:%M:%S") valid_until = datetime.datetime.fromtimestamp(expire).strftime("%Y-%m-%d %H:%M:%S")
c_kv += 1 c_kv += 1
value_str = str(value) lines.append(f"[{ctx_name:20s}] {valid_until} {key:12}" f" --> ({type(value).__name__}) {value} ")
if len(value_str) > 120:
value_str = f"{value_str[:120]} ..."
lines.append(
f"[{ctx_name:20s}] {valid_until} {key:12}"
f" --> ({type(value).__name__}:{len(value)}) {value_str} "
)
lines.append(f"Number of contexts: {c_ctx}") lines.append(f"Number of contexts: {c_ctx}")
lines.append(f"number of key/value pairs: {c_kv}") lines.append(f"number of key/value pairs: {c_kv}")
@@ -444,10 +438,12 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
def get(self, key: str, default: typing.Any = None, ctx: str | None = None) -> typing.Any: def get(self, key: str, default: typing.Any = None, ctx: str | None = None) -> typing.Any:
"""Get value of ``key`` from table given by argument ``ctx``. If """Get value of ``key`` from table given by argument ``ctx``. If
``ctx`` argument is ``None`` (the default), a table name is generated ``ctx`` argument is ``None`` (the default), a table name is generated
from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists in from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists (in
the table or the table not exists, the ``default`` value is returned. table), the ``default`` value is returned.
""" """
table = ctx table = ctx
self.maintenance()
if not table: if not table:
table = self.normalize_name(self.cfg.name) table = self.normalize_name(self.cfg.name)
@@ -455,39 +451,24 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
if table not in self.table_names: if table not in self.table_names:
return default return default
# Before values are taken from the table, a maintenance interval may sql = f"SELECT value FROM {table} WHERE key = ?"
# need to be carried out.
self.maintenance()
sql = f"SELECT value, expire 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``.
If ``ctx`` argument is ``None`` (the default), a table name is If ``ctx`` argument is ``None`` (the default), a table name is
generated from the :py:obj:`ExpireCacheCfg.name`.""" generated from the :py:obj:`ExpireCacheCfg.name`."""
table = ctx table = ctx
self.maintenance()
if not table: if not table:
table = self.normalize_name(self.cfg.name) table = self.normalize_name(self.cfg.name)
if table in self.table_names: if table in self.table_names:
# Before values are taken from the table, a maintenance interval may
# need to be carried out.
self.maintenance()
for row in self.DB.execute(f"SELECT key, value FROM {table}"): for row in self.DB.execute(f"SELECT key, value FROM {table}"):
yield row[0], self.deserialize(row[1]) yield row[0], self.deserialize(row[1])

View File

@@ -6,7 +6,7 @@ make data.all
""" """
# pylint: disable=invalid-name # pylint: disable=invalid-name
__all__ = ["ahmia_blacklist_loader", "data_dir", "get_cache"] __all__ = ["ahmia_blacklist_loader", "gsa_useragents_loader", "data_dir", "get_cache"]
import json import json
import typing as t import typing as t
@@ -63,6 +63,7 @@ lazy_globals = {
"ENGINE_TRAITS": None, "ENGINE_TRAITS": None,
"LOCALES": None, "LOCALES": None,
"TRACKER_PATTERNS": TrackerPatternsDB(), "TRACKER_PATTERNS": TrackerPatternsDB(),
"GSA_USER_AGENTS": None,
} }
data_json_files = { data_json_files = {
@@ -105,3 +106,24 @@ def ahmia_blacklist_loader() -> list[str]:
""" """
with open(data_dir / 'ahmia_blacklist.txt', encoding='utf-8') as f: with open(data_dir / 'ahmia_blacklist.txt', encoding='utf-8') as f:
return f.read().split() return f.read().split()
def gsa_useragents_loader() -> list[str]:
"""Load data from `gsa_useragents.txt` and return a list of user agents
suitable for Google. The user agents are fetched by::
searxng_extra/update/update_gsa_useragents.py
This function is used by :py:mod:`searx.engines.google`.
"""
data = lazy_globals["GSA_USER_AGENTS"]
if data is not None:
return data
log.debug("init searx.data.%s", "GSA_USER_AGENTS")
with open(data_dir / 'gsa_useragents.txt', encoding='utf-8') as f:
lazy_globals["GSA_USER_AGENTS"] = f.read().splitlines()
return lazy_globals["GSA_USER_AGENTS"]

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,6 @@
"sr": "УАЕ дирхам", "sr": "УАЕ дирхам",
"sv": "Emiratisk dirham", "sv": "Emiratisk dirham",
"ta": "ஐக்கிய அரபு அமீரக திர்கம்", "ta": "ஐக்கிய அரபு அமீரக திர்கம்",
"te": "యూఏఈ దిర్హామ్",
"tr": "Birleşik Arap Emirlikleri dirhemi", "tr": "Birleşik Arap Emirlikleri dirhemi",
"tt": "БГӘ дирһәме", "tt": "БГӘ дирһәме",
"uk": "дирхам ОАЕ" "uk": "дирхам ОАЕ"
@@ -166,7 +165,7 @@
"pap": "lek albanes", "pap": "lek albanes",
"pl": "lek", "pl": "lek",
"pt": "lek", "pt": "lek",
"ro": "lek albanez", "ro": "Lek",
"ru": "албанский лек", "ru": "албанский лек",
"sk": "Albánsky lek", "sk": "Albánsky lek",
"sl": "albanski lek", "sl": "albanski lek",
@@ -739,7 +738,6 @@
"sr": "брунејски долар", "sr": "брунејски долар",
"sv": "Bruneisk dollar", "sv": "Bruneisk dollar",
"ta": "புரூணை டாலர்", "ta": "புரூணை டாலர்",
"te": "బ్రూనై డాలర్",
"th": "ดอลลาร์บรูไน", "th": "ดอลลาร์บรูไน",
"tr": "Brunei doları", "tr": "Brunei doları",
"tt": "Бруней доллары", "tt": "Бруней доллары",
@@ -773,7 +771,7 @@
"nl": "Boliviaanse boliviano", "nl": "Boliviaanse boliviano",
"oc": "Boliviano", "oc": "Boliviano",
"pa": "ਬੋਲੀਵੀਆਨੋ", "pa": "ਬੋਲੀਵੀਆਨੋ",
"pl": "boliwiano", "pl": "boliviano",
"pt": "Boliviano", "pt": "Boliviano",
"ro": "boliviano", "ro": "boliviano",
"ru": "боливиано", "ru": "боливиано",
@@ -913,7 +911,6 @@
"sr": "бутански нгултрум", "sr": "бутански нгултрум",
"sv": "Ngultrum", "sv": "Ngultrum",
"ta": "பூட்டானின் இங்குல்ட்ரம்", "ta": "பூட்டானின் இங்குல்ட்ரம்",
"te": "భూటానీస్ గుల్త్రమ్",
"th": "งุลตรัมภูฏาน", "th": "งุลตรัมภูฏาน",
"tr": "Ngultrum", "tr": "Ngultrum",
"tt": "ңгултрум", "tt": "ңгултрум",
@@ -992,7 +989,7 @@
"pa": "ਬੈਲਾਰੂਸੀ ਰੂਬਲ", "pa": "ਬੈਲਾਰੂਸੀ ਰੂਬਲ",
"pl": "Rubel białoruski", "pl": "Rubel białoruski",
"pt": "Rublo bielorrusso", "pt": "Rublo bielorrusso",
"ro": "rublă belarusă", "ro": "Rublă belarusă",
"ru": "Белорусский рубль", "ru": "Белорусский рубль",
"sk": "Bieloruský rubeľ", "sk": "Bieloruský rubeľ",
"sl": "beloruski rubelj", "sl": "beloruski rubelj",
@@ -1086,7 +1083,6 @@
"sr": "канадски долар", "sr": "канадски долар",
"sv": "kanadensisk dollar", "sv": "kanadensisk dollar",
"ta": "கனடா டொலர்", "ta": "கனடா டொலர்",
"te": "కెనడియన్ డాలర్",
"th": "ดอลลาร์แคนาดา", "th": "ดอลลาร์แคนาดา",
"tr": "Kanada doları", "tr": "Kanada doları",
"tt": "Канада дуллыры", "tt": "Канада дуллыры",
@@ -1107,7 +1103,7 @@
"fi": "Kongon frangi", "fi": "Kongon frangi",
"fr": "franc congolais", "fr": "franc congolais",
"ga": "franc an Chongó", "ga": "franc an Chongó",
"gl": "franco congolés", "gl": "Franco congolés",
"he": "פרנק קונגולזי", "he": "פרנק קונגולזי",
"hr": "Kongoanski franak", "hr": "Kongoanski franak",
"hu": "kongói frank", "hu": "kongói frank",
@@ -1235,7 +1231,6 @@
"sl": "čilski peso", "sl": "čilski peso",
"sr": "чилеански пезос", "sr": "чилеански пезос",
"sv": "Chilensk peso", "sv": "Chilensk peso",
"te": "చిలీ పెసో",
"th": "เปโซชิลี", "th": "เปโซชิลี",
"tr": "Şili pesosu", "tr": "Şili pesosu",
"tt": "Чили песосы", "tt": "Чили песосы",
@@ -1289,7 +1284,6 @@
"sr": "ренминби", "sr": "ренминби",
"sv": "Renminbi", "sv": "Renminbi",
"ta": "ரென்மின்பி", "ta": "ரென்மின்பி",
"te": "రెన్మిన్బి",
"th": "เหรินหมินปี้", "th": "เหรินหมินปี้",
"tr": "Renminbi", "tr": "Renminbi",
"tt": "юән", "tt": "юән",
@@ -1636,7 +1630,7 @@
"fi": "Algerian dinaari", "fi": "Algerian dinaari",
"fr": "dinar algérien", "fr": "dinar algérien",
"ga": "dinar na hAilgéire", "ga": "dinar na hAilgéire",
"gl": "dinar alxeriano", "gl": "Dinar alxeriano",
"he": "דינר אלג'ירי", "he": "דינר אלג'ירי",
"hr": "Alžirski dinar", "hr": "Alžirski dinar",
"hu": "algériai dinár", "hu": "algériai dinár",
@@ -1651,7 +1645,7 @@
"pap": "dinar argelino", "pap": "dinar argelino",
"pl": "dinar algierski", "pl": "dinar algierski",
"pt": "dinar argelino", "pt": "dinar argelino",
"ro": "dinar algerian", "ro": "Dinar algerian",
"ru": "алжирский динар", "ru": "алжирский динар",
"sk": "Alžírský dinár", "sk": "Alžírský dinár",
"sl": "alžirski dinar", "sl": "alžirski dinar",
@@ -1708,7 +1702,7 @@
"vi": "Bảng Ai Cập" "vi": "Bảng Ai Cập"
}, },
"ERN": { "ERN": {
"ar": قفة", "ar": اكفا",
"ca": "nakfa", "ca": "nakfa",
"cs": "Eritrejská nakfa", "cs": "Eritrejská nakfa",
"da": "Nakfa", "da": "Nakfa",
@@ -1782,7 +1776,7 @@
"bg": "евро", "bg": "евро",
"bn": "ইউরো", "bn": "ইউরো",
"ca": "euro", "ca": "euro",
"cs": "Euro", "cs": "euro",
"cy": "Ewro", "cy": "Ewro",
"da": "Euro", "da": "Euro",
"de": "Euro", "de": "Euro",
@@ -1800,10 +1794,10 @@
"hu": "euró", "hu": "euró",
"ia": "Euro", "ia": "Euro",
"id": "Euro", "id": "Euro",
"it": "Euro", "it": "euro",
"ja": "ユーロ", "ja": "ユーロ",
"ko": "유로", "ko": "유로",
"lt": "euras", "lt": "Euras",
"lv": "eiro", "lv": "eiro",
"ml": "യൂറോ", "ml": "യൂറോ",
"ms": "Euro", "ms": "Euro",
@@ -1811,11 +1805,11 @@
"oc": "Èuro", "oc": "Èuro",
"pa": "ਯੂਰੋ", "pa": "ਯੂਰੋ",
"pap": "Euro", "pap": "Euro",
"pl": "Euro", "pl": "euro",
"pt": "Euro", "pt": "Euro",
"ro": "Euro", "ro": "euro",
"ru": "евро", "ru": "евро",
"sk": "Euro", "sk": "euro",
"sl": "evro", "sl": "evro",
"sr": "евро", "sr": "евро",
"sv": "Euro", "sv": "Euro",
@@ -1941,7 +1935,7 @@
"sk": "libra šterlingov", "sk": "libra šterlingov",
"sl": "funt šterling", "sl": "funt šterling",
"sr": "британска фунта", "sr": "британска фунта",
"sv": "brittiskt pund", "sv": "Brittiskt pund",
"ta": "பிரித்தானிய பவுண்டு", "ta": "பிரித்தானிய பவுண்டு",
"th": "ปอนด์สเตอร์ลิง", "th": "ปอนด์สเตอร์ลิง",
"tr": "İngiliz sterlini", "tr": "İngiliz sterlini",
@@ -2416,7 +2410,6 @@
"pl": "rupia indonezyjska", "pl": "rupia indonezyjska",
"pt": "rupia indonésia", "pt": "rupia indonésia",
"ru": "индонезийская рупия", "ru": "индонезийская рупия",
"sk": "Indonézska rupia",
"sl": "indonezijska rupija", "sl": "indonezijska rupija",
"sr": "индонежанска рупија", "sr": "индонежанска рупија",
"sv": "Rupiah", "sv": "Rupiah",
@@ -2470,7 +2463,6 @@
"sr": "нови израелски шекел", "sr": "нови израелски шекел",
"sv": "Shekel", "sv": "Shekel",
"ta": "புது இசுரேலிய சேக்கல்", "ta": "புது இசுரேலிய சேக்கல்",
"te": "ఇజ్రాయెల్ షెకెల్",
"tr": "Yeni İsrail Şekeli", "tr": "Yeni İsrail Şekeli",
"tt": "Исраил шекеле", "tt": "Исраил шекеле",
"uk": "ізраїльський новий шекель" "uk": "ізраїльський новий шекель"
@@ -2759,14 +2751,13 @@
"pa": "ਜਪਾਨੀ ਯੈੱਨ", "pa": "ਜਪਾਨੀ ਯੈੱਨ",
"pl": "jen", "pl": "jen",
"pt": "iene", "pt": "iene",
"ro": "yen", "ro": "yeni",
"ru": "японская иена", "ru": "японская иена",
"sk": "jen", "sk": "jen",
"sl": "japonski jen", "sl": "japonski jen",
"sr": "јапански јен", "sr": "јапански јен",
"sv": "yen", "sv": "yen",
"ta": "யென்", "ta": "யென்",
"te": "జపనీస్ యెన్",
"th": "เยน", "th": "เยน",
"tr": "Japon yeni", "tr": "Japon yeni",
"tt": "япон иенасы", "tt": "япон иенасы",
@@ -2832,7 +2823,6 @@
"ja": "キルギス・ソム", "ja": "キルギス・ソム",
"ko": "키르기스스탄 솜", "ko": "키르기스스탄 솜",
"lt": "somas", "lt": "somas",
"lv": "Kirgizstānas soms",
"nl": "Kirgizische som", "nl": "Kirgizische som",
"pa": "ਕਿਰਗਿਜ਼ਸਤਾਨੀ ਸੋਮ", "pa": "ਕਿਰਗਿਜ਼ਸਤਾਨੀ ਸੋਮ",
"pl": "som", "pl": "som",
@@ -3246,7 +3236,6 @@
"sr": "шриланчанска рупија", "sr": "шриланчанска рупија",
"sv": "Lankesisk rupie", "sv": "Lankesisk rupie",
"ta": "இலங்கை ரூபாய்", "ta": "இலங்கை ரூபாய்",
"te": "శ్రీలంక రూపాయి",
"tr": "Sri Lanka rupisi", "tr": "Sri Lanka rupisi",
"tt": "Шри-Ланка рупиясе", "tt": "Шри-Ланка рупиясе",
"uk": "ланкійська рупія", "uk": "ланкійська рупія",
@@ -3337,7 +3326,6 @@
"fi": "Libyan dinaari", "fi": "Libyan dinaari",
"fr": "dinar libyen", "fr": "dinar libyen",
"ga": "dinar na Libia", "ga": "dinar na Libia",
"gl": "dinar libio",
"he": "דינר לובי ", "he": "דינר לובי ",
"hr": "Libijski dinar", "hr": "Libijski dinar",
"hu": "líbiai dinár", "hu": "líbiai dinár",
@@ -3540,7 +3528,6 @@
"ja": "チャット", "ja": "チャット",
"ko": "미얀마 짯", "ko": "미얀마 짯",
"lt": "Kijatas", "lt": "Kijatas",
"lv": "Kjats",
"ml": "ബർമ്മീസ് ക്യാറ്റ്", "ml": "ബർമ്മീസ് ക്യാറ്റ്",
"nl": "Myanmarese kyat", "nl": "Myanmarese kyat",
"oc": "Kyat", "oc": "Kyat",
@@ -3823,7 +3810,6 @@
"MXV": { "MXV": {
"de": "UNIDAD DE INVERSION", "de": "UNIDAD DE INVERSION",
"en": "unidad de inversión", "en": "unidad de inversión",
"eo": "Meksika unuo de investo",
"es": "Unidades de Inversión", "es": "Unidades de Inversión",
"ja": "メキシコ投資単位" "ja": "メキシコ投資単位"
}, },
@@ -3915,7 +3901,7 @@
"de": "Namibia-Dollar", "de": "Namibia-Dollar",
"en": "Namibian dollar", "en": "Namibian dollar",
"eo": "namibia dolaro", "eo": "namibia dolaro",
"es": "dólar namibio", "es": "Dólar namibio",
"fi": "Namibian dollari", "fi": "Namibian dollari",
"fr": "Dollar namibien", "fr": "Dollar namibien",
"ga": "dollar na Namaibe", "ga": "dollar na Namaibe",
@@ -4104,7 +4090,6 @@
"sr": "непалска рупија", "sr": "непалска рупија",
"sv": "Nepalesisk rupee", "sv": "Nepalesisk rupee",
"ta": "நேபாள ரூபாய்", "ta": "நேபாள ரூபாய்",
"te": "నేపాలీ రూపాయి",
"th": "รูปีเนปาล", "th": "รูปีเนปาล",
"tr": "Nepal rupisi", "tr": "Nepal rupisi",
"tt": "Непал рупиясе", "tt": "Непал рупиясе",
@@ -4312,7 +4297,7 @@
"ar": "بيسو فلبيني", "ar": "بيسو فلبيني",
"bg": "Филипинско песо", "bg": "Филипинско песо",
"ca": "peso filipí", "ca": "peso filipí",
"cs": "filipínské peso", "cs": "Filipínské peso",
"de": "philippinischer Peso", "de": "philippinischer Peso",
"en": "Philippine peso", "en": "Philippine peso",
"eo": "filipina peso", "eo": "filipina peso",
@@ -4340,7 +4325,6 @@
"sr": "филипински пезо", "sr": "филипински пезо",
"sv": "Filippinsk peso", "sv": "Filippinsk peso",
"ta": "பிலிப்பைன் பெசோ", "ta": "பிலிப்பைன் பெசோ",
"te": "ఫిలిప్పీన్ పెసో",
"th": "เปโซฟิลิปปินส์", "th": "เปโซฟิลิปปินส์",
"tr": "Filipinler pesosu", "tr": "Filipinler pesosu",
"tt": "Филипин писысы", "tt": "Филипин писысы",
@@ -4386,7 +4370,6 @@
"sr": "пакистанска рупија", "sr": "пакистанска рупија",
"sv": "Pakistansk rupee", "sv": "Pakistansk rupee",
"ta": "பாக்கித்தானிய ரூபாய்", "ta": "பாக்கித்தானிய ரூபாய்",
"te": "పాకిస్థానీ రూపాయి",
"tr": "Pakistan rupisi", "tr": "Pakistan rupisi",
"tt": "Пакстан рупиясе", "tt": "Пакстан рупиясе",
"uk": "пакистанська рупія", "uk": "пакистанська рупія",
@@ -4566,7 +4549,7 @@
"de": "rumänischer Leu", "de": "rumänischer Leu",
"en": "Romanian Leu", "en": "Romanian Leu",
"eo": "rumana leo", "eo": "rumana leo",
"es": "leu rumano", "es": "Leu rumano",
"et": "Rumeenia leu", "et": "Rumeenia leu",
"fi": "Romanian leu", "fi": "Romanian leu",
"fr": "leu roumain", "fr": "leu roumain",
@@ -4616,7 +4599,7 @@
"fi": "Serbian dinaari", "fi": "Serbian dinaari",
"fr": "dinar serbe", "fr": "dinar serbe",
"ga": "Dinar na Seirbia", "ga": "Dinar na Seirbia",
"gl": "dinar serbio", "gl": "Dinar serbio",
"he": "דינר סרבי", "he": "דינר סרבי",
"hr": "srpski dinar", "hr": "srpski dinar",
"hu": "szerb dinár", "hu": "szerb dinár",
@@ -4767,7 +4750,6 @@
"sr": "саудијски ријал", "sr": "саудијски ријал",
"sv": "Saudiarabisk rial", "sv": "Saudiarabisk rial",
"ta": "சவூதி ரியால்", "ta": "சவூதி ரியால்",
"te": "సౌదీ రియాల్",
"tr": "Suudi Arabistan riyali", "tr": "Suudi Arabistan riyali",
"tt": "Согуд риялы", "tt": "Согуд риялы",
"uk": "саудівський ріал", "uk": "саудівський ріал",
@@ -4966,7 +4948,6 @@
"sr": "сингапурски долар", "sr": "сингапурски долар",
"sv": "Singaporiansk dollar", "sv": "Singaporiansk dollar",
"ta": "சிங்கப்பூர் வெள்ளி", "ta": "சிங்கப்பூர் வெள்ளி",
"te": "సింగపూర్ డాలర్",
"th": "ดอลลาร์สิงคโปร์", "th": "ดอลลาร์สิงคโปร์",
"tr": "Singapur doları", "tr": "Singapur doları",
"tt": "Сингапур доллары", "tt": "Сингапур доллары",
@@ -5194,7 +5175,7 @@
"de": "syrische Lira", "de": "syrische Lira",
"en": "Syrian pound", "en": "Syrian pound",
"eo": "siria pundo", "eo": "siria pundo",
"es": "libra siria", "es": "Dolar sirio",
"fi": "Syyrian punta", "fi": "Syyrian punta",
"fr": "livre syrienne", "fr": "livre syrienne",
"ga": "punt na Siria", "ga": "punt na Siria",
@@ -5333,7 +5314,7 @@
"pa": "ਤਾਜਿਕਿਸਤਾਨੀ ਸੋਮੋਨੀ", "pa": "ਤਾਜਿਕਿਸਤਾਨੀ ਸੋਮੋਨੀ",
"pl": "Somoni", "pl": "Somoni",
"pt": "Somoni", "pt": "Somoni",
"ro": "somoni tadjic", "ro": "Somoni tadjic",
"ru": "таджикский сомони", "ru": "таджикский сомони",
"sk": "tadžický som", "sk": "tadžický som",
"sl": "tadžikistanski somoni", "sl": "tadžikistanski somoni",
@@ -5396,7 +5377,6 @@
"fi": "Tunisian dinaari", "fi": "Tunisian dinaari",
"fr": "dinar tunisien", "fr": "dinar tunisien",
"ga": "dinar na Túinéise", "ga": "dinar na Túinéise",
"gl": "dinar tunisiano",
"he": "דינר תוניסאי", "he": "דינר תוניסאי",
"hr": "tuniski dinar", "hr": "tuniski dinar",
"hu": "tunéziai dinár", "hu": "tunéziai dinár",
@@ -5769,7 +5749,7 @@
"fi": "Uruguayn peso", "fi": "Uruguayn peso",
"fr": "peso uruguayen", "fr": "peso uruguayen",
"ga": "peso Uragua", "ga": "peso Uragua",
"gl": "peso uruguaio", "gl": "Peso uruguaio",
"he": "פסו של אורוגוואי", "he": "פסו של אורוגוואי",
"hr": "Urugvajski pezo", "hr": "Urugvajski pezo",
"hu": "uruguayi peso", "hu": "uruguayi peso",
@@ -5997,7 +5977,7 @@
"ms": "Franc CFA Afrika Tengah", "ms": "Franc CFA Afrika Tengah",
"nl": "Central African CFA franc", "nl": "Central African CFA franc",
"oc": "Franc CFA d'Africa Centrala", "oc": "Franc CFA d'Africa Centrala",
"pl": "frank CFA", "pl": "środkowoafrykański frank CFA",
"pt": "franco", "pt": "franco",
"ro": "Franc CFA BEAC", "ro": "Franc CFA BEAC",
"ru": "франк КФА BEAC", "ru": "франк КФА BEAC",
@@ -6019,7 +5999,6 @@
"fr": "argent d'investissement", "fr": "argent d'investissement",
"ja": "投資対象としての銀", "ja": "投資対象としての銀",
"ms": "Perak sebagai pelaburan", "ms": "Perak sebagai pelaburan",
"pt": "Prata como investimento",
"ru": "серебро как инвестиция", "ru": "серебро как инвестиция",
"sv": "Silver som investering", "sv": "Silver som investering",
"tr": "Yatırım olarak gümüş", "tr": "Yatırım olarak gümüş",
@@ -6038,7 +6017,6 @@
"lv": "zelts kā investīcija", "lv": "zelts kā investīcija",
"ml": "സ്വർണവും സാമ്പത്തിക ശാസ്ത്രവും", "ml": "സ്വർണവും സാമ്പത്തിക ശാസ്ത്രവും",
"ms": "emas sebagai pelaburan", "ms": "emas sebagai pelaburan",
"pt": "Ouro como investimento",
"ru": "золото как инвестиция", "ru": "золото как инвестиция",
"sr": "investiciono zlato", "sr": "investiciono zlato",
"sv": "investeringsguld", "sv": "investeringsguld",
@@ -6150,7 +6128,7 @@
"oc": "Drechs de tiratge Especials", "oc": "Drechs de tiratge Especials",
"pl": "specjalne prawa ciągnienia", "pl": "specjalne prawa ciągnienia",
"pt": "direitos especiais de saque", "pt": "direitos especiais de saque",
"ro": "drepturi speciale de tragere", "ro": "Drepturi speciale de tragere",
"ru": "специальные права заимствования", "ru": "специальные права заимствования",
"sk": "Zvláštne práva čerpania", "sk": "Zvláštne práva čerpania",
"sl": "posebne pravice črpanja", "sl": "posebne pravice črpanja",
@@ -6228,7 +6206,6 @@
"ja": "CFPフラン", "ja": "CFPフラン",
"ko": "CFP 프랑", "ko": "CFP 프랑",
"lt": "CFP frankas", "lt": "CFP frankas",
"lv": "Klusā okeāna franks",
"ms": "Franc CFP", "ms": "Franc CFP",
"nl": "CFP-frank", "nl": "CFP-frank",
"oc": "Franc CFP", "oc": "Franc CFP",
@@ -7059,7 +7036,6 @@
"bolivjano": "BOB", "bolivjano": "BOB",
"bolivya bolivianosu": "BOB", "bolivya bolivianosu": "BOB",
"bolivya bolivyanosu": "BOB", "bolivya bolivyanosu": "BOB",
"boliwiano": "BOB",
"bolívar digital": "VED", "bolívar digital": "VED",
"bolívar soberano": "VES", "bolívar soberano": "VES",
"bolívar sobirà": "VES", "bolívar sobirà": "VES",
@@ -7929,6 +7905,7 @@
"dolar de las islas caimán": "KYD", "dolar de las islas caimán": "KYD",
"dolar de las islas salomon": "SBD", "dolar de las islas salomon": "SBD",
"dolar de las islas salomón": "SBD", "dolar de las islas salomón": "SBD",
"dolar de namibia": "NAD",
"dolar de nueva zelanda": "NZD", "dolar de nueva zelanda": "NZD",
"dolar de singapor": "SGD", "dolar de singapor": "SGD",
"dolar de singapur": "SGD", "dolar de singapur": "SGD",
@@ -7986,6 +7963,7 @@
"dolar namibia": "NAD", "dolar namibia": "NAD",
"dolar namibian": "NAD", "dolar namibian": "NAD",
"dolar namibijski": "NAD", "dolar namibijski": "NAD",
"dolar namibio": "NAD",
"dolar neocelandes": "NZD", "dolar neocelandes": "NZD",
"dolar neocelandés": "NZD", "dolar neocelandés": "NZD",
"dolar neozeelandez": "NZD", "dolar neozeelandez": "NZD",
@@ -8000,6 +7978,7 @@
"dolar singapura": "SGD", "dolar singapura": "SGD",
"dolar singapurski": "SGD", "dolar singapurski": "SGD",
"dolar singapurtar": "SGD", "dolar singapurtar": "SGD",
"dolar sirio": "SYP",
"dolar sua": "USD", "dolar sua": "USD",
"dolar surinamdar": "SRD", "dolar surinamdar": "SRD",
"dolar suriname": "SRD", "dolar suriname": "SRD",
@@ -8200,7 +8179,6 @@
"dominicaanse peso": "DOP", "dominicaanse peso": "DOP",
"dominican peso": "DOP", "dominican peso": "DOP",
"dominican peso oro": "DOP", "dominican peso oro": "DOP",
"dominički pezo": "DOP",
"dominik pesosu": "DOP", "dominik pesosu": "DOP",
"dominika peso": "DOP", "dominika peso": "DOP",
"dominikaanisen tasavallan peso": "DOP", "dominikaanisen tasavallan peso": "DOP",
@@ -8430,7 +8408,6 @@
"drame arménio": "AMD", "drame arménio": "AMD",
"dramm": "AMD", "dramm": "AMD",
"drechs de tiratge especials": "XDR", "drechs de tiratge especials": "XDR",
"drept special de tragere": "XDR",
"drepturi speciale de tragere": "XDR", "drepturi speciale de tragere": "XDR",
"drets especials de gir": "XDR", "drets especials de gir": "XDR",
"droits de tirage speciaux": "XDR", "droits de tirage speciaux": "XDR",
@@ -8551,6 +8528,7 @@
"etiópsky birr": "ETB", "etiópsky birr": "ETB",
"eua 17": "XBD", "eua 17": "XBD",
"eua 9": "XBC", "eua 9": "XBC",
"eur": "EUR",
"euras": "EUR", "euras": "EUR",
"eurco": "XBA", "eurco": "XBA",
"euro": "EUR", "euro": "EUR",
@@ -8881,6 +8859,7 @@
"frank kongijski": "CDF", "frank kongijski": "CDF",
"frank rwandyjski": "RWF", "frank rwandyjski": "RWF",
"frank szwajcarski": "CHF", "frank szwajcarski": "CHF",
"frank zachodnioafrykański": "XOF",
"franko suitzar": "CHF", "franko suitzar": "CHF",
"frw": "RWF", "frw": "RWF",
"ft": "HUF", "ft": "HUF",
@@ -9201,7 +9180,6 @@
"indonezijska rupija": "IDR", "indonezijska rupija": "IDR",
"indonéská rupie": "IDR", "indonéská rupie": "IDR",
"indonéz rúpia": "IDR", "indonéz rúpia": "IDR",
"indonézska rupia": "IDR",
"indonēziešu rūpija": "IDR", "indonēziešu rūpija": "IDR",
"indonēzijas rūpija": "IDR", "indonēzijas rūpija": "IDR",
"inglise nael": "GBP", "inglise nael": "GBP",
@@ -9516,16 +9494,12 @@
"kirgizia somo": "KGS", "kirgizia somo": "KGS",
"kirgizische som": "KGS", "kirgizische som": "KGS",
"kirgizistansk som": "KGS", "kirgizistansk som": "KGS",
"kirgizstānas soms": "KGS",
"kirgīzu soms": "KGS",
"kiwi dollar": "NZD", "kiwi dollar": "NZD",
"kínai jüan": "CNY", "kínai jüan": "CNY",
"kíp": "LAK", "kíp": "LAK",
"kíp lào": "LAK", "kíp lào": "LAK",
"kīp": "LAK", "kīp": "LAK",
"kjat": "MMK", "kjat": "MMK",
"kjats": "MMK",
"klusā okeāna franks": "XPF",
"km": "BAM", "km": "BAM",
"kmf": "KMF", "kmf": "KMF",
"koeweitse dinar": "KWD", "koeweitse dinar": "KWD",
@@ -9836,6 +9810,7 @@
"lek na halbáine": "ALL", "lek na halbáine": "ALL",
"lek novo": "ALL", "lek novo": "ALL",
"lekas": "ALL", "lekas": "ALL",
"lekă albaneză": "ALL",
"lekë": "ALL", "lekë": "ALL",
"leko": "ALL", "leko": "ALL",
"lempira": "HNL", "lempira": "HNL",
@@ -10006,7 +9981,6 @@
"lira libanesa": "LBP", "lira libanesa": "LBP",
"lira libanese": "LBP", "lira libanese": "LBP",
"lira na tuirce": "TRY", "lira na tuirce": "TRY",
"lira siria": "SYP",
"lira siriana": "SYP", "lira siriana": "SYP",
"lira síria": "SYP", "lira síria": "SYP",
"lira thổ nhĩ kỳ": "TRY", "lira thổ nhĩ kỳ": "TRY",
@@ -10031,7 +10005,6 @@
"lire sterline": "GBP", "lire sterline": "GBP",
"lire turque": "TRY", "lire turque": "TRY",
"lisente": "LSL", "lisente": "LSL",
"list of syrian coins": "SYP",
"liura de gibartar": "GIP", "liura de gibartar": "GIP",
"liura egipciana": "EGP", "liura egipciana": "EGP",
"liura esterlina": "GBP", "liura esterlina": "GBP",
@@ -10318,7 +10291,6 @@
"meksički pezo": "MXN", "meksički pezo": "MXN",
"meksika peso": "MXN", "meksika peso": "MXN",
"meksika pesosu": "MXN", "meksika pesosu": "MXN",
"meksika unuo de investo": "MXV",
"meksikaanse peso": "MXN", "meksikaanse peso": "MXN",
"meksikas peso": "MXN", "meksikas peso": "MXN",
"meksikon peso": "MXN", "meksikon peso": "MXN",
@@ -10774,7 +10746,6 @@
"ouguiya mauritanien": "MRU", "ouguiya mauritanien": "MRU",
"ouguiya mawritania": "MRU", "ouguiya mawritania": "MRU",
"ouguiya na máratáine": "MRU", "ouguiya na máratáine": "MRU",
"ouro como investimento": "XAU",
"ouro do zimbábue": "ZWG", "ouro do zimbábue": "ZWG",
"örmény dram": "AMD", "örmény dram": "AMD",
"östkaribisk dollar": "XCD", "östkaribisk dollar": "XCD",
@@ -11140,7 +11111,6 @@
"põhja korea vonn": "KPW", "põhja korea vonn": "KPW",
"põhja korea won": "KPW", "põhja korea won": "KPW",
"põhja makedoonia denaar": "MKD", "põhja makedoonia denaar": "MKD",
"prata como investimento": "XAG",
"pula": "BWP", "pula": "BWP",
"pula botswana": "BWP", "pula botswana": "BWP",
"pula botswanais": "BWP", "pula botswanais": "BWP",
@@ -11322,6 +11292,7 @@
"riel camboxano": "KHR", "riel camboxano": "KHR",
"riel camboyano": "KHR", "riel camboyano": "KHR",
"riel campuchia": "KHR", "riel campuchia": "KHR",
"riel kambodżański": "KHR",
"riel kamboja": "KHR", "riel kamboja": "KHR",
"riel na cambóide": "KHR", "riel na cambóide": "KHR",
"rietumāfrikas franks": "XOF", "rietumāfrikas franks": "XOF",
@@ -11930,7 +11901,6 @@
"somoni taxico": "TJS", "somoni taxico": "TJS",
"somoni tayiko": "TJS", "somoni tayiko": "TJS",
"somonis": "TJS", "somonis": "TJS",
"soms": "KGS",
"sonderziehungsrecht": "XDR", "sonderziehungsrecht": "XDR",
"sos": "SOS", "sos": "SOS",
"sosh": "SOS", "sosh": "SOS",
@@ -12132,6 +12102,7 @@
"szyling tanzański": "TZS", "szyling tanzański": "TZS",
"szyling ugandyjski": "UGX", "szyling ugandyjski": "UGX",
"sırp dinarı": "RSD", "sırp dinarı": "RSD",
"środkowoafrykański frank cfa": "XAF",
"šalamounský dolar": "SBD", "šalamounský dolar": "SBD",
"šalomounský dolar": "SBD", "šalomounský dolar": "SBD",
"šekel chadaš": "ILS", "šekel chadaš": "ILS",
@@ -12265,7 +12236,6 @@
"thaise baht": "THB", "thaise baht": "THB",
"thajský baht": "THB", "thajský baht": "THB",
"thb": "THB", "thb": "THB",
"the australian dollar": "AUD",
"thebe": "BWP", "thebe": "BWP",
"third belarusian ruble": "BYN", "third belarusian ruble": "BYN",
"tical": "THB", "tical": "THB",
@@ -12749,7 +12719,6 @@
"yen": "JPY", "yen": "JPY",
"yen giapponese": "JPY", "yen giapponese": "JPY",
"yen japones": "JPY", "yen japones": "JPY",
"yen japonez": "JPY",
"yen japonés": "JPY", "yen japonés": "JPY",
"yeni": "JPY", "yeni": "JPY",
"yeni i̇srail şekeli": "ILS", "yeni i̇srail şekeli": "ILS",
@@ -12816,6 +12785,7 @@
"zimbabwe zig": "ZWG", "zimbabwe zig": "ZWG",
"zimbabwean dollar": "ZWL", "zimbabwean dollar": "ZWL",
"zimbabwean gold": "ZWG", "zimbabwean gold": "ZWG",
"zimbabwean zig": "ZWG",
"zimbabwen kulta": "ZWG", "zimbabwen kulta": "ZWG",
"zimbabwiansky zlatý": "ZWG", "zimbabwiansky zlatý": "ZWG",
"zimbabwský dolar": "ZWL", "zimbabwský dolar": "ZWL",
@@ -12974,7 +12944,6 @@
"FKP", "FKP",
"EGP" "EGP"
], ],
"£S": "SYP",
"£e": "EGP", "£e": "EGP",
"£s": "SYP", "£s": "SYP",
"¥": [ "¥": [
@@ -14048,7 +14017,6 @@
"самоанский доллар": "WST", "самоанский доллар": "WST",
"самоанська тала": "WST", "самоанська тала": "WST",
"сан томе һәм принсипи добрасы": "STN", "сан томе һәм принсипи добрасы": "STN",
"саотомеанска добра": "STN",
"саотомска добра": "STN", "саотомска добра": "STN",
"саудитски риал": "SAR", "саудитски риал": "SAR",
"саудијски риал": "SAR", "саудијски риал": "SAR",
@@ -14969,8 +14937,6 @@
"فورنت مجري": "HUF", "فورنت مجري": "HUF",
"فورينت مجري": "HUF", "فورينت مجري": "HUF",
"فِرَنْكٌ رُوَنْدِيٌّ": "RWF", "فِرَنْكٌ رُوَنْدِيٌّ": "RWF",
"قائمة النقود المعدنية السورية": "SYP",
"قائمة عملات سوريا المعدنية": "SYP",
"ك": "KWD", "ك": "KWD",
"كتزال غواتيمالي": "GTQ", "كتزال غواتيمالي": "GTQ",
"كرونة آيسلندية": "ISK", "كرونة آيسلندية": "ISK",
@@ -15384,28 +15350,13 @@
"ஹங்கேரிய போரிண்ட்": "HUF", "ஹங்கேரிய போரிண்ட்": "HUF",
"ஹிருன்யா": "UAH", "ஹிருன்யா": "UAH",
"ஹொங்கொங் டொலர்": "HKD", "ஹொங்கொங் டொலர்": "HKD",
"అమెరికన్ డాలర్": "USD",
"ఇజ్రాయెల్ షెకెల్": "ILS",
"కెనడియన్ డాలర్": "CAD",
"చిలీ పెసో": "CLP",
"జపనీస్ యెన్": "JPY",
"డిజిటల్ రూపాయి": "INR", "డిజిటల్ రూపాయి": "INR",
"నేపాలీ రూపాయి": "NPR",
"పాకిస్థానీ రూపాయి": "PKR",
"ఫిలిప్పీన్ పెసో": "PHP",
"బ్రూనై డాలర్": "BND",
"భారత రూపాయి": "INR", "భారత రూపాయి": "INR",
"భారతదేశ రూపాయి": "INR", "భారతదేశ రూపాయి": "INR",
"భారతీయ రూపాయి": "INR", "భారతీయ రూపాయి": "INR",
"భూటానీస్ గుల్త్రమ్": "BTN",
"యునైటెడ్ స్టేట్స్ డాలర్": "USD", "యునైటెడ్ స్టేట్స్ డాలర్": "USD",
"యూఏఈ దిర్హామ్": "AED",
"యూరో": "EUR", "యూరో": "EUR",
"రూపాయి": "INR", "రూపాయి": "INR",
"రెన్మిన్బి": "CNY",
"శ్రీలంక రూపాయి": "LKR",
"సింగపూర్ డాలర్": "SGD",
"సౌదీ రియాల్": "SAR",
"స్విస్ ఫ్రాంక్": "CHF", "స్విస్ ఫ్రాంక్": "CHF",
"അൾജീരിയൻ ദിനാർ": "DZD", "അൾജീരിയൻ ദിനാർ": "DZD",
"ഇന്തോനേഷ്യൻ റുപിയ": "IDR", "ഇന്തോനേഷ്യൻ റുപിയ": "IDR",
@@ -15656,8 +15607,6 @@
"부탄 뉘땀": "BTN", "부탄 뉘땀": "BTN",
"부탄눌트럼": "BTN", "부탄눌트럼": "BTN",
"북마케도니아 데나르": "MKD", "북마케도니아 데나르": "MKD",
"북조선 원": "KPW",
"북한 원": "KPW",
"브라질 레알": "BRL", "브라질 레알": "BRL",
"브라질 헤알": "BRL", "브라질 헤알": "BRL",
"브루나이 달러": "BND", "브루나이 달러": "BND",
@@ -16250,6 +16199,7 @@
"香港・ドル": "HKD", "香港・ドル": "HKD",
"香港元": "HKD", "香港元": "HKD",
"﷼": "IRR", "﷼": "IRR",
"﷼'": "YER" "﷼'": "YER",
"💶": "EUR"
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -69,6 +69,11 @@
"symbol": "pk (US)", "symbol": "pk (US)",
"to_si_factor": 0.008809768 "to_si_factor": 0.008809768
}, },
"Q101427917": {
"si_name": "Q25517",
"symbol": "pk (UK)",
"to_si_factor": 0.00909218
},
"Q101428098": { "Q101428098": {
"si_name": "Q44395", "si_name": "Q44395",
"symbol": "dbar", "symbol": "dbar",
@@ -79,11 +84,6 @@
"symbol": "μbar", "symbol": "μbar",
"to_si_factor": 0.1 "to_si_factor": 0.1
}, },
"Q101435213": {
"si_name": "Q101435195",
"symbol": "Jy s",
"to_si_factor": 1e-26
},
"Q101435332": { "Q101435332": {
"si_name": "Q44395", "si_name": "Q44395",
"symbol": "cm Hg", "symbol": "cm Hg",
@@ -124,6 +124,11 @@
"symbol": "pm²", "symbol": "pm²",
"to_si_factor": 1e-24 "to_si_factor": 1e-24
}, },
"Q101463679": {
"si_name": "Q25343",
"symbol": "hm²",
"to_si_factor": 10000.0
},
"Q101464050": { "Q101464050": {
"si_name": "Q25343", "si_name": "Q25343",
"symbol": "Mm²", "symbol": "Mm²",
@@ -194,11 +199,6 @@
"symbol": "d⁻¹", "symbol": "d⁻¹",
"to_si_factor": 1.15741e-05 "to_si_factor": 1.15741e-05
}, },
"Q102129764": {
"si_name": "Q6137407",
"symbol": "a⁻¹",
"to_si_factor": 3.17098e-08
},
"Q102130673": { "Q102130673": {
"si_name": "Q182429", "si_name": "Q182429",
"symbol": "ym/s", "symbol": "ym/s",
@@ -425,9 +425,9 @@
"to_si_factor": 10.0 "to_si_factor": 10.0
}, },
"Q1042866": { "Q1042866": {
"si_name": "Q199", "si_name": null,
"symbol": "Zib", "symbol": "Zib",
"to_si_factor": 1.1805916207174113e+21 "to_si_factor": null
}, },
"Q104317437": { "Q104317437": {
"si_name": "Q25381181", "si_name": "Q25381181",
@@ -995,9 +995,9 @@
"to_si_factor": 1000000000000.0 "to_si_factor": 1000000000000.0
}, },
"Q106247880": { "Q106247880": {
"si_name": "Q6137407", "si_name": null,
"symbol": "digit/s", "symbol": "digit/s",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q106247940": { "Q106247940": {
"si_name": null, "si_name": null,
@@ -1504,11 +1504,6 @@
"symbol": "m²/(s² K)", "symbol": "m²/(s² K)",
"to_si_factor": 1.0 "to_si_factor": 1.0
}, },
"Q106707206": {
"si_name": null,
"symbol": "HU",
"to_si_factor": null
},
"Q106707404": { "Q106707404": {
"si_name": "Q106707404", "si_name": "Q106707404",
"symbol": "kg m²/(s² K)", "symbol": "kg m²/(s² K)",
@@ -2319,124 +2314,79 @@
"symbol": "cm²/(sr erg)", "symbol": "cm²/(sr erg)",
"to_si_factor": 1000.0 "to_si_factor": 1000.0
}, },
"Q107710161": {
"si_name": "Q199",
"symbol": "J/bit",
"to_si_factor": 1.0
},
"Q107821494": { "Q107821494": {
"si_name": "Q21401573",
"symbol": "bit/m³",
"to_si_factor": 1.0
},
"Q107822428": {
"si_name": "Q11547251",
"symbol": "bit/m",
"to_si_factor": 1.0
},
"Q107824325": {
"si_name": "Q11547252",
"symbol": "bit/m²",
"to_si_factor": 1.0
},
"Q107825143": {
"si_name": "Q11547251",
"symbol": "Eibit/m",
"to_si_factor": 1.152921504606847e+18
},
"Q107825584": {
"si_name": "Q11547252",
"symbol": "Eibit/m²",
"to_si_factor": 1.152921504606847e+18
},
"Q107825716": {
"si_name": "Q21401573",
"symbol": "Eibit/m³",
"to_si_factor": 1.152921504606847e+18
},
"Q107862736": {
"si_name": "Q11547251",
"symbol": "Gibit/m",
"to_si_factor": 1073741824.0
},
"Q107862746": {
"si_name": "Q11547252",
"symbol": "Gibit/m²",
"to_si_factor": 1073741824.0
},
"Q107862762": {
"si_name": "Q21401573",
"symbol": "Gibit/m³",
"to_si_factor": 1073741824.0
},
"Q107862770": {
"si_name": "Q11547251",
"symbol": "Kibit/m",
"to_si_factor": 1024.0
},
"Q107862783": {
"si_name": "Q11547252",
"symbol": "Kibit/m",
"to_si_factor": 1024.0
},
"Q107862850": {
"si_name": "Q21401573",
"symbol": "Kibit/m³",
"to_si_factor": 1024.0
},
"Q107862870": {
"si_name": "Q11547251",
"symbol": "Mibit/m",
"to_si_factor": 1048576.0
},
"Q107862884": {
"si_name": "Q11547252",
"symbol": "Mibit/m²",
"to_si_factor": 1048576.0
},
"Q107862898": {
"si_name": "Q21401573",
"symbol": "Mibit/m³",
"to_si_factor": 1048576.0
},
"Q107970215": {
"si_name": "Q11547251",
"symbol": "Pibit/m",
"to_si_factor": 1125899906842624.0
},
"Q107970224": {
"si_name": "Q11547252",
"symbol": "Pibit/m²",
"to_si_factor": 1125899906842624.0
},
"Q107970230": {
"si_name": "Q21401573",
"symbol": "Pibit/m³",
"to_si_factor": 1125899906842624.0
},
"Q107970235": {
"si_name": "Q11547251",
"symbol": "Tibit/m",
"to_si_factor": 1099511627776.0
},
"Q107970256": {
"si_name": "Q21401573",
"symbol": "Tibit/m³",
"to_si_factor": 1099511627776.0
},
"Q107970266": {
"si_name": "Q11547252",
"symbol": "Tibit/m²",
"to_si_factor": 1099511627776.0
},
"Q108112819": {
"si_name": null, "si_name": null,
"symbol": "€/kWh", "symbol": "bit/m³",
"to_si_factor": null "to_si_factor": null
}, },
"Q108112891": { "Q107822428": {
"si_name": null, "si_name": null,
"symbol": "€/(MW h)", "symbol": "bit/m",
"to_si_factor": null
},
"Q107824325": {
"si_name": null,
"symbol": "bit/m²",
"to_si_factor": null
},
"Q107862770": {
"si_name": null,
"symbol": "Kibit/m",
"to_si_factor": null
},
"Q107862783": {
"si_name": null,
"symbol": "Kibit/m",
"to_si_factor": null
},
"Q107862850": {
"si_name": null,
"symbol": "Kibit/m³",
"to_si_factor": null
},
"Q107862870": {
"si_name": null,
"symbol": "Mibit/m",
"to_si_factor": null
},
"Q107862884": {
"si_name": null,
"symbol": "Mibit/m²",
"to_si_factor": null
},
"Q107862898": {
"si_name": null,
"symbol": "Mibit/m³",
"to_si_factor": null
},
"Q107970215": {
"si_name": null,
"symbol": "Pibit/m",
"to_si_factor": null
},
"Q107970224": {
"si_name": null,
"symbol": "Pibit/m²",
"to_si_factor": null
},
"Q107970230": {
"si_name": null,
"symbol": "Pibit/m³",
"to_si_factor": null
},
"Q107970235": {
"si_name": null,
"symbol": "Tibit/m",
"to_si_factor": null
},
"Q107970256": {
"si_name": null,
"symbol": "Tibit/m³",
"to_si_factor": null
},
"Q107970266": {
"si_name": null,
"symbol": "Tibit/m²",
"to_si_factor": null "to_si_factor": null
}, },
"Q108270163": { "Q108270163": {
@@ -2445,9 +2395,9 @@
"to_si_factor": 3.169e-05 "to_si_factor": 3.169e-05
}, },
"Q1084321": { "Q1084321": {
"si_name": "Q6137407", "si_name": null,
"symbol": "Tb/s", "symbol": "Tb/s",
"to_si_factor": 1000000000000.0 "to_si_factor": null
}, },
"Q108533173": { "Q108533173": {
"si_name": "Q108533173", "si_name": "Q108533173",
@@ -2484,16 +2434,6 @@
"symbol": "GeV/c²", "symbol": "GeV/c²",
"to_si_factor": 1.7826619216278976e-27 "to_si_factor": 1.7826619216278976e-27
}, },
"Q108913970": {
"si_name": null,
"symbol": "person/km²",
"to_si_factor": null
},
"Q108914485": {
"si_name": null,
"symbol": "person/m²",
"to_si_factor": null
},
"Q108920356": { "Q108920356": {
"si_name": "Q25406", "si_name": "Q25406",
"symbol": "esu", "symbol": "esu",
@@ -2524,11 +2464,6 @@
"symbol": "e.u.", "symbol": "e.u.",
"to_si_factor": 4.184 "to_si_factor": 4.184
}, },
"Q109337616": {
"si_name": "Q139710667",
"symbol": "1/(100 eV)",
"to_si_factor": 6.2415e+16
},
"Q109448508": { "Q109448508": {
"si_name": null, "si_name": null,
"symbol": "man-Sv", "symbol": "man-Sv",
@@ -2564,11 +2499,6 @@
"symbol": "nm²", "symbol": "nm²",
"to_si_factor": 1e-18 "to_si_factor": 1e-18
}, },
"Q11123": {
"si_name": "Q25517",
"symbol": "pt (UK)",
"to_si_factor": 0.00056826125
},
"Q111494193": { "Q111494193": {
"si_name": "Q111494193", "si_name": "Q111494193",
"symbol": "J/(Hz mol)", "symbol": "J/(Hz mol)",
@@ -2579,11 +2509,6 @@
"symbol": "%", "symbol": "%",
"to_si_factor": 0.01 "to_si_factor": 0.01
}, },
"Q112659472": {
"si_name": null,
"symbol": "PVU",
"to_si_factor": null
},
"Q1131660": { "Q1131660": {
"si_name": "Q11570", "si_name": "Q11570",
"symbol": "st", "symbol": "st",
@@ -2615,14 +2540,14 @@
"to_si_factor": 31688087.81402895 "to_si_factor": 31688087.81402895
}, },
"Q1140444": { "Q1140444": {
"si_name": "Q199", "si_name": null,
"symbol": "Zb", "symbol": "Zb",
"to_si_factor": 1e+21 "to_si_factor": null
}, },
"Q1140577": { "Q1140577": {
"si_name": "Q199", "si_name": null,
"symbol": "Yb", "symbol": "Yb",
"to_si_factor": 0.0 "to_si_factor": null
}, },
"Q114559346": { "Q114559346": {
"si_name": null, "si_name": null,
@@ -2635,14 +2560,14 @@
"to_si_factor": null "to_si_factor": null
}, },
"Q1152074": { "Q1152074": {
"si_name": "Q199", "si_name": null,
"symbol": "Pb", "symbol": "Pb",
"to_si_factor": 1000000000000000.0 "to_si_factor": null
}, },
"Q1152323": { "Q1152323": {
"si_name": "Q199", "si_name": null,
"symbol": "Tb", "symbol": "Tb",
"to_si_factor": 1000000000000.0 "to_si_factor": null
}, },
"Q115277430": { "Q115277430": {
"si_name": null, "si_name": null,
@@ -2885,14 +2810,14 @@
"to_si_factor": 4.4482216152605 "to_si_factor": 4.4482216152605
}, },
"Q1194580": { "Q1194580": {
"si_name": "Q199", "si_name": null,
"symbol": "Mib", "symbol": "Mib",
"to_si_factor": 1048576.0 "to_si_factor": null
}, },
"Q1195111": { "Q1195111": {
"si_name": "Q199", "si_name": null,
"symbol": "Eb", "symbol": "Eb",
"to_si_factor": 1e+18 "to_si_factor": null
}, },
"Q1196837": { "Q1196837": {
"si_name": "Q1063756", "si_name": "Q1063756",
@@ -2940,20 +2865,15 @@
"to_si_factor": 0.03110348 "to_si_factor": 0.03110348
}, },
"Q1204894": { "Q1204894": {
"si_name": "Q199", "si_name": null,
"symbol": "Gib", "symbol": "Gib",
"to_si_factor": 1073741824.0 "to_si_factor": null
}, },
"Q12129": { "Q12129": {
"si_name": "Q11573", "si_name": "Q11573",
"symbol": "pc", "symbol": "pc",
"to_si_factor": 3.085677581491367e+16 "to_si_factor": 3.085677581491367e+16
}, },
"Q12145303": {
"si_name": "Q11573",
"symbol": "rd",
"to_si_factor": 5.02921
},
"Q121965382": { "Q121965382": {
"si_name": "Q121965382", "si_name": "Q121965382",
"symbol": "mol/mol", "symbol": "mol/mol",
@@ -3194,11 +3114,6 @@
"symbol": "QF", "symbol": "QF",
"to_si_factor": 1e+30 "to_si_factor": 1e+30
}, },
"Q12558489": {
"si_name": "Q25517",
"symbol": "pk (UK)",
"to_si_factor": 0.00909218
},
"Q125962250": { "Q125962250": {
"si_name": null, "si_name": null,
"symbol": "Ry", "symbol": "Ry",
@@ -3272,7 +3187,7 @@
"Q128822": { "Q128822": {
"si_name": "Q182429", "si_name": "Q182429",
"symbol": "kn", "symbol": "kn",
"to_si_factor": 0.514 "to_si_factor": 0.5144444444444445
}, },
"Q12912288": { "Q12912288": {
"si_name": null, "si_name": null,
@@ -3325,14 +3240,14 @@
"to_si_factor": 1e-30 "to_si_factor": 1e-30
}, },
"Q131395783": { "Q131395783": {
"si_name": "Q199", "si_name": null,
"symbol": "Rib", "symbol": "Rib",
"to_si_factor": 1.2379400392853803e+27 "to_si_factor": null
}, },
"Q131395793": { "Q131395793": {
"si_name": "Q199", "si_name": null,
"symbol": "Qib", "symbol": "Qib",
"to_si_factor": 1.2676506002282294e+30 "to_si_factor": null
}, },
"Q13147228": { "Q13147228": {
"si_name": "Q844211", "si_name": "Q844211",
@@ -3394,11 +3309,6 @@
"symbol": "1/K", "symbol": "1/K",
"to_si_factor": null "to_si_factor": null
}, },
"Q133796439": {
"si_name": null,
"symbol": "T/mm²",
"to_si_factor": null
},
"Q13400897": { "Q13400897": {
"si_name": "Q1051665", "si_name": "Q1051665",
"symbol": "g", "symbol": "g",
@@ -3407,23 +3317,18 @@
"Q13479685": { "Q13479685": {
"si_name": "Q44395", "si_name": "Q44395",
"symbol": "mm H2O", "symbol": "mm H2O",
"to_si_factor": 9.80665 "to_si_factor": 9.80638
}, },
"Q1351253": { "Q1351253": {
"si_name": "Q199", "si_name": null,
"symbol": "Eib", "symbol": "Eib",
"to_si_factor": 1.152921504606847e+18 "to_si_factor": null
}, },
"Q1351334": { "Q1351334": {
"si_name": null, "si_name": null,
"symbol": "Pib", "symbol": "Pib",
"to_si_factor": null "to_si_factor": null
}, },
"Q135373020": {
"si_name": null,
"symbol": "GTexel",
"to_si_factor": null
},
"Q135415097": { "Q135415097": {
"si_name": "Q25236", "si_name": "Q25236",
"symbol": "shp", "symbol": "shp",
@@ -3475,9 +3380,9 @@
"to_si_factor": 1e-06 "to_si_factor": 1e-06
}, },
"Q136039973": { "Q136039973": {
"si_name": "Q6137407", "si_name": null,
"symbol": "FPS", "symbol": "FPS",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q1361854": { "Q1361854": {
"si_name": "Q11570", "si_name": "Q11570",
@@ -3545,9 +3450,9 @@
"to_si_factor": 1000000000000.0 "to_si_factor": 1000000000000.0
}, },
"Q139054848": { "Q139054848": {
"si_name": "Q117899185", "si_name": null,
"symbol": "A·h/m²", "symbol": "A·h/m²",
"to_si_factor": 3600.0 "to_si_factor": null
}, },
"Q139086088": { "Q139086088": {
"si_name": "Q69425409", "si_name": "Q69425409",
@@ -3737,7 +3642,7 @@
"Q1654435": { "Q1654435": {
"si_name": "Q25250", "si_name": "Q25250",
"symbol": "IRE", "symbol": "IRE",
"to_si_factor": 0.007143 "to_si_factor": 0.007
}, },
"Q16673974": { "Q16673974": {
"si_name": null, "si_name": null,
@@ -3915,9 +3820,9 @@
"to_si_factor": 0.01 "to_si_factor": 0.01
}, },
"Q18434272": { "Q18434272": {
"si_name": "Q199", "si_name": null,
"symbol": "°Balling", "symbol": "°Balling",
"to_si_factor": 0.01 "to_si_factor": null
}, },
"Q185078": { "Q185078": {
"si_name": "Q25343", "si_name": "Q25343",
@@ -3945,9 +3850,9 @@
"to_si_factor": 1e-21 "to_si_factor": 1e-21
}, },
"Q188768": { "Q188768": {
"si_name": "Q6137407", "si_name": null,
"symbol": "FLOPS", "symbol": "FLOPS",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q190095": { "Q190095": {
"si_name": "Q190095", "si_name": "Q190095",
@@ -4084,11 +3989,6 @@
"symbol": "ppb", "symbol": "ppb",
"to_si_factor": 1e-09 "to_si_factor": 1e-09
}, },
"Q206037": {
"si_name": "Q6137407",
"symbol": "r/min",
"to_si_factor": 0.0166667
},
"Q2064166": { "Q2064166": {
"si_name": "Q179836", "si_name": "Q179836",
"symbol": "fc", "symbol": "fc",
@@ -4420,9 +4320,9 @@
"to_si_factor": 10.0 "to_si_factor": 10.0
}, },
"Q2243141": { "Q2243141": {
"si_name": "Q6137407", "si_name": null,
"symbol": "Gb/s", "symbol": "Gb/s",
"to_si_factor": 1000000000.0 "to_si_factor": null
}, },
"Q2254856": { "Q2254856": {
"si_name": "Q25343", "si_name": "Q25343",
@@ -4435,9 +4335,9 @@
"to_si_factor": 0.00508 "to_si_factor": 0.00508
}, },
"Q2269250": { "Q2269250": {
"si_name": "Q6137407", "si_name": null,
"symbol": "kb/s", "symbol": "kb/s",
"to_si_factor": 1000.0 "to_si_factor": null
}, },
"Q2278977": { "Q2278977": {
"si_name": null, "si_name": null,
@@ -4635,9 +4535,9 @@
"to_si_factor": 898755178700.0 "to_si_factor": 898755178700.0
}, },
"Q25325238": { "Q25325238": {
"si_name": "Q106919394", "si_name": null,
"symbol": "bhp/cm³", "symbol": "bhp/cm³",
"to_si_factor": 745700000.0 "to_si_factor": null
}, },
"Q253276": { "Q253276": {
"si_name": "Q11573", "si_name": "Q11573",
@@ -4645,9 +4545,9 @@
"to_si_factor": 1609.344 "to_si_factor": 1609.344
}, },
"Q2533495": { "Q2533495": {
"si_name": "Q199", "si_name": null,
"symbol": "°P", "symbol": "°P",
"to_si_factor": 0.01 "to_si_factor": null
}, },
"Q25343": { "Q25343": {
"si_name": "Q25343", "si_name": "Q25343",
@@ -4985,9 +4885,9 @@
"to_si_factor": 9.80665 "to_si_factor": 9.80665
}, },
"Q28657331": { "Q28657331": {
"si_name": "Q106688958", "si_name": null,
"symbol": "erg/(s cm²)", "symbol": "erg/(s cm²)",
"to_si_factor": 0.001 "to_si_factor": null
}, },
"Q28683485": { "Q28683485": {
"si_name": "Q28683485", "si_name": "Q28683485",
@@ -5020,9 +4920,9 @@
"to_si_factor": 0.001 "to_si_factor": 0.001
}, },
"Q29463526": { "Q29463526": {
"si_name": "Q199", "si_name": null,
"symbol": "hr/yr", "symbol": "hr/yr",
"to_si_factor": 1.14155e-06 "to_si_factor": null
}, },
"Q296936": { "Q296936": {
"si_name": "Q25269", "si_name": "Q25269",
@@ -5255,9 +5155,9 @@
"to_si_factor": 1e-15 "to_si_factor": 1e-15
}, },
"Q3194304": { "Q3194304": {
"si_name": "Q199", "si_name": null,
"symbol": "kb", "symbol": "kb",
"to_si_factor": 1000.0 "to_si_factor": null
}, },
"Q3196665": { "Q3196665": {
"si_name": "Q215571", "si_name": "Q215571",
@@ -5395,9 +5295,9 @@
"to_si_factor": 3517.0 "to_si_factor": 3517.0
}, },
"Q3332814": { "Q3332814": {
"si_name": "Q199", "si_name": null,
"symbol": "Mb", "symbol": "Mb",
"to_si_factor": 1000000.0 "to_si_factor": null
}, },
"Q33680": { "Q33680": {
"si_name": "Q33680", "si_name": "Q33680",
@@ -5490,9 +5390,9 @@
"to_si_factor": 3.085677581e+22 "to_si_factor": 3.085677581e+22
}, },
"Q3815076": { "Q3815076": {
"si_name": "Q199", "si_name": null,
"symbol": "Kib", "symbol": "Kib",
"to_si_factor": 1024.0 "to_si_factor": null
}, },
"Q3858002": { "Q3858002": {
"si_name": "Q25406", "si_name": "Q25406",
@@ -5510,9 +5410,9 @@
"to_si_factor": 0.3048 "to_si_factor": 0.3048
}, },
"Q389062": { "Q389062": {
"si_name": "Q199", "si_name": null,
"symbol": "Tib", "symbol": "Tib",
"to_si_factor": 1099511627776.0 "to_si_factor": null
}, },
"Q3902688": { "Q3902688": {
"si_name": "Q25517", "si_name": "Q25517",
@@ -5634,11 +5534,6 @@
"symbol": "D", "symbol": "D",
"to_si_factor": 3.335640951981521e-30 "to_si_factor": 3.335640951981521e-30
}, },
"Q4063874": {
"si_name": "Q21401573",
"symbol": "amg",
"to_si_factor": 2.6868e+25
},
"Q4068266": { "Q4068266": {
"si_name": "Q11570", "si_name": "Q11570",
"symbol": "Ʒ", "symbol": "Ʒ",
@@ -5745,9 +5640,9 @@
"to_si_factor": null "to_si_factor": null
}, },
"Q474533": { "Q474533": {
"si_name": "Q25272", "si_name": null,
"symbol": "At", "symbol": "At",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q48013": { "Q48013": {
"si_name": "Q11570", "si_name": "Q11570",
@@ -5810,14 +5705,14 @@
"to_si_factor": 0.01 "to_si_factor": 0.01
}, },
"Q50094": { "Q50094": {
"si_name": "Q199", "si_name": null,
"symbol": "Np", "symbol": "Np",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q50098": { "Q50098": {
"si_name": "Q199", "si_name": null,
"symbol": "B", "symbol": "B",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q50190518": { "Q50190518": {
"si_name": "Q25377184", "si_name": "Q25377184",
@@ -5860,9 +5755,9 @@
"to_si_factor": 1000.0 "to_si_factor": 1000.0
}, },
"Q524410": { "Q524410": {
"si_name": "Q11574", "si_name": null,
"symbol": "Ga", "symbol": "Ga",
"to_si_factor": 3.15576e+16 "to_si_factor": null
}, },
"Q531": { "Q531": {
"si_name": "Q11573", "si_name": "Q11573",
@@ -5870,9 +5765,9 @@
"to_si_factor": 9460730472580800.0 "to_si_factor": 9460730472580800.0
}, },
"Q5329": { "Q5329": {
"si_name": "Q50098", "si_name": null,
"symbol": "dB", "symbol": "dB",
"to_si_factor": 0.1 "to_si_factor": null
}, },
"Q53393488": { "Q53393488": {
"si_name": "Q39369", "si_name": "Q39369",
@@ -6515,9 +6410,9 @@
"to_si_factor": 3.4262590996353905 "to_si_factor": 3.4262590996353905
}, },
"Q549389": { "Q549389": {
"si_name": "Q6137407", "si_name": null,
"symbol": "b/s", "symbol": "b/s",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q550341": { "Q550341": {
"si_name": "Q550341", "si_name": "Q550341",
@@ -6545,9 +6440,9 @@
"to_si_factor": null "to_si_factor": null
}, },
"Q5558595": { "Q5558595": {
"si_name": "Q199", "si_name": null,
"symbol": "GFLOPS", "symbol": "GFLOPS",
"to_si_factor": 1000000000.0 "to_si_factor": null
}, },
"Q55663153": { "Q55663153": {
"si_name": "Q55663153", "si_name": "Q55663153",
@@ -6624,25 +6519,20 @@
"symbol": "GtCO2eq", "symbol": "GtCO2eq",
"to_si_factor": null "to_si_factor": null
}, },
"Q57084839": {
"si_name": null,
"symbol": "gCDE/km",
"to_si_factor": null
},
"Q57084901": { "Q57084901": {
"si_name": null, "si_name": null,
"symbol": "KgCO2eq", "symbol": "KgCO2eq",
"to_si_factor": null "to_si_factor": null
}, },
"Q57084921": { "Q57084921": {
"si_name": "Q11570", "si_name": null,
"symbol": "gCO2eq", "symbol": "gCO2eq",
"to_si_factor": 0.001 "to_si_factor": null
}, },
"Q5711255": { "Q5711255": {
"si_name": "Q25517", "si_name": null,
"symbol": "aL", "symbol": "aL",
"to_si_factor": 1e-21 "to_si_factor": null
}, },
"Q5711261": { "Q5711261": {
"si_name": "Q25517", "si_name": "Q25517",
@@ -6784,6 +6674,11 @@
"symbol": "in", "symbol": "in",
"to_si_factor": 0.0254 "to_si_factor": 0.0254
}, },
"Q61793198": {
"si_name": "Q11573",
"symbol": "rd",
"to_si_factor": 5.02921
},
"Q61794766": { "Q61794766": {
"si_name": "Q11573", "si_name": "Q11573",
"symbol": "ch (US survey)", "symbol": "ch (US survey)",
@@ -6815,9 +6710,9 @@
"to_si_factor": null "to_si_factor": null
}, },
"Q61995006": { "Q61995006": {
"si_name": "Q106682512", "si_name": null,
"symbol": "KWth", "symbol": "KWth",
"to_si_factor": 1000.0 "to_si_factor": null
}, },
"Q61996348": { "Q61996348": {
"si_name": "Q794261", "si_name": "Q794261",
@@ -6860,14 +6755,14 @@
"to_si_factor": 1.1574074e-05 "to_si_factor": 1.1574074e-05
}, },
"Q64740041": { "Q64740041": {
"si_name": "Q106688958", "si_name": null,
"symbol": "kWh/(m² yr)", "symbol": "kWh/(m² yr)",
"to_si_factor": 0.000114 "to_si_factor": null
}, },
"Q64740314": { "Q64740314": {
"si_name": "Q106688958", "si_name": null,
"symbol": "kWh/(m² day)", "symbol": "kWh/(m² day)",
"to_si_factor": 41.67 "to_si_factor": null
}, },
"Q64748817": { "Q64748817": {
"si_name": "Q80374519", "si_name": "Q80374519",
@@ -6930,9 +6825,9 @@
"to_si_factor": 1016.0469088 "to_si_factor": 1016.0469088
}, },
"Q66778234": { "Q66778234": {
"si_name": "Q199", "si_name": null,
"symbol": "TFLOPS", "symbol": "TFLOPS",
"to_si_factor": 1000000000000.0 "to_si_factor": null
}, },
"Q66778809": { "Q66778809": {
"si_name": null, "si_name": null,
@@ -6944,11 +6839,6 @@
"symbol": "PFLOPS", "symbol": "PFLOPS",
"to_si_factor": null "to_si_factor": null
}, },
"Q66778951": {
"si_name": null,
"symbol": "ZFLOPS",
"to_si_factor": null
},
"Q67060736": { "Q67060736": {
"si_name": "Q67060736", "si_name": "Q67060736",
"symbol": "W/kg", "symbol": "W/kg",
@@ -7335,9 +7225,9 @@
"to_si_factor": 1.0 "to_si_factor": 1.0
}, },
"Q7350781": { "Q7350781": {
"si_name": "Q6137407", "si_name": null,
"symbol": "Mb/s", "symbol": "Mb/s",
"to_si_factor": 1000000.0 "to_si_factor": null
}, },
"Q743895": { "Q743895": {
"si_name": "Q39369", "si_name": "Q39369",
@@ -7655,9 +7545,9 @@
"to_si_factor": 1.0 "to_si_factor": 1.0
}, },
"Q836941": { "Q836941": {
"si_name": "Q199", "si_name": null,
"symbol": "°Bx", "symbol": "°Bx",
"to_si_factor": 0.01 "to_si_factor": null
}, },
"Q83853845": { "Q83853845": {
"si_name": "Q83853845", "si_name": "Q83853845",
@@ -7709,11 +7599,6 @@
"symbol": "hm", "symbol": "hm",
"to_si_factor": 100.0 "to_si_factor": 100.0
}, },
"Q84451486": {
"si_name": "Q84451486",
"symbol": "K m/W",
"to_si_factor": 1.0
},
"Q844976": { "Q844976": {
"si_name": "Q2844478", "si_name": "Q2844478",
"symbol": "Oe", "symbol": "Oe",
@@ -7745,9 +7630,9 @@
"to_si_factor": 1000000000.0 "to_si_factor": 1000000000.0
}, },
"Q855161": { "Q855161": {
"si_name": "Q199", "si_name": null,
"symbol": "Yib", "symbol": "Yib",
"to_si_factor": 1.2089258196146292e+24 "to_si_factor": null
}, },
"Q856240": { "Q856240": {
"si_name": "Q794261", "si_name": "Q794261",
@@ -7774,11 +7659,6 @@
"symbol": "abA", "symbol": "abA",
"to_si_factor": 10.0 "to_si_factor": 10.0
}, },
"Q86897783": {
"si_name": "Q86897783",
"symbol": "Pa² s",
"to_si_factor": 1.0
},
"Q87047886": { "Q87047886": {
"si_name": "Q87047886", "si_name": "Q87047886",
"symbol": "Pa s/m³", "symbol": "Pa s/m³",
@@ -7800,14 +7680,14 @@
"to_si_factor": 1e-12 "to_si_factor": 1e-12
}, },
"Q8799": { "Q8799": {
"si_name": "Q199", "si_name": null,
"symbol": "B", "symbol": "B",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q8805": { "Q8805": {
"si_name": "Q8805", "si_name": null,
"symbol": "bit", "symbol": "bit",
"to_si_factor": 1.0 "to_si_factor": null
}, },
"Q88296091": { "Q88296091": {
"si_name": "Q25517", "si_name": "Q25517",
@@ -7834,6 +7714,11 @@
"symbol": "bu (UK)", "symbol": "bu (UK)",
"to_si_factor": 0.03636872 "to_si_factor": 0.03636872
}, },
"Q89662131": {
"si_name": "Q25517",
"symbol": "pt (UK)",
"to_si_factor": 0.00056826125
},
"Q89992008": { "Q89992008": {
"si_name": "Q89992008", "si_name": "Q89992008",
"symbol": "F⁻¹", "symbol": "F⁻¹",
@@ -7860,9 +7745,9 @@
"to_si_factor": null "to_si_factor": null
}, },
"Q9048643": { "Q9048643": {
"si_name": "Q25517", "si_name": null,
"symbol": "nl", "symbol": "nl",
"to_si_factor": 1e-12 "to_si_factor": null
}, },
"Q905912": { "Q905912": {
"si_name": "Q281096", "si_name": "Q281096",

View File

@@ -3,14 +3,13 @@
- :py:obj:`searx.enginelib.EngineCache` - :py:obj:`searx.enginelib.EngineCache`
- :py:obj:`searx.enginelib.Engine` - :py:obj:`searx.enginelib.Engine`
- :py:obj:`searx.enginelib.EngineAbout`
- :py:obj:`searx.enginelib.traits` - :py:obj:`searx.enginelib.traits`
There is a command line for developer purposes and for deeper analysis. Here is There is a command line for developer purposes and for deeper analysis. Here is
an example in which the command line is called in the development environment:: an example in which the command line is called in the development environment::
$ ./manage dev.env $ ./manage pyenv.cmd bash --norc --noprofile
(dev.env)$ python -m searx.enginelib --help (py3) python -m searx.enginelib --help
.. hint:: .. hint::
@@ -24,7 +23,7 @@ an example in which the command line is called in the development environment::
""" """
__all__ = ["EngineCache", "Engine", "EngineAbout", "ENGINES_CACHE"] __all__ = ["EngineCache", "Engine", "ENGINES_CACHE"]
import typing as t import typing as t
import abc import abc
@@ -32,7 +31,6 @@ from collections.abc import Callable
import logging import logging
import string import string
import typer import typer
import msgspec
from ..cache import ExpireCacheSQLite, ExpireCacheCfg from ..cache import ExpireCacheSQLite, ExpireCacheCfg
@@ -41,14 +39,13 @@ if t.TYPE_CHECKING:
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.search.processors import OfflineParamTypes, OnlineParamTypes, ProcessorType from searx.search.processors import OfflineParamTypes, OnlineParamTypes
ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache( 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
) )
) )
"""Global :py:obj:`searx.cache.ExpireCacheSQLite` instance where the cached """Global :py:obj:`searx.cache.ExpireCacheSQLite` instance where the cached
@@ -74,9 +71,9 @@ def state():
@app.command() @app.command()
def maintenance(force: bool = True, truncate: bool = False): def maintenance(force: bool = True):
"""Carry out maintenance on cache of the engines.""" """Carry out maintenance on cache of the engines."""
ENGINES_CACHE.maintenance(force=force, truncate=truncate) ENGINES_CACHE.maintenance(force=force)
class EngineCache: class EngineCache:
@@ -114,8 +111,8 @@ class EngineCache:
For introspection of the DB, jump into developer environment and run command to For introspection of the DB, jump into developer environment and run command to
show cache state:: show cache state::
$ ./manage dev.env $ ./manage pyenv.cmd bash --norc --noprofile
(dev.env)$ python -m searx.enginelib cache state (py3) python -m searx.enginelib cache state
cache tables and key/values cache tables and key/values
=========================== ===========================
@@ -162,8 +159,7 @@ class EngineCache:
def __init__(self, engine_name: str, expire: int | None = None): def __init__(self, engine_name: str, expire: int | None = None):
self.expire: int = expire or ENGINES_CACHE.cfg.MAXHOLD_TIME self.expire: int = expire or ENGINES_CACHE.cfg.MAXHOLD_TIME
_valid = "-_." + string.ascii_letters + string.digits _valid = "-_." + string.ascii_letters + string.digits
# engine_name is a table and SQL table names must start with a letter self.table_name: str = "".join([c if c in _valid else "_" for c in engine_name])
self.table_name: str = "eng_" + "".join([c if c in _valid else "_" for c in engine_name])
def set(self, key: str, value: t.Any, expire: int | None = None) -> bool: def set(self, key: str, value: t.Any, expire: int | None = None) -> bool:
return ENGINES_CACHE.set( return ENGINES_CACHE.set(
@@ -180,58 +176,11 @@ class EngineCache:
return ENGINES_CACHE.secret_hash(name=name) return ENGINES_CACHE.secret_hash(name=name)
class EngineAbout(msgspec.Struct, kw_only=True):
"""Additional fields describing the engine.
.. code:: yaml
about:
website: https://example.com
wikidata_id: Q306656
official_api_documentation: https://example.com/api-doc
use_official_api: true
require_api_key: true
results: HTML
"""
# pylint: disable=too-few-public-methods
website: str = ""
"""Official web-site of the origin."""
wikidata_id: str = ""
"""`Wikidata ID <https://www.wikidata.org/wiki/Wikidata:Identifiers>`_"""
official_api_documentation: str = ""
"""URL of the official API (regardless of whether it is used)"""
use_official_api: bool = False
"""SearXNG engine makes use of the official API or not"""
require_api_key: bool = False
"""API requires a key or not."""
results: str = ""
"""Data format of the source (online-engines: of the response)."""
description: str = ""
"""Brief description of the engine and where it gets its data from.
This value should only be set as long as no description of the data source
is available via a :py:obj:`EngineAbout.wikidata_id`."""
language: str = ""
"""Deprecated! Migrate your setting from `engine.about.language` to
`engine.language`"""
class Engine(abc.ABC): # pylint: disable=too-few-public-methods class Engine(abc.ABC): # pylint: disable=too-few-public-methods
"""Class of engine instances build from YAML settings. """Class of engine instances build from YAML settings.
Further documentation see :ref:`general engine configuration`. Further documentation see :ref:`general engine configuration`.
The defaults are taken from :py:obj:`searx.engines.ENGINE_DEFAULT_ARGS`.
.. hint:: .. hint::
This class is currently never initialized and only used for type hinting. This class is currently never initialized and only used for type hinting.
@@ -239,71 +188,39 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
logger: logging.Logger logger: logging.Logger
# Common options of the engine module # Common options in the engine module
engine_type: "ProcessorType" = "online" engine_type: str
"""Type of the engine (:ref:`searx.search.processors`)""" """Type of the engine (:ref:`searx.search.processors`)"""
paging: bool = False paging: bool
"""Engine supports multiple pages.""" """Engine supports multiple pages."""
max_page: int = 0 max_page: int = 0
"""If the engine supports paging, then this is the value for the last page """If the engine supports paging, then this is the value for the last page
that is still supported. ``0`` means unlimited numbers of pages.""" that is still supported. ``0`` means unlimited numbers of pages."""
time_range_support: bool = False time_range_support: bool
"""Engine supports search time range.""" """Engine supports search time range."""
safesearch: bool = False safesearch: bool
"""Engine supports SafeSearch""" """Engine supports SafeSearch"""
language_support: bool = False language_support: bool
"""Engine supports languages (locales) search.""" """Engine supports languages (locales) search."""
fetch_traits: "Callable[[EngineTraits, bool], None]" language: str
"""Function to to fetch engine's traits from origin.""" """For an engine, when there is ``language: ...`` in the YAML settings the engine
does support only this one language:
traits: "traits.EngineTraits"
"""Traits of the engine."""
# settings.yml
name: str
"""Name that will be used across SearXNG to define this engine. In settings, on
the result page .."""
engine: str
"""Name of the python file used to handle requests and responses to and from
this search engine (file name from :origin:`searx/engines` without
``.py``)."""
categories: list[str] = ["general"]
"""Specifies to which :ref:`engine categories` the engine should be added."""
language: str = ""
"""If the engine supports only one language, this language is specified here
(``en``, ``de``, ``"no"`` or ..); otherwise, the value remains empty. For
the YAML configuration: think of the `YAML-Norway problem
<https://ruuda.nl/2023/the-yaml-document-from-hell#the-norway-problem>`_
.. code:: yaml .. code:: yaml
- name: google norway - name: google french
engine: google engine: google
language: "no" language: fr
Depending on ``language_support``, this value has similar but also slightly
different meanings.
- When ``language_support`` is **true**, the map of
:py:obj:`traits.EngineTraits.languages` is reduced to the selected
language
- When ``language_support`` is **false**, then the implementation of the
engine only supports this one ``language``
""" """
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::
@@ -314,6 +231,26 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
region: fr-BE region: fr-BE
""" """
fetch_traits: "Callable[[EngineTraits, bool], None]"
"""Function to to fetch engine's traits from origin."""
traits: "traits.EngineTraits"
"""Traits of the engine."""
# settings.yml
categories: list[str]
"""Specifies to which :ref:`engine categories` the engine should be added."""
name: str
"""Name that will be used across SearXNG to define this engine. In settings, on
the result page .."""
engine: str
"""Name of the python file used to handle requests and responses to and from
this search engine (file name from :origin:`searx/engines` without
``.py``)."""
enable_http: bool enable_http: bool
"""Enable HTTP (by default only HTTPS is enabled).""" """Enable HTTP (by default only HTTPS is enabled)."""
@@ -326,31 +263,6 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
display_error_messages: bool display_error_messages: bool
"""Display error messages on the web UI.""" """Display error messages on the web UI."""
disabled: bool = False
"""To disable by default the engine, but not deleting it. It will allow the
user to manually activate it in the settings."""
inactive: bool = False
"""Remove the engine from the settings (*disabled & removed*)."""
about: EngineAbout = EngineAbout()
"""Additional fields describing the engine."""
using_tor_proxy: bool = False
"""Using tor proxy (``true``) or not (``false``) for this engine."""
send_accept_language_header: bool = True
"""When this option is activated (default), the language (locale) that is
selected by the user is used to build and send a ``Accept-Language`` header
in the request to the origin search engine."""
tokens: list[str] = []
"""A list of secret tokens to make this engine *private*, more details see
:ref:`private engines`."""
weight: float = 1.0
"""Weighting of the results of this engine (:ref:`weight <settings engines>`)."""
proxies: dict[str, dict[str, str]] proxies: dict[str, dict[str, str]]
"""Set proxies for a specific engine (YAML): """Set proxies for a specific engine (YAML):
@@ -361,43 +273,73 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
https: socks5://proxy:port https: socks5://proxy:port
""" """
def setup(self, engine_settings: dict[str, t.Any]) -> bool | None: # pylint: disable=unused-argument disabled: bool
"""To disable by default the engine, but not deleting it. It will allow the
user to manually activate it in the settings."""
inactive: bool
"""Remove the engine from the settings (*disabled & removed*)."""
about: dict[str, dict[str, str]]
"""Additional fields describing the engine.
.. code:: yaml
about:
website: https://example.com
wikidata_id: Q306656
official_api_documentation: https://example.com/api-doc
use_official_api: true
require_api_key: true
results: HTML
"""
using_tor_proxy: bool
"""Using tor proxy (``true``) or not (``false``) for this engine."""
send_accept_language_header: bool
"""When this option is activated (default), the language (locale) that is
selected by the user is used to build and send a ``Accept-Language`` header
in the request to the origin search engine."""
tokens: list[str]
"""A list of secret tokens to make this engine *private*, more details see
:ref:`private engines`."""
weight: int
"""Weighting of the results of this engine (:ref:`weight <settings engines>`)."""
def setup(self, engine_settings: dict[str, t.Any]) -> bool: # pylint: disable=unused-argument
"""Dynamic setup of the engine settings. """Dynamic setup of the engine settings.
With this method, the engine's setup is carried out. For example, to With this method, the engine's setup is carried out. For example, to
check or dynamically adapt the values handed over in the parameter check or dynamically adapt the values handed over in the parameter
``engine_settings``. ``engine_settings``. The return value (True/False) indicates whether
the setup was successful and the engine can be built or rejected.
Whether the initialization was successful can be indicated by the return The method is optional and is called synchronously as part of the
value ``True`` or even ``False``.
- If no return value (``None`` ) is given from this method , this is
equivalent to ``True``.
- If an exception is thrown as part of the initialization, this is
equivalent to ``False``.
The method is optional and is called **synchronously** as part of the
initialization of the service and is therefore only suitable for simple initialization of the service and is therefore only suitable for simple
(local) exams/changes at the engine setting. (local) exams/changes at the engine setting. The :py:obj:`Engine.init`
method must be used for longer tasks in which values of a remote must be
The :py:obj:`Engine.init` method must be used for longer tasks in which determined, for example.
values of a remote must be determined, for example.
""" """
return True return True
def init(self, engine_settings: dict[str, t.Any]) -> bool | None: # pylint: disable=unused-argument def init(self, engine_settings: dict[str, t.Any]) -> bool | None: # pylint: disable=unused-argument
"""Initialization of the engine. """Initialization of the engine.
The method is optional and called **asynchronous** (in a thread). The The method is optional and asynchronous (in a thread). It is suitable,
method is comparable to :py:obj:`Engine.setup`, it is suitable, for for example, for setting up a cache (for the engine) or for querying
caching data that first needs to be requested from a remote. values (required by the engine) from a remote.
The method is optional and runs **asynchronously** (in a thread), it is Whether the initialization was successful can be indicated by the return
comparable to :py:obj:`Engine.setup`. For instance, it is suitable for value ``True`` or even ``False``.
caching data that first needs to be requested from a remote source.
The evaluation of the return value is analogous to :py:obj:`Engine.setup`. - If no return value is given from this init method (``None``), this is
equivalent to ``True``.
- If an exception is thrown as part of the initialization, this is
equivalent to ``False``.
""" """
return True return True

View File

@@ -116,6 +116,19 @@ class EngineTraits:
return self.all_locale return self.all_locale
return locales.get_engine_locale(searxng_locale, self.regions, default=default) return locales.get_engine_locale(searxng_locale, self.regions, default=default)
def is_locale_supported(self, searxng_locale: str) -> bool:
"""A *locale* (SearXNG's internal representation) is considered to be
supported by the engine if the *region* or the *language* is supported
by the engine.
For verification the functions :py:func:`EngineTraits.get_region` and
:py:func:`EngineTraits.get_language` are used.
"""
if self.data_type == "traits_v1":
return bool(self.get_region(searxng_locale) or self.get_language(searxng_locale))
raise TypeError("engine traits of type %s is unknown" % self.data_type)
def copy(self): def copy(self):
"""Create a copy of the dataclass object.""" """Create a copy of the dataclass object."""
return EngineTraits(**dataclasses.asdict(self)) return EngineTraits(**dataclasses.asdict(self))
@@ -142,11 +155,11 @@ class EngineTraits:
""" """
if self.data_type == "traits_v1": if self.data_type == "traits_v1":
self._set_traits_v1(engine) # pyright: ignore[reportArgumentType] self._set_traits_v1(engine)
else: else:
raise TypeError("engine traits of type %s is unknown" % self.data_type) raise TypeError("engine traits of type %s is unknown" % self.data_type)
def _set_traits_v1(self, engine: "Engine") -> None: def _set_traits_v1(self, engine: "Engine | types.ModuleType") -> None:
# For an engine, when there is `language: ...` in the YAML settings the engine # For an engine, when there is `language: ...` in the YAML settings the engine
# does support only this one language (region):: # does support only this one language (region)::
# #
@@ -159,25 +172,22 @@ class EngineTraits:
_msg = "settings.yml - engine: '%s' / %s: '%s' not supported" _msg = "settings.yml - engine: '%s' / %s: '%s' not supported"
if engine.language: languages = traits.languages
if engine.language_support: if hasattr(engine, "language"):
if not len(traits.languages) > 1: if engine.language not in languages:
raise ValueError( raise ValueError(_msg % (engine.name, "language", engine.language))
f"engine {engine.name}: activated language_support with just one or less languages" traits.languages = {engine.language: languages[engine.language]}
)
if engine.language not in traits.languages:
raise ValueError(_msg % (engine.name, "language", engine.language))
traits.languages = {engine.language: traits.languages[engine.language]}
if engine.region: regions = traits.regions
if engine.region not in traits.regions: if hasattr(engine, "region"):
if engine.region not in regions:
raise ValueError(_msg % (engine.name, "region", engine.region)) raise ValueError(_msg % (engine.name, "region", engine.region))
traits.regions = {engine.region: traits.regions[engine.region]} traits.regions = {engine.region: regions[engine.region]}
engine.language_support = bool(traits.languages or traits.regions) engine.language_support = bool(traits.languages or traits.regions)
# set the copied & modified traits in engine's namespace # set the copied & modified traits in engine's namespace
engine.traits = traits engine.traits = traits # pyright: ignore[reportAttributeAccessIssue]
class EngineTraitsMap(dict[str, EngineTraits]): class EngineTraitsMap(dict[str, EngineTraits]):

View File

@@ -22,8 +22,8 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "zh",
} }
language = "zh"
# Engine Configuration # Engine Configuration
categories = ["general"] categories = ["general"]
@@ -84,10 +84,6 @@ def request(query, params):
def response(resp): def response(resp):
# sometimes 360search returns empty response when called from non-chinese ips
if not resp.text or not resp.text.strip():
return []
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
results = [] results = []

View File

@@ -1,137 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=invalid-name
"""500px_ is a online network for photographers with millions of members
worldwide. Photographers come to 500px to discover and share incredible photos,
gain meaningful exposure, compete in photo contests, and license their photos
through our exclusive distribution partners.
.. _500px: https://500px.com
"""
import typing as t
import codecs
import random
import string
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
# about
about = {
"website": "https://500px.com",
"wikidata_id": "Q354894",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://500px.com"
api_url = "https://api.500px.com"
categories = ["images"]
paging = True
results_per_page = 30
"""Number of results to return in the request.
The default was taken from the WEB UI, where the GraphQL query sets the value to
*static*: ``first: 30``.
"""
SXNG_query = """query PhotoSearchPaginationContainerQuery(
$first: Int, $cursor: String, $search: String!, $sort: PhotoSort, $filters: [PhotoSearchFilter!], $nlp: Boolean
) {
...SXNG_query
}
fragment SXNG_query on Query {
photoSearch(sort: $sort, first: $first, after: $cursor, search: $search, filters: $filters, nlp: $nlp) {
edges {
node {
id
canonicalPath
name
description
width
height
photographer: uploader {
displayName
}
images(sizes: [35, 33]) {
size
url
jpegUrl
webpUrl
id
}
}
cursor
}
}
}
"""
def setup(_) -> bool:
global SXNG_query # pylint: disable=global-statement
rand_str: str = "".join(random.choice(string.ascii_letters) for _ in range(5))
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
return True
def request(query: str, params: "OnlineParams") -> None:
# cursor is the base64 hash of the string "pos-<offset-1>", e.g. "pos-29" -> "cG9zLTI5"
offset = ((params["pageno"] - 1) * results_per_page) - 1
cursor = codecs.encode(f"pos-{offset}".encode("utf-8"), "base64").decode("utf-8")
params["url"] = f"{api_url}/graphql"
params["method"] = "POST"
params["json"] = {
"operationName": "PhotoSearchPaginationContainerQuery",
"variables": {
"first": results_per_page,
"cursor": cursor,
"search": query,
"sort": "RELEVANCE",
"filters": [],
"nlp": False,
},
"query": SXNG_query,
}
def response(resp: "SXNG_Response"):
res = EngineResults()
json_data = resp.json()["data"]["photoSearch"]
for edge in json_data["edges"]:
node = edge["node"] # pyright: ignore[reportAny]
if not node["images"]:
continue
images: list[dict[str, str]] = sorted(node["images"], key=lambda i: i["size"])
thumbnail_src = images[0]["url"]
img_src = images[-1]["url"]
res.add(
res.types.LegacyResult(
{
"template": "images.html",
"url": base_url + node["canonicalPath"],
"thumbnail_src": thumbnail_src,
"img_src": img_src,
"title": node["name"],
"content": node["description"],
"author": node["photographer"]["displayName"],
"resolution": f"{node['width']}x{node['height']}",
}
)
)
return res

View File

@@ -5,19 +5,19 @@ intended monkey patching of the engine modules.
.. attention:: .. attention::
Monkey-patching modules is a practice from the past that shouldn't be Monkey-patching modules is a practice from the past that shouldn't be
expanded upon. In the long run, engines should be instances of expanded upon. In the long run, there should be an engine class that can be
:py:obj:`searx.enginelib.Engine`. However, as long as long as all engine inherited. However, as long as this class doesn't exist, and as long as all
modules aren't converted to this class, these builtin types will still be engine modules aren't converted to an engine class, these builtin types will
needed. still be needed.
""" """
import logging import logging
from searx.enginelib import traits as _traits from searx.enginelib import traits as _traits
logger: logging.Logger logger: logging.Logger
supported_languages: str
language_aliases: str
language_support: bool language_support: bool
language: str
region: str
traits: _traits.EngineTraits traits: _traits.EngineTraits
# from searx.engines.ENGINE_DEFAULT_ARGS # from searx.engines.ENGINE_DEFAULT_ARGS

View File

@@ -12,50 +12,41 @@ import typing as t
import sys import sys
import copy import copy
import os
from os.path import realpath, dirname from os.path import realpath, dirname
import warnings
import types import types
import inspect import inspect
import msgspec
from searx import logger, settings from searx import logger, settings
from searx.utils import load_module from searx.utils import load_module
from searx.data import ENGINE_TRAITS
from searx.enginelib import Engine, EngineAbout if t.TYPE_CHECKING:
from searx.enginelib import Engine
logger = logger.getChild('engines') logger = logger.getChild('engines')
ENGINE_DIR = dirname(realpath(__file__)) ENGINE_DIR = dirname(realpath(__file__))
# Defaults for the namespace of an engine module, see load_engine() # Defaults for the namespace of an engine module, see load_engine()
ENGINE_DEFAULT_ARGS: dict[str, t.Any] = { ENGINE_DEFAULT_ARGS: dict[str, int | str | list[t.Any] | dict[str, t.Any] | bool] = {
# Common options in the engine module # Common options in the engine module
"engine_type": "online", "engine_type": "online",
"paging": False, "paging": False,
"max_page": 0,
"time_range_support": False, "time_range_support": False,
"safesearch": False, "safesearch": False,
"language_support": False,
# settings.yml # settings.yml
"categories": ["general"], "categories": ["general"],
"language": "",
"region": "",
"enable_http": False, "enable_http": False,
"shortcut": "-", "shortcut": "-",
"timeout": settings["outgoing"]["request_timeout"], "timeout": settings["outgoing"]["request_timeout"],
"display_error_messages": True, "display_error_messages": True,
"disabled": False, "disabled": False,
"inactive": False, "inactive": False,
"about": EngineAbout(), "about": {},
"using_tor_proxy": False, "using_tor_proxy": False,
"send_accept_language_header": True, "send_accept_language_header": True,
"tokens": [], "tokens": [],
"weight": 1.0, "max_page": 0,
} }
"""Default values that are set in an engine of type *module*, please compare
with the class :py:obj:`searx.enginelib.Engine`."""
# set automatically when an engine does not have any tab category # set automatically when an engine does not have any tab category
DEFAULT_CATEGORY = 'other' DEFAULT_CATEGORY = 'other'
@@ -185,41 +176,14 @@ 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
# set engine attributes from engine_data # set engine attributes from engine_data
kvargs: dict[str, t.Any]
if isinstance(engine.about, EngineAbout):
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
else:
kvargs = {**engine.about, **engine_data.get("about", {})}
try:
engine.about = EngineAbout(**kvargs)
except TypeError as exc:
raise TypeError(
f"engine '{engine_data['name']}' ({engine_data['engine']}) - in the about section --> {exc}"
) from exc
# warn about deprecated engine settings
if engine.about.language:
if hasattr(engine, "language") and not engine.language:
engine.language = engine.about.language
warnings.warn(
f"engine '{engine_data['name']}' ({engine_data['engine']})"
f" - migrate engine.about.language to engine.language!",
DeprecationWarning,
2,
)
for param_name, param_value in engine_data.items(): for param_name, param_value in engine_data.items():
if param_name == "about":
continue
if param_name == 'categories': if param_name == 'categories':
if isinstance(param_value, str): if isinstance(param_value, str):
param_value = list(map(str.strip, param_value.split(','))) param_value = list(map(str.strip, param_value.split(',')))
engine.categories = param_value # type: ignore engine.categories = param_value # type: ignore
elif hasattr(engine, 'about') and param_name == 'about':
engine.about = {**engine.about, **engine_data['about']} # type: ignore
else: else:
setattr(engine, param_name, param_value) setattr(engine, param_name, param_value)
@@ -228,9 +192,6 @@ def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: d
if not hasattr(engine, arg_name): if not hasattr(engine, arg_name):
setattr(engine, arg_name, copy.deepcopy(arg_value)) setattr(engine, arg_name, copy.deepcopy(arg_value))
if ENGINE_TRAITS.get(engine.name, {}).get("languages") and not engine.language_support:
raise ValueError(f"engine '{engine.name}' ({engine_data['engine']}) language_support should be set to True")
def update_attributes_for_tor(engine: "Engine | types.ModuleType"): def update_attributes_for_tor(engine: "Engine | types.ModuleType"):
if using_tor_proxy(engine) and hasattr(engine, 'onion_url'): if using_tor_proxy(engine) and hasattr(engine, 'onion_url'):
@@ -269,27 +230,21 @@ def is_engine_active(engine: "Engine | types.ModuleType"):
def call_engine_setup(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]) -> bool: def call_engine_setup(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]) -> bool:
setup_ok = False
setup_ok: bool | None = False
setup_func = getattr(engine, "setup", None) setup_func = getattr(engine, "setup", None)
if setup_func is None: if setup_func is None:
setup_ok = True setup_ok = True
elif not callable(setup_func): elif not callable(setup_func):
logger.error(f"engine's setup method isn't a callable (is of type: {type(setup_func)})") logger.error("engine's setup method isn't a callable (is of type: %s)", type(setup_func))
else: else:
try: try:
setup_ok = engine.setup(engine_data) setup_ok = engine.setup(engine_data)
except Exception as e: # pylint: disable=broad-except except Exception as e: # pylint: disable=broad-except
logger.exception(f"(PID {os.getpid()}) {engine.name}: engine SETUP failed, exception: {e}") logger.exception('exception : {0}'.format(e))
setup_ok = False
# The evaluation of the return value is analogous to Engine.init
if setup_ok is None:
setup_ok = True
if not setup_ok: if not setup_ok:
logger.error(f"(PID {os.getpid()}) {engine.name}: engine setup was not successful") logger.error("%s: Engine setup was not successful, engine is set to inactive.", engine.name)
return setup_ok return setup_ok
@@ -317,16 +272,12 @@ def load_engines(engine_list: list[dict[str, t.Any]]):
for engine_data in engine_list: for engine_data in engine_list:
if engine_data.get("inactive") is True: if engine_data.get("inactive") is True:
continue continue
engine = load_engine(engine_data) engine = load_engine(engine_data)
if engine: if engine:
register_engine(engine) register_engine(engine)
else: else:
# if an engine can't be loaded (if for example the engine is missing # if an engine can't be loaded (if for example the engine is missing
# tor or some other requirements) its set to inactive! # tor or some other requirements) its set to inactive!
logger.error( logger.error("loading engine %s failed: set engine to inactive!", engine_data.get("name", "???"))
f"(PID {os.getpid()}) {engine_data.get('name', '???')}: can't register engine (loading engine failed)"
)
engine_data["inactive"] = True engine_data["inactive"] = True
return engines return engines

View File

@@ -16,12 +16,12 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "HTML", "results": "HTML",
"language": "zh",
} }
# Engine Configuration # Engine Configuration
categories = ["videos"] categories = ["videos"]
paging = True paging = True
language = "zh"
# Base URL # Base URL
base_url = "https://www.acfun.cn" base_url = "https://www.acfun.cn"
@@ -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

@@ -39,6 +39,7 @@ url_xpath = './h4/a/@href'
title_xpath = './h4/a[1]' title_xpath = './h4/a[1]'
content_xpath = './/p[1]' content_xpath = './/p[1]'
correction_xpath = '//*[@id="didYouMean"]//a' correction_xpath = '//*[@id="didYouMean"]//a'
number_of_results_xpath = '//*[@id="totalResults"]'
name_token_xpath = '//form[@id="searchForm"]/input[@type="hidden"]/@name' name_token_xpath = '//form[@id="searchForm"]/input[@type="hidden"]/@name'
value_token_xpath = '//form[@id="searchForm"]/input[@type="hidden"]/@value' value_token_xpath = '//form[@id="searchForm"]/input[@type="hidden"]/@value'
@@ -106,6 +107,14 @@ def response(resp):
for correction in eval_xpath_list(dom, correction_xpath): for correction in eval_xpath_list(dom, correction_xpath):
results.append({'correction': extract_text(correction)}) results.append({'correction': extract_text(correction)})
# get number of results
number_of_results = eval_xpath(dom, number_of_results_xpath)
if number_of_results:
try:
results.append({'number_of_results': int(extract_text(number_of_results))})
except: # pylint: disable=bare-except
pass
# Update the tokens to the newest ones # Update the tokens to the newest ones
token_str = _get_tokens(dom) token_str = _get_tokens(dom)
CACHE.set('ahmia-tokens', token_str, expire=60 * 60) CACHE.set('ahmia-tokens', token_str, expire=60 * 60)

View File

@@ -64,7 +64,6 @@ about: dict[str, t.Any] = {
# engine dependent config # engine dependent config
categories = ["files", "books"] categories = ["files", "books"]
paging: bool = True paging: bool = True
language_support = True
# search-url # search-url
base_url: list[str] | str = [] base_url: list[str] | str = []

View File

@@ -42,8 +42,8 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'HTML', 'results': 'HTML',
'language': 'it',
} }
language = "it"
def request(query, params): def request(query, params):

208
searx/engines/aol.py Normal file
View File

@@ -0,0 +1,208 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""AOL supports WEB, image, and video search. Internally, it uses the Bing
index.
AOL doesn't seem to support setting the language via request parameters, instead
the results are based on the URL. For example, there is
- `search.aol.com <https://search.aol.com>`_ for English results
- `suche.aol.de <https://suche.aol.de>`_ for German results
However, AOL offers its services only in a few regions:
- en-US: search.aol.com
- de-DE: suche.aol.de
- fr-FR: recherche.aol.fr
- en-GB: search.aol.co.uk
- en-CA: search.aol.ca
In order to still offer sufficient support for language and region, the `search
keywords`_ known from Bing, ``language`` and ``loc`` (region), are added to the
search term (AOL is basically just a proxy for Bing).
.. _search keywords:
https://support.microsoft.com/en-us/topic/advanced-search-keywords-ea595928-5d63-4a0b-9c6b-0b769865e78a
"""
from urllib.parse import urlencode, unquote_plus
import typing as t
from lxml import html
from dateutil import parser
from searx.result_types import EngineResults
from searx.utils import eval_xpath_list, eval_xpath, extract_text
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.aol.com",
"wikidata_id": "Q2407",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
categories = ["general"]
search_type = "search" # supported: search, image, video
paging = True
safesearch = True
time_range_support = True
results_per_page = 10
base_url = "https://search.aol.com"
time_range_map = {"day": "1d", "week": "1w", "month": "1m", "year": "1y"}
safesearch_map = {0: "p", 1: "r", 2: "i"}
def init(_):
if search_type not in ("search", "image", "video"):
raise ValueError(f"unsupported search type {search_type}")
def request(query: str, params: "OnlineParams") -> None:
language, region = (params["searxng_locale"].split("-") + [None])[:2]
if language and language != "all":
query = f"{query} language:{language}"
if region:
query = f"{query} loc:{region}"
args: dict[str, str | int | None] = {
"q": query,
"b": params["pageno"] * results_per_page + 1, # page is 1-indexed
"pz": results_per_page,
}
if params["time_range"]:
args["fr2"] = "time"
args["age"] = params["time_range"]
else:
args["fr2"] = "sb-top-search"
params["cookies"]["sB"] = f"vm={safesearch_map[params['safesearch']]}"
params["url"] = f"{base_url}/aol/{search_type}?{urlencode(args)}"
logger.debug(params)
def _deobfuscate_url(obfuscated_url: str) -> str | None:
# URL looks like "https://search.aol.com/click/_ylt=AwjFSDjd;_ylu=JfsdjDFd/RV=2/RE=1774058166/RO=10/RU=https%3a%2f%2fen.wikipedia.org%2fwiki%2fTree/RK=0/RS=BP2CqeMLjscg4n8cTmuddlEQA2I-" # pylint: disable=line-too-long
if not obfuscated_url:
return None
for part in obfuscated_url.split("/"):
if part.startswith("RU="):
return unquote_plus(part[3:])
# pattern for de-obfuscating URL not found, fall back to Yahoo's tracking link
return obfuscated_url
def _general_results(doc: html.HtmlElement) -> EngineResults:
res = EngineResults()
for result in eval_xpath_list(doc, "//div[@id='web']//ol/li[not(contains(@class, 'first'))]"):
obfuscated_url = extract_text(eval_xpath(result, ".//h3/a/@href"))
if not obfuscated_url:
continue
url = _deobfuscate_url(obfuscated_url)
if not url:
continue
res.add(
res.types.MainResult(
url=url,
title=extract_text(eval_xpath(result, ".//h3/a")) or "",
content=extract_text(eval_xpath(result, ".//div[contains(@class, 'compText')]")) or "",
thumbnail=extract_text(eval_xpath(result, ".//a[contains(@class, 'thm')]/img/@data-src")) or "",
)
)
return res
def _video_results(doc: html.HtmlElement) -> EngineResults:
res = EngineResults()
for result in eval_xpath_list(doc, "//div[contains(@class, 'results')]//ol/li"):
obfuscated_url = extract_text(eval_xpath(result, ".//a/@href"))
if not obfuscated_url:
continue
url = _deobfuscate_url(obfuscated_url)
if not url:
continue
published_date_raw = extract_text(eval_xpath(result, ".//div[contains(@class, 'v-age')]"))
try:
published_date = parser.parse(published_date_raw or "")
except parser.ParserError:
published_date = None
res.add(
res.types.LegacyResult(
{
"template": "videos.html",
"url": url,
"title": extract_text(eval_xpath(result, ".//h3")),
"content": extract_text(eval_xpath(result, ".//div[contains(@class, 'compText')]")),
"thumbnail": extract_text(eval_xpath(result, ".//img[contains(@class, 'thm')]/@src")),
"length": extract_text(eval_xpath(result, ".//span[contains(@class, 'v-time')]")),
"publishedDate": published_date,
}
)
)
return res
def _image_results(doc: html.HtmlElement) -> EngineResults:
res = EngineResults()
for result in eval_xpath_list(doc, "//section[@id='results']//ul/li"):
obfuscated_url = extract_text(eval_xpath(result, "./a/@href"))
if not obfuscated_url:
continue
url = _deobfuscate_url(obfuscated_url)
if not url:
continue
res.add(
res.types.LegacyResult(
{
"template": "images.html",
# results don't have an extra URL, only the image source
"url": url,
"title": extract_text(eval_xpath(result, ".//a/@aria-label")),
"thumbnail_src": extract_text(eval_xpath(result, ".//img/@src")),
"img_src": url,
}
)
)
return res
def response(resp: "SXNG_Response") -> EngineResults:
doc = html.fromstring(resp.text)
match search_type:
case "search":
results = _general_results(doc)
case "image":
results = _image_results(doc)
case "video":
results = _video_results(doc)
case _:
raise ValueError("unsupported search type")
for suggestion in eval_xpath_list(doc, ".//ol[contains(@class, 'searchRightBottom')]//table//a"):
results.add(results.types.LegacyResult({"suggestion": extract_text(suggestion)}))
return results

View File

@@ -35,7 +35,6 @@ about = {
categories = ["it", "software wikis"] categories = ["it", "software wikis"]
paging = True paging = True
main_wiki = "wiki.archlinux.org" main_wiki = "wiki.archlinux.org"
language_support = True
def request(query, params): def request(query, params):

View File

@@ -21,7 +21,7 @@ about = {
categories = ['images'] categories = ['images']
paging = True paging = True
page_size = 20 nb_per_page = 20
search_api = 'https://api.artic.edu/api/v1/artworks/search?' search_api = 'https://api.artic.edu/api/v1/artworks/search?'
image_api = 'https://www.artic.edu/iiif/2/' image_api = 'https://www.artic.edu/iiif/2/'
@@ -34,7 +34,7 @@ def request(query, params):
'q': query, 'q': query,
'page': params['pageno'], 'page': params['pageno'],
'fields': 'id,title,artist_display,medium_display,image_id,date_display,dimensions,artist_titles', 'fields': 'id,title,artist_display,medium_display,image_id,date_display,dimensions,artist_titles',
'limit': page_size, 'limit': nb_per_page,
} }
) )
params['url'] = search_api + args params['url'] = search_api + args

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(

75
searx/engines/ask.py Normal file
View File

@@ -0,0 +1,75 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Ask.com"""
from urllib.parse import urlencode
import dateutil
from lxml import html
from searx import utils
# Metadata
about = {
"website": "https://www.ask.com/",
"wikidata_id": 'Q847564',
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
# Engine Configuration
categories = ['general']
paging = True
max_page = 5
"""Ask.com has at max 5 pages."""
# Base URL
base_url = "https://www.ask.com/web"
def request(query, params):
query_params = {
"q": query,
"page": params["pageno"],
}
params["url"] = f"{base_url}?{urlencode(query_params)}"
return params
def response(resp):
start_tag = 'window.MESON.initialState = {'
end_tag = '}};'
dom = html.fromstring(resp.text)
script = utils.eval_xpath_getindex(dom, '//script', 0, default=None).text
pos = script.index(start_tag) + len(start_tag) - 1
script = script[pos:]
pos = script.index(end_tag) + len(end_tag) - 1
script = script[:pos]
json_resp = utils.js_obj_str_to_python(script)
results = []
for item in json_resp['search']['webResults']['results']:
pubdate_original = item.get('pubdate_original')
if pubdate_original:
pubdate_original = dateutil.parser.parse(pubdate_original)
metadata = [item.get(field) for field in ['category_l1', 'catsy'] if item.get(field)]
results.append(
{
"url": item['url'].split('&ueid')[0],
"title": item['title'],
"content": item['abstract'],
"publishedDate": pubdate_original,
# "thumbnail": item.get('image_url') or None, # these are not thumbs / to large
"metadata": ' | '.join(metadata),
}
)
return results

View File

@@ -54,8 +54,8 @@ about = {
"use_official_api": True, "use_official_api": True,
"require_api_key": True, "require_api_key": True,
"results": "JSON", "results": "JSON",
"language": "en",
} }
language = "en"
CACHE: EngineCache CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire`` """Persistent (SQLite) key/value cache that deletes its values after ``expire``

View File

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

@@ -26,7 +26,7 @@ base_url = (
# engine dependent config # engine dependent config
paging = True paging = True
page_size = 10 number_of_results = 10
# shortcuts for advanced search # shortcuts for advanced search
shortcut_dict = { shortcut_dict = {
@@ -57,12 +57,12 @@ def request(query, params):
query = re.sub(key, val, query) query = re.sub(key, val, query)
# basic search # basic search
offset = (params['pageno'] - 1) * page_size offset = (params['pageno'] - 1) * number_of_results
string_args = { string_args = {
'query': urlencode({'query': query}), 'query': urlencode({'query': query}),
'offset': offset, 'offset': offset,
'hits': page_size, 'hits': number_of_results,
} }
params['url'] = base_url.format(**string_args) params['url'] = base_url.format(**string_args)

View File

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

View File

@@ -13,6 +13,7 @@ implementations are shared by other engines:
""" """
import base64 import base64
import re
import typing as t import typing as t
from urllib.parse import parse_qs, urlencode, urlparse from urllib.parse import parse_qs, urlencode, urlparse
@@ -47,7 +48,7 @@ _safesearch_map: dict[int, str] = {
} }
"""Filter results. 0: None, 1: Moderate, 2: Strict""" """Filter results. 0: None, 1: Moderate, 2: Strict"""
base_url = "https://www.bing.com" base_url = "https://www.bing.com/search"
"""Bing-Web search URL""" """Bing-Web search URL"""
@@ -93,7 +94,7 @@ def override_accept_language(params: "OnlineParams", engine_region: str | None)
params["headers"]["Accept-Language"] = f"{engine_region},{lang};q=0.9" params["headers"]["Accept-Language"] = f"{engine_region},{lang};q=0.9"
def request(query: str, params: "OnlineParams"): def request(query: str, params: "OnlineParams") -> "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)
@@ -109,7 +110,13 @@ def request(query: str, params: "OnlineParams"):
if locale_params: if locale_params:
query_params.update(locale_params) query_params.update(locale_params)
params["url"] = f"{base_url}/search?{urlencode(query_params)}" params["url"] = f"{base_url}?{urlencode(query_params)}"
# in some regions where geoblocking is employed (e.g. China),
# www.bing.com redirects to the regional version of Bing
params["allow_redirects"] = True
return params
def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]: def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]:
@@ -152,6 +159,12 @@ def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]:
results.append({"url": href, "title": title, "content": content}) results.append({"url": href, "title": title, "content": content})
if results:
result_len_container = "".join(eval_xpath(dom, '//span[@class="sb_count"]//text()'))
result_len_container = re.sub(r"[^0-9]", "", result_len_container)
if result_len_container:
results.append({"number_of_results": int(result_len_container)})
return results return results

View File

@@ -34,7 +34,7 @@ time_map = {
"year": 60 * 24 * 365, "year": 60 * 24 * 365,
} }
base_url = "https://www.bing.com" base_url = "https://www.bing.com/images/async"
"""Bing-Image search URL""" """Bing-Image search URL"""
@@ -64,7 +64,9 @@ def request(query, params):
if params["time_range"]: if params["time_range"]:
query_params["qft"] = "filterui:age-lt%s" % time_map[params["time_range"]] query_params["qft"] = "filterui:age-lt%s" % time_map[params["time_range"]]
params["url"] = base_url + "/images/async?" + urlencode(query_params) params["url"] = base_url + "?" + urlencode(query_params)
return params
def response(resp): def response(resp):

View File

@@ -44,7 +44,7 @@ time_map = {
difference of *last day* and *last week* in the result list is just marginally. difference of *last day* and *last week* in the result list is just marginally.
Bing does not have news range ``year`` / we use ``month`` instead.""" Bing does not have news range ``year`` / we use ``month`` instead."""
base_url = "https://www.bing.com" base_url = "https://www.bing.com/news/infinitescrollajax"
"""Bing (News) search URL""" """Bing (News) search URL"""
@@ -74,7 +74,9 @@ def request(query, params):
if params["time_range"]: if params["time_range"]:
query_params["qft"] = time_map.get(params["time_range"], 'interval="9"') query_params["qft"] = time_map.get(params["time_range"], 'interval="9"')
params["url"] = base_url + "/news/infinitescrollajax?" + urlencode(query_params) params["url"] = base_url + "?" + urlencode(query_params)
return params
def response(resp): def response(resp):

View File

@@ -29,7 +29,7 @@ paging = True
safesearch = True safesearch = True
time_range_support = True time_range_support = True
base_url = "https://www.bing.com" base_url = "https://www.bing.com/videos/asyncv2"
"""Bing-Video search URL""" """Bing-Video search URL"""
@@ -60,7 +60,7 @@ def request(query, params):
query_params["form"] = "VRFLTR" query_params["form"] = "VRFLTR"
query_params["qft"] = " filterui:videoage-lt%s" % time_map[params["time_range"]] query_params["qft"] = " filterui:videoage-lt%s" % time_map[params["time_range"]]
params["url"] = base_url + "/videos/asyncv2?" + urlencode(query_params) params["url"] = base_url + "?" + urlencode(query_params)
return params return 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

@@ -34,7 +34,6 @@ about = {
categories = ["general", "social media"] categories = ["general", "social media"]
paging = True paging = True
time_range_support = True time_range_support = True
language_support = True
base_url = "https://boardreader.com" base_url = "https://boardreader.com"
time_range_map = {"day": "1", "week": "7", "month": "30", "year": "365"} time_range_map = {"day": "1", "week": "7", "month": "30", "year": "365"}
@@ -104,7 +103,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

@@ -13,8 +13,8 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'JSON', 'results': 'JSON',
'language': 'de',
} }
language = "de"
paging = True paging = True
categories = ['general'] categories = ['general']

View File

@@ -31,7 +31,6 @@ from dateutil import parser
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.utils import html_to_text
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -76,7 +75,6 @@ def request(query: str, params: "OnlineParams") -> None:
"q": query, "q": query,
"count": results_per_page, "count": results_per_page,
"offset": (params["pageno"] - 1) * results_per_page, "offset": (params["pageno"] - 1) * results_per_page,
"text_decorations": False,
} }
# Apply time filter if specified # Apply time filter if specified
@@ -114,19 +112,14 @@ def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults() res = EngineResults()
data = resp.json() data = resp.json()
for result in (data.get("web") or {}).get("results", []): for result in data.get("web", {}).get("results", []):
thumbnail_obj = result.get("thumbnail")
thumbnail = ""
if thumbnail_obj and not thumbnail_obj.get("logo", False):
thumbnail = thumbnail_obj.get("src") or ""
res.add( res.add(
res.types.MainResult( res.types.MainResult(
url=result["url"], url=result["url"],
title=html_to_text(result["title"]), title=result["title"],
content=html_to_text(result.get("description", "")), content=result.get("description", ""),
publishedDate=_extract_published_date(result.get("age")), publishedDate=_extract_published_date(result.get("age")),
thumbnail=thumbnail, thumbnail=result.get("thumbnail", {}).get("src"),
), ),
) )

View File

@@ -1,85 +0,0 @@
# 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

@@ -1,115 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Chatnoir is an open source search engine developed by Webis, a network of
researchers from the universities of Weimar, Halle and Leipzig. It supports
different different text corpora as indexes, e.g. CommonCrawl. See its
`announcement`_ for more information.
.. _announcement : https://groups.google.com/g/common-crawl/c/3o2dOHpeRxo/m/H2Osqz9dAAAJ
"""
import typing as t
from searx.exceptions import SearxEngineAPIException
from searx.extended_types import SXNG_Response
from searx.network import get, post
from searx.result_types import EngineResults
from searx.utils import html_to_text
if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams
about = {
"website": "https://www.chatnoir.eu",
"official_api_documentation": "https://www.chatnoir.eu/docs/api-general",
"use_official_api": True,
"require_api_key": False,
"results": "JSON",
}
base_url = "https://www.chatnoir.eu"
categories = ["general"]
paging = True
page_size = 10
api_key = ""
"""You can optionally provide your own API key here. This one will then be used
instead of scraping an API key."""
search_index = "cw22"
"""Search index to browse in. See `the API documentation
<https://www.chatnoir.eu/docs/api-general>`_ for a full list."""
def _obtain_api_key() -> tuple[str, str, str]:
home_resp = get(base_url)
if not home_resp.ok:
raise SearxEngineAPIException("failed to obtain api key")
csrf_token = home_resp.cookies["csrftoken"]
token_resp = post(
"https://www.chatnoir.eu/?init",
headers={
"Referer": f"{base_url}/",
"X-Requested-With": "XMLHttpRequest",
"X-Csrf-Token": csrf_token,
},
cookies=home_resp.cookies,
)
if not token_resp.ok:
raise SearxEngineAPIException("failed to obtain api key")
session_id = token_resp.cookies["sessionid"]
scraped_api_key = token_resp.json()["token"]["token"]
return csrf_token, session_id, scraped_api_key
def request(query: str, params: "OnlineParams"):
if api_key:
# use user-provided API key instead of scraping one
headers = {
"Authorization": f"Bearer {api_key}",
}
params["headers"].update(headers)
else:
csrf_token, session_id, scraped_api_key = _obtain_api_key()
headers = {
"Authorization": f"Bearer {scraped_api_key}",
"X-Csrf-Token": csrf_token,
}
params["headers"].update(headers)
params["cookies"] = {"csrftoken": session_id, "sessionid": session_id}
params["url"] = f"{base_url}/api/v1/_search"
params["method"] = "POST"
json_data = {
"query": query,
"index": [
search_index,
],
"from": (params["pageno"] - 1) * page_size,
"size": page_size,
"_extended_meta": True,
}
params["json"] = json_data
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
results = resp.json()["results"]
for result in results:
res.add(
res.types.MainResult(
url=result["target_uri"],
title=html_to_text(result["title"]),
content=html_to_text(result["snippet"]),
)
)
return res

View File

@@ -10,13 +10,13 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'JSON', 'results': 'JSON',
'language': 'de',
} }
language = "de"
paging = True paging = True
categories = [] categories = []
page_size = 20 number_of_results = 20
skip_premium = True skip_premium = True
@@ -25,7 +25,7 @@ thumbnail_format = "crop-240x300"
def request(query, params): def request(query, params):
args = {'query': query, 'limit': page_size, 'offset': (params['pageno'] - 1) * page_size} args = {'query': query, 'limit': number_of_results, 'offset': (params['pageno'] - 1) * number_of_results}
params['url'] = f"{base_url}/v2/search-gateway/recipes?{urlencode(args)}" params['url'] = f"{base_url}/v2/search-gateway/recipes?{urlencode(args)}"
return params return params
@@ -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

@@ -19,9 +19,11 @@ Configuration
The engine has the following additional settings: The engine has the following additional settings:
- :py:obj:`chinaso_category` (:py:obj:`ChinasoCategoryType`)
- :py:obj:`chinaso_news_source` (:py:obj:`ChinasoNewsSourceType`) - :py:obj:`chinaso_news_source` (:py:obj:`ChinasoNewsSourceType`)
In the example below, ChinaSO is configured for news search. In the example below, all three ChinaSO engines are using the :ref:`network
<engine network>` from the ``chinaso news`` engine.
.. code:: yaml .. code:: yaml
@@ -29,8 +31,23 @@ In the example below, ChinaSO is configured for news search.
engine: chinaso engine: chinaso
shortcut: chinaso shortcut: chinaso
categories: [news] categories: [news]
chinaso_category: news
chinaso_news_source: all chinaso_news_source: all
- name: chinaso images
engine: chinaso
network: chinaso news
shortcut: chinasoi
categories: [images]
chinaso_category: images
- name: chinaso videos
engine: chinaso
network: chinaso news
shortcut: chinasov
categories: [videos]
chinaso_category: videos
Implementations Implementations
=============== ===============
@@ -53,13 +70,26 @@ about = {
"use_official_api": False, "use_official_api": False,
"require_api_key": False, "require_api_key": False,
"results": "JSON", "results": "JSON",
"language": "zh",
} }
paging = True paging = True
time_range_support = True time_range_support = True
results_per_page = 10 results_per_page = 10
categories = [] categories = []
language = "zh"
ChinasoCategoryType = t.Literal['news', 'videos', 'images']
"""ChinaSo supports news, videos, images search.
- ``news``: search for news
- ``videos``: search for videos
- ``images``: search for images
In the category ``news`` you can additionally filter by option
:py:obj:`chinaso_news_source`.
"""
chinaso_category = 'news'
"""Configure ChinaSo category (:py:obj:`ChinasoCategoryType`)."""
ChinasoNewsSourceType = t.Literal['CENTRAL', 'LOCAL', 'BUSINESS', 'EPAPER', 'all'] ChinasoNewsSourceType = t.Literal['CENTRAL', 'LOCAL', 'BUSINESS', 'EPAPER', 'all']
"""Filtering ChinaSo-News results by source: """Filtering ChinaSo-News results by source:
@@ -79,24 +109,39 @@ base_url = "https://www.chinaso.com"
def init(_): def init(_):
if chinaso_news_source not in t.get_args(ChinasoNewsSourceType): if chinaso_category not in ('news', 'videos', 'images'):
raise ValueError(f"Unsupported category: {chinaso_category}")
if chinaso_category == 'news' and 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}")
def request(query, params): def request(query, params):
query_params = {'q': query, 'pn': params["pageno"], 'ps': results_per_page} query_params = {"q": query}
if time_range_dict.get(params['time_range']): if time_range_dict.get(params['time_range']):
query_params["stime"] = time_range_dict[params['time_range']] query_params["stime"] = time_range_dict[params['time_range']]
query_params["etime"] = 'now' query_params["etime"] = 'now'
category_config = {
'news': {'endpoint': '/v5/general/v1/web/search', 'params': {'pn': params["pageno"], 'ps': results_per_page}},
'images': {
'endpoint': '/v5/general/v1/search/image',
'params': {'start_index': (params["pageno"] - 1) * results_per_page, 'rn': results_per_page},
},
'videos': {
'endpoint': '/v5/general/v1/search/video',
'params': {'start_index': (params["pageno"] - 1) * results_per_page, 'rn': results_per_page},
},
}
if chinaso_news_source != 'all': if chinaso_news_source != 'all':
if chinaso_news_source == 'EPAPER': if chinaso_news_source == 'EPAPER':
query_params["type"] = 'EPAPER' category_config['news']['params']["type"] = 'EPAPER'
else: else:
query_params["cate"] = chinaso_news_source category_config['news']['params']["cate"] = chinaso_news_source
params["url"] = f"{base_url}/v5/general/v1/web/search?{urlencode(query_params)}" query_params.update(category_config[chinaso_category]['params'])
params["url"] = f"{base_url}{category_config[chinaso_category]['endpoint']}?{urlencode(query_params)}"
cookie = { cookie = {
"uid": base64.b64encode(secrets.token_bytes(16)).decode("utf-8"), "uid": base64.b64encode(secrets.token_bytes(16)).decode("utf-8"),
} }
@@ -111,13 +156,12 @@ def response(resp):
except Exception as e: except Exception as e:
raise SearxEngineAPIException(f"Invalid response: {e}") from e raise SearxEngineAPIException(f"Invalid response: {e}") from e
# Upstream returns {'status': 0, 'msg': 'empty result', 'data': {}} when there parsers = {'news': parse_news, 'images': parse_images, 'videos': parse_videos}
# are no results; this is a valid empty result rather than an API error.
if not isinstance(data, dict) or "data" not in data:
raise SearxEngineAPIException("Invalid response")
if not data["data"]:
return []
return parsers[chinaso_category](data)
def parse_news(data):
results = [] results = []
if not data.get("data", {}).get("data"): if not data.get("data", {}).get("data"):
raise SearxEngineAPIException("Invalid response") raise SearxEngineAPIException("Invalid response")
@@ -139,3 +183,47 @@ def response(resp):
} }
) )
return results return results
def parse_images(data):
results = []
if not data.get("data", {}).get("arrRes"):
raise SearxEngineAPIException("Invalid response")
for entry in data["data"]["arrRes"]:
results.append(
{
'url': entry["web_url"],
'title': html_to_text(entry["title"]),
'content': html_to_text(entry.get("ImageInfo", "")),
'template': 'images.html',
'img_src': entry["url"].replace("http://", "https://"),
'thumbnail_src': entry["largeimage"].replace("http://", "https://"),
}
)
return results
def parse_videos(data):
results = []
if not data.get("data", {}).get("arrRes"):
raise SearxEngineAPIException("Invalid response")
for entry in data["data"]["arrRes"]:
published_date = None
if entry.get("VideoPubDate"):
try:
published_date = datetime.fromtimestamp(int(entry["VideoPubDate"]))
except (ValueError, TypeError):
pass
results.append(
{
'url': entry["url"],
'title': html_to_text(entry["raw_title"]),
'template': 'videos.html',
'publishedDate': published_date,
'thumbnail': entry["image_src"].replace("http://", "https://"),
}
)
return results

View File

@@ -56,7 +56,7 @@ the API key in the engine :ref:`core engine config`."""
categories = ["science", "scientific publications"] categories = ["science", "scientific publications"]
paging = True paging = True
page_size = 10 nb_per_page = 10
base_url = "https://api.core.ac.uk/v3/search/works/" base_url = "https://api.core.ac.uk/v3/search/works/"
@@ -77,8 +77,8 @@ def request(query: str, params: "OnlineParams") -> None:
# API v3 uses different parameters # API v3 uses different parameters
search_params = { search_params = {
"q": query, "q": query,
"offset": (params["pageno"] - 1) * page_size, "offset": (params["pageno"] - 1) * nb_per_page,
"limit": page_size, "limit": nb_per_page,
"sort": "relevance", "sort": "relevance",
} }

View File

@@ -38,9 +38,8 @@ about = {
# engine dependent config # engine dependent config
categories = ["videos"] categories = ["videos"]
paging = True paging = True
page_size = 10 number_of_results = 10
language_support = True
time_range_support = True time_range_support = True
time_delta_dict = { time_delta_dict = {
"day": timedelta(days=1), "day": timedelta(days=1),
@@ -114,7 +113,7 @@ def request(query, params):
"password_protected": "false", "password_protected": "false",
"private": "false", "private": "false",
"sort": "relevance", "sort": "relevance",
"limit": page_size, "limit": number_of_results,
"fields": ",".join(result_fields), "fields": ",".join(result_fields),
} }

View File

@@ -24,7 +24,7 @@ import typing as t
import json import json
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineCache, EngineAbout from searx.enginelib import EngineCache
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.search.processors import RequestParams from searx.search.processors import RequestParams
@@ -35,11 +35,13 @@ categories = ["general"]
disabled = True disabled = True
timeout = 2.0 timeout = 2.0
language = "en" about = {
about = EngineAbout( "wikidata_id": None,
results="JSON", "official_api_documentation": None,
description="Demo offline engine Engine with results in the English language.", "use_official_api": False,
) "require_api_key": False,
"results": "JSON",
}
# if there is a need for globals, use a leading underline # if there is a need for globals, use a leading underline
_my_offline_engine: str = "" _my_offline_engine: str = ""
@@ -107,6 +109,7 @@ def search(query: str, params: "RequestParams") -> EngineResults:
kvmap=kvmap, kvmap=kvmap,
) )
) )
res.add(res.types.LegacyResult(number_of_results=count))
# cache counter value for 20sec # cache counter value for 20sec
CACHE.set("count", count, expire=20) CACHE.set("count", count, expire=20)

View File

@@ -25,7 +25,6 @@ import typing as t
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.result_types import EngineResults from searx.result_types import EngineResults
from searx.enginelib import EngineAbout
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response from searx.extended_types import SXNG_Response
@@ -44,14 +43,14 @@ page_size = 20
search_api = "https://api.artic.edu/api/v1/artworks/search" search_api = "https://api.artic.edu/api/v1/artworks/search"
image_api = "https://www.artic.edu/iiif/2/" image_api = "https://www.artic.edu/iiif/2/"
about = EngineAbout( about = {
website="https://www.artic.edu", "website": "https://www.artic.edu",
wikidata_id="Q239303", "wikidata_id": "Q239303",
official_api_documentation="http://api.artic.edu/docs/", "official_api_documentation": "http://api.artic.edu/docs/",
use_official_api=True, "use_official_api": True,
require_api_key=False, "require_api_key": False,
results="JSON", "results": "JSON",
) }
# if there is a need for globals, use a leading underline # if there is a need for globals, use a leading underline

View File

@@ -11,8 +11,8 @@ about = {
'use_official_api': False, 'use_official_api': False,
'require_api_key': False, 'require_api_key': False,
'results': 'HTML', 'results': 'HTML',
'language': 'de',
} }
language = "de"
categories = [] categories = []
paging = True paging = True

View File

@@ -176,4 +176,6 @@ def response(resp):
results.append(result) results.append(result)
results.append({'number_of_results': len(json_data['topics'])})
return results return results

View File

@@ -1,101 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Dogpile is a metasearch engine by the American advertising company `System1`_.
.. _System1: https://system1.com/
"""
import typing as t
from datetime import datetime, timezone
import html
from searx.utils import format_duration, html_to_text, humanize_number
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.dogpile.com",
"wikidata_id": "Q3595363",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "JSON",
}
paging = True
safesearch = True
categories = ["general"]
dogpile_categ = "search"
"""Category to search in. Can be either "search", "images", "videos" or "news"."""
base_url = "https://www.dogpile.com"
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
def init(_):
if dogpile_categ not in ("search", "images", "videos", "news"):
raise ValueError("invalid search type: %s" % dogpile_categ)
def request(query: str, params: "OnlineParams"):
params["url"] = f"{base_url}/api/{dogpile_categ}"
params["method"] = "POST"
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
return params
def response(resp: "SXNG_Response"):
res = EngineResults()
json_resp = resp.json()
for result in json_resp["results"]:
if dogpile_categ == "search":
res.add(
res.types.MainResult(
url=result["clickUrl"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
)
)
elif dogpile_categ == "news":
res.add(
res.types.MainResult(
url=result["clickUrl"],
title=html_to_text(html.unescape(result["title"])),
content=html_to_text(html.unescape(result["description"])),
thumbnail=result["thumbnailUrl"],
publishedDate=datetime.fromtimestamp(result["date"], tz=timezone.utc),
)
)
elif dogpile_categ == "videos":
res.add(
res.types.LegacyResult(
template="videos.html",
url=result["clickUrl"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
thumbnail=result["thumbnailUrl"],
publishedDate=datetime.fromisoformat(result["publishDate"]),
length=format_duration(result["duration"]),
views=humanize_number(result["viewCount"]),
)
)
elif dogpile_categ == "images":
res.add(
res.types.Image(
url=result["altClickUrl"],
title=html_to_text(result["title"]),
content=html_to_text(result["description"]),
img_src=result["clickUrl"],
thumbnail_src=result["thumbnailUrl"],
resolution=f"{result['width']}x{result['height']}",
img_format=result["format"],
)
)
return res

View File

@@ -21,6 +21,7 @@ about = {
# engine dependent config # engine dependent config
categories = ['general'] # 'images', 'music', 'videos', 'files' categories = ['general'] # 'images', 'music', 'videos', 'files'
paging = False paging = False
number_of_results = 5
# search-url # search-url
# Doku is OpenSearch compatible # Doku is OpenSearch compatible

View File

@@ -203,7 +203,6 @@ about: dict[str, str | bool] = {
categories: list[str] = ["general", "web"] categories: list[str] = ["general", "web"]
paging: bool = True paging: bool = True
time_range_support: bool = True time_range_support: bool = True
language_support = True
safesearch: bool = True safesearch: bool = True
"""DDG-lite: user can't select but the results are filtered.""" """DDG-lite: user can't select but the results are filtered."""

View File

@@ -28,7 +28,6 @@ about = {
"require_api_key": False, "require_api_key": False,
"results": "JSON (site requires js to get images)", "results": "JSON (site requires js to get images)",
} }
language_support = True
# engine dependent config # engine dependent config
categories = [] categories = []
@@ -42,9 +41,7 @@ safesearch_cookies = {0: "-2", 1: None, 2: "1"}
safesearch_args = {0: "1", 1: None, 2: "1"} safesearch_args = {0: "1", 1: None, 2: "1"}
search_path_map = {"images": "i", "videos": "v", "news": "news"} search_path_map = {"images": "i", "videos": "v", "news": "news"}
_HTTP_User_Agent: str = gen_useragent() _HTTP_User_Agent: str = gen_useragent()
send_accept_language_header = False
def init(engine_settings: dict[str, t.Any]): def init(engine_settings: dict[str, t.Any]):

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