1 Commits

Author SHA1 Message Date
searxng-bot
c6707dc0b6 [data] update searx.data - update_engine_traits.py 2026-06-29 00:44:13 +00:00
108 changed files with 5529 additions and 5012 deletions

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@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.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@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "container-${{ matrix.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-
container-${{ matrix.arch }}- container-${{ matrix.arch }}-
path: "/var/tmp/buildah-cache-*/*" path: "/var/tmp/buildah-cache-*/*"
- name: Build - if: ${{ matrix.emulation }}
id: build name: Setup QEMU
env: uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.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@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
persist-credentials: "false" persist-credentials: "false"
- if: ${{ matrix.emulation }}
name: Setup QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
registry: "docker.io" persist-credentials: "false"
username: "${{ secrets.DOCKER_USER }}"
password: "${{ secrets.DOCKER_TOKEN }}"
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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
@@ -50,7 +51,7 @@ jobs:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -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,7 +25,7 @@ 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
@@ -43,7 +43,7 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -65,7 +65,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:
@@ -44,7 +44,7 @@ jobs:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -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@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "26"
check-latest: "true"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: "./.nvmrc"
- name: Setup cache Node.js
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
path: "./client/simple/node_modules/"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
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,9 +26,9 @@ 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
@@ -46,7 +46,7 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -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
@@ -94,7 +94,7 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: |
@@ -107,7 +107,7 @@ jobs:
- name: Setup Weblate - name: Setup Weblate
run: | run: |
mkdir -p ~/.config mkdir -p ~/.config
echo "${{ secrets.WEBLATE_CONFIG }}" >~/.config/weblate echo "${{ secrets.WEBLATE_CONFIG }}" > ~/.config/weblate
- name: Setup Git - name: Setup Git
run: | run: |
@@ -118,17 +118,23 @@ jobs:
run: make V=1 weblate.translations.commit run: make V=1 weblate.translations.commit
- name: Create PR - name: Create PR
id: cpr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with: with:
author: "searxng-bot <searxng-bot@users.noreply.github.com>" author: "searxng-bot <searxng-bot@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>" committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
title: "[mod] i18n: update translations from Weblate" title: "[l10n] update translations from Weblate"
commit-message: "[mod] i18n: update translations from Weblate" commit-message: "[l10n] update translations from Weblate"
branch: "translations_update" branch: "translations_update"
delete-branch: "true" delete-branch: "true"
draft: "false" draft: "false"
signoff: "false" signoff: "false"
body: | body: |
Update translations from Weblate [l10n] update translations from Weblate
labels: | labels: |
area:i18n area:i18n
- name: Display information
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

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

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

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

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.3", "@biomejs/biome": "2.5.0",
"@types/node": "^26.1.1", "@types/node": "^26.0.0",
"browserslist": "^4.28.6", "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.1",
"less": "^4.6.7", "less": "^4.6.6",
"mathjs": "^15.2.0", "mathjs": "^15.2.0",
"sharp": "~0.35.3", "sharp": "~0.35.1",
"sort-package-json": "^4.0.0", "sort-package-json": "^4.0.0",
"stylelint": "^17.14.0", "stylelint": "^17.13.0",
"stylelint-config-standard-less": "^4.1.0", "stylelint-config-standard-less": "^4.1.0",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svgo": "^4.0.2", "svgo": "^4.0.1",
"typescript": "~7.0.2", "typescript": "~6.0.3",
"vite": "^8.1.4", "vite": "^8.0.16",
"vite-bundle-analyzer": "^1.3.8" "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

@@ -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 ./

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

@@ -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

@@ -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``,
``privacywall``, ``wikipedia``, ``swisscows``, ``qwant`` ]
Service which completes words as you type.
``safesearch`` : default from :ref:`settings search`
[ ``0``, ``1``, ``2`` ]
Filter search results of engines which support safe search. See if an engine Filter search results of engines which support safe search. See if an engine
supports safe search in the preferences page of an instance. supports safe search in the preferences page of an instance.
``theme`` : default ``simple`` : [ ``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

@@ -4,7 +4,7 @@ cov-core==1.15.0
black==25.9.0 black==25.9.0
pylint==4.0.6 pylint==4.0.6
splinter==0.21.0 splinter==0.21.0
selenium==4.46.0 selenium==4.45.0
Sphinx==8.2.3;python_version <= "3.11" Sphinx==8.2.3;python_version <= "3.11"
Sphinx==9.1.0; python_version > "3.11" Sphinx==9.1.0; python_version > "3.11"
sphinx-issues==6.0.0 sphinx-issues==6.0.0
@@ -18,11 +18,11 @@ myst-parser==5.0.0
linuxdoc==20260504 linuxdoc==20260504
aiounittest==1.5.0 aiounittest==1.5.0
yamllint==1.38.0 yamllint==1.38.0
wlc==2.1.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.6
basedpyright==1.39.9 basedpyright==1.39.8
types-lxml==2026.2.16 types-lxml==2026.2.16

View File

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

View File

@@ -13,7 +13,7 @@ sniffio==1.3.1
valkey==6.1.1 valkey==6.1.1
markdown-it-py==4.2.0 markdown-it-py==4.2.0
msgspec==0.21.1 msgspec==0.21.1
typer==0.27.0 typer==0.26.7
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

@@ -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(
{ {

View File

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

View File

@@ -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

@@ -773,7 +773,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": "боливиано",
@@ -1107,7 +1107,7 @@
"fi": "Kongon frangi", "fi": "Kongon frangi",
"fr": "franc congolais", "fr": "franc congolais",
"ga": "franc an Chongó", "ga": "franc an Chongó",
"gl": "franco congolés", "gl": "Franco congolés",
"he": "פרנק קונגולזי", "he": "פרנק קונגולזי",
"hr": "Kongoanski franak", "hr": "Kongoanski franak",
"hu": "kongói frank", "hu": "kongói frank",
@@ -1636,7 +1636,7 @@
"fi": "Algerian dinaari", "fi": "Algerian dinaari",
"fr": "dinar algérien", "fr": "dinar algérien",
"ga": "dinar na hAilgéire", "ga": "dinar na hAilgéire",
"gl": "dinar alxeriano", "gl": "Dinar alxeriano",
"he": "דינר אלג'ירי", "he": "דינר אלג'ירי",
"hr": "Alžirski dinar", "hr": "Alžirski dinar",
"hu": "algériai dinár", "hu": "algériai dinár",
@@ -1651,7 +1651,7 @@
"pap": "dinar argelino", "pap": "dinar argelino",
"pl": "dinar algierski", "pl": "dinar algierski",
"pt": "dinar argelino", "pt": "dinar argelino",
"ro": "dinar algerian", "ro": "Dinar algerian",
"ru": "алжирский динар", "ru": "алжирский динар",
"sk": "Alžírský dinár", "sk": "Alžírský dinár",
"sl": "alžirski dinar", "sl": "alžirski dinar",
@@ -1786,7 +1786,7 @@
"cy": "Ewro", "cy": "Ewro",
"da": "Euro", "da": "Euro",
"de": "Euro", "de": "Euro",
"en": "euro", "en": "Euro",
"eo": "eŭro", "eo": "eŭro",
"es": "Euro", "es": "Euro",
"et": "Euro", "et": "Euro",
@@ -1803,7 +1803,7 @@
"it": "Euro", "it": "Euro",
"ja": "ユーロ", "ja": "ユーロ",
"ko": "유로", "ko": "유로",
"lt": "euras", "lt": "Euras",
"lv": "eiro", "lv": "eiro",
"ml": "യൂറോ", "ml": "യൂറോ",
"ms": "Euro", "ms": "Euro",
@@ -2759,7 +2759,7 @@
"pa": "ਜਪਾਨੀ ਯੈੱਨ", "pa": "ਜਪਾਨੀ ਯੈੱਨ",
"pl": "jen", "pl": "jen",
"pt": "iene", "pt": "iene",
"ro": "yen", "ro": "yeni",
"ru": "японская иена", "ru": "японская иена",
"sk": "jen", "sk": "jen",
"sl": "japonski jen", "sl": "japonski jen",
@@ -3337,7 +3337,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 +3539,6 @@
"ja": "チャット", "ja": "チャット",
"ko": "미얀마 짯", "ko": "미얀마 짯",
"lt": "Kijatas", "lt": "Kijatas",
"lv": "Kjats",
"ml": "ബർമ്മീസ് ക്യാറ്റ്", "ml": "ബർമ്മീസ് ക്യാറ്റ്",
"nl": "Myanmarese kyat", "nl": "Myanmarese kyat",
"oc": "Kyat", "oc": "Kyat",
@@ -4312,7 +4310,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",
@@ -4616,7 +4614,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",
@@ -5333,7 +5331,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 +5394,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 +5766,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",
@@ -6150,7 +6147,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 +6225,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 +7055,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",
@@ -8430,7 +8425,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",
@@ -9524,8 +9518,6 @@
"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",
@@ -10031,7 +10023,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",
@@ -11322,6 +11313,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",
@@ -12265,7 +12257,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 +12740,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 +12806,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 +12965,6 @@
"FKP", "FKP",
"EGP" "EGP"
], ],
"£S": "SYP",
"£e": "EGP", "£e": "EGP",
"£s": "SYP", "£s": "SYP",
"¥": [ "¥": [
@@ -14969,8 +14959,6 @@
"فورنت مجري": "HUF", "فورنت مجري": "HUF",
"فورينت مجري": "HUF", "فورينت مجري": "HUF",
"فِرَنْكٌ رُوَنْدِيٌّ": "RWF", "فِرَنْكٌ رُوَنْدِيٌّ": "RWF",
"قائمة النقود المعدنية السورية": "SYP",
"قائمة عملات سوريا المعدنية": "SYP",
"ك": "KWD", "ك": "KWD",
"كتزال غواتيمالي": "GTQ", "كتزال غواتيمالي": "GTQ",
"كرونة آيسلندية": "ISK", "كرونة آيسلندية": "ISK",

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", "151.0",
"151.0" "150.0"
] ]
} }

View File

@@ -3272,7 +3272,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,

View File

@@ -47,7 +47,7 @@ ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache(
ExpireCacheCfg( ExpireCacheCfg(
name="ENGINES_CACHE", name="ENGINES_CACHE",
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
MAINTENANCE_PERIOD=60 * 60, # 1h MAINTENANCE_PERIOD=60 * 60, # 2h
MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB
) )
) )
@@ -361,43 +361,37 @@ 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 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

@@ -269,27 +269,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 +311,14 @@ def load_engines(engine_list: list[dict[str, t.Any]]):
for engine_data in engine_list: for engine_data in engine_list:
if engine_data.get("inactive") is True: if engine_data.get("inactive") is True:
continue continue
engine = load_engine(engine_data) engine = load_engine(engine_data)
if engine: if engine:
register_engine(engine) register_engine(engine)
else: else:
# if an engine can't be loaded (if for example the engine is missing # if an engine can't be loaded (if for example the engine is missing
# tor or some other requirements) its set to inactive! # tor or some other requirements) its set to inactive!
logger.error( logger.error(
f"(PID {os.getpid()}) {engine_data.get('name', '???')}: can't register engine (loading engine failed)" f"(PID {os.getpid()}) loading engine %s failed: set engine to inactive!", engine_data.get("name", "???")
) )
engine_data["inactive"] = True engine_data["inactive"] = True
return engines return engines

View File

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

View File

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

View File

@@ -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 = {
@@ -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

@@ -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,11 +50,6 @@ def request(query, params):
"search_type": "video", "search_type": "video",
} }
if params.get("time_range") in time_range_dict:
pubtime_begin_s, pubtime_end_s = _pubtime_range(params["time_range"])
query_params["pubtime_begin_s"] = pubtime_begin_s
query_params["pubtime_end_s"] = pubtime_end_s
params["url"] = f"{base_url}?{urlencode(query_params)}" params["url"] = f"{base_url}?{urlencode(query_params)}"
params["headers"]["Referer"] = "https://www.bilibili.com/" params["headers"]["Referer"] = "https://www.bilibili.com/"
params["headers"]["Accept"] = "application/json, text/javascript, */*; q=0.01" params["headers"]["Accept"] = "application/json, text/javascript, */*; q=0.01"

View File

@@ -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

@@ -104,7 +104,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
title=_remove_keyword_marker(result["Subject"]), title=_remove_keyword_marker(result["Subject"]),
content=_remove_keyword_marker(result["Text"]), content=_remove_keyword_marker(result["Text"]),
url=result["Url"], url=result["Url"],
publishedDate=datetime.fromisoformat(result["Published"]), publishedDate=datetime.strptime(result["Published"], "%Y-%m-%d %H:%M:%S"),
metadata=gettext.gettext("Posted by {author}").format(author=result["Author"]), metadata=gettext.gettext("Posted by {author}").format(author=result["Author"]),
) )
) )

View File

@@ -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

@@ -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

@@ -37,7 +37,7 @@ def response(resp):
for item in search_res: for item in search_res:
img = 'https://s3.thehackerblog.com/findthatmeme/' + item['image_path'] img = 'https://s3.thehackerblog.com/findthatmeme/' + item['image_path']
thumb = 'https://s3.thehackerblog.com/findthatmeme/thumb/' + item.get('thumbnail', '') thumb = 'https://s3.thehackerblog.com/findthatmeme/thumb/' + item.get('thumbnail', '')
date = datetime.fromisoformat(item["updated_at"].split("T")[0]) date = datetime.strptime(item["updated_at"].split("T")[0], "%Y-%m-%d")
formatted_date = datetime.fromtimestamp(date.timestamp()) formatted_date = datetime.fromtimestamp(date.timestamp())
results.append( results.append(

View File

@@ -47,7 +47,7 @@ def response(resp: "SXNG_Response"):
title=result["title"], title=result["title"],
content=result["description"], content=result["description"],
thumbnail=result["smallImageURL"], thumbnail=result["smallImageURL"],
publishedDate=datetime.fromisoformat(result["status_since"]), publishedDate=datetime.strptime(result["status_since"], "%Y-%m-%d %H:%M:%S"),
metadata=f"Rank: {result['rank']} || {result['episode_count']} episodes", metadata=f"Rank: {result['rank']} || {result['episode_count']} episodes",
) )
) )

View File

@@ -32,6 +32,7 @@ from searx.utils import (
eval_xpath_getindex, eval_xpath_getindex,
eval_xpath_list, eval_xpath_list,
extract_text, extract_text,
gen_gsa_useragent,
) )
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
@@ -196,7 +197,7 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
# https://developers.google.com/custom-search/docs/xml_results_appendices#interfaceLanguages # https://developers.google.com/custom-search/docs/xml_results_appendices#interfaceLanguages
# https://github.com/searxng/searxng/issues/2515#issuecomment-1607150817 # https://github.com/searxng/searxng/issues/2515#issuecomment-1607150817
ret_val["params"]["hl"] = f"{lang_code}" ret_val["params"]["hl"] = f"{lang_code}-{country}"
# lr parameter: # lr parameter:
# The lr (language restrict) parameter restricts search results to # The lr (language restrict) parameter restricts search results to
@@ -267,6 +268,7 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
# HTTP headers # HTTP headers
ret_val["headers"]["Accept"] = "*/*" ret_val["headers"]["Accept"] = "*/*"
ret_val["headers"]["User-Agent"] = gen_gsa_useragent()
# Cookies # Cookies

View File

@@ -1,190 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Google Custom Search Engine"""
import datetime
import typing as t
from json import loads
from urllib.parse import urlencode
from searx.enginelib import EngineCache
from searx.exceptions import SearxEngineAPIException, SearxEngineTooManyRequestsException
from searx.network import get
from searx.result_types import EngineResults, Result, MainResult, Image
from searx.engines.google import fetch_traits # pylint: disable=unused-import
from searx.engines.google import filter_mapping, get_google_info
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": "https://www.google.com",
"wikidata_id": "Q2233943",
"official_api_documentation": "https://developers.google.com/custom-search/docs/element",
"use_official_api": False,
"require_api_key": False,
"results": "JSONP",
"description": "Platform for creating custom search engines based on Google Search.",
}
categories = ["general", "web"]
paging = True
max_page = 5
page_size = 20
time_range_support = True
language_support = True
safesearch = True
GoogleCategType = t.Literal["", "image"]
google_categ: GoogleCategType = ""
"""Google CSE category. Set to ``""`` for web search."""
CX = "partner-pub-8993703457585266:4862972284" # blackle.com
CACHE: EngineCache
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement
if google_categ not in t.get_args(GoogleCategType):
raise ValueError("invalid google cse category: %s" % google_categ)
CACHE = EngineCache(engine_settings["name"])
return True
def _cse_token() -> dict[str, str]:
token: dict[str, str] = CACHE.get(CX)
if token:
return token
resp = get(f"https://www.google.com/cse/cse.js?cx={CX}", timeout=10)
if not resp.ok:
raise SearxEngineAPIException("failed to obtain cse token")
end = resp.text.rfind("});")
start = resp.text.rfind("({")
opts: dict[str, str] = loads(resp.text[start + 1 : end + 1])
cse_tok = opts.get("cse_token")
if not cse_tok:
raise SearxEngineAPIException("failed to obtain cse token")
exp = opts.get("exp")
token = {
"cse_tok": cse_tok,
"cselibv": opts.get("cselibVersion", ""),
"exp": ",".join(exp) if exp else "",
}
CACHE.set(CX, token, expire=3600)
return token
def _get_start_and_end_date_str(time_range: str) -> tuple[str, str]:
time_range_map = {"day": 1, "week": 7, "month": 30, "year": 365}
end_date = datetime.datetime.now()
start_date = end_date - datetime.timedelta(days=time_range_map[time_range])
return start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")
def request(query: str, params: "OnlineParams") -> None:
token = _cse_token()
google_info = get_google_info(params, traits)
info: dict[str, str] = google_info["params"]
args = {
"rsz": "filtered_cse",
"num": str(page_size),
"hl": info["hl"],
"cselibv": token["cselibv"],
"cx": CX,
"q": query,
"safe": filter_mapping[params["safesearch"]],
"cse_tok": token["cse_tok"],
"callback": "_",
"rurl": "",
"searchtype": google_categ,
}
if params["time_range"]:
start_date, end_date = _get_start_and_end_date_str(params["time_range"])
args["sort"] = f"date:r:{start_date}:{end_date}"
if info.get("lr"):
args["lr"] = info["lr"]
if info.get("cr"):
args["cr"] = info["cr"]
if google_info["country"] not in (None, "ZZ"):
args["gl"] = google_info["country"]
if token["exp"]:
args["exp"] = token["exp"]
start = (params["pageno"] - 1) * page_size
if start:
args["start"] = str(start)
params["url"] = "https://cse.google.com/cse/element/v1?" + urlencode(args)
params["cookies"] = google_info["cookies"]
params["headers"].update(google_info["headers"])
params["headers"]["Referer"] = "https://cse.google.com/"
def response(resp: "SXNG_Response") -> EngineResults:
json_resp = resp.text[resp.text.find("{") : resp.text.rfind("}") + 1]
data = loads(json_resp)
# not the real types, but a sufficient approximation
item: dict[str, str]
error: dict[str, str | int]
if error := data.get("error"):
message = error.get("message", "unknown error")
if error.get("code") == 429:
raise SearxEngineTooManyRequestsException(message=f"google cse: {message}")
raise SearxEngineAPIException(f"google cse: {message}")
results = EngineResults()
for item in data.get("results", []):
res: Result | None
if google_categ == "":
res = web_item(item)
elif google_categ == "image":
res = img_item(item)
if res is not None:
results.add(res)
return results
def web_item(item: dict[str, str]) -> MainResult | None:
url = item.get("unescapedUrl")
if not url:
return None
return MainResult(
url=url,
title=item.get("titleNoFormatting", ""),
content=item.get("contentNoFormatting", ""),
thumbnail=item.get("richSnippet", {}).get("cseThumbnail", {}).get("src", ""), # type: ignore
)
def img_item(item: dict[str, str]) -> Image | None:
resolution = ""
if item.get("height") and item.get("width"):
resolution = f"{item['width']}x{item['height']}"
return Image(
url=item["originalContextUrl"],
title=item.get("titleNoFormatting", ""),
content=item.get("contentNoFormatting", ""),
img_src=item["unescapedUrl"],
thumbnail_src=item["tbUrl"],
resolution=resolution,
img_format=item["fileFormat"].split("/")[-1],
)

View File

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

View File

@@ -91,7 +91,7 @@ def response(resp) -> EngineResults:
published_date = None published_date = None
try: try:
published_date = datetime.fromisoformat(entry["createdAt"].rstrip("Z")) published_date = datetime.strptime(entry["createdAt"], "%Y-%m-%dT%H:%M:%S.%fZ")
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass

View File

@@ -43,7 +43,7 @@ def _result(video: dict[str, typing.Any], album_info: dict[str, typing.Any]):
release_time = album_info.get("releaseTime", {}).get("value") release_time = album_info.get("releaseTime", {}).get("value")
if release_time: if release_time:
try: try:
published_date = datetime.fromisoformat(release_time) published_date = datetime.strptime(release_time, "%Y-%m-%d")
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass

View File

@@ -42,14 +42,15 @@ To enable Kagi, add the following to the ``engines`` seciton of
.. _Api Portal: https://help.kagi.com/kagi/api/overview.html .. _Api Portal: https://help.kagi.com/kagi/api/overview.html
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta
import typing as t import typing as t
import html
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.utils import html_to_text, parse_duration_string from searx.utils import parse_duration_string
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from searx.search.processors import OnlineParams from searx.search.processors import OnlineParams
@@ -76,12 +77,7 @@ kagi_categ: t.Literal["search", "images", "news", "videos"] = "search"
base_url = "https://kagi.com" base_url = "https://kagi.com"
safe_search_map = {0: False, 1: True, 2: True} safe_search_map = {0: False, 1: True, 2: True}
time_range_to_days_map: dict[TimeRangeType, int] = { time_range_to_days_map: dict[TimeRangeType, int] = {"day": 1, "week": 7, "month": 30, "year": 365}
"day": 1,
"week": 7,
"month": 30,
"year": 365,
}
api_key = "" api_key = ""
"""Kagi API key. Required for using this engine.""" """Kagi API key. Required for using this engine."""
@@ -139,13 +135,9 @@ def response(resp: "SXNG_Response") -> EngineResults:
if kagi_categ in ("images", "videos"): if kagi_categ in ("images", "videos"):
# the JSON key is "image" for "images" and "video" for "videos" # the JSON key is "image" for "images" and "video" for "videos"
json_results = json_data["data"].get(kagi_categ[:-1]) json_results = json_data["data"][kagi_categ[:-1]]
else: else:
json_results = json_data["data"].get(kagi_categ) json_results = json_data["data"][kagi_categ]
# if no results were found, the response doesn't contain the results field
if not json_results:
return res
for result in json_results: for result in json_results:
published_date: datetime | None = None published_date: datetime | None = None
@@ -156,8 +148,8 @@ def response(resp: "SXNG_Response") -> EngineResults:
res.add( res.add(
res.types.MainResult( res.types.MainResult(
url=result["url"], url=result["url"],
title=html_to_text(result.get("title", "no title available")), title=html.unescape(result["title"]),
content=html_to_text(result.get("snippet", "")), content=html.unescape(result["snippet"]),
thumbnail=result.get("image", {}).get("url") or "", thumbnail=result.get("image", {}).get("url") or "",
publishedDate=published_date, publishedDate=published_date,
) )
@@ -166,15 +158,15 @@ def response(resp: "SXNG_Response") -> EngineResults:
res.add( res.add(
res.types.Image( res.types.Image(
url=result["url"], url=result["url"],
title=html_to_text(result.get("title", "no title available")), title=html.unescape(result.get("title")),
img_src=result.get("image", {}).get("url"), img_src=result.get("image", {}).get("url"),
resolution=f"{result.get('image', {}).get('width')}x{result.get('image', {}).get('height')}", resolution=f"{result['image']['width']}x{result['image']['height']}",
thumbnail_src=result.get("props", {}).get("thumbnail", {}).get("url"), thumbnail_src=result.get("props", {}).get("thumbnail", {}).get("url"),
) )
) )
elif kagi_categ == "videos": elif kagi_categ == "videos":
length: timedelta | None = None length: timedelta | None = None
if result.get("props", {}).get("duration"): if result["props"].get("duration"):
length = parse_duration_string(result["props"]["duration"]) length = parse_duration_string(result["props"]["duration"])
res.add( res.add(
@@ -182,11 +174,11 @@ def response(resp: "SXNG_Response") -> EngineResults:
{ {
"template": "videos.html", "template": "videos.html",
"url": result["url"], "url": result["url"],
"title": html_to_text(result.get("title", "no title available")), "title": html.unescape(result["title"]),
"content": html_to_text(result.get("snippet", "")), "content": html.unescape(result["snippet"]),
"thumbnail": result.get("image", {}).get("url"), "thumbnail": result.get("image", {}).get("url"),
"publishedDate": published_date, "publishedDate": published_date,
"author": result.get("props", {}).get("creator_name"), "author": result["props"].get("creator_name"),
"length": length, "length": length,
} }
) )

View File

@@ -92,7 +92,7 @@ def _get_communities(json):
'title': result['community']['title'], 'title': result['community']['title'],
'content': markdown_to_text(result['community'].get('description', '')), 'content': markdown_to_text(result['community'].get('description', '')),
'thumbnail': result['community'].get('icon', result['community'].get('banner')), 'thumbnail': result['community'].get('icon', result['community'].get('banner')),
'publishedDate': datetime.fromisoformat(counts['published'][:19]), 'publishedDate': datetime.strptime(counts['published'][:19], '%Y-%m-%dT%H:%M:%S'),
'metadata': metadata, 'metadata': metadata,
} }
) )
@@ -141,7 +141,7 @@ def _get_posts(json):
'title': result['post']['name'], 'title': result['post']['name'],
'content': content, 'content': content,
'thumbnail': thumbnail, 'thumbnail': thumbnail,
'publishedDate': datetime.fromisoformat(result['post']['published'][:19]), 'publishedDate': datetime.strptime(result['post']['published'][:19], '%Y-%m-%dT%H:%M:%S'),
'metadata': metadata, 'metadata': metadata,
} }
) )
@@ -170,7 +170,7 @@ def _get_comments(json):
'url': result['comment']['ap_id'], 'url': result['comment']['ap_id'],
'title': result['post']['name'], 'title': result['post']['name'],
'content': markdown_to_text(result['comment']['content']), 'content': markdown_to_text(result['comment']['content']),
'publishedDate': datetime.fromisoformat(result['comment']['published'][:19]), 'publishedDate': datetime.strptime(result['comment']['published'][:19], '%Y-%m-%dT%H:%M:%S'),
'metadata': metadata, 'metadata': metadata,
} }
) )

View File

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

View File

@@ -60,7 +60,7 @@ def response(resp):
'title': result['username'] + f" ({result['followers_count']} followers)", 'title': result['username'] + f" ({result['followers_count']} followers)",
'content': result['note'], 'content': result['note'],
'thumbnail': result.get('avatar'), 'thumbnail': result.get('avatar'),
'publishedDate': datetime.fromisoformat(result['created_at'][:10]), 'publishedDate': datetime.strptime(result['created_at'][:10], "%Y-%m-%d"),
} }
) )
elif mastodon_type == "hashtags": elif mastodon_type == "hashtags":

View File

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

View File

@@ -1,66 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Neocities_ is open source software for creating blogs.
.. _Neocities : https://github.com/neocities/neocities
"""
from urllib.parse import urlencode
import typing as t
from lxml import html
from searx.utils import eval_xpath, eval_xpath_list, extract_text
from searx.result_types import EngineResults
if t.TYPE_CHECKING:
from extended_types import SXNG_Response
from search.processors import OnlineParams
# Engine metadata
about = {
"website": "https://neocities.org/",
"wikidata_id": "Q17071099",
"official_api_documentation": None,
"use_official_api": False,
"require_api_key": False,
"results": "HTML",
}
# Engine configuration
categories = ["general", "blogs"]
paging = True
# Search URL
base_url = "https://neocities.org"
results_xpath = "//div[@class='result-item']"
url_xpath = './/div[@class="result-url"]/a/@href'
title_xpath = './/h3[@class="result-title"]/a/text()'
content_xpath = './/p[@class="result-snippet"]//text()'
screenshot_xpath = './/a[@class="result-screenshot"]/img/@src'
def request(query: str, params: "OnlineParams") -> None:
query_params: dict[str, t.Any] = {"q": query}
if params['pageno'] > 1:
offset = (params["pageno"] - 1) * 100
query_params["start"] = offset
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
def response(resp: "SXNG_Response") -> EngineResults:
results = EngineResults()
dom = html.fromstring(resp.text)
for result in eval_xpath_list(dom, results_xpath):
results.add(
results.types.MainResult(
url=extract_text(eval_xpath(result, url_xpath)),
title=extract_text(eval_xpath(result, title_xpath)) or "",
content=extract_text(eval_xpath(result, content_xpath)) or "",
thumbnail=base_url + (extract_text(eval_xpath(result, screenshot_xpath)) or ""),
)
)
return results

View File

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

View File

@@ -44,7 +44,7 @@ def response(resp) -> EngineResults:
cve_id = item["cve"]["id"] cve_id = item["cve"]["id"]
description = item["cve"]["descriptions"][0]["value"] description = item["cve"]["descriptions"][0]["value"]
date = datetime.fromisoformat(item["cve"]["published"]) date = datetime.strptime(item["cve"]["published"], "%Y-%m-%dT%H:%M:%S.%f")
# Extract severity (Low, Medium, High, or Critical) and CVSS score, if available # Extract severity (Low, Medium, High, or Critical) and CVSS score, if available
info = item["cve"].get("metrics", {}).get("cvssMetricV31", [{}])[0].get("cvssData", {}) info = item["cve"].get("metrics", {}).get("cvssMetricV31", [{}])[0].get("cvssData", {})

View File

@@ -76,7 +76,7 @@ def response(resp):
release_time = item["release_time"] release_time = item["release_time"]
duration = item["duration"] duration = item["duration"]
release_date = datetime.fromisoformat(release_time.split("T")[0]) release_date = datetime.strptime(release_time.split("T")[0], "%Y-%m-%d")
formatted_date = datetime.fromtimestamp(release_date.timestamp()) formatted_date = datetime.fromtimestamp(release_date.timestamp())
url = f"https://odysee.com/{name}:{claim_id}" url = f"https://odysee.com/{name}:{claim_id}"

View File

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

View File

@@ -54,7 +54,7 @@ def response(resp: "SXNG_Response"):
title=result["title"], title=result["title"],
content=result["description"], content=result["description"],
thumbnail=result["image_url"], thumbnail=result["image_url"],
publishedDate=datetime.fromisoformat(result["created_at"]), publishedDate=datetime.strptime(result["created_at"], "%Y-%m-%d %H:%M:%S"),
metadata=" | ".join(metadata), metadata=" | ".join(metadata),
) )
) )

View File

@@ -140,7 +140,7 @@ def response(resp):
results.append( results.append(
{ {
'template': 'images.html', 'template': 'images.html',
'url': _clean_url(f"{pdia_base_url}/images/{result['objectID']}"), 'url': _clean_url(f"{about['website']}/images/{result['objectID']}"),
'img_src': _clean_url(base_image_url), 'img_src': _clean_url(base_image_url),
'thumbnail_src': _clean_url(base_image_url + THUMBNAIL_SUFFIX), 'thumbnail_src': _clean_url(base_image_url + THUMBNAIL_SUFFIX),
'title': f"{result['title'].strip()} by {result['artist']} {result.get('displayYear', '')}", 'title': f"{result['title'].strip()} by {result['artist']} {result.get('displayYear', '')}",

View File

@@ -292,7 +292,7 @@ def parse_news_uchq(data):
results = [] results = []
for item in data.get('feed', []): for item in data.get('feed', []):
try: try:
published_date = datetime.fromisoformat(item.get('time')) published_date = datetime.strptime(item.get('time'), "%Y-%m-%d")
except (ValueError, TypeError): except (ValueError, TypeError):
# Sometime Quark will return non-standard format like "1天前", set published_date as None # Sometime Quark will return non-standard format like "1天前", set published_date as None
published_date = None published_date = None

View File

@@ -38,7 +38,6 @@ Implementations
""" """
import random
import typing as t import typing as t
from datetime import ( from datetime import (
@@ -51,7 +50,6 @@ from urllib.parse import urlencode
import babel import babel
from flask_babel import gettext # pyright: ignore[reportUnknownVariableType] from flask_babel import gettext # pyright: ignore[reportUnknownVariableType]
from searx.enginelib import EngineCache
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.exceptions import ( from searx.exceptions import (
SearxEngineAccessDeniedException, SearxEngineAccessDeniedException,
@@ -91,11 +89,6 @@ qwant_categ: str = None # pyright: ignore[reportAssignmentType]
safesearch = True safesearch = True
# tgp seems to be short for "test group" - its actual value doesn't matter, as
# long as it's sent and at the correct position in the query params and doesn't
# change too frequently
test_group_value = random.randint(1, 3)
# fmt: off # fmt: off
qwant_news_locales = [ qwant_news_locales = [
"ca_ad", "ca_es", "ca_fr", "co_fr", "de_at", "de_ch", "de_de", "en_au", "ca_ad", "ca_es", "ca_fr", "co_fr", "de_at", "de_ch", "de_de", "en_au",
@@ -106,19 +99,9 @@ qwant_news_locales = [
] ]
# fmt: on # fmt: on
base_url = "https://www.qwant.com"
api_url = "https://api.qwant.com/v3/search/" api_url = "https://api.qwant.com/v3/search/"
"""URL of Qwant's API (JSON)""" """URL of Qwant's API (JSON)"""
CACHE: EngineCache
"""Cache for storing the ``datadome`` cookie."""
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return True
def request(query: str, params: "OnlineParams") -> None: def request(query: str, params: "OnlineParams") -> None:
"""Qwant search request""" """Qwant search request"""
@@ -131,38 +114,26 @@ def request(query: str, params: "OnlineParams") -> None:
results_per_page = 10 results_per_page = 10
if qwant_categ == "images": if qwant_categ == "images":
results_per_page = 50 results_per_page = 50
args = { args = {
"q": query, "q": query,
"count": results_per_page, "count": results_per_page,
"locale": q_locale, "locale": q_locale,
"offset": (params["pageno"] - 1) * results_per_page, "offset": (params["pageno"] - 1) * results_per_page,
"tgp": test_group_value,
"device": "desktop", "device": "desktop",
"safesearch": params["safesearch"], "safesearch": params["safesearch"],
# True would be encoded to "True", instead of "true", which makes the request "tgp": 1,
# easier to detect and block "display": True,
"displayed": "true", "llm": True,
"llm": "true",
} }
params["raise_for_httperror"] = False params["raise_for_httperror"] = False
params["url"] = f"{api_url}{qwant_categ}?{urlencode(args)}" params["url"] = f"{api_url}{qwant_categ}?{urlencode(args)}"
params["cookies"]["datadome"] = CACHE.get("datadome")
params["headers"].update({"Accept": "application/json", "Referer": f"{base_url}/", "Origin": base_url})
def response(resp: "SXNG_Response") -> EngineResults: def response(resp: "SXNG_Response") -> EngineResults:
"""Parse results from Qwant's API""" """Parse results from Qwant's API"""
# pylint: disable=too-many-locals, too-many-branches, too-many-statements # pylint: disable=too-many-locals, too-many-branches, too-many-statements
# cache datadome cookie - changes on each request
datadome = resp.cookies.get("datadome")
if datadome:
CACHE.set("datadome", datadome)
res = EngineResults() res = EngineResults()
# Try to load JSON result # Try to load JSON result
@@ -179,8 +150,8 @@ def response(resp: "SXNG_Response") -> EngineResults:
error_code = data.get("error_code") error_code = data.get("error_code")
if error_code == 24: if error_code == 24:
raise SearxEngineTooManyRequestsException() raise SearxEngineTooManyRequestsException()
if search_results.get("url") is not None: if search_results.get("data", {}).get("error_data", {}).get("captchaUrl") is not None:
raise SearxEngineCaptchaException(suspended_time=0) raise SearxEngineCaptchaException()
if resp.status_code == 403: if resp.status_code == 403:
raise SearxEngineAccessDeniedException() raise SearxEngineAccessDeniedException()
msg = ",".join(data.get("message", ["unknown"])) msg = ",".join(data.get("message", ["unknown"]))
@@ -219,6 +190,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
mainline_items: list[dict[str, t.Any]] = row.get("items", []) mainline_items: list[dict[str, t.Any]] = row.get("items", [])
for item in mainline_items: for item in mainline_items:
title: str = item.get("title", "") title: str = item.get("title", "")
res_url: str = item.get("url", "") res_url: str = item.get("url", "")
pub_date: datetime | None = None pub_date: datetime | None = None
@@ -318,7 +290,7 @@ def fetch_traits(engine_traits: EngineTraits):
from searx.utils import extr from searx.utils import extr
resp = get( resp = get(
base_url, # pyright: ignore[reportArgumentType] about["website"], # pyright: ignore[reportArgumentType]
timeout=5, timeout=5,
) )
if not resp.ok: if not resp.ok:

View File

@@ -58,7 +58,7 @@ def response(resp):
title = extract_text(result_dom.xpath(title_xpath)) title = extract_text(result_dom.xpath(title_xpath))
p_date = extract_text(result_dom.xpath(published_date)) p_date = extract_text(result_dom.xpath(published_date))
# fix offset date for line 644 webapp.py check # fix offset date for line 644 webapp.py check
fixed_date = datetime.fromisoformat(p_date) fixed_date = datetime.strptime(p_date, '%Y-%m-%dT%H:%M:%S%z')
earned = extract_text(result_dom.xpath(earned_xpath)) earned = extract_text(result_dom.xpath(earned_xpath))
views = extract_text(result_dom.xpath(views_xpath)) views = extract_text(result_dom.xpath(views_xpath))
rumbles = extract_text(result_dom.xpath(rumbles_xpath)) rumbles = extract_text(result_dom.xpath(rumbles_xpath))

View File

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

View File

@@ -120,7 +120,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
publishedDate: datetime | None publishedDate: datetime | None
if "pubDate" in result: if "pubDate" in result:
publishedDate = datetime.fromisoformat(result["pubDate"]) publishedDate = datetime.strptime(result["pubDate"], "%Y-%m-%d")
else: else:
publishedDate = None publishedDate = None

View File

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

View File

@@ -95,8 +95,7 @@ def _parse_date(text):
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", text) date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", text)
if date_match: if date_match:
try: try:
y, m, d = date_match.group(1).split("-") return datetime.strptime(date_match.group(1), "%Y-%m-%d")
return datetime(year=int(y), month=int(m), day=int(d))
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
return None return None

View File

@@ -54,7 +54,7 @@ def response(resp):
published_date = None published_date = None
if entry.get("date") and entry.get("duration"): if entry.get("date") and entry.get("duration"):
try: try:
published_date = datetime.fromisoformat(entry['date']) published_date = datetime.strptime(entry['date'], "%Y-%m-%d")
except (ValueError, TypeError): except (ValueError, TypeError):
published_date = None published_date = None

View File

@@ -134,7 +134,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
return str(record.get(k, "")) return str(record.get(k, ""))
for record in json_data["records"]: for record in json_data["records"]:
published = datetime.fromisoformat(record["publicationDate"]) published = datetime.strptime(record["publicationDate"], "%Y-%m-%d")
authors: list[str] = [" ".join(author["creator"].split(", ")[::-1]) for author in record["creators"]] authors: list[str] = [" ".join(author["creator"].split(", ")[::-1]) for author in record["creators"]]
pdf_url = "" pdf_url = ""

View File

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

View File

@@ -81,7 +81,7 @@ def _story(item):
return { return {
'title': item['title'], 'title': item['title'],
'thumbnail': item.get('teaserImage', {}).get('imageVariants', {}).get('16x9-256'), 'thumbnail': item.get('teaserImage', {}).get('imageVariants', {}).get('16x9-256'),
'publishedDate': datetime.fromisoformat(item['date'][:19]), 'publishedDate': datetime.strptime(item['date'][:19], '%Y-%m-%dT%H:%M:%S'),
'content': item.get('firstSentence'), 'content': item.get('firstSentence'),
'url': item['shareURL'] if use_source_url else item['detailsweb'], 'url': item['shareURL'] if use_source_url else item['detailsweb'],
} }
@@ -103,7 +103,7 @@ def _video(item):
'template': 'videos.html', 'template': 'videos.html',
'title': title, 'title': title,
'thumbnail': item.get('teaserImage', {}).get('imageVariants', {}).get('16x9-256'), 'thumbnail': item.get('teaserImage', {}).get('imageVariants', {}).get('16x9-256'),
'publishedDate': datetime.fromisoformat(item['date'][:19]), 'publishedDate': datetime.strptime(item['date'][:19], '%Y-%m-%dT%H:%M:%S'),
'content': item.get('firstSentence', ''), 'content': item.get('firstSentence', ''),
'iframe_src': video_url, 'iframe_src': video_url,
'url': url, 'url': url,

View File

@@ -117,7 +117,7 @@ def _obtain_session_code() -> str:
if not code: if not code:
raise SearxEngineAPIException("failed to obtain session code") raise SearxEngineAPIException("failed to obtain session code")
CACHE.set("session", code, expire=60 * 24 * 60 * 60) # cookie is valid for two months CACHE.set("session", code, expire=60 * 24 * 60) # cookie is valid for two months
return code return code

View File

@@ -121,7 +121,7 @@ def parse_tineye_match(match_json):
crawl_date = backlink_json.get("crawl_date") crawl_date = backlink_json.get("crawl_date")
if crawl_date: if crawl_date:
crawl_date = datetime.fromisoformat(crawl_date) crawl_date = datetime.strptime(crawl_date, '%Y-%m-%d')
else: else:
crawl_date = datetime.min crawl_date = datetime.min

View File

@@ -51,7 +51,7 @@ def response(resp):
'title': title, 'title': title,
'content': html_to_text(result['content']), 'content': html_to_text(result['content']),
'thumbnail': thumbnail, 'thumbnail': thumbnail,
'publishedDate': datetime.fromisoformat(result['created_at']), 'publishedDate': datetime.strptime(result['created_at'], '%Y-%m-%d %H:%M:%S'),
} }
) )

View File

@@ -13,7 +13,7 @@ from dateutil import parser
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.network import get from searx.network import get
from searx.utils import gen_useragent, html_to_text from searx.utils import html_to_text
from searx.result_types import EngineResults from searx.result_types import EngineResults
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
@@ -52,7 +52,7 @@ def _obtain_x_sid() -> tuple[str, str]:
The header key is usually called `x-sid-{UUIDv4}`, and the value is The header key is usually called `x-sid-{UUIDv4}`, and the value is
usually a plain UUIDv4 (but a different one than in the header key). usually a plain UUIDv4 (but a different one than in the header key).
""" """
resp = get(f"{api_url}/revcontent/embed.js", headers={"User-Agent": gen_useragent()}) resp = get(f"{api_url}/revcontent/embed.js")
if not resp.ok: if not resp.ok:
raise SearxEngineAPIException("failed to obtain request x-sid token") raise SearxEngineAPIException("failed to obtain request x-sid token")
@@ -89,14 +89,12 @@ def request(query: str, params: "OnlineParams") -> None:
params["url"] = f"{api_url}/Search/Web?{urlencode(args)}" params["url"] = f"{api_url}/Search/Web?{urlencode(args)}"
x_sid_header, x_sid_value = _obtain_x_sid() x_sid_header, x_sid_value = _obtain_x_sid()
params["headers"].update( params["headers"] = {
{ x_sid_header: x_sid_value,
x_sid_header: x_sid_value, # required - we send a random longitude and latitude instead of the actual user location
# required - we send a random longitude and latitude instead of the actual user location 'x-lon': str(random.random() * 90),
"x-lon": str(round(random.random() * 90, 4)), 'x-lat': str(random.random() * 90),
"x-lat": str(round(random.random() * 90, 4)), }
}
)
def response(resp: "SXNG_Response"): def response(resp: "SXNG_Response"):

View File

@@ -79,7 +79,7 @@ def response(resp):
'img_src': result['path'], 'img_src': result['path'],
'thumbnail_src': result['thumbs']['small'], 'thumbnail_src': result['thumbs']['small'],
'resolution': result['resolution'].replace('x', ' x '), 'resolution': result['resolution'].replace('x', ' x '),
'publishedDate': datetime.fromisoformat(result['created_at']), 'publishedDate': datetime.strptime(result['created_at'], '%Y-%m-%d %H:%M:%S'),
'img_format': result['file_type'], 'img_format': result['file_type'],
'filesize': humanize_bytes(result['file_size']), 'filesize': humanize_bytes(result['file_size']),
} }

View File

@@ -52,12 +52,6 @@ def _normalize_url_fields(result: "Result | LegacyResult"):
result.parsed_url = urllib.parse.urlparse(result.url) result.parsed_url = urllib.parse.urlparse(result.url)
if result.parsed_url: if result.parsed_url:
# properly format special characters (e.g. "ä", "ö") in IDN domains
# e.g. "xn--strung-xxa.de" becomes "störung.de"
if result.parsed_url.netloc.startswith("xn--"):
netloc = result.parsed_url.netloc.encode().decode("idna")
result.parsed_url = result.parsed_url._replace(netloc=netloc)
result.parsed_url = result.parsed_url._replace( result.parsed_url = result.parsed_url._replace(
# if the result has no scheme, use http as default # if the result has no scheme, use http as default
scheme=result.parsed_url.scheme or "http", scheme=result.parsed_url.scheme or "http",

View File

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

View File

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

View File

@@ -477,24 +477,6 @@ engines:
engine: arxiv engine: arxiv
shortcut: arx shortcut: arx
- name: avalw
engine: json_engine
shortcut: av
categories: general
paging: true
search_url: https://avalw.org/api/search?q={query}&page={pageno}&limit=10&tbm=all
results_query: results
url_query: url
title_query: title
content_query: snippet
thumbnail: image_url
disabled: true
inactive: true
about:
website: https://avalw.com
description: "Search engine from the Romanian news company AVALW S.R.L."
results: JSON
- name: ayo - name: ayo
engine: xpath engine: xpath
categories: general categories: general
@@ -1217,12 +1199,10 @@ engines:
- name: google - name: google
engine: google engine: google
shortcut: go shortcut: go
inactive: true
- name: google images - name: google images
engine: google_images engine: google_images
shortcut: goi shortcut: goi
inactive: true
- name: google news - name: google news
engine: google_news engine: google_news
@@ -1231,17 +1211,6 @@ engines:
- name: google videos - name: google videos
engine: google_videos engine: google_videos
shortcut: gov shortcut: gov
inactive: true
- name: google cse
engine: google_cse
shortcut: goc
- name: google cse images
engine: google_cse
categories: [images, web]
google_categ: image
shortcut: goci
- name: google scholar - name: google scholar
engine: google_scholar engine: google_scholar
@@ -1440,27 +1409,6 @@ engines:
# api_key: "" # required # api_key: "" # required
# kagi_categ: videos # kagi_categ: videos
- name: kavunka demo
engine: xpath
categories: general
paging: true
shortcut: kav
search_url: https://kavunka.net/search/kavunka-srv.php
method: POST
request_body: request={query}&np={pageno}
headers:
Content-Type: application/x-www-form-urlencoded
disabled: true
inactive: true
results_xpath: //div[contains(@class, "issue")]
url_xpath: .//h2/a/@href
title_xpath: .//h2
content_xpath: .//div[contains(@class, "snip")]
about:
website: https://kavunka.net
description: "Demo index of Kavunka, a statistical search engine"
results: HTML
- name: kozmonavt - name: kozmonavt
engine: xpath engine: xpath
search_url: https://kozmonavt.su/s?q={query} search_url: https://kozmonavt.su/s?q={query}
@@ -1637,11 +1585,6 @@ engines:
disabled: true disabled: true
inactive: true inactive: true
- name: magnific
engine: magnific
shortcut: mag
disabled: true
- name: marginalia - name: marginalia
engine: marginalia engine: marginalia
shortcut: mar shortcut: mar
@@ -1743,12 +1686,6 @@ engines:
shortcut: mwm shortcut: mwm
disabled: true disabled: true
- name: neocities
engine: neocities
shortcut: nc
disabled: true
inactive: true
- name: niconico - name: niconico
engine: niconico engine: niconico
shortcut: nico shortcut: nico
@@ -1920,11 +1857,6 @@ engines:
engine: photon engine: photon
shortcut: ph shortcut: ph
- name: picjumbo
engine: picjumbo
shortcut: pj
disabled: true
- name: pinterest - name: pinterest
engine: pinterest engine: pinterest
shortcut: pin shortcut: pin
@@ -2233,11 +2165,6 @@ engines:
engine: sepiasearch engine: sepiasearch
shortcut: sep shortcut: sep
- name: shopify stock
engine: shopify_stock
shortcut: shs
disabled: true
- name: sogou - name: sogou
engine: sogou engine: sogou
shortcut: sogou shortcut: sogou
@@ -2268,11 +2195,6 @@ engines:
api_site: 'stackoverflow' api_site: 'stackoverflow'
categories: [it, q&a] categories: [it, q&a]
- name: stocksnap
engine: stocksnap
shortcut: sto
disabled: true
- name: askubuntu - name: askubuntu
engine: stackexchange engine: stackexchange
shortcut: ubuntu shortcut: ubuntu
@@ -2912,12 +2834,6 @@ engines:
shortcut: nvrv shortcut: nvrv
disabled: true disabled: true
- name: neosearch
engine: neosearch
shortcut: neo
disabled: true
inactive: true
- name: rubygems - name: rubygems
shortcut: rbg shortcut: rbg
engine: xpath engine: xpath
@@ -3047,17 +2963,44 @@ engines:
results: HTML results: HTML
- name: searchzee - name: searchzee
engine: searchzee engine: json_engine
search_url: https://searchzee.com/api/search?q={query}&type=web&offset={pageno}
paging: true
first_page_num: 0
results_query: results
url_query: url
title_query: title
content_query: summary
content_html_to_text: true
categories: general categories: general
searchzee_categ: web
shortcut: sz shortcut: sz
disabled: true disabled: true
inactive: true inactive: true
about:
website: https://searchzee.com
use_official_api: false
require_api_key: false
results: JSON
- name: searchzee news - name: searchzee news
engine: searchzee engine: json_engine
search_url: https://searchzee.com/api/search?q={query}&type=news&offset={pageno}{time_range}
paging: true
first_page_num: 0
time_range_support: true
time_range_url: "&freshness={time_range_val}"
time_range_map:
day: pd
week: pw
month: pm
year: py
results_query: results
url_query: url
title_query: title
content_query: summary
thumbnail_query: thumbnail
content_html_to_text: true
categories: news categories: news
searchzee_categ: news
shortcut: sznw shortcut: sznw
disabled: true disabled: true
inactive: true inactive: true
@@ -3342,36 +3285,6 @@ engines:
base_url: https://info.searchtoday.site base_url: https://info.searchtoday.site
disabled: true disabled: true
- name: sina
engine: json_engine
shortcut: sina
categories: news
paging: true
first_page_num: 1
search_url: https://search.sina.com.cn/api/search?q={query}&tp=mix&sort=0&page={pageno}&size=10&from=search_result
results_query: data/list
url_query: url
title_query: title
content_query: intro
thumbnail_query: thumb
title_html_to_text: true
time_range_map:
day: d
week: w
month: m
year: y
time_range_support: true
time_range_url: "&from=advanced_search&time={time_range_val}"
disabled: true
inactive: true
language: zh
about:
website: https://search.sina.com.cn
wikidata_id: Q938668
use_official_api: false
require_api_key: false
results: JSON
# - name: webcrawler # - name: webcrawler
# engine: s1search # engine: s1search
# shortcut: wc # shortcut: wc

View File

@@ -1 +1 @@
{"version":3,"file":"5Ako-qGW.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/search.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { listen } from \"../toolkit.ts\";\nimport { getElement } from \"../util/getElement.ts\";\n\nconst searchForm: HTMLFormElement = getElement<HTMLFormElement>(\"search\");\nconst searchInput: HTMLInputElement = getElement<HTMLInputElement>(\"q\");\nconst searchReset: HTMLButtonElement = getElement<HTMLButtonElement>(\"clear_search\");\n\nconst isMobile: boolean = window.matchMedia(\"(max-width: 50em)\").matches;\nconst isResultsPage: boolean = document.querySelector(\"main\")?.id === \"main_results\";\n\nconst categoryButtons: HTMLButtonElement[] = Array.from(\n document.querySelectorAll<HTMLButtonElement>(\"#categories_container button.category\")\n);\n\nif (searchInput.value.length === 0) {\n searchReset.classList.add(\"empty\");\n}\n\n// focus search input on large screens\nif (!(isMobile || isResultsPage)) {\n searchInput.focus();\n}\n\n// On mobile, move cursor to the end of the input on focus\nif (isMobile) {\n listen(\"focus\", searchInput, () => {\n // Defer cursor move until the next frame to prevent a visual jump\n requestAnimationFrame(() => {\n const end = searchInput.value.length;\n searchInput.setSelectionRange(end, end);\n searchInput.scrollLeft = searchInput.scrollWidth;\n });\n });\n}\n\nlisten(\"input\", searchInput, () => {\n searchReset.classList.toggle(\"empty\", searchInput.value.length === 0);\n});\n\nlisten(\"click\", searchReset, (event: MouseEvent) => {\n event.preventDefault();\n searchInput.value = \"\";\n searchInput.focus();\n searchReset.classList.add(\"empty\");\n});\n\nfor (const button of categoryButtons) {\n listen(\"click\", button, (event: MouseEvent) => {\n if (event.shiftKey) {\n event.preventDefault();\n button.classList.toggle(\"selected\");\n return;\n }\n\n // deselect all other categories\n for (const categoryButton of categoryButtons) {\n categoryButton.classList.toggle(\"selected\", categoryButton === button);\n }\n });\n}\n\nif (document.querySelector(\"div.search_filters\")) {\n const safesearchElement = document.getElementById(\"safesearch\");\n if (safesearchElement) {\n listen(\"change\", safesearchElement, () => searchForm.submit());\n }\n\n const timeRangeElement = document.getElementById(\"time_range\");\n if (timeRangeElement) {\n listen(\"change\", timeRangeElement, () => searchForm.submit());\n }\n\n const languageElement = document.getElementById(\"language\");\n if (languageElement) {\n listen(\"change\", languageElement, () => searchForm.submit());\n }\n}\n\n// override searchForm submit event\nlisten(\"submit\", searchForm, (event: Event) => {\n event.preventDefault();\n\n if (categoryButtons.length > 0) {\n const searchCategories = getElement<HTMLInputElement>(\"selected-categories\");\n searchCategories.value = categoryButtons\n .filter((button) => button.classList.contains(\"selected\"))\n .map((button) => button.name.replace(\"category_\", \"\"))\n .join(\",\");\n }\n\n searchForm.submit();\n});\n"],"mappings":"yEAKA,IAAM,EAA8B,EAA4B,QAAQ,EAClE,EAAgC,EAA6B,GAAG,EAChE,EAAiC,EAA8B,cAAc,EAE7E,EAAoB,OAAO,WAAW,mBAAmB,CAAC,CAAC,QAC3D,EAAyB,SAAS,cAAc,MAAM,CAAC,EAAE,KAAO,eAEhE,EAAuC,MAAM,KACjD,SAAS,iBAAoC,uCAAuC,CACtF,EAEI,EAAY,MAAM,SAAW,GAC/B,EAAY,UAAU,IAAI,OAAO,EAI7B,GAAY,GAChB,EAAY,MAAM,EAIhB,GACF,EAAO,QAAS,MAAmB,CAEjC,0BAA4B,CAC1B,IAAM,EAAM,EAAY,MAAM,OAC9B,EAAY,kBAAkB,EAAK,CAAG,EACtC,EAAY,WAAa,EAAY,WACvC,CAAC,CACH,CAAC,EAGH,EAAO,QAAS,MAAmB,CACjC,EAAY,UAAU,OAAO,QAAS,EAAY,MAAM,SAAW,CAAC,CACtE,CAAC,EAED,EAAO,QAAS,EAAc,GAAsB,CAClD,EAAM,eAAe,EACrB,EAAY,MAAQ,GACpB,EAAY,MAAM,EAClB,EAAY,UAAU,IAAI,OAAO,CACnC,CAAC,EAED,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,EAAS,GAAsB,CAC7C,GAAI,EAAM,SAAU,CAClB,EAAM,eAAe,EACrB,EAAO,UAAU,OAAO,UAAU,EAClC,MACF,CAGA,IAAK,IAAM,KAAkB,EAC3B,EAAe,UAAU,OAAO,WAAY,IAAmB,CAAM,CAEzE,CAAC,EAGH,GAAI,SAAS,cAAc,oBAAoB,EAAG,CAChD,IAAM,EAAoB,SAAS,eAAe,YAAY,EAC1D,GACF,EAAO,SAAU,MAAyB,EAAW,OAAO,CAAC,EAG/D,IAAM,EAAmB,SAAS,eAAe,YAAY,EACzD,GACF,EAAO,SAAU,MAAwB,EAAW,OAAO,CAAC,EAG9D,IAAM,EAAkB,SAAS,eAAe,UAAU,EACtD,GACF,EAAO,SAAU,MAAuB,EAAW,OAAO,CAAC,CAE/D,CAGA,EAAO,SAAU,EAAa,GAAiB,CAG7C,GAFA,EAAM,eAAe,EAEjB,EAAgB,OAAS,EAAG,CAC9B,IAAM,EAAmB,EAA6B,qBAAqB,EAC3E,EAAiB,MAAQ,EACtB,OAAQ,GAAW,EAAO,UAAU,SAAS,UAAU,CAAC,CAAC,CACzD,IAAK,GAAW,EAAO,KAAK,QAAQ,YAAa,EAAE,CAAC,CAAC,CACrD,KAAK,GAAG,CACb,CAEA,EAAW,OAAO,CACpB,CAAC"} {"version":3,"file":"5Ako-qGW.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/search.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { listen } from \"../toolkit.ts\";\nimport { getElement } from \"../util/getElement.ts\";\n\nconst searchForm: HTMLFormElement = getElement<HTMLFormElement>(\"search\");\nconst searchInput: HTMLInputElement = getElement<HTMLInputElement>(\"q\");\nconst searchReset: HTMLButtonElement = getElement<HTMLButtonElement>(\"clear_search\");\n\nconst isMobile: boolean = window.matchMedia(\"(max-width: 50em)\").matches;\nconst isResultsPage: boolean = document.querySelector(\"main\")?.id === \"main_results\";\n\nconst categoryButtons: HTMLButtonElement[] = Array.from(\n document.querySelectorAll<HTMLButtonElement>(\"#categories_container button.category\")\n);\n\nif (searchInput.value.length === 0) {\n searchReset.classList.add(\"empty\");\n}\n\n// focus search input on large screens\nif (!(isMobile || isResultsPage)) {\n searchInput.focus();\n}\n\n// On mobile, move cursor to the end of the input on focus\nif (isMobile) {\n listen(\"focus\", searchInput, () => {\n // Defer cursor move until the next frame to prevent a visual jump\n requestAnimationFrame(() => {\n const end = searchInput.value.length;\n searchInput.setSelectionRange(end, end);\n searchInput.scrollLeft = searchInput.scrollWidth;\n });\n });\n}\n\nlisten(\"input\", searchInput, () => {\n searchReset.classList.toggle(\"empty\", searchInput.value.length === 0);\n});\n\nlisten(\"click\", searchReset, (event: MouseEvent) => {\n event.preventDefault();\n searchInput.value = \"\";\n searchInput.focus();\n searchReset.classList.add(\"empty\");\n});\n\nfor (const button of categoryButtons) {\n listen(\"click\", button, (event: MouseEvent) => {\n if (event.shiftKey) {\n event.preventDefault();\n button.classList.toggle(\"selected\");\n return;\n }\n\n // deselect all other categories\n for (const categoryButton of categoryButtons) {\n categoryButton.classList.toggle(\"selected\", categoryButton === button);\n }\n });\n}\n\nif (document.querySelector(\"div.search_filters\")) {\n const safesearchElement = document.getElementById(\"safesearch\");\n if (safesearchElement) {\n listen(\"change\", safesearchElement, () => searchForm.submit());\n }\n\n const timeRangeElement = document.getElementById(\"time_range\");\n if (timeRangeElement) {\n listen(\"change\", timeRangeElement, () => searchForm.submit());\n }\n\n const languageElement = document.getElementById(\"language\");\n if (languageElement) {\n listen(\"change\", languageElement, () => searchForm.submit());\n }\n}\n\n// override searchForm submit event\nlisten(\"submit\", searchForm, (event: Event) => {\n event.preventDefault();\n\n if (categoryButtons.length > 0) {\n const searchCategories = getElement<HTMLInputElement>(\"selected-categories\");\n searchCategories.value = categoryButtons\n .filter((button) => button.classList.contains(\"selected\"))\n .map((button) => button.name.replace(\"category_\", \"\"))\n .join(\",\");\n }\n\n searchForm.submit();\n});\n"],"mappings":"yEAKA,IAAM,EAA8B,EAA4B,QAAQ,EAClE,EAAgC,EAA6B,GAAG,EAChE,EAAiC,EAA8B,cAAc,EAE7E,EAAoB,OAAO,WAAW,mBAAmB,EAAE,QAC3D,EAAyB,SAAS,cAAc,MAAM,GAAG,KAAO,eAEhE,EAAuC,MAAM,KACjD,SAAS,iBAAoC,uCAAuC,CACtF,EAEI,EAAY,MAAM,SAAW,GAC/B,EAAY,UAAU,IAAI,OAAO,EAI7B,GAAY,GAChB,EAAY,MAAM,EAIhB,GACF,EAAO,QAAS,MAAmB,CAEjC,0BAA4B,CAC1B,IAAM,EAAM,EAAY,MAAM,OAC9B,EAAY,kBAAkB,EAAK,CAAG,EACtC,EAAY,WAAa,EAAY,WACvC,CAAC,CACH,CAAC,EAGH,EAAO,QAAS,MAAmB,CACjC,EAAY,UAAU,OAAO,QAAS,EAAY,MAAM,SAAW,CAAC,CACtE,CAAC,EAED,EAAO,QAAS,EAAc,GAAsB,CAClD,EAAM,eAAe,EACrB,EAAY,MAAQ,GACpB,EAAY,MAAM,EAClB,EAAY,UAAU,IAAI,OAAO,CACnC,CAAC,EAED,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,EAAS,GAAsB,CAC7C,GAAI,EAAM,SAAU,CAClB,EAAM,eAAe,EACrB,EAAO,UAAU,OAAO,UAAU,EAClC,MACF,CAGA,IAAK,IAAM,KAAkB,EAC3B,EAAe,UAAU,OAAO,WAAY,IAAmB,CAAM,CAEzE,CAAC,EAGH,GAAI,SAAS,cAAc,oBAAoB,EAAG,CAChD,IAAM,EAAoB,SAAS,eAAe,YAAY,EAC1D,GACF,EAAO,SAAU,MAAyB,EAAW,OAAO,CAAC,EAG/D,IAAM,EAAmB,SAAS,eAAe,YAAY,EACzD,GACF,EAAO,SAAU,MAAwB,EAAW,OAAO,CAAC,EAG9D,IAAM,EAAkB,SAAS,eAAe,UAAU,EACtD,GACF,EAAO,SAAU,MAAuB,EAAW,OAAO,CAAC,CAE/D,CAGA,EAAO,SAAU,EAAa,GAAiB,CAG7C,GAFA,EAAM,eAAe,EAEjB,EAAgB,OAAS,EAAG,CAC9B,IAAM,EAAmB,EAA6B,qBAAqB,EAC3E,EAAiB,MAAQ,EACtB,OAAQ,GAAW,EAAO,UAAU,SAAS,UAAU,CAAC,EACxD,IAAK,GAAW,EAAO,KAAK,QAAQ,YAAa,EAAE,CAAC,EACpD,KAAK,GAAG,CACb,CAEA,EAAW,OAAO,CACpB,CAAC"}

View File

@@ -0,0 +1,11 @@
import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";
/*!
* swiped-events.js - v@version@
* Pure JavaScript swipe events
* https://github.com/john-doherty/swiped-events
* @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
* @author John Doherty <www.johndoherty.info>
* @license MIT
*/
(function(e,t){typeof e.CustomEvent!=`function`&&(e.CustomEvent=function(e,n){n||={bubbles:!1,cancelable:!1,detail:void 0};var r=t.createEvent(`CustomEvent`);return r.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),r},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener(`touchstart`,u,!1),t.addEventListener(`touchmove`,d,!1),t.addEventListener(`touchend`,l,!1);var n=null,r=null,i=null,a=null,o=null,s=null,c=0;function l(e){if(s===e.target){var l=parseInt(f(s,`data-swipe-threshold`,`20`),10),u=f(s,`data-swipe-unit`,`px`),d=parseInt(f(s,`data-swipe-timeout`,`500`),10),p=Date.now()-o,m=``,h=e.changedTouches||e.touches||[];if(u===`vh`&&(l=Math.round(l/100*t.documentElement.clientHeight)),u===`vw`&&(l=Math.round(l/100*t.documentElement.clientWidth)),Math.abs(i)>Math.abs(a)?Math.abs(i)>l&&p<d&&(m=i>0?`swiped-left`:`swiped-right`):Math.abs(a)>l&&p<d&&(m=a>0?`swiped-up`:`swiped-down`),m!==``){var g={dir:m.replace(/swiped-/,``),touchType:(h[0]||{}).touchType||`direct`,fingers:c,xStart:parseInt(n,10),xEnd:parseInt((h[0]||{}).clientX||-1,10),yStart:parseInt(r,10),yEnd:parseInt((h[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent(`swiped`,{bubbles:!0,cancelable:!0,detail:g})),s.dispatchEvent(new CustomEvent(m,{bubbles:!0,cancelable:!0,detail:g}))}n=null,r=null,o=null}}function u(e){e.target.getAttribute(`data-swipe-ignore`)!==`true`&&(s=e.target,o=Date.now(),n=e.touches[0].clientX,r=e.touches[0].clientY,i=0,a=0,c=e.touches.length)}function d(e){if(!(!n||!r)){var t=e.touches[0].clientX,o=e.touches[0].clientY;i=n-t,a=r-o}}function f(e,n,r){for(;e&&e!==t.documentElement;){var i=e.getAttribute(n);if(i)return i;e=e.parentNode}return r}})(window,document);var i,a=t=>{i&&clearTimeout(i);let n=t.querySelector(`.result-images-source img`);if(!n)return;let r=t.querySelector(`.image_thumbnail`);if(r){if(r.src===`${e.theme_static_path}/img/img_load_error.svg`)return;n.onerror=()=>{n.src=r.src},n.src=r.src}let a=n.getAttribute(`data-src`);a&&(i=setTimeout(()=>{n.src=a,n.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail`);for(let t of o)t.complete&&t.naturalWidth===0&&(t.src=`${e.theme_static_path}/img/img_load_error.svg`),t.onerror=()=>{t.src=`${e.theme_static_path}/img/img_load_error.svg`};document.querySelector(`#search_url button#copy_url`)?.style.setProperty(`display`,`block`),n.selectImage=e=>{document.getElementById(`results`)?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,n.scrollPageToSelected?.(),e&&a(e)},n.closeDetail=()=>{document.getElementById(`results`)?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),n.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let e=this.getAttribute(`data-btn-text-collapsed`),t=this.getAttribute(`data-btn-text-not-collapsed`),n=this.getAttribute(`data-target`);if(!(n&&e&&t))return;let i=document.querySelector(n);r(i);let a=this.classList.contains(`collapsed`),o=a?t:e,s=a?e:t;this.innerHTML=this.innerHTML.replace(s,o),this.classList.toggle(`collapsed`),i.classList.toggle(`invisible`)}),t(`click`,`.media-loader`,function(){let e=this.getAttribute(`data-target`);if(!e)return;let t=document.querySelector(`${e} > iframe`);if(r(t),!t.getAttribute(`src`)){let e=t.getAttribute(`data-src`);e&&t.setAttribute(`src`,e)}}),t(`click`,`#copy_url`,async function(){let e=this.parentElement?.querySelector(`pre`);if(r(e),window.isSecureContext)await navigator.clipboard.writeText(e.innerText);else{let t=window.getSelection();if(t){let n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),document.execCommand(`copy`)}}this.dataset.copiedText&&(this.innerText=this.dataset.copiedText)}),t(`click`,`.result-detail-close`,e=>{e.preventDefault(),n.closeDetail?.()}),t(`click`,`.result-detail-previous`,e=>{e.preventDefault(),n.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,e=>{e.preventDefault(),n.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&n.closeDetail?.()});var s=document.querySelectorAll(`.swipe-horizontal`);for(let e of s)t(`swiped-left`,e,()=>{n.selectNext?.(!1)}),t(`swiped-right`,e,()=>{n.selectPrevious?.(!1)});window.addEventListener(`scroll`,()=>{let e=document.getElementById(`backToTop`),t=document.getElementById(`results`);if(e&&t){let e=(document.documentElement.scrollTop||document.body.scrollTop)>=100;t.classList.toggle(`scrolling`,e)}},!0);
//# sourceMappingURL=B8prKeWj.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"e2-9fzwE.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/preferences.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { http, listen, settings } from \"../toolkit.ts\";\nimport { assertElement } from \"../util/assertElement.ts\";\n\nlet engineDescriptions: Record<string, [string, string]> | undefined;\n\nconst loadEngineDescriptions = async (): Promise<void> => {\n if (engineDescriptions) return;\n try {\n const res = await http(\"GET\", \"engine_descriptions.json\");\n engineDescriptions = await res.json();\n } catch (error) {\n console.error(\"Error fetching engineDescriptions:\", error);\n }\n if (!engineDescriptions) return;\n\n for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {\n const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name=\"${engine_name}\"] .engine-description`);\n const sourceText = ` (<i>${settings.translations?.Source}:&nbsp;${source}</i>)`;\n\n for (const element of elements) {\n element.innerHTML = description + sourceText;\n }\n }\n};\n\nconst toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {\n for (const engineToggle of engineToggles) {\n // check if element visible, so that only engines of the current category are modified\n if (engineToggle.offsetParent) {\n engineToggle.checked = !enable;\n }\n }\n};\n\nconst engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\"[data-engine-name]\");\nfor (const engineElement of engineElements) {\n listen(\"mouseenter\", engineElement, loadEngineDescriptions);\n}\n\nconst engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(\n \"tbody input[type=checkbox][class~=checkbox-onoff]\"\n);\n\nconst enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".enable-all-engines\");\nfor (const engine of enableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(true, engineToggles));\n}\n\nconst disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".disable-all-engines\");\nfor (const engine of disableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(false, engineToggles));\n}\n\nlisten(\"click\", \"#copy-hash\", async function (this: HTMLElement) {\n const target = this.parentElement?.querySelector<HTMLPreElement>(\"pre\");\n assertElement(target);\n\n if (window.isSecureContext) {\n await navigator.clipboard.writeText(target.innerText);\n } else {\n const selection = window.getSelection();\n if (selection) {\n const range = document.createRange();\n range.selectNodeContents(target);\n selection.removeAllRanges();\n selection.addRange(range);\n document.execCommand(\"copy\");\n }\n }\n\n if (this.dataset.copiedText) {\n this.innerText = this.dataset.copiedText;\n }\n});\n"],"mappings":"4FAKA,IAAI,EAEE,EAAyB,SAA2B,CACpD,MACJ,IAAI,CAEF,EAAqB,MAAM,MADT,EAAK,MAAO,0BAA0B,EAAA,CACzB,KAAK,CACtC,OAAS,EAAO,CACd,QAAQ,MAAM,qCAAsC,CAAK,CAC3D,CACK,KAEL,IAAK,GAAM,CAAC,EAAa,CAAC,EAAa,MAAY,OAAO,QAAQ,CAAkB,EAAG,CACrF,IAAM,EAAW,SAAS,iBAA8B,sBAAsB,EAAY,uBAAuB,EAC3G,EAAa,QAAQ,EAAS,cAAc,OAAO,SAAS,EAAO,OAEzE,IAAK,IAAM,KAAW,EACpB,EAAQ,UAAY,EAAc,CAEtC,CAVA,CAWF,EAEM,GAAiB,EAAiB,IAAsD,CAC5F,IAAK,IAAM,KAAgB,EAErB,EAAa,eACf,EAAa,QAAU,CAAC,EAG9B,EAEM,EAA0C,SAAS,iBAA8B,oBAAoB,EAC3G,IAAK,IAAM,KAAiB,EAC1B,EAAO,aAAc,EAAe,CAAsB,EAG5D,IAAM,EAA8C,SAAS,iBAC3D,mDACF,EAEM,EAA4C,SAAS,iBAA8B,qBAAqB,EAC9G,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAM,CAAa,CAAC,EAGlE,IAAM,EAA6C,SAAS,iBAA8B,sBAAsB,EAChH,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAO,CAAa,CAAC,EAGnE,EAAO,QAAS,aAAc,gBAAmC,CAC/D,IAAM,EAAS,KAAK,eAAe,cAA8B,KAAK,EAGtE,GAFA,EAAc,CAAM,EAEhB,OAAO,gBACT,MAAM,UAAU,UAAU,UAAU,EAAO,SAAS,MAC/C,CACL,IAAM,EAAY,OAAO,aAAa,EACtC,GAAI,EAAW,CACb,IAAM,EAAQ,SAAS,YAAY,EACnC,EAAM,mBAAmB,CAAM,EAC/B,EAAU,gBAAgB,EAC1B,EAAU,SAAS,CAAK,EACxB,SAAS,YAAY,MAAM,CAC7B,CACF,CAEI,KAAK,QAAQ,aACf,KAAK,UAAY,KAAK,QAAQ,WAElC,CAAC"} {"version":3,"file":"e2-9fzwE.min.js","names":[],"sources":["../../../../../client/simple/src/js/main/preferences.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { http, listen, settings } from \"../toolkit.ts\";\nimport { assertElement } from \"../util/assertElement.ts\";\n\nlet engineDescriptions: Record<string, [string, string]> | undefined;\n\nconst loadEngineDescriptions = async (): Promise<void> => {\n if (engineDescriptions) return;\n try {\n const res = await http(\"GET\", \"engine_descriptions.json\");\n engineDescriptions = await res.json();\n } catch (error) {\n console.error(\"Error fetching engineDescriptions:\", error);\n }\n if (!engineDescriptions) return;\n\n for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {\n const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name=\"${engine_name}\"] .engine-description`);\n const sourceText = ` (<i>${settings.translations?.Source}:&nbsp;${source}</i>)`;\n\n for (const element of elements) {\n element.innerHTML = description + sourceText;\n }\n }\n};\n\nconst toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {\n for (const engineToggle of engineToggles) {\n // check if element visible, so that only engines of the current category are modified\n if (engineToggle.offsetParent) {\n engineToggle.checked = !enable;\n }\n }\n};\n\nconst engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\"[data-engine-name]\");\nfor (const engineElement of engineElements) {\n listen(\"mouseenter\", engineElement, loadEngineDescriptions);\n}\n\nconst engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(\n \"tbody input[type=checkbox][class~=checkbox-onoff]\"\n);\n\nconst enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".enable-all-engines\");\nfor (const engine of enableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(true, engineToggles));\n}\n\nconst disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".disable-all-engines\");\nfor (const engine of disableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(false, engineToggles));\n}\n\nlisten(\"click\", \"#copy-hash\", async function (this: HTMLElement) {\n const target = this.parentElement?.querySelector<HTMLPreElement>(\"pre\");\n assertElement(target);\n\n if (window.isSecureContext) {\n await navigator.clipboard.writeText(target.innerText);\n } else {\n const selection = window.getSelection();\n if (selection) {\n const range = document.createRange();\n range.selectNodeContents(target);\n selection.removeAllRanges();\n selection.addRange(range);\n document.execCommand(\"copy\");\n }\n }\n\n if (this.dataset.copiedText) {\n this.innerText = this.dataset.copiedText;\n }\n});\n"],"mappings":"4FAKA,IAAI,EAEE,EAAyB,SAA2B,CACpD,MACJ,IAAI,CAEF,EAAqB,MAAM,MADT,EAAK,MAAO,0BAA0B,GACzB,KAAK,CACtC,OAAS,EAAO,CACd,QAAQ,MAAM,qCAAsC,CAAK,CAC3D,CACK,KAEL,IAAK,GAAM,CAAC,EAAa,CAAC,EAAa,MAAY,OAAO,QAAQ,CAAkB,EAAG,CACrF,IAAM,EAAW,SAAS,iBAA8B,sBAAsB,EAAY,uBAAuB,EAC3G,EAAa,QAAQ,EAAS,cAAc,OAAO,SAAS,EAAO,OAEzE,IAAK,IAAM,KAAW,EACpB,EAAQ,UAAY,EAAc,CAEtC,CAVA,CAWF,EAEM,GAAiB,EAAiB,IAAsD,CAC5F,IAAK,IAAM,KAAgB,EAErB,EAAa,eACf,EAAa,QAAU,CAAC,EAG9B,EAEM,EAA0C,SAAS,iBAA8B,oBAAoB,EAC3G,IAAK,IAAM,KAAiB,EAC1B,EAAO,aAAc,EAAe,CAAsB,EAG5D,IAAM,EAA8C,SAAS,iBAC3D,mDACF,EAEM,EAA4C,SAAS,iBAA8B,qBAAqB,EAC9G,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAM,CAAa,CAAC,EAGlE,IAAM,EAA6C,SAAS,iBAA8B,sBAAsB,EAChH,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAO,CAAa,CAAC,EAGnE,EAAO,QAAS,aAAc,gBAAmC,CAC/D,IAAM,EAAS,KAAK,eAAe,cAA8B,KAAK,EAGtE,GAFA,EAAc,CAAM,EAEhB,OAAO,gBACT,MAAM,UAAU,UAAU,UAAU,EAAO,SAAS,MAC/C,CACL,IAAM,EAAY,OAAO,aAAa,EACtC,GAAI,EAAW,CACb,IAAM,EAAQ,SAAS,YAAY,EACnC,EAAM,mBAAmB,CAAM,EAC/B,EAAU,gBAAgB,EAC1B,EAAU,SAAS,CAAK,EACxB,SAAS,YAAY,MAAM,CAC7B,CACF,CAEI,KAAK,QAAQ,aACf,KAAK,UAAY,KAAK,QAAQ,WAElC,CAAC"}

View File

@@ -1,11 +0,0 @@
import{i as e,n as t,r as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";
/*!
* swiped-events.js - v@version@
* Pure JavaScript swipe events
* https://github.com/john-doherty/swiped-events
* @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
* @author John Doherty <www.johndoherty.info>
* @license MIT
*/
(function(e,t){typeof e.CustomEvent!=`function`&&(e.CustomEvent=function(e,n){n||={bubbles:!1,cancelable:!1,detail:void 0};var r=t.createEvent(`CustomEvent`);return r.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),r},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener(`touchstart`,u,!1),t.addEventListener(`touchmove`,d,!1),t.addEventListener(`touchend`,l,!1);var n=null,r=null,i=null,a=null,o=null,s=null,c=0;function l(e){if(s===e.target){var l=parseInt(f(s,`data-swipe-threshold`,`20`),10),u=f(s,`data-swipe-unit`,`px`),d=parseInt(f(s,`data-swipe-timeout`,`500`),10),p=Date.now()-o,m=``,h=e.changedTouches||e.touches||[];if(u===`vh`&&(l=Math.round(l/100*t.documentElement.clientHeight)),u===`vw`&&(l=Math.round(l/100*t.documentElement.clientWidth)),Math.abs(i)>Math.abs(a)?Math.abs(i)>l&&p<d&&(m=i>0?`swiped-left`:`swiped-right`):Math.abs(a)>l&&p<d&&(m=a>0?`swiped-up`:`swiped-down`),m!==``){var g={dir:m.replace(/swiped-/,``),touchType:(h[0]||{}).touchType||`direct`,fingers:c,xStart:parseInt(n,10),xEnd:parseInt((h[0]||{}).clientX||-1,10),yStart:parseInt(r,10),yEnd:parseInt((h[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent(`swiped`,{bubbles:!0,cancelable:!0,detail:g})),s.dispatchEvent(new CustomEvent(m,{bubbles:!0,cancelable:!0,detail:g}))}n=null,r=null,o=null}}function u(e){e.target.getAttribute(`data-swipe-ignore`)!==`true`&&(s=e.target,o=Date.now(),n=e.touches[0].clientX,r=e.touches[0].clientY,i=0,a=0,c=e.touches.length)}function d(e){if(!(!n||!r)){var t=e.touches[0].clientX,o=e.touches[0].clientY;i=n-t,a=r-o}}function f(e,n,r){for(;e&&e!==t.documentElement;){var i=e.getAttribute(n);if(i)return i;e=e.parentNode}return r}})(window,document);var i,a=t=>{i&&clearTimeout(i);let n=t.querySelector(`.result-images-source img`);if(!n)return;let r=t.querySelector(`.image_thumbnail`);if(r){if(r.src===`${e.theme_static_path}/img/img_load_error.svg`)return;n.onerror=()=>{n.src=r.src},n.src=r.src}let a=n.getAttribute(`data-src`);a&&(i=setTimeout(()=>{n.src=a,n.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail, img.thumbnail`);for(let t of o)t.complete&&t.naturalWidth===0&&(t.src=`${e.theme_static_path}/img/img_load_error.svg`),t.onerror=()=>{t.src=`${e.theme_static_path}/img/img_load_error.svg`};document.querySelector(`#search_url button#copy_url`)?.style.setProperty(`display`,`block`),n.selectImage=e=>{document.getElementById(`results`)?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,n.scrollPageToSelected?.(),e&&a(e)},n.closeDetail=()=>{document.getElementById(`results`)?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),n.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let e=this.getAttribute(`data-btn-text-collapsed`),t=this.getAttribute(`data-btn-text-not-collapsed`),n=this.getAttribute(`data-target`);if(!(n&&e&&t))return;let i=document.querySelector(n);r(i);let a=this.classList.contains(`collapsed`),o=a?t:e,s=a?e:t;this.innerHTML=this.innerHTML.replace(s,o),this.classList.toggle(`collapsed`),i.classList.toggle(`invisible`)}),t(`click`,`.media-loader`,function(){let e=this.getAttribute(`data-target`);if(!e)return;let t=document.querySelector(`${e} > iframe`);if(r(t),!t.getAttribute(`src`)){let e=t.getAttribute(`data-src`);e&&t.setAttribute(`src`,e)}}),t(`click`,`#copy_url`,async function(){let e=this.parentElement?.querySelector(`pre`);if(r(e),window.isSecureContext)await navigator.clipboard.writeText(e.innerText);else{let t=window.getSelection();if(t){let n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),document.execCommand(`copy`)}}this.dataset.copiedText&&(this.innerText=this.dataset.copiedText)}),t(`click`,`.result-detail-close`,e=>{e.preventDefault(),n.closeDetail?.()}),t(`click`,`.result-detail-previous`,e=>{e.preventDefault(),n.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,e=>{e.preventDefault(),n.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&n.closeDetail?.()});var s=document.querySelectorAll(`.swipe-horizontal`);for(let e of s)t(`swiped-left`,e,()=>{n.selectNext?.(!1)}),t(`swiped-right`,e,()=>{n.selectPrevious?.(!1)});window.addEventListener(`scroll`,()=>{let e=document.getElementById(`backToTop`),t=document.getElementById(`results`);if(e&&t){let e=(document.documentElement.scrollTop||document.body.scrollTop)>=100;t.classList.toggle(`scrolling`,e)}},!0);
//# sourceMappingURL=od7pNHfk.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -57,7 +57,7 @@
] ]
}, },
"src/js/main/results.ts": { "src/js/main/results.ts": {
"file": "chunk/od7pNHfk.min.js", "file": "chunk/B8prKeWj.min.js",
"name": "results", "name": "results",
"src": "src/js/main/results.ts", "src": "src/js/main/results.ts",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -77,7 +77,7 @@
] ]
}, },
"src/js/plugin/Calculator.ts": { "src/js/plugin/Calculator.ts": {
"file": "chunk/C8c7HJzp.min.js", "file": "chunk/CTP0QVHn.min.js",
"name": "calculator", "name": "calculator",
"src": "src/js/plugin/Calculator.ts", "src": "src/js/plugin/Calculator.ts",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -98,7 +98,7 @@
] ]
}, },
"src/js/plugin/MapView.ts": { "src/js/plugin/MapView.ts": {
"file": "chunk/BnnvKC7b.min.js", "file": "chunk/U6YV4Y8e.min.js",
"name": "mapview", "name": "mapview",
"src": "src/js/plugin/MapView.ts", "src": "src/js/plugin/MapView.ts",
"isDynamicEntry": true, "isDynamicEntry": true,

View File

@@ -1,3 +1,3 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/BnnvKC7b.min.js","./sxng-mapview.min.css","./chunk/DpvWr1cn.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/C8c7HJzp.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/DvCYLbJr.min.js","./chunk/od7pNHfk.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]); const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/U6YV4Y8e.min.js","./sxng-mapview.min.css","./chunk/DpvWr1cn.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/CTP0QVHn.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/DvCYLbJr.min.js","./chunk/B8prKeWj.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=p(t,n),t=s(t),t in m)return;m[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:f,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/BnnvKC7b.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DpvWr1cn.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/C8c7HJzp.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/od7pNHfk.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t}; var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/U6YV4Y8e.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DpvWr1cn.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/CTP0QVHn.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([6,3]),import.meta.url),h(()=>import(`./chunk/B8prKeWj.min.js`),__vite__mapDeps([9,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([7,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([8,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([10,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t};
//# sourceMappingURL=sxng-core.min.js.map //# sourceMappingURL=sxng-core.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,6 @@
''' '''
sxng_locales = ( sxng_locales = (
('af', 'Afrikaans', '', 'Afrikaans', '\U0001f310'),
('ar', 'العربية', '', 'Arabic', '\U0001f310'), ('ar', 'العربية', '', 'Arabic', '\U0001f310'),
('ar-SA', 'العربية', 'المملكة العربية السعودية', 'Arabic', '\U0001f1f8\U0001f1e6'), ('ar-SA', 'العربية', 'المملكة العربية السعودية', 'Arabic', '\U0001f1f8\U0001f1e6'),
('bg', 'Български', '', 'Bulgarian', '\U0001f310'), ('bg', 'Български', '', 'Bulgarian', '\U0001f310'),
@@ -46,7 +45,6 @@ sxng_locales = (
('es-ES', 'Español', 'España', 'Spanish', '\U0001f1ea\U0001f1f8'), ('es-ES', 'Español', 'España', 'Spanish', '\U0001f1ea\U0001f1f8'),
('es-MX', 'Español', 'México', 'Spanish', '\U0001f1f2\U0001f1fd'), ('es-MX', 'Español', 'México', 'Spanish', '\U0001f1f2\U0001f1fd'),
('es-PE', 'Español', 'Perú', 'Spanish', '\U0001f1f5\U0001f1ea'), ('es-PE', 'Español', 'Perú', 'Spanish', '\U0001f1f5\U0001f1ea'),
('es-VE', 'Español', 'Venezuela', 'Spanish', '\U0001f1fb\U0001f1ea'),
('et', 'Eesti', '', 'Estonian', '\U0001f310'), ('et', 'Eesti', '', 'Estonian', '\U0001f310'),
('et-EE', 'Eesti', 'Eesti', 'Estonian', '\U0001f1ea\U0001f1ea'), ('et-EE', 'Eesti', 'Eesti', 'Estonian', '\U0001f1ea\U0001f1ea'),
('fi', 'Suomi', '', 'Finnish', '\U0001f310'), ('fi', 'Suomi', '', 'Finnish', '\U0001f310'),
@@ -58,7 +56,6 @@ sxng_locales = (
('fr-CA', 'Français', 'Canada', 'French', '\U0001f1e8\U0001f1e6'), ('fr-CA', 'Français', 'Canada', 'French', '\U0001f1e8\U0001f1e6'),
('fr-CH', 'Français', 'Suisse', 'French', '\U0001f1e8\U0001f1ed'), ('fr-CH', 'Français', 'Suisse', 'French', '\U0001f1e8\U0001f1ed'),
('fr-FR', 'Français', 'France', 'French', '\U0001f1eb\U0001f1f7'), ('fr-FR', 'Français', 'France', 'French', '\U0001f1eb\U0001f1f7'),
('gl', 'Galego', '', 'Galician', '\U0001f310'),
('hi', 'हिन्दी', '', 'Hindi', '\U0001f310'), ('hi', 'हिन्दी', '', 'Hindi', '\U0001f310'),
('hi-IN', 'हिन्दी', 'भारत', 'Hindi', '\U0001f1ee\U0001f1f3'), ('hi-IN', 'हिन्दी', 'भारत', 'Hindi', '\U0001f1ee\U0001f1f3'),
('hr', 'Hrvatski', '', 'Croatian', '\U0001f310'), ('hr', 'Hrvatski', '', 'Croatian', '\U0001f310'),
@@ -92,8 +89,6 @@ sxng_locales = (
('ro-RO', 'Română', 'România', 'Romanian', '\U0001f1f7\U0001f1f4'), ('ro-RO', 'Română', 'România', 'Romanian', '\U0001f1f7\U0001f1f4'),
('ru', 'Русский', '', 'Russian', '\U0001f310'), ('ru', 'Русский', '', 'Russian', '\U0001f310'),
('ru-RU', 'Русский', 'Россия', 'Russian', '\U0001f1f7\U0001f1fa'), ('ru-RU', 'Русский', 'Россия', 'Russian', '\U0001f1f7\U0001f1fa'),
('sk', 'Slovenčina', '', 'Slovak', '\U0001f310'),
('sq', 'Shqip', '', 'Albanian', '\U0001f310'),
('sv', 'Svenska', '', 'Swedish', '\U0001f310'), ('sv', 'Svenska', '', 'Swedish', '\U0001f310'),
('sv-FI', 'Svenska', 'Finland', 'Swedish', '\U0001f1eb\U0001f1ee'), ('sv-FI', 'Svenska', 'Finland', 'Swedish', '\U0001f1eb\U0001f1ee'),
('sv-SE', 'Svenska', 'Sverige', 'Swedish', '\U0001f1f8\U0001f1ea'), ('sv-SE', 'Svenska', 'Sverige', 'Swedish', '\U0001f1f8\U0001f1ea'),
@@ -101,7 +96,6 @@ sxng_locales = (
('th-TH', 'ไทย', 'ไทย', 'Thai', '\U0001f1f9\U0001f1ed'), ('th-TH', 'ไทย', 'ไทย', 'Thai', '\U0001f1f9\U0001f1ed'),
('tr', 'Türkçe', '', 'Turkish', '\U0001f310'), ('tr', 'Türkçe', '', 'Turkish', '\U0001f310'),
('tr-TR', 'Türkçe', 'Türkiye', 'Turkish', '\U0001f1f9\U0001f1f7'), ('tr-TR', 'Türkçe', 'Türkiye', 'Turkish', '\U0001f1f9\U0001f1f7'),
('uk', 'Українська', '', 'Ukrainian', '\U0001f310'),
('vi', 'Tiếng Việt', '', 'Vietnamese', '\U0001f310'), ('vi', 'Tiếng Việt', '', 'Vietnamese', '\U0001f310'),
('vi-VN', 'Tiếng Việt', 'Việt Nam', 'Vietnamese', '\U0001f1fb\U0001f1f3'), ('vi-VN', 'Tiếng Việt', 'Việt Nam', 'Vietnamese', '\U0001f1fb\U0001f1f3'),
('zh', '中文', '', 'Chinese', '\U0001f310'), ('zh', '中文', '', 'Chinese', '\U0001f310'),

View File

@@ -60,7 +60,7 @@
</p> </p>
<p class="result-filesize">{{ _label(_("Filesize"), result.filesize) }}</p> <p class="result-filesize">{{ _label(_("Filesize"), result.filesize) }}</p>
<p class="result-source">{{ _label(_("Source"), result.source) }}</p> <p class="result-source">{{ _label(_("Source"), result.source) }}</p>
<p class="result-engines">{{ _label(_("Engines"), ", ".join(result.engines)) }}</p> <p class="result-engine">{{ _label(_("Engine"), result.engine) }}</p>
<p class="result-url"><span>{{ _("View source") }}:</span>{{- "" -}} <p class="result-url"><span>{{ _("View source") }}:</span>{{- "" -}}
<a {{ _target(results_on_new_tab) }} href="{{ result.url }}">{{ result.url }}</a> <a {{ _target(results_on_new_tab) }} href="{{ result.url }}">{{ result.url }}</a>
</p> </p>

View File

@@ -28,23 +28,21 @@
# DZDevelopers <dzdevelopers@noreply.codeberg.org>, 2025. # DZDevelopers <dzdevelopers@noreply.codeberg.org>, 2025.
# youtherthyf <youtherthyf@noreply.codeberg.org>, 2025. # youtherthyf <youtherthyf@noreply.codeberg.org>, 2025.
# mustafa-phd <mustafa-phd@noreply.codeberg.org>, 2026. # mustafa-phd <mustafa-phd@noreply.codeberg.org>, 2026.
# nebras <nebras@noreply.codeberg.org>, 2026.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-06-13 11:31+0000\n" "POT-Creation-Date: 2026-06-13 11:31+0000\n"
"PO-Revision-Date: 2026-06-28 18:07+0000\n" "PO-Revision-Date: 2026-05-27 12:07+0000\n"
"Last-Translator: nebras <nebras@noreply.codeberg.org>\n" "Last-Translator: mustafa-phd <mustafa-phd@noreply.codeberg.org>\n"
"Language-Team: Arabic <https://translate.codeberg.org/projects/searxng/"
"searxng/ar/>\n"
"Language: ar\n" "Language: ar\n"
"Language-Team: Arabic "
"<https://translate.codeberg.org/projects/searxng/searxng/ar/>\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : "
"n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 2026.6.1\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING'] #. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -1655,11 +1653,11 @@ msgstr "الدقة"
#: searx/templates/simple/result_templates/images.html:55 #: searx/templates/simple/result_templates/images.html:55
msgid "Image formats" msgid "Image formats"
msgstr "صيغ الصور" msgstr ""
#: searx/templates/simple/result_templates/images.html:56 #: searx/templates/simple/result_templates/images.html:56
msgid "original format" msgid "original format"
msgstr "الصيغة الاصلية" msgstr ""
#: searx/templates/simple/result_templates/images.html:63 #: searx/templates/simple/result_templates/images.html:63
msgid "Engine" msgid "Engine"
@@ -2404,3 +2402,4 @@ msgstr "إخفاء الفيديو"
#~ msgid "Format" #~ msgid "Format"
#~ msgstr "صيغة" #~ msgstr "صيغة"

View File

@@ -15,25 +15,24 @@
# hankskyjames777 <hankskyjames777@users.noreply.translate.codeberg.org>, # hankskyjames777 <hankskyjames777@users.noreply.translate.codeberg.org>,
# 2025. # 2025.
# johnmartzbuntia <johnmartzbuntia@users.noreply.translate.codeberg.org>, # johnmartzbuntia <johnmartzbuntia@users.noreply.translate.codeberg.org>,
# marc-lopez <marc-lopez@noreply.codeberg.org>, 2025, 2026. # marc-lopez <marc-lopez@noreply.codeberg.org>, 2025.
# Kita Ikuyo <kitakita@disroot.org>, 2025. # Kita Ikuyo <kitakita@disroot.org>, 2025.
# return42 <return42@noreply.codeberg.org>, 2025, 2026. # return42 <return42@noreply.codeberg.org>, 2025, 2026.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-06-13 11:31+0000\n" "POT-Creation-Date: 2026-06-13 11:31+0000\n"
"PO-Revision-Date: 2026-07-08 00:07+0000\n" "PO-Revision-Date: 2026-05-19 12:07+0000\n"
"Last-Translator: marc-lopez <marc-lopez@noreply.codeberg.org>\n" "Last-Translator: return42 <return42@noreply.codeberg.org>\n"
"Language-Team: Filipino <https://translate.codeberg.org/projects/searxng/"
"searxng/fil/>\n"
"Language: fil\n" "Language: fil\n"
"Language-Team: Filipino "
"<https://translate.codeberg.org/projects/searxng/searxng/fil/>\n"
"Plural-Forms: nplurals=2; plural=(n == 1 || n==2 || n==3) || (n % 10 != 4"
" || n % 10 != 6 || n % 10 != 9);\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n == 1 || n==2 || n==3) || (n % 10 != 4 || "
"n % 10 != 6 || n % 10 != 9);\n"
"X-Generator: Weblate 2026.6.1\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING'] #. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -1638,7 +1637,7 @@ msgstr "Uri"
#: searx/templates/simple/result_templates/file.html:67 #: searx/templates/simple/result_templates/file.html:67
msgid "Download" msgid "Download"
msgstr "I-download" msgstr ""
#: searx/templates/simple/result_templates/images.html:53 #: searx/templates/simple/result_templates/images.html:53
msgid "Resolution" msgid "Resolution"
@@ -1646,11 +1645,11 @@ msgstr "Resolusyon"
#: searx/templates/simple/result_templates/images.html:55 #: searx/templates/simple/result_templates/images.html:55
msgid "Image formats" msgid "Image formats"
msgstr "Mga format ng larawan" msgstr ""
#: searx/templates/simple/result_templates/images.html:56 #: searx/templates/simple/result_templates/images.html:56
msgid "original format" msgid "original format"
msgstr "orihinal na format" msgstr ""
#: searx/templates/simple/result_templates/images.html:63 #: searx/templates/simple/result_templates/images.html:63
msgid "Engine" msgid "Engine"
@@ -2424,3 +2423,4 @@ msgstr "itago ang video"
#~ msgid "Format" #~ msgid "Format"
#~ msgstr "Anyo" #~ msgstr "Anyo"

View File

@@ -16,23 +16,21 @@
# return42 <return42@noreply.codeberg.org>, 2025, 2026. # return42 <return42@noreply.codeberg.org>, 2025, 2026.
# whytf <whytf@noreply.codeberg.org>, 2025. # whytf <whytf@noreply.codeberg.org>, 2025.
# kolegacik <kolegacik@noreply.codeberg.org>, 2025. # kolegacik <kolegacik@noreply.codeberg.org>, 2025.
# lukisko <lukisko@noreply.codeberg.org>, 2026.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: searx\n" "Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-06-13 11:31+0000\n" "POT-Creation-Date: 2026-06-13 11:31+0000\n"
"PO-Revision-Date: 2026-07-08 00:07+0000\n" "PO-Revision-Date: 2026-05-19 12:08+0000\n"
"Last-Translator: lukisko <lukisko@noreply.codeberg.org>\n" "Last-Translator: return42 <return42@noreply.codeberg.org>\n"
"Language-Team: Slovak <https://translate.codeberg.org/projects/searxng/"
"searxng/sk/>\n"
"Language: sk\n" "Language: sk\n"
"Language-Team: Slovak "
"<https://translate.codeberg.org/projects/searxng/searxng/sk/>\n"
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 "
"&& n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && "
"n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
"X-Generator: Weblate 2026.6.1\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING'] #. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -288,7 +286,7 @@ msgstr "Jasno"
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Partly cloudy" msgid "Partly cloudy"
msgstr "Čiastočne zamračené" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
@@ -298,7 +296,7 @@ msgstr "Oblačno"
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Fair" msgid "Fair"
msgstr "Polojasno" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
@@ -308,102 +306,102 @@ msgstr "Hmla"
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light rain and thunder" msgid "Light rain and thunder"
msgstr "Slabý dážď a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light rain showers and thunder" msgid "Light rain showers and thunder"
msgstr "Slabé dažďové prehánky a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light rain showers" msgid "Light rain showers"
msgstr "Slabé dažďové prehánky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light rain" msgid "Light rain"
msgstr "Slabý dážď" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Rain and thunder" msgid "Rain and thunder"
msgstr "Dážď a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Rain showers and thunder" msgid "Rain showers and thunder"
msgstr "Dažďové prehánky a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Rain showers" msgid "Rain showers"
msgstr "Dažďové prehánky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Rain" msgid "Rain"
msgstr "Dážď" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy rain and thunder" msgid "Heavy rain and thunder"
msgstr "Silný dážď a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy rain showers and thunder" msgid "Heavy rain showers and thunder"
msgstr "Silné dažďové prehánky s búrkami" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy rain showers" msgid "Heavy rain showers"
msgstr "Silné dažďové prehánký" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy rain" msgid "Heavy rain"
msgstr "Silný dážď" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light sleet and thunder" msgid "Light sleet and thunder"
msgstr "Slabý dážď so snehom a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light sleet showers and thunder" msgid "Light sleet showers and thunder"
msgstr "Slabé prehánky dažďa so snehom a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light sleet showers" msgid "Light sleet showers"
msgstr "Slabé prehánky dažda so snehom" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light sleet" msgid "Light sleet"
msgstr "Slabý dážď so snehom" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Sleet and thunder" msgid "Sleet and thunder"
msgstr "Dážď so snehom a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Sleet showers and thunder" msgid "Sleet showers and thunder"
msgstr "Prehánky dažďu so snehom a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Sleet showers" msgid "Sleet showers"
msgstr "Prehánky dažďa so snehom" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Sleet" msgid "Sleet"
msgstr "Dážď so snehom" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
@@ -428,62 +426,62 @@ msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light snow and thunder" msgid "Light snow and thunder"
msgstr "Slabé sneženie a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light snow showers and thunder" msgid "Light snow showers and thunder"
msgstr "Slabé snehové prehánky a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light snow showers" msgid "Light snow showers"
msgstr "Slabé snehové prehánky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Light snow" msgid "Light snow"
msgstr "Slabé sneženie" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Snow and thunder" msgid "Snow and thunder"
msgstr "Sneženie s búrkami" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Snow showers and thunder" msgid "Snow showers and thunder"
msgstr "Snehové prehánky s búrkami" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Snow showers" msgid "Snow showers"
msgstr "Snehové prehánky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Snow" msgid "Snow"
msgstr "Sneženie" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy snow and thunder" msgid "Heavy snow and thunder"
msgstr "Silné sneženie a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy snow showers and thunder" msgid "Heavy snow showers and thunder"
msgstr "Silné snehové prehánky a búrky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy snow showers" msgid "Heavy snow showers"
msgstr "Silné snehové prehánky" msgstr ""
#. WEATHER_CONDITIONS #. WEATHER_CONDITIONS
#: searx/searxng.msg #: searx/searxng.msg
msgid "Heavy snow" msgid "Heavy snow"
msgstr "Silné sneženie" msgstr ""
#. SOCIAL_MEDIA_TERMS['SUBSCRIBERS'] #. SOCIAL_MEDIA_TERMS['SUBSCRIBERS']
#: searx/engines/lemmy.py:85 searx/searxng.msg #: searx/engines/lemmy.py:85 searx/searxng.msg
@@ -649,7 +647,7 @@ msgstr "Vypočítať {func} z argumentov"
#: searx/engines/boardreader.py:107 #: searx/engines/boardreader.py:107
#, python-brace-format #, python-brace-format
msgid "Posted by {author}" msgid "Posted by {author}"
msgstr "Autor: {author}" msgstr ""
#: searx/engines/openstreetmap.py:155 #: searx/engines/openstreetmap.py:155
msgid "Show route in map .." msgid "Show route in map .."
@@ -733,11 +731,11 @@ msgstr "Odfiltruj výsledky onion, ktoré sa zobrazujú na čiernej listine Ahmi
#: searx/plugins/calculator.py:25 #: searx/plugins/calculator.py:25
msgid "Calculator" msgid "Calculator"
msgstr "Kalkulačka" msgstr ""
#: searx/plugins/calculator.py:26 #: searx/plugins/calculator.py:26
msgid "Parses and solves mathematical expressions." msgid "Parses and solves mathematical expressions."
msgstr "Zpracováva a rieši matematické výrazy." msgstr ""
#: searx/plugins/hash_plugin.py:33 #: searx/plugins/hash_plugin.py:33
msgid "Hash plugin" msgid "Hash plugin"
@@ -748,8 +746,6 @@ msgid ""
"Converts strings to different hash digests. Available functions: md5, " "Converts strings to different hash digests. Available functions: md5, "
"sha1, sha224, sha256, sha384, sha512." "sha1, sha224, sha256, sha384, sha512."
msgstr "" msgstr ""
"Previest text an rôzne hash digesty. Dostupné funkcie: md5, sha1, sha224, "
"sha256, sha384, sha512."
#: searx/plugins/hash_plugin.py:63 #: searx/plugins/hash_plugin.py:63
msgid "hash digest" msgid "hash digest"
@@ -762,8 +758,6 @@ msgstr "Plugin názvov hostiteľov"
#: searx/plugins/hostnames.py:120 #: searx/plugins/hostnames.py:120
msgid "Rewrite hostnames and remove or prioritize results based on the hostname" msgid "Rewrite hostnames and remove or prioritize results based on the hostname"
msgstr "" msgstr ""
"Prepíše názov hostitelov a odstráni alebo prioritizuje výsledky na základe "
"názvu hostitela"
#: searx/plugins/infinite_scroll.py:25 #: searx/plugins/infinite_scroll.py:25
msgid "Infinite scroll" msgid "Infinite scroll"
@@ -811,11 +805,11 @@ msgstr "Váš používateľský agent je: "
#: searx/plugins/time_zone.py:33 #: searx/plugins/time_zone.py:33
msgid "Timezones plugin" msgid "Timezones plugin"
msgstr "Doplnok časových pásiem" msgstr ""
#: searx/plugins/time_zone.py:34 #: searx/plugins/time_zone.py:34
msgid "Display the current time on different time zones." msgid "Display the current time on different time zones."
msgstr "Zobraziť aktuálny čas v rozličných časových zónach." msgstr ""
#: searx/plugins/tor_check.py:41 #: searx/plugins/tor_check.py:41
msgid "Tor check plugin" msgid "Tor check plugin"
@@ -986,11 +980,11 @@ msgstr ""
#: searx/templates/simple/preferences.html:158 #: searx/templates/simple/preferences.html:158
msgid "Press save to copy these preferences to your browser." msgid "Press save to copy these preferences to your browser."
msgstr "Stlačte uložiť na zkopírovanie týchto preferencií do vášho prehliadača." msgstr ""
#: searx/templates/simple/preferences.html:159 #: searx/templates/simple/preferences.html:159
msgid "Click here to view your browser preferences instead:" msgid "Click here to view your browser preferences instead:"
msgstr "Kliknutím tu namiesto toho zobrazíte preferencie vášho prehliadača:" msgstr ""
#: searx/templates/simple/preferences.html:169 #: searx/templates/simple/preferences.html:169
msgid "General" msgid "General"
@@ -1152,7 +1146,7 @@ msgstr "Synonymá"
#: searx/templates/simple/answer/weather.html:19 #: searx/templates/simple/answer/weather.html:19
msgid "Feels Like" msgid "Feels Like"
msgstr "Pocitovo ako" msgstr ""
#: searx/templates/simple/elements/answers.html:2 #: searx/templates/simple/elements/answers.html:2
msgid "Answers" msgid "Answers"
@@ -2402,3 +2396,4 @@ msgstr "skryť video"
#~ msgid "Format" #~ msgid "Format"
#~ msgstr "Formát" #~ msgstr "Formát"

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