mirror of
https://github.com/searxng/searxng.git
synced 2026-09-11 16:56:05 +00:00
Compare commits
1 Commits
ffe96f8a6f
...
update_dat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
872e838fa4 |
39
.github/scripts/ai_policy.cjs
vendored
39
.github/scripts/ai_policy.cjs
vendored
@@ -1,39 +0,0 @@
|
|||||||
// Closes issues and prs whose authors/agents don't accept the ai policy
|
|
||||||
// https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
|
||||||
|
|
||||||
module.exports = async ({ github, context }) => {
|
|
||||||
const item = context.payload.pull_request || context.payload.issue;
|
|
||||||
const body = item.body || '';
|
|
||||||
const kind = context.payload.pull_request ? 'pull request' : 'issue';
|
|
||||||
|
|
||||||
// https://github.com/searxng/searxng/pull/6476#discussion_r3683782481
|
|
||||||
const hasBox = /\[[Xx]\].*AI Policy/.test(body);
|
|
||||||
const hasRef = /\[AI Policy\]:\s*https:\/\/github\.com\/searxng\/searxng\/.*AI_POLICY/.test(body);
|
|
||||||
if (hasBox && hasRef) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
body:
|
|
||||||
'Hello! Thank you for your contribution.\n\n' +
|
|
||||||
`Unfortunately your ${kind} was closed as the AI Policy has not been accepted.\n\n` +
|
|
||||||
`Please open a new ${kind} after confirming your contribution aligns with our AI Policy.`,
|
|
||||||
});
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
labels: ['invalid:slop'],
|
|
||||||
});
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
state: 'closed',
|
|
||||||
state_reason: 'not_planned',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
38
.github/workflows/ai-policy.yml
vendored
38
.github/workflows/ai-policy.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
# yamllint disable rule:line-length
|
|
||||||
name: AI Policy
|
|
||||||
|
|
||||||
# Closes any new issues and PRs from people (or agents) who don't accept the AI Policy
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
name: Check AI Policy
|
|
||||||
# for issues with an author who has not contributed before
|
|
||||||
if: >-
|
|
||||||
github.event.sender.type != 'Bot' &&
|
|
||||||
contains(fromJSON('["NONE","FIRST_TIMER","FIRST_TIME_CONTRIBUTOR"]'),
|
|
||||||
github.event.issue.author_association ||
|
|
||||||
github.event.pull_request.author_association)
|
|
||||||
runs-on: ubuntu-26.04-arm
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: "false"
|
|
||||||
|
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const script = require('./.github/scripts/ai_policy.cjs');
|
|
||||||
await script({ github, context });
|
|
||||||
155
.github/workflows/container.yml
vendored
155
.github/workflows/container.yml
vendored
@@ -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,83 +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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
- name: Setup podman
|
||||||
with:
|
env:
|
||||||
registry: "ghcr.io"
|
PODMAN_VERSION: "v5.7.1"
|
||||||
username: "${{ github.repository_owner }}"
|
run: |
|
||||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
sudo apt-get purge -y podman runc crun conmon
|
||||||
|
|
||||||
|
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz"
|
||||||
|
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz.asc"
|
||||||
|
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 0CCF102C4F95D89E583FF1D4F8B5AF50344BB503
|
||||||
|
gpg --batch --verify "podman-linux-${{ matrix.march }}.tar.gz.asc" "podman-linux-${{ matrix.march }}.tar.gz"
|
||||||
|
|
||||||
|
tar -xzf "podman-linux-${{ matrix.march }}.tar.gz"
|
||||||
|
sudo cp -rfv ./podman-linux-${{ matrix.march }}/etc/. /etc/
|
||||||
|
sudo cp -rfv ./podman-linux-${{ matrix.march }}/usr/. /usr/
|
||||||
|
|
||||||
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||||
|
# yamllint enable rule:line-length
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
|
||||||
persist-credentials: "false"
|
persist-credentials: "false"
|
||||||
fetch-depth: "0"
|
fetch-depth: "0"
|
||||||
|
|
||||||
- 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
|
|
||||||
# FIXME: https://github.com/searxng/searxng/pull/6655#issuecomment-5550293085
|
|
||||||
# - runner: ubuntu-26.04-arm
|
|
||||||
# arch: armv7
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@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@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
|
id: build
|
||||||
|
env:
|
||||||
|
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||||
|
run: make podman.build
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test (${{ matrix.arch }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
needs: build
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: amd64
|
||||||
|
os: ubuntu-24.04
|
||||||
|
emulation: false
|
||||||
|
- arch: arm64
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
|
emulation: false
|
||||||
|
- arch: armv7
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
|
emulation: true
|
||||||
|
|
||||||
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
|
||||||
persist-credentials: "false"
|
persist-credentials: "false"
|
||||||
|
|
||||||
|
- if: ${{ matrix.emulation }}
|
||||||
|
name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@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 }}"
|
||||||
@@ -135,7 +165,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
if: github.repository_owner == 'searxng' && github.ref_name == 'master'
|
if: github.repository_owner == 'searxng' && github.ref_name == 'master'
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-26.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
- test
|
- test
|
||||||
@@ -144,25 +174,24 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Docker Hub
|
- name: Checkout
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
registry: "docker.io"
|
persist-credentials: "false"
|
||||||
username: "${{ secrets.DOCKER_USER }}"
|
|
||||||
password: "${{ secrets.DOCKER_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
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:
|
||||||
|
|||||||
25
.github/workflows/data-update.yml
vendored
25
.github/workflows/data-update.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
data:
|
data:
|
||||||
if: github.repository_owner == 'searxng'
|
if: github.repository_owner == 'searxng'
|
||||||
name: ${{ matrix.fetch }}
|
name: ${{ matrix.fetch }}
|
||||||
runs-on: ubuntu-26.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -31,8 +31,9 @@ jobs:
|
|||||||
- update_external_bangs.py
|
- update_external_bangs.py
|
||||||
- update_firefox_version.py
|
- update_firefox_version.py
|
||||||
- update_engine_traits.py
|
- update_engine_traits.py
|
||||||
- update_wikidata.py
|
- update_wikidata_units.py
|
||||||
- update_engine_descriptions.py
|
- update_engine_descriptions.py
|
||||||
|
- update_gsa_useragents.py
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -40,17 +41,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: "false"
|
persist-credentials: "false"
|
||||||
|
|
||||||
- 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 }}"
|
||||||
|
|||||||
12
.github/workflows/documentation.yml
vendored
12
.github/workflows/documentation.yml
vendored
@@ -25,25 +25,25 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
|
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-26.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
permissions:
|
permissions:
|
||||||
# for JamesIves/github-pages-deploy-action to push
|
# for JamesIves/github-pages-deploy-action to push
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: "false"
|
persist-credentials: "false"
|
||||||
fetch-depth: "0"
|
fetch-depth: "0"
|
||||||
|
|
||||||
- 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: |
|
||||||
@@ -61,11 +61,11 @@ jobs:
|
|||||||
|
|
||||||
- if: github.ref_name == 'master'
|
- if: github.ref_name == 'master'
|
||||||
name: Release
|
name: Release
|
||||||
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4.9.0
|
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
|
||||||
with:
|
with:
|
||||||
folder: "dist/docs"
|
folder: "dist/docs"
|
||||||
branch: "gh-pages"
|
branch: "gh-pages"
|
||||||
commit-message: "[mod] docs: build from commit ${{ github.sha }}"
|
commit-message: "[doc] build from commit ${{ github.sha }}"
|
||||||
# Automatically remove deleted files from the deploy branch
|
# Automatically remove deleted files from the deploy branch
|
||||||
clean: "true"
|
clean: "true"
|
||||||
single-commit: "true"
|
single-commit: "true"
|
||||||
|
|||||||
41
.github/workflows/integration.yml
vendored
41
.github/workflows/integration.yml
vendored
@@ -23,7 +23,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Python ${{ matrix.python-version }}
|
name: Python ${{ matrix.python-version }}
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
@@ -34,17 +34,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python-version }}"
|
python-version: "${{ matrix.python-version }}"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: "false"
|
persist-credentials: "false"
|
||||||
|
|
||||||
- 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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
||||||
with:
|
|
||||||
node-version: "26"
|
|
||||||
check-latest: "true"
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: "false"
|
persist-credentials: "false"
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: "./.nvmrc"
|
||||||
|
|
||||||
|
- name: Setup cache Node.js
|
||||||
|
uses: actions/cache@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
|
||||||
|
|
||||||
|
|||||||
30
.github/workflows/l10n.yml
vendored
30
.github/workflows/l10n.yml
vendored
@@ -26,27 +26,27 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update:
|
update:
|
||||||
if: github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'searxng'
|
if: github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success'
|
||||||
name: Update
|
name: Update
|
||||||
runs-on: ubuntu-26.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
permissions:
|
permissions:
|
||||||
# For "make V=1 weblate.push.translations"
|
# For "make V=1 weblate.push.translations"
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||||
fetch-depth: "0"
|
fetch-depth: "0"
|
||||||
|
|
||||||
- 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: |
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
github.repository_owner == 'searxng'
|
github.repository_owner == 'searxng'
|
||||||
&& (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule')
|
&& (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule')
|
||||||
name: Pull Request
|
name: Pull Request
|
||||||
runs-on: ubuntu-26.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
permissions:
|
permissions:
|
||||||
# For "make V=1 weblate.translations.commit"
|
# For "make V=1 weblate.translations.commit"
|
||||||
contents: write
|
contents: write
|
||||||
@@ -83,18 +83,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
python-version: "${{ env.PYTHON_VERSION }}"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||||
fetch-depth: "0"
|
fetch-depth: "0"
|
||||||
|
|
||||||
- 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: |
|
||||||
@@ -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
46
.github/workflows/security.yml
vendored
Normal 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"
|
||||||
2
Makefile
2
Makefile
@@ -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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
this file is generated automatically by searxng_extra/update/update_pygments.py
|
this file is generated automatically by searxng_extra/update/update_pygments.py
|
||||||
using pygments version 2.21.0:
|
using pygments version 2.20.0:
|
||||||
|
|
||||||
./manage templates.simple.pygments
|
./manage templates.simple.pygments
|
||||||
*/
|
*/
|
||||||
@@ -114,14 +114,14 @@
|
|||||||
.gd { color: #FF4689 } /* Generic.Deleted */
|
.gd { color: #FF4689 } /* Generic.Deleted */
|
||||||
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
|
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
|
||||||
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||||
.gr { color: #FF4689 } /* Generic.Error */
|
.gr { color: #F8F8F2 } /* Generic.Error */
|
||||||
.gh { color: #F8F8F2 } /* Generic.Heading */
|
.gh { color: #F8F8F2 } /* Generic.Heading */
|
||||||
.gi { color: #A6E22E } /* Generic.Inserted */
|
.gi { color: #A6E22E } /* Generic.Inserted */
|
||||||
.go { color: #66D9EF } /* Generic.Output */
|
.go { color: #66D9EF } /* Generic.Output */
|
||||||
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
|
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
|
||||||
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
|
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
|
||||||
.gu { color: #959077 } /* Generic.Subheading */
|
.gu { color: #959077 } /* Generic.Subheading */
|
||||||
.gt { color: #66D9EF } /* Generic.Traceback */
|
.gt { color: #F8F8F2 } /* Generic.Traceback */
|
||||||
.kc { color: #66D9EF } /* Keyword.Constant */
|
.kc { color: #66D9EF } /* Keyword.Constant */
|
||||||
.kd { color: #66D9EF } /* Keyword.Declaration */
|
.kd { color: #66D9EF } /* Keyword.Declaration */
|
||||||
.kn { color: #FF4689 } /* Keyword.Namespace */
|
.kn { color: #FF4689 } /* Keyword.Namespace */
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
.m { color: #AE81FF } /* Literal.Number */
|
.m { color: #AE81FF } /* Literal.Number */
|
||||||
.s { color: #E6DB74 } /* Literal.String */
|
.s { color: #E6DB74 } /* Literal.String */
|
||||||
.na { color: #A6E22E } /* Name.Attribute */
|
.na { color: #A6E22E } /* Name.Attribute */
|
||||||
.nb { color: #A6E22E } /* Name.Builtin */
|
.nb { color: #F8F8F2 } /* Name.Builtin */
|
||||||
.nc { color: #A6E22E } /* Name.Class */
|
.nc { color: #A6E22E } /* Name.Class */
|
||||||
.no { color: #66D9EF } /* Name.Constant */
|
.no { color: #66D9EF } /* Name.Constant */
|
||||||
.nd { color: #A6E22E } /* Name.Decorator */
|
.nd { color: #A6E22E } /* Name.Decorator */
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
.sr { color: #E6DB74 } /* Literal.String.Regex */
|
.sr { color: #E6DB74 } /* Literal.String.Regex */
|
||||||
.s1 { color: #E6DB74 } /* Literal.String.Single */
|
.s1 { color: #E6DB74 } /* Literal.String.Single */
|
||||||
.ss { color: #E6DB74 } /* Literal.String.Symbol */
|
.ss { color: #E6DB74 } /* Literal.String.Symbol */
|
||||||
.bp { color: #A6E22E } /* Name.Builtin.Pseudo */
|
.bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */
|
||||||
.fm { color: #A6E22E } /* Name.Function.Magic */
|
.fm { color: #A6E22E } /* Name.Function.Magic */
|
||||||
.vc { color: #F8F8F2 } /* Name.Variable.Class */
|
.vc { color: #F8F8F2 } /* Name.Variable.Class */
|
||||||
.vg { color: #F8F8F2 } /* Name.Variable.Global */
|
.vg { color: #F8F8F2 } /* Name.Variable.Global */
|
||||||
|
|||||||
1818
client/simple/package-lock.json
generated
1818
client/simple/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,27 +23,27 @@
|
|||||||
"not dead"
|
"not dead"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ionicons": "^8.1.0",
|
"ionicons": "^8.0.13",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"ol": "^10.10.0",
|
"ol": "^10.9.0",
|
||||||
"swiped-events": "1.2.0"
|
"swiped-events": "1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.11",
|
"@biomejs/biome": "2.5.0",
|
||||||
"@types/node": "^26.4.0",
|
"@types/node": "^26.0.0",
|
||||||
"browserslist": "^4.28.8",
|
"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.9.0",
|
"less": "^4.6.6",
|
||||||
"mathjs": "^15.2.0",
|
"mathjs": "^15.2.0",
|
||||||
"sharp": "~0.35.4",
|
"sharp": "~0.35.1",
|
||||||
"sort-package-json": "^4.0.0",
|
"sort-package-json": "^4.0.0",
|
||||||
"stylelint": "^17.14.1",
|
"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.1.0",
|
"svgo": "^4.0.1",
|
||||||
"typescript": "~7.0.2",
|
"typescript": "~6.0.3",
|
||||||
"vite": "^8.2.2",
|
"vite": "^8.0.16",
|
||||||
"vite-bundle-analyzer": "^1.3.9"
|
"vite-bundle-analyzer": "^1.3.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import { assertElement } from "../util/assertElement.ts";
|
|||||||
|
|
||||||
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const res = await http("GET", `./autocompleter?q=${query}`);
|
let res: Response;
|
||||||
|
|
||||||
|
if (settings.method === "GET") {
|
||||||
|
res = await http("GET", `./autocompleter?q=${query}`);
|
||||||
|
} else {
|
||||||
|
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
|
||||||
|
}
|
||||||
|
|
||||||
const results = await res.json();
|
const results = await res.json();
|
||||||
|
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
|||||||
@@ -80,12 +80,7 @@ export default class Calculator extends Plugin {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const node = Calculator.math.parse(searchInput.value);
|
const node = Calculator.math.parse(searchInput.value);
|
||||||
const value = node.evaluate();
|
return `${node.toString()} = ${node.evaluate()}`;
|
||||||
if (typeof value !== "number") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${node.toString()} = ${value}`;
|
|
||||||
} catch {
|
} catch {
|
||||||
// not a compatible math expression
|
// not a compatible math expression
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const appendAnswerElement = (element: HTMLElement | string | number): voi
|
|||||||
|
|
||||||
if (!(element instanceof HTMLElement)) {
|
if (!(element instanceof HTMLElement)) {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.textContent = element.toString();
|
span.innerHTML = element.toString();
|
||||||
// biome-ignore lint/style/noParameterAssign: TODO
|
// biome-ignore lint/style/noParameterAssign: TODO
|
||||||
element = span;
|
element = span;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ./
|
||||||
|
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -112,15 +112,6 @@ if [ "$(id -u)" -eq 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ENVs aliases
|
# ENVs aliases
|
||||||
# https://github.com/searxng/searxng/issues/5934
|
export GRANIAN_PORT="${SEARXNG_PORT:-$GRANIAN_PORT}"
|
||||||
case "${SEARXNG_PORT:-}" in
|
|
||||||
'') ;;
|
|
||||||
*[!0-9]*)
|
|
||||||
unset SEARXNG_PORT
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
export GRANIAN_PORT="$SEARXNG_PORT"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exec /usr/local/searxng/.venv/bin/granian searx.webapp:app
|
exec /usr/local/searxng/.venv/bin/granian searx.webapp:app
|
||||||
|
|||||||
@@ -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``
|
||||||
===============================
|
===============================
|
||||||
|
|||||||
@@ -29,11 +29,10 @@ By default and without any extensions, SearXNG serves these resolvers:
|
|||||||
- ``duckduckgo``
|
- ``duckduckgo``
|
||||||
- ``allesedv``
|
- ``allesedv``
|
||||||
- ``google``
|
- ``google``
|
||||||
- ``kagi``
|
|
||||||
- ``yandex``
|
- ``yandex``
|
||||||
|
|
||||||
With the above setting favicons are displayed, the user has the option to
|
With the above setting favicons are displayed, the user has the option to
|
||||||
deactivate this feature in their settings. If the user is to have the option of
|
deactivate this feature in his settings. If the user is to have the option of
|
||||||
selecting from several *resolvers*, a further setting is required / but this
|
selecting from several *resolvers*, a further setting is required / but this
|
||||||
setting will be discussed :ref:`later <register resolvers>` in this article,
|
setting will be discussed :ref:`later <register resolvers>` in this article,
|
||||||
first we have to setup the favicons cache.
|
first we have to setup the favicons cache.
|
||||||
@@ -209,7 +208,6 @@ choose from, the following configuration could be used:
|
|||||||
"duckduckgo" = "searx.favicons.resolvers.duckduckgo"
|
"duckduckgo" = "searx.favicons.resolvers.duckduckgo"
|
||||||
"allesedv" = "searx.favicons.resolvers.allesedv"
|
"allesedv" = "searx.favicons.resolvers.allesedv"
|
||||||
# "google" = "searx.favicons.resolvers.google"
|
# "google" = "searx.favicons.resolvers.google"
|
||||||
# "kagi" = "searx.favicons.resolvers.kagi"
|
|
||||||
# "yandex" = "searx.favicons.resolvers.yandex"
|
# "yandex" = "searx.favicons.resolvers.yandex"
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -228,7 +226,6 @@ into the *proxy*:
|
|||||||
- :py:obj:`searx.favicons.resolvers.duckduckgo`
|
- :py:obj:`searx.favicons.resolvers.duckduckgo`
|
||||||
- :py:obj:`searx.favicons.resolvers.allesedv`
|
- :py:obj:`searx.favicons.resolvers.allesedv`
|
||||||
- :py:obj:`searx.favicons.resolvers.google`
|
- :py:obj:`searx.favicons.resolvers.google`
|
||||||
- :py:obj:`searx.favicons.resolvers.kagi`
|
|
||||||
- :py:obj:`searx.favicons.resolvers.yandex`
|
- :py:obj:`searx.favicons.resolvers.yandex`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ engine is shown. Most of the options have a default value or even are optional.
|
|||||||
|
|
||||||
# overwrite values from section 'outgoing:'
|
# overwrite values from section 'outgoing:'
|
||||||
enable_http2: false
|
enable_http2: false
|
||||||
enable_http3: false
|
|
||||||
retries: 1
|
retries: 1
|
||||||
max_connections: 100
|
max_connections: 100
|
||||||
|
max_keepalive_connections: 10
|
||||||
|
keepalive_expiry: 5.0
|
||||||
using_tor_proxy: false
|
using_tor_proxy: false
|
||||||
proxies:
|
proxies:
|
||||||
http:
|
http:
|
||||||
@@ -162,16 +163,6 @@ engine is shown. Most of the options have a default value or even are optional.
|
|||||||
``enable_http`` : optional
|
``enable_http`` : optional
|
||||||
Enable HTTP for this engine (by default only HTTPS is enabled).
|
Enable HTTP for this engine (by default only HTTPS is enabled).
|
||||||
|
|
||||||
``enable_http3`` : optional
|
|
||||||
Use HTTP/3 (falls back to HTTP/2). Default ``false``.
|
|
||||||
Ignored when a proxy is set.
|
|
||||||
|
|
||||||
.. hint::
|
|
||||||
|
|
||||||
HTTP/3 places demands on the IP infrastructure that are not met in every
|
|
||||||
environment. Enable this option only if you are aware of these requirements
|
|
||||||
and the extent to which they are met.
|
|
||||||
|
|
||||||
``retry_on_http_error`` : optional
|
``retry_on_http_error`` : optional
|
||||||
Retry request on some HTTP status code.
|
Retry request on some HTTP status code.
|
||||||
|
|
||||||
@@ -188,12 +179,20 @@ engine is shown. Most of the options have a default value or even are optional.
|
|||||||
Using tor proxy (``true``) or not (``false``) for this engine. The default is
|
Using tor proxy (``true``) or not (``false``) for this engine. The default is
|
||||||
taken from ``using_tor_proxy`` of the :ref:`settings outgoing`.
|
taken from ``using_tor_proxy`` of the :ref:`settings outgoing`.
|
||||||
|
|
||||||
.. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
|
.. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration
|
||||||
|
|
||||||
|
``max_keepalive_connection#s`` :
|
||||||
|
`Pool limit configuration`_, overwrites value ``pool_maxsize`` from
|
||||||
|
:ref:`settings outgoing` for this engine.
|
||||||
|
|
||||||
``max_connections`` :
|
``max_connections`` :
|
||||||
`Pool limit configuration`_, overwrites value ``pool_connections`` from
|
`Pool limit configuration`_, overwrites value ``pool_connections`` from
|
||||||
:ref:`settings outgoing` for this engine.
|
:ref:`settings outgoing` for this engine.
|
||||||
|
|
||||||
|
``keepalive_expiry`` :
|
||||||
|
`Pool limit configuration`_, overwrites value ``keepalive_expiry`` from
|
||||||
|
:ref:`settings outgoing` for this engine.
|
||||||
|
|
||||||
|
|
||||||
.. _private engines:
|
.. _private engines:
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,20 @@ Communication with search engines.
|
|||||||
request_timeout: 2.0 # default timeout in seconds, can be override by engine
|
request_timeout: 2.0 # default timeout in seconds, can be override by engine
|
||||||
max_request_timeout: 10.0 # the maximum timeout in seconds
|
max_request_timeout: 10.0 # the maximum timeout in seconds
|
||||||
useragent_suffix: "" # information like an email address to the administrator
|
useragent_suffix: "" # information like an email address to the administrator
|
||||||
pool_connections: 100 # Maximum number of concurrent connections (default: 100)
|
pool_connections: 100 # Maximum number of allowable connections, or null
|
||||||
enable_http2: true # Enables the use of HTTP2
|
# for no limits. The default is 100.
|
||||||
|
pool_maxsize: 10 # Number of allowable keep-alive connections, or null
|
||||||
|
# to always allow. The default is 10.
|
||||||
|
enable_http2: true # See https://www.python-httpx.org/http2/
|
||||||
# uncomment below section if you want to use a custom server certificate
|
# uncomment below section if you want to use a custom server certificate
|
||||||
|
# see https://www.python-httpx.org/advanced/#changing-the-verification-defaults
|
||||||
|
# and https://www.python-httpx.org/compatibility/#ssl-configuration
|
||||||
# verify: ~/.mitmproxy/mitmproxy-ca-cert.cer
|
# verify: ~/.mitmproxy/mitmproxy-ca-cert.cer
|
||||||
#
|
#
|
||||||
# uncomment below section if you want to use a proxy
|
# uncomment below section if you want to use a proxyq see: SOCKS proxies
|
||||||
|
# https://2.python-requests.org/en/latest/user/advanced/#proxies
|
||||||
|
# are also supported: see
|
||||||
|
# https://2.python-requests.org/en/latest/user/advanced/#socks
|
||||||
#
|
#
|
||||||
# proxies:
|
# proxies:
|
||||||
# all://:
|
# all://:
|
||||||
@@ -38,26 +46,30 @@ Communication with search engines.
|
|||||||
timeout to load). Can be override by ``timeout`` in the :ref:`settings engines`.
|
timeout to load). Can be override by ``timeout`` in the :ref:`settings engines`.
|
||||||
|
|
||||||
``useragent_suffix`` :
|
``useragent_suffix`` :
|
||||||
Suffix to add when an engine's User-Agent is set via searxng_useragent().
|
Suffix to the user-agent SearXNG uses to send requests to others engines. If an
|
||||||
Contact info here may be useful to avoid an engine blocking you.
|
engine wish to block you, a contact info here may be useful to avoid that.
|
||||||
|
|
||||||
.. _Pool limit configuration: https://curl-cffi.readthedocs.io/en/latest/api.html#sessions
|
.. _Pool limit configuration: https://www.python-httpx.org/advanced/#pool-limit-configuration
|
||||||
|
|
||||||
|
``pool_maxsize``:
|
||||||
|
Number of allowable keep-alive connections, or ``null`` to always allow. The
|
||||||
|
default is 10. See ``max_keepalive_connections`` `Pool limit configuration`_.
|
||||||
|
|
||||||
``pool_connections`` :
|
``pool_connections`` :
|
||||||
Maximum number of concurrent connections. The default is 100.
|
Maximum number of allowable connections, or ``null`` # for no limits. The
|
||||||
See ``max_clients`` `Pool limit configuration`_.
|
default is 100. See ``max_connections`` `Pool limit configuration`_.
|
||||||
|
|
||||||
.. _curl_cffi proxies: https://curl-cffi.readthedocs.io/en/latest/quick_start.html
|
``keepalive_expiry`` :
|
||||||
|
Number of seconds to keep a connection in the pool. By default 5.0 seconds.
|
||||||
|
See ``keepalive_expiry`` `Pool limit configuration`_.
|
||||||
|
|
||||||
|
.. _httpx proxies: https://www.python-httpx.org/advanced/#http-proxying
|
||||||
|
|
||||||
``proxies`` :
|
``proxies`` :
|
||||||
Define one or more proxies you wish to use, see `curl_cffi proxies`_.
|
Define one or more proxies you wish to use, see `httpx proxies`_.
|
||||||
If there are more than one proxy for one protocol (http, https),
|
If there are more than one proxy for one protocol (http, https),
|
||||||
requests to the engines are distributed in a round-robin fashion.
|
requests to the engines are distributed in a round-robin fashion.
|
||||||
|
|
||||||
HTTP, HTTPS, SOCKS4, SOCKS5 and SOCKS5h proxies are supported
|
|
||||||
(``http://``, ``https://``, ``socks4://``, ``socks5://``, ``socks5h://``). You should
|
|
||||||
use ``socks5h://`` when using Tor so hostnames are resolved by the proxy.
|
|
||||||
|
|
||||||
``source_ips`` :
|
``source_ips`` :
|
||||||
If you use multiple network interfaces, define from which IP the requests must
|
If you use multiple network interfaces, define from which IP the requests must
|
||||||
be made. Example:
|
be made. Example:
|
||||||
@@ -75,15 +87,18 @@ Communication with search engines.
|
|||||||
different proxy and source ip.
|
different proxy and source ip.
|
||||||
|
|
||||||
``enable_http2`` :
|
``enable_http2`` :
|
||||||
Enable by default (HTTP/2). Set to ``false`` to force HTTP/1.1.
|
Enable by default. Set to ``false`` to disable HTTP/2.
|
||||||
HTTP/3 is opt-in per engine (``enable_http3``).
|
|
||||||
|
.. _httpx verification defaults: https://www.python-httpx.org/advanced/#changing-the-verification-defaults
|
||||||
|
.. _httpx ssl configuration: https://www.python-httpx.org/compatibility/#ssl-configuration
|
||||||
|
|
||||||
``verify``: : ``$SSL_CERT_FILE``, ``$SSL_CERT_DIR``
|
``verify``: : ``$SSL_CERT_FILE``, ``$SSL_CERT_DIR``
|
||||||
HTTPS verification uses the OS's trust store by default.
|
Allow to specify a path to certificate.
|
||||||
Set a path to use a custom CA file.
|
see `httpx verification defaults`_.
|
||||||
|
|
||||||
In addition to ``verify``, SearXNG supports the ``$SSL_CERT_FILE`` (for a file) and
|
In addition to ``verify``, SearXNG supports the ``$SSL_CERT_FILE`` (for a file) and
|
||||||
``$SSL_CERT_DIR`` (for a directory) OpenSSL variables.
|
``$SSL_CERT_DIR`` (for a directory) OpenSSL variables.
|
||||||
|
see `httpx ssl configuration`_.
|
||||||
|
|
||||||
``max_redirects`` :
|
``max_redirects`` :
|
||||||
30 by default. Maximum redirect before it is an error.
|
30 by default. Maximum redirect before it is an error.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
search:
|
search:
|
||||||
safe_search: 0
|
safe_search: 0
|
||||||
autocomplete: "duckduckgo"
|
autocomplete: ""
|
||||||
favicon_resolver: ""
|
favicon_resolver: ""
|
||||||
default_lang: ""
|
default_lang: ""
|
||||||
ban_time_on_fail: 5
|
ban_time_on_fail: 5
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
- ``2``: Strict
|
- ``2``: Strict
|
||||||
|
|
||||||
``autocomplete``:
|
``autocomplete``:
|
||||||
Existing autocomplete backends, set blank to turn it off.
|
Existing autocomplete backends, leave blank to turn it off.
|
||||||
|
|
||||||
- ``360search``
|
- ``360search``
|
||||||
- ``baidu``
|
- ``baidu``
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
- ``dbpedia``
|
- ``dbpedia``
|
||||||
- ``duckduckgo``
|
- ``duckduckgo``
|
||||||
- ``google``
|
- ``google``
|
||||||
- ``kagi``
|
|
||||||
- ``mwmbl``
|
- ``mwmbl``
|
||||||
- ``naver``
|
- ``naver``
|
||||||
- ``privacywall``
|
- ``privacywall``
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
limiter: false
|
limiter: false
|
||||||
public_instance: false
|
public_instance: false
|
||||||
image_proxy: false
|
image_proxy: false
|
||||||
method: "GET"
|
method: "POST"
|
||||||
default_http_headers:
|
default_http_headers:
|
||||||
X-Content-Type-Options : nosniff
|
X-Content-Type-Options : nosniff
|
||||||
X-Download-Options : noopen
|
X-Download-Options : noopen
|
||||||
@@ -58,8 +58,8 @@
|
|||||||
|
|
||||||
``method`` : ``GET`` | ``POST``
|
``method`` : ``GET`` | ``POST``
|
||||||
|
|
||||||
HTTP method. By default, ``GET`` is used / The ``POST`` method has the
|
HTTP method. By defaults ``POST`` is used / The ``POST`` method has the
|
||||||
advantage with some browsers that the history is not saved, but
|
advantage with some WEB browsers that the history is not easy to read, but
|
||||||
there are also various disadvantages that sometimes **severely restrict the
|
there are also various disadvantages that sometimes **severely restrict the
|
||||||
ease of use for the end user** (e.g. back button to jump back to the previous
|
ease of use for the end user** (e.g. back button to jump back to the previous
|
||||||
search page and drag & drop of search term to new tabs do not work as
|
search page and drag & drop of search term to new tabs do not work as
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ parameters with default value can be redefined for special purposes.
|
|||||||
data dict ``{}``
|
data dict ``{}``
|
||||||
cookies dict ``{}``
|
cookies dict ``{}``
|
||||||
verify bool ``True``
|
verify bool ``True``
|
||||||
headers.User-Agent str ``''``
|
headers.User-Agent str a random User-Agent
|
||||||
category str current category, like ``'general'``
|
category str current category, like ``'general'``
|
||||||
safesearch int ``0``, between ``0`` and ``2`` (normal, moderate, strict)
|
safesearch int ``0``, between ``0`` and ``2`` (normal, moderate, strict)
|
||||||
time_range Optional[str] ``None``, can be ``day``, ``week``, ``month``, ``year``
|
time_range Optional[str] ``None``, can be ``day``, ``week``, ``month``, ``year``
|
||||||
@@ -229,8 +229,6 @@ following parameters can be used to specify a search request:
|
|||||||
max_redirects int maximum redirects, hard limit
|
max_redirects int maximum redirects, hard limit
|
||||||
soft_max_redirects int maximum redirects, soft limit. Record an error but don't stop the engine
|
soft_max_redirects int maximum redirects, soft limit. Record an error but don't stop the engine
|
||||||
raise_for_httperror bool True by default: raise an exception if the HTTP code of response is >= 300
|
raise_for_httperror bool True by default: raise an exception if the HTTP code of response is >= 300
|
||||||
impersonate str curl_cffi impersonate target (default: chrome, none to disable)
|
|
||||||
curl_options dict Any extra libcurl options for the request
|
|
||||||
=================== =========== ==========================================================================
|
=================== =========== ==========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
8
docs/dev/engines/online/cara.rst
Normal file
8
docs/dev/engines/online/cara.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _cara engine:
|
||||||
|
|
||||||
|
===========
|
||||||
|
Cara Images
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.cara
|
||||||
|
:members:
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.. _europepmc engine:
|
|
||||||
|
|
||||||
==========
|
|
||||||
Europe PMC
|
|
||||||
==========
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.europepmc
|
|
||||||
:members:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.. _exaapi engine:
|
|
||||||
|
|
||||||
==============
|
|
||||||
Exa API Engine
|
|
||||||
==============
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.exaapi
|
|
||||||
:members:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.. _jina engine:
|
|
||||||
|
|
||||||
===========
|
|
||||||
Jina Engine
|
|
||||||
===========
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.jina
|
|
||||||
:members:
|
|
||||||
8
docs/dev/engines/online/presearch.rst
Normal file
8
docs/dev/engines/online/presearch.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _engine presearch:
|
||||||
|
|
||||||
|
================
|
||||||
|
Presearch Engine
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.presearch
|
||||||
|
:members:
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.. _yandex api engine:
|
|
||||||
|
|
||||||
=================
|
|
||||||
Yandex Search API
|
|
||||||
=================
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.yandex_api
|
|
||||||
:members:
|
|
||||||
@@ -80,8 +80,8 @@ same environment, here are a few examples::
|
|||||||
# to test one of the update scripts
|
# to test one of the update scripts
|
||||||
(dev.env)$ searxng_extra/update/update_engine_traits.py --help
|
(dev.env)$ searxng_extra/update/update_engine_traits.py --help
|
||||||
|
|
||||||
# to test the update of the wikidata units and property names
|
# to test the update of the wikidata units
|
||||||
(dev.env)$ searxng_extra/update/update_wikidata.py
|
(dev.env)$ searxng_extra/update/update_wikidata_units.py
|
||||||
|
|
||||||
|
|
||||||
.. sidebar:: further read
|
.. sidebar:: further read
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ content becomes smart.
|
|||||||
files & folders origin :origin:`docs/dev/reST.rst` ``:origin:`docs/dev/reST.rst```
|
files & folders origin :origin:`docs/dev/reST.rst` ``:origin:`docs/dev/reST.rst```
|
||||||
pull request :pull:`4` ``:pull:`4```
|
pull request :pull:`4` ``:pull:`4```
|
||||||
patch :patch:`af2cae6` ``:patch:`af2cae6```
|
patch :patch:`af2cae6` ``:patch:`af2cae6```
|
||||||
PyPi package :pypi:`curl_cffi` ``:pypi:`curl_cffi```
|
PyPi package :pypi:`httpx` ``:pypi:`httpx```
|
||||||
manual page man :man:`bash` ``:man:`bash```
|
manual page man :man:`bash` ``:man:`bash```
|
||||||
intersphinx_
|
intersphinx_
|
||||||
--------------------------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ Scripts to update static data in :origin:`searx/data/`
|
|||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
``update_wikidata.py``
|
``update_wikidata_units.py``
|
||||||
============================
|
============================
|
||||||
|
|
||||||
:origin:`[source] <searxng_extra/update/update_wikidata.py>`
|
:origin:`[source] <searxng_extra/update/update_wikidata_units.py>`
|
||||||
|
|
||||||
.. automodule:: searxng_extra.update.update_wikidata
|
.. automodule:: searxng_extra.update.update_wikidata_units
|
||||||
:members:
|
:members:
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ If you don't trust anyone, you can set up your own, see :ref:`installation`.
|
|||||||
|
|
||||||
- :ref:`self hosted <installation>`
|
- :ref:`self hosted <installation>`
|
||||||
- :ref:`no user tracking / no profiling <SearXNG protect privacy>`
|
- :ref:`no user tracking / no profiling <SearXNG protect privacy>`
|
||||||
- javascript & cookies are optional
|
- script & cookies are optional
|
||||||
|
- secure, encrypted connections
|
||||||
- :ref:`{{engines | length}} search engines <configured engines>`
|
- :ref:`{{engines | length}} search engines <configured engines>`
|
||||||
- `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_
|
- `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_
|
||||||
- about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_
|
- about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_
|
||||||
- :ref:`easy integration of search engines <demo online engine>`
|
- :ref:`easy integration of search engines <demo online engine>`
|
||||||
|
- professional development: `CI <https://github.com/searxng/searxng/actions>`_,
|
||||||
|
`quality assurance <https://dev.searxng.org/>`_ &
|
||||||
|
`automated tested UI <https://dev.searxng.org/screenshots.html>`_
|
||||||
|
|
||||||
.. sidebar:: be a part
|
.. sidebar:: be a part
|
||||||
|
|
||||||
|
|||||||
2
manage
2
manage
@@ -48,7 +48,7 @@ PATH="${PY_ENV}/bin:${REPO_ROOT}/node_modules/.bin:${GOROOT}/bin:${GOPATH}/bin:$
|
|||||||
|
|
||||||
PYOBJECTS="searx"
|
PYOBJECTS="searx"
|
||||||
PY_SETUP_EXTRAS='[test]'
|
PY_SETUP_EXTRAS='[test]'
|
||||||
GECKODRIVER_VERSION="v0.37.0"
|
GECKODRIVER_VERSION="v0.36.0"
|
||||||
# SPHINXOPTS=
|
# SPHINXOPTS=
|
||||||
BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization")
|
BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization")
|
||||||
BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests")
|
BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests")
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ mock==5.2.0
|
|||||||
nose2[coverage_plugin]==0.16.0
|
nose2[coverage_plugin]==0.16.0
|
||||||
cov-core==1.15.0
|
cov-core==1.15.0
|
||||||
black==25.9.0
|
black==25.9.0
|
||||||
pylint==4.0.8
|
pylint==4.0.6
|
||||||
splinter==0.21.0
|
splinter==0.21.0
|
||||||
selenium==4.48.0
|
selenium==4.45.0
|
||||||
Sphinx==8.2.3;python_version <= "3.11"
|
Sphinx==8.2.3;python_version <= "3.11"
|
||||||
Sphinx==9.1.0; python_version > "3.11"
|
Sphinx==9.1.0; python_version > "3.11"
|
||||||
sphinx-issues==6.0.0
|
sphinx-issues==6.0.0
|
||||||
@@ -18,11 +18,11 @@ myst-parser==5.0.0
|
|||||||
linuxdoc==20260504
|
linuxdoc==20260504
|
||||||
aiounittest==1.5.0
|
aiounittest==1.5.0
|
||||||
yamllint==1.38.0
|
yamllint==1.38.0
|
||||||
wlc==2.1.1
|
wlc==2.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.8.2
|
granian[reload]==2.7.6
|
||||||
basedpyright==1.39.10
|
basedpyright==1.39.8
|
||||||
types-lxml==2026.2.16
|
types-lxml==2026.2.16
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
granian==2.8.2
|
granian==2.7.6
|
||||||
granian[pname]==2.8.2
|
granian[pname]==2.7.6
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
certifi==2026.7.22
|
certifi==2026.6.17
|
||||||
babel==2.18.0
|
babel==2.18.0
|
||||||
flask-babel==4.0.0
|
flask-babel==4.0.0
|
||||||
flask==3.1.3
|
flask==3.1.3
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
lxml==6.1.2
|
lxml==6.1.1
|
||||||
pygments==2.21.0
|
pygments==2.20.0
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
pyyaml==6.0.3
|
pyyaml==6.0.3
|
||||||
curl_cffi==0.16.1
|
httpx[http2]==0.28.1
|
||||||
|
httpx-socks[asyncio]==0.10.0
|
||||||
|
sniffio==1.3.1
|
||||||
valkey==6.1.1
|
valkey==6.1.1
|
||||||
markdown-it-py==4.2.0
|
markdown-it-py==4.2.0
|
||||||
msgspec==0.21.1
|
msgspec==0.21.1
|
||||||
typer==0.27.2
|
typer==0.26.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
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from . import Answerer, AnswererInfo
|
|||||||
|
|
||||||
def random_characters():
|
def random_characters():
|
||||||
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
|
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
|
||||||
return random.choices(random_string_letters, k=random.randint(8, 32))
|
return [random.choice(random_string_letters) for _ in range(random.randint(8, 32))]
|
||||||
|
|
||||||
|
|
||||||
def random_string():
|
def random_string():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from urllib.parse import urlencode
|
|||||||
|
|
||||||
import lxml.etree
|
import lxml.etree
|
||||||
import lxml.html
|
import lxml.html
|
||||||
from curl_cffi.requests.exceptions import RequestException
|
from httpx import HTTPError
|
||||||
|
|
||||||
from searx import settings
|
from searx import settings
|
||||||
from searx.engines import (
|
from searx.engines import (
|
||||||
@@ -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
|
||||||
@@ -62,8 +60,8 @@ def bing(query: str, _sxng_locale: str) -> list[str]:
|
|||||||
# bing search autocompleter
|
# bing search autocompleter
|
||||||
base_url = "https://www.bing.com/AS/Suggestions?"
|
base_url = "https://www.bing.com/AS/Suggestions?"
|
||||||
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters
|
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters
|
||||||
cvid = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32))
|
cvid = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||||||
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}), enable_http3=True)
|
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}))
|
||||||
results: list[str] = []
|
results: list[str] = []
|
||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
@@ -83,7 +81,7 @@ def brave(query: str, _sxng_locale: str) -> list[str]:
|
|||||||
url = 'https://search.brave.com/api/suggest?'
|
url = 'https://search.brave.com/api/suggest?'
|
||||||
url += urlencode({'q': query})
|
url += urlencode({'q': query})
|
||||||
country = 'all'
|
country = 'all'
|
||||||
kwargs = {'cookies': {'country': country}, 'enable_http3': True}
|
kwargs = {'cookies': {'country': country}}
|
||||||
resp = get(url, **kwargs)
|
resp = get(url, **kwargs)
|
||||||
results: list[str] = []
|
results: list[str] = []
|
||||||
|
|
||||||
@@ -127,17 +125,16 @@ def duckduckgo(query: str, sxng_locale: str) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def google_complete(query: str, sxng_locale: str) -> list[str]:
|
def google_complete(query: str, sxng_locale: str) -> list[str]:
|
||||||
"""Autocomplete from Google. Supports Google's languages
|
"""Autocomplete from Google. Supports Google's languages and subdomains
|
||||||
(:py:obj:`searx.engines.google.get_google_info`) by using the async REST
|
(:py:obj:`searx.engines.google.get_google_info`) by using the async REST
|
||||||
API::
|
API::
|
||||||
|
|
||||||
https://www.google.com/complete/search?{args}
|
https://{subdomain}/complete/search?{args}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = ENGINE_TRAITS.get("google") or {}
|
google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, engines['google'].traits)
|
||||||
traits = EngineTraits(**data)
|
url = 'https://{subdomain}/complete/search?{args}'
|
||||||
google_info: dict[str, t.Any] = google.get_google_info({'searxng_locale': sxng_locale}, traits)
|
|
||||||
args = urlencode(
|
args = urlencode(
|
||||||
{
|
{
|
||||||
'q': query,
|
'q': query,
|
||||||
@@ -147,7 +144,7 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
|
|||||||
)
|
)
|
||||||
results: list[str] = []
|
results: list[str] = []
|
||||||
|
|
||||||
resp = get('https://www.google.com/complete/search?' + args, enable_http3=True)
|
resp = get(url.format(subdomain=google_info['subdomain'], args=args))
|
||||||
if resp and resp.ok:
|
if resp and resp.ok:
|
||||||
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
|
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
|
||||||
data = json.loads(json_txt)
|
data = json.loads(json_txt)
|
||||||
@@ -156,24 +153,6 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def kagi(query: str, sxng_locale: str) -> list[str]:
|
|
||||||
"""Autocomplete from Kagi."""
|
|
||||||
|
|
||||||
args: dict[str, str] = {'q': query}
|
|
||||||
|
|
||||||
if '-' in sxng_locale:
|
|
||||||
args['r'] = sxng_locale.split('-')[1].lower()
|
|
||||||
|
|
||||||
resp = get("https://kagisuggest.com/api/autosuggest?" + urlencode(args))
|
|
||||||
results: list[str] = []
|
|
||||||
|
|
||||||
if resp.ok:
|
|
||||||
data = resp.json()
|
|
||||||
if len(data) > 1:
|
|
||||||
results = data[1]
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def mwmbl(query: str, _sxng_locale: str) -> list[str]:
|
def mwmbl(query: str, _sxng_locale: str) -> list[str]:
|
||||||
"""Autocomplete from Mwmbl_."""
|
"""Autocomplete from Mwmbl_."""
|
||||||
|
|
||||||
@@ -397,7 +376,6 @@ backends: dict[str, t.Callable[[str, str], list[str]]] = {
|
|||||||
'dbpedia': dbpedia,
|
'dbpedia': dbpedia,
|
||||||
'duckduckgo': duckduckgo,
|
'duckduckgo': duckduckgo,
|
||||||
'google': google_complete,
|
'google': google_complete,
|
||||||
'kagi': kagi,
|
|
||||||
'mwmbl': mwmbl,
|
'mwmbl': mwmbl,
|
||||||
'naver': naver,
|
'naver': naver,
|
||||||
'privacywall': privacywall,
|
'privacywall': privacywall,
|
||||||
@@ -418,5 +396,5 @@ def search_autocomplete(backend_name: str, query: str, sxng_locale: str) -> list
|
|||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
return backend(query, sxng_locale)
|
return backend(query, sxng_locale)
|
||||||
except (RequestException, SearxEngineResponseException):
|
except (HTTPError, SearxEngineResponseException):
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -151,6 +151,6 @@ def get_token() -> str:
|
|||||||
if token:
|
if token:
|
||||||
token = token.decode('UTF-8') # type: ignore
|
token = token.decode('UTF-8') # type: ignore
|
||||||
else:
|
else:
|
||||||
token = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16))
|
token = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
|
||||||
valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
|
valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
|
||||||
return token
|
return token
|
||||||
|
|||||||
@@ -63,20 +63,6 @@ class ProxyFix:
|
|||||||
proxy_list: list[str] = cfg.get("botdetection.trusted_proxies", default=[])
|
proxy_list: list[str] = cfg.get("botdetection.trusted_proxies", default=[])
|
||||||
return [ip_network(net, strict=False) for net in proxy_list]
|
return [ip_network(net, strict=False) for net in proxy_list]
|
||||||
|
|
||||||
def is_trusted_proxy(
|
|
||||||
self,
|
|
||||||
addr: IPv4Address | IPv6Address | None,
|
|
||||||
trusted_proxies: list[IPv4Network | IPv6Network],
|
|
||||||
) -> bool:
|
|
||||||
if addr is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for net in trusted_proxies:
|
|
||||||
if addr.version == net.version and addr in net:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def trusted_remote_addr(
|
def trusted_remote_addr(
|
||||||
self,
|
self,
|
||||||
x_forwarded_for: list[IPv4Address | IPv6Address],
|
x_forwarded_for: list[IPv4Address | IPv6Address],
|
||||||
@@ -84,8 +70,16 @@ class ProxyFix:
|
|||||||
) -> str:
|
) -> str:
|
||||||
# always rtl
|
# always rtl
|
||||||
for addr in reversed(x_forwarded_for):
|
for addr in reversed(x_forwarded_for):
|
||||||
if not self.is_trusted_proxy(addr, trusted_proxies):
|
trust: bool = False
|
||||||
logger.debug("client address from X-Forwarded-For: %s", addr)
|
|
||||||
|
for net in trusted_proxies:
|
||||||
|
if addr.version == net.version and addr in net:
|
||||||
|
logger.debug("trust proxy %s (member of %s)", addr, net)
|
||||||
|
trust = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# client address
|
||||||
|
if not trust:
|
||||||
return addr.compressed
|
return addr.compressed
|
||||||
|
|
||||||
# fallback to first address
|
# fallback to first address
|
||||||
@@ -101,21 +95,19 @@ class ProxyFix:
|
|||||||
# in this function!
|
# in this function!
|
||||||
|
|
||||||
orig_remote_addr: str | None = environ.pop("REMOTE_ADDR")
|
orig_remote_addr: str | None = environ.pop("REMOTE_ADDR")
|
||||||
orig_remote_ip: IPv4Address | IPv6Address | None = None
|
|
||||||
|
|
||||||
# Validate the IPs involved in this game and delete all invalid ones
|
# Validate the IPs involved in this game and delete all invalid ones
|
||||||
# from the WSGI environment.
|
# from the WSGI environment.
|
||||||
|
|
||||||
if orig_remote_addr:
|
if orig_remote_addr:
|
||||||
try:
|
try:
|
||||||
orig_remote_ip = ip_address(orig_remote_addr)
|
addr = ip_address(orig_remote_addr)
|
||||||
if orig_remote_ip.version == 6 and orig_remote_ip.ipv4_mapped:
|
if addr.version == 6 and addr.ipv4_mapped:
|
||||||
orig_remote_ip = orig_remote_ip.ipv4_mapped
|
addr = addr.ipv4_mapped
|
||||||
orig_remote_addr = orig_remote_ip.compressed
|
orig_remote_addr = addr.compressed
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.error("REMOTE_ADDR: %s / discard REMOTE_ADDR from WSGI environment", exc)
|
logger.error("REMOTE_ADDR: %s / discard REMOTE_ADDR from WSGI environment", exc)
|
||||||
orig_remote_addr = None
|
orig_remote_addr = None
|
||||||
orig_remote_ip = None
|
|
||||||
|
|
||||||
x_real_ip: str | None = environ.get("HTTP_X_REAL_IP")
|
x_real_ip: str | None = environ.get("HTTP_X_REAL_IP")
|
||||||
if x_real_ip:
|
if x_real_ip:
|
||||||
@@ -149,13 +141,11 @@ class ProxyFix:
|
|||||||
if not x_forwarded_for and not x_real_ip:
|
if not x_forwarded_for and not x_real_ip:
|
||||||
log_error_only_once("X-Forwarded-For nor X-Real-IP header is set!")
|
log_error_only_once("X-Forwarded-For nor X-Real-IP header is set!")
|
||||||
|
|
||||||
if x_forwarded_for or x_real_ip:
|
if x_forwarded_for and not trusted_proxies:
|
||||||
if not trusted_proxies:
|
|
||||||
log_error_only_once("missing botdetection.trusted_proxies config")
|
log_error_only_once("missing botdetection.trusted_proxies config")
|
||||||
|
# without trusted_proxies, this variable is useless for determining
|
||||||
if not self.is_trusted_proxy(orig_remote_ip, trusted_proxies):
|
# the real IP
|
||||||
x_forwarded_for = []
|
x_forwarded_for = []
|
||||||
x_real_ip = None
|
|
||||||
|
|
||||||
# securing the WSGI environment variables that are adjusted
|
# securing the WSGI environment variables that are adjusted
|
||||||
|
|
||||||
|
|||||||
@@ -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``.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -32,13 +32,6 @@ class WikiDataUnitType(t.TypedDict):
|
|||||||
to_si_factor: float
|
to_si_factor: float
|
||||||
|
|
||||||
|
|
||||||
WikiDataPropertyNameType = str | dict[str, str]
|
|
||||||
"""Name of a Wikidata property. Can be either the plain name or a dictionary of
|
|
||||||
language code to property name, e.g. ``{"en": "Date of birth"}``."""
|
|
||||||
WikiDataPropertiesType = dict[str, WikiDataPropertyNameType]
|
|
||||||
"""Dictionary from wikidata property ID to property name."""
|
|
||||||
|
|
||||||
|
|
||||||
class LocalesType(t.TypedDict):
|
class LocalesType(t.TypedDict):
|
||||||
"""Data structure of an item in ``locales.json``"""
|
"""Data structure of an item in ``locales.json``"""
|
||||||
|
|
||||||
@@ -48,7 +41,6 @@ class LocalesType(t.TypedDict):
|
|||||||
|
|
||||||
USER_AGENTS: UserAgentType
|
USER_AGENTS: UserAgentType
|
||||||
WIKIDATA_UNITS: dict[str, WikiDataUnitType]
|
WIKIDATA_UNITS: dict[str, WikiDataUnitType]
|
||||||
WIKIDATA_PROPERTIES: WikiDataPropertiesType
|
|
||||||
TRACKER_PATTERNS: TrackerPatternsDB
|
TRACKER_PATTERNS: TrackerPatternsDB
|
||||||
LOCALES: LocalesType
|
LOCALES: LocalesType
|
||||||
CURRENCIES: CurrenciesDB
|
CURRENCIES: CurrenciesDB
|
||||||
@@ -60,25 +52,24 @@ ENGINE_DESCRIPTIONS: dict[str, dict[str, t.Any]]
|
|||||||
ENGINE_TRAITS: dict[str, dict[str, t.Any]]
|
ENGINE_TRAITS: dict[str, dict[str, t.Any]]
|
||||||
|
|
||||||
|
|
||||||
lazy_globals: dict[str, t.Any] = {
|
lazy_globals = {
|
||||||
"CURRENCIES": CurrenciesDB(),
|
"CURRENCIES": CurrenciesDB(),
|
||||||
"USER_AGENTS": None,
|
"USER_AGENTS": None,
|
||||||
"EXTERNAL_URLS": None,
|
"EXTERNAL_URLS": None,
|
||||||
"WIKIDATA_UNITS": None,
|
"WIKIDATA_UNITS": None,
|
||||||
"WIKIDATA_PROPERTIES": None,
|
|
||||||
"EXTERNAL_BANGS": None,
|
"EXTERNAL_BANGS": None,
|
||||||
"OSM_KEYS_TAGS": None,
|
"OSM_KEYS_TAGS": None,
|
||||||
"ENGINE_DESCRIPTIONS": None,
|
"ENGINE_DESCRIPTIONS": None,
|
||||||
"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 = {
|
||||||
"USER_AGENTS": "useragents.json",
|
"USER_AGENTS": "useragents.json",
|
||||||
"EXTERNAL_URLS": "external_urls.json",
|
"EXTERNAL_URLS": "external_urls.json",
|
||||||
"WIKIDATA_UNITS": "wikidata_units.json",
|
"WIKIDATA_UNITS": "wikidata_units.json",
|
||||||
"WIKIDATA_PROPERTIES": "wikidata_properties.json",
|
|
||||||
"EXTERNAL_BANGS": "external_bangs.json",
|
"EXTERNAL_BANGS": "external_bangs.json",
|
||||||
"OSM_KEYS_TAGS": "osm_keys_tags.json",
|
"OSM_KEYS_TAGS": "osm_keys_tags.json",
|
||||||
"ENGINE_DESCRIPTIONS": "engine_descriptions.json",
|
"ENGINE_DESCRIPTIONS": "engine_descriptions.json",
|
||||||
@@ -115,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
@@ -288,7 +288,7 @@
|
|||||||
"oc": "Kwanza",
|
"oc": "Kwanza",
|
||||||
"pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ",
|
"pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ",
|
||||||
"pl": "Kwanza",
|
"pl": "Kwanza",
|
||||||
"pt": "kwanza",
|
"pt": "Kwanza",
|
||||||
"ru": "ангольская кванза",
|
"ru": "ангольская кванза",
|
||||||
"si": "ක්වන්සා",
|
"si": "ක්වන්සා",
|
||||||
"sr": "анголска кванза",
|
"sr": "анголска кванза",
|
||||||
@@ -334,7 +334,6 @@
|
|||||||
"ro": "Peso argentinian",
|
"ro": "Peso argentinian",
|
||||||
"ru": "аргентинское песо",
|
"ru": "аргентинское песо",
|
||||||
"sk": "Argentinské peso",
|
"sk": "Argentinské peso",
|
||||||
"sl": "argentinski peso",
|
|
||||||
"sr": "аргентински пезос",
|
"sr": "аргентински пезос",
|
||||||
"sv": "Argentinsk peso",
|
"sv": "Argentinsk peso",
|
||||||
"ta": "ஆர்ஜென்டின பீசோ",
|
"ta": "ஆர்ஜென்டின பீசோ",
|
||||||
@@ -774,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": "боливиано",
|
||||||
@@ -1108,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",
|
||||||
@@ -1637,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",
|
||||||
@@ -1652,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",
|
||||||
@@ -1787,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",
|
||||||
@@ -1804,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",
|
||||||
@@ -2003,7 +2002,7 @@
|
|||||||
"eo": "ganaa cedio",
|
"eo": "ganaa cedio",
|
||||||
"es": "cedi",
|
"es": "cedi",
|
||||||
"fi": "Cedi",
|
"fi": "Cedi",
|
||||||
"fr": "cedi",
|
"fr": "Cedi",
|
||||||
"ga": "cedi",
|
"ga": "cedi",
|
||||||
"gl": "Cedi",
|
"gl": "Cedi",
|
||||||
"he": "סדי גאני",
|
"he": "סדי גאני",
|
||||||
@@ -2760,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",
|
||||||
@@ -2953,7 +2952,7 @@
|
|||||||
"pap": "won nortkoreano",
|
"pap": "won nortkoreano",
|
||||||
"pl": "won północnokoreański",
|
"pl": "won północnokoreański",
|
||||||
"pt": "won norte-coreano",
|
"pt": "won norte-coreano",
|
||||||
"ro": "won nord-coreean",
|
"ro": "Won nord-coreean",
|
||||||
"ru": "вона КНДР",
|
"ru": "вона КНДР",
|
||||||
"sk": "severokorejsky won",
|
"sk": "severokorejsky won",
|
||||||
"sl": "severnokorejski von",
|
"sl": "severnokorejski von",
|
||||||
@@ -3095,7 +3094,6 @@
|
|||||||
"ca": "tenge",
|
"ca": "tenge",
|
||||||
"cs": "Tenge",
|
"cs": "Tenge",
|
||||||
"cy": "tenge Casachstan",
|
"cy": "tenge Casachstan",
|
||||||
"da": "Tenge",
|
|
||||||
"de": "Tenge",
|
"de": "Tenge",
|
||||||
"en": "Kazakhstani tenge",
|
"en": "Kazakhstani tenge",
|
||||||
"eo": "kazaĥa tengo",
|
"eo": "kazaĥa tengo",
|
||||||
@@ -3339,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",
|
||||||
@@ -3542,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",
|
||||||
@@ -4314,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",
|
||||||
@@ -4618,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",
|
||||||
@@ -4836,7 +4832,6 @@
|
|||||||
"nl": "Seychelse roepie",
|
"nl": "Seychelse roepie",
|
||||||
"pl": "Rupia seszelska",
|
"pl": "Rupia seszelska",
|
||||||
"pt": "rupia das Seicheles",
|
"pt": "rupia das Seicheles",
|
||||||
"ro": "rupie seychelloză",
|
|
||||||
"ru": "сейшельская рупия",
|
"ru": "сейшельская рупия",
|
||||||
"sk": "Seychelská rupia",
|
"sk": "Seychelská rupia",
|
||||||
"sl": "sejšelska rupija",
|
"sl": "sejšelska rupija",
|
||||||
@@ -5067,7 +5062,6 @@
|
|||||||
"nl": "Somalische shilling",
|
"nl": "Somalische shilling",
|
||||||
"pl": "Szyling somalijski",
|
"pl": "Szyling somalijski",
|
||||||
"pt": "xelim somaliano",
|
"pt": "xelim somaliano",
|
||||||
"ro": "șiling somalez",
|
|
||||||
"ru": "сомалийский шиллинг",
|
"ru": "сомалийский шиллинг",
|
||||||
"sk": "Somálsky šiling",
|
"sk": "Somálsky šiling",
|
||||||
"sl": "somalski šiling",
|
"sl": "somalski šiling",
|
||||||
@@ -5337,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",
|
||||||
@@ -5400,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",
|
||||||
@@ -5773,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",
|
||||||
@@ -5887,7 +5880,6 @@
|
|||||||
"ja": "ドン",
|
"ja": "ドン",
|
||||||
"ko": "베트남 동",
|
"ko": "베트남 동",
|
||||||
"lt": "Vietnamo dongas",
|
"lt": "Vietnamo dongas",
|
||||||
"ms": "Dồng Vietnam",
|
|
||||||
"nl": "Vietnamese dong",
|
"nl": "Vietnamese dong",
|
||||||
"oc": "Dong",
|
"oc": "Dong",
|
||||||
"pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ",
|
"pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ",
|
||||||
@@ -6127,8 +6119,7 @@
|
|||||||
"ro": "Gulden caraibian",
|
"ro": "Gulden caraibian",
|
||||||
"ru": "Карибский гульден",
|
"ru": "Карибский гульден",
|
||||||
"sk": "Karibský gulden",
|
"sk": "Karibský gulden",
|
||||||
"sl": "karibski goldinar",
|
"sl": "karibski goldinar"
|
||||||
"sv": "Karibisk gulden"
|
|
||||||
},
|
},
|
||||||
"XDR": {
|
"XDR": {
|
||||||
"ar": "حقوق السحب الخاصة",
|
"ar": "حقوق السحب الخاصة",
|
||||||
@@ -6156,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",
|
||||||
@@ -6234,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",
|
||||||
@@ -6730,8 +6720,6 @@
|
|||||||
"antilliaanse gulden": "ANG",
|
"antilliaanse gulden": "ANG",
|
||||||
"antilski gulden": "ANG",
|
"antilski gulden": "ANG",
|
||||||
"aoa": "AOA",
|
"aoa": "AOA",
|
||||||
"apvienotās karalistes ekonomika": "GBP",
|
|
||||||
"apvienotās karalistes saimniecība": "GBP",
|
|
||||||
"apvienotās karalistes sterliņu mārciņa": "GBP",
|
"apvienotās karalistes sterliņu mārciņa": "GBP",
|
||||||
"ar": "MGA",
|
"ar": "MGA",
|
||||||
"arab accounting dinar": "XAD",
|
"arab accounting dinar": "XAD",
|
||||||
@@ -6844,7 +6832,6 @@
|
|||||||
"avustralya doları": "AUD",
|
"avustralya doları": "AUD",
|
||||||
"awg": "AWG",
|
"awg": "AWG",
|
||||||
"az arany mint befektetés": "XAU",
|
"az arany mint befektetés": "XAU",
|
||||||
"az egyesült királyság gazdasága": "GBP",
|
|
||||||
"azerbaidžanin manat": "AZN",
|
"azerbaidžanin manat": "AZN",
|
||||||
"azerbaidžano manatas": "AZN",
|
"azerbaidžano manatas": "AZN",
|
||||||
"azerbaidžānas manats": "AZN",
|
"azerbaidžānas manats": "AZN",
|
||||||
@@ -7028,7 +7015,6 @@
|
|||||||
"bir etíope": "ETB",
|
"bir etíope": "ETB",
|
||||||
"biras": "ETB",
|
"biras": "ETB",
|
||||||
"birleşik arap emirlikleri dirhemi": "AED",
|
"birleşik arap emirlikleri dirhemi": "AED",
|
||||||
"birleşik krallık ekonomisi": "GBP",
|
|
||||||
"birma kjato": "MMK",
|
"birma kjato": "MMK",
|
||||||
"birr": "ETB",
|
"birr": "ETB",
|
||||||
"birr da etiópia": "ETB",
|
"birr da etiópia": "ETB",
|
||||||
@@ -7069,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",
|
||||||
@@ -7116,19 +7101,15 @@
|
|||||||
"brit font": "GBP",
|
"brit font": "GBP",
|
||||||
"brita pundo": "GBP",
|
"brita pundo": "GBP",
|
||||||
"britaj pundoj": "GBP",
|
"britaj pundoj": "GBP",
|
||||||
"britannian talous": "GBP",
|
|
||||||
"britanska funta": "GBP",
|
"britanska funta": "GBP",
|
||||||
"britanski funt": "GBP",
|
"britanski funt": "GBP",
|
||||||
"britische wirtschaft": "GBP",
|
|
||||||
"britisches pfund": "GBP",
|
"britisches pfund": "GBP",
|
||||||
"british economy": "GBP",
|
|
||||||
"british pound": "GBP",
|
"british pound": "GBP",
|
||||||
"britisk pund": "GBP",
|
"britisk pund": "GBP",
|
||||||
"britiske pund": "GBP",
|
"britiske pund": "GBP",
|
||||||
"brits pond": "GBP",
|
"brits pond": "GBP",
|
||||||
"britse pond": "GBP",
|
"britse pond": "GBP",
|
||||||
"britská libra": "GBP",
|
"britská libra": "GBP",
|
||||||
"brittisk ekonomi": "GBP",
|
|
||||||
"brittiska pund": "GBP",
|
"brittiska pund": "GBP",
|
||||||
"brittiskt pund": "GBP",
|
"brittiskt pund": "GBP",
|
||||||
"brunei doları": "BND",
|
"brunei doları": "BND",
|
||||||
@@ -7212,7 +7193,6 @@
|
|||||||
"cedi du ghana": "GHS",
|
"cedi du ghana": "GHS",
|
||||||
"cedi ghana": "GHS",
|
"cedi ghana": "GHS",
|
||||||
"cedi ghanese": "GHS",
|
"cedi ghanese": "GHS",
|
||||||
"cedi ghanéen": "GHS",
|
|
||||||
"centr afrika franko": "XAF",
|
"centr afrika franko": "XAF",
|
||||||
"central african cfa franc": "XAF",
|
"central african cfa franc": "XAF",
|
||||||
"centralafrikansk cfa franc": "XAF",
|
"centralafrikansk cfa franc": "XAF",
|
||||||
@@ -7315,6 +7295,7 @@
|
|||||||
"colón costa ricense": "CRC",
|
"colón costa ricense": "CRC",
|
||||||
"colón costa riquenho": "CRC",
|
"colón costa riquenho": "CRC",
|
||||||
"colón costa riquense": "CRC",
|
"colón costa riquense": "CRC",
|
||||||
|
"colón costa riqueny": "CRC",
|
||||||
"colón costaricain": "CRC",
|
"colón costaricain": "CRC",
|
||||||
"colón costaricano": "CRC",
|
"colón costaricano": "CRC",
|
||||||
"colón costaricien": "CRC",
|
"colón costaricien": "CRC",
|
||||||
@@ -8427,7 +8408,6 @@
|
|||||||
"dólares canadenses": "CAD",
|
"dólares canadenses": "CAD",
|
||||||
"dólares estadounidenses": "USD",
|
"dólares estadounidenses": "USD",
|
||||||
"dólares neozelandeses": "NZD",
|
"dólares neozelandeses": "NZD",
|
||||||
"dồng vietnam": "VND",
|
|
||||||
"dram": "AMD",
|
"dram": "AMD",
|
||||||
"dram armean": "AMD",
|
"dram armean": "AMD",
|
||||||
"dram armenia": "AMD",
|
"dram armenia": "AMD",
|
||||||
@@ -8445,13 +8425,11 @@
|
|||||||
"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",
|
||||||
"droits de tirage spéciaux": "XDR",
|
"droits de tirage spéciaux": "XDR",
|
||||||
"dschibuti franc": "DJF",
|
"dschibuti franc": "DJF",
|
||||||
"dvn": "VND",
|
|
||||||
"dzd": "DZD",
|
"dzd": "DZD",
|
||||||
"dzsibuti frank": "DJF",
|
"dzsibuti frank": "DJF",
|
||||||
"džibučio frankas": "DJF",
|
"džibučio frankas": "DJF",
|
||||||
@@ -8465,21 +8443,6 @@
|
|||||||
"eastern caribbean currency union": "XCD",
|
"eastern caribbean currency union": "XCD",
|
||||||
"eastern caribbean dollar": "XCD",
|
"eastern caribbean dollar": "XCD",
|
||||||
"ec$": "XCD",
|
"ec$": "XCD",
|
||||||
"economi'r deyrnas unedig": "GBP",
|
|
||||||
"economia": "GBP",
|
|
||||||
"economia del regne unit": "GBP",
|
|
||||||
"economia del regno unito": "GBP",
|
|
||||||
"economia del reialme unit": "GBP",
|
|
||||||
"economia del reino unido": "GBP",
|
|
||||||
"economia do reino unido": "GBP",
|
|
||||||
"economia regatului unit": "GBP",
|
|
||||||
"economie du royaume uni": "GBP",
|
|
||||||
"economie van het verenigd koninkrijk": "GBP",
|
|
||||||
"economía del reino unido": "GBP",
|
|
||||||
"economía do reino unido": "GBP",
|
|
||||||
"economy": "GBP",
|
|
||||||
"economy of the uk": "GBP",
|
|
||||||
"economy of the united kingdom": "GBP",
|
|
||||||
"egipatska funta": "EGP",
|
"egipatska funta": "EGP",
|
||||||
"egipta pundo": "EGP",
|
"egipta pundo": "EGP",
|
||||||
"egipto svaras": "EGP",
|
"egipto svaras": "EGP",
|
||||||
@@ -8499,12 +8462,6 @@
|
|||||||
"einr": "INR",
|
"einr": "INR",
|
||||||
"eiro": "EUR",
|
"eiro": "EUR",
|
||||||
"ekialdeko karibeko dolar": "XCD",
|
"ekialdeko karibeko dolar": "XCD",
|
||||||
"ekonomi britania raya": "GBP",
|
|
||||||
"ekonomi united kingdom": "GBP",
|
|
||||||
"ekonomie van die verenigde koninkryk": "GBP",
|
|
||||||
"ekonomika spojeného království": "GBP",
|
|
||||||
"ekonomika v spojenom kráľovstve": "GBP",
|
|
||||||
"ekonomio de britujo": "GBP",
|
|
||||||
"el peso": "GTQ",
|
"el peso": "GTQ",
|
||||||
"emalangeni": "SZL",
|
"emalangeni": "SZL",
|
||||||
"emas sebagai pelaburan": "XAU",
|
"emas sebagai pelaburan": "XAU",
|
||||||
@@ -8536,7 +8493,6 @@
|
|||||||
"ermenistan dramı": "AMD",
|
"ermenistan dramı": "AMD",
|
||||||
"ern": "ERN",
|
"ern": "ERN",
|
||||||
"erreal brasildar": "BRL",
|
"erreal brasildar": "BRL",
|
||||||
"erresuma batuko ekonomia": "GBP",
|
|
||||||
"errublo": "RUB",
|
"errublo": "RUB",
|
||||||
"errublo errusiar": "RUB",
|
"errublo errusiar": "RUB",
|
||||||
"errupia indiar": "INR",
|
"errupia indiar": "INR",
|
||||||
@@ -8607,8 +8563,6 @@
|
|||||||
"eyrir": "ISK",
|
"eyrir": "ISK",
|
||||||
"e£": "EGP",
|
"e£": "EGP",
|
||||||
"èuro": "EUR",
|
"èuro": "EUR",
|
||||||
"économie britannique": "GBP",
|
|
||||||
"économie du royaume uni": "GBP",
|
|
||||||
"észak ír font": "GBP",
|
"észak ír font": "GBP",
|
||||||
"észak koreai von": "KPW",
|
"észak koreai von": "KPW",
|
||||||
"e₹": "INR",
|
"e₹": "INR",
|
||||||
@@ -8742,9 +8696,6 @@
|
|||||||
"forintti": "HUF",
|
"forintti": "HUF",
|
||||||
"forinți": "HUF",
|
"forinți": "HUF",
|
||||||
"fòrint": "HUF",
|
"fòrint": "HUF",
|
||||||
"förenade konungariket storbritannien och irlands ekonomi": "GBP",
|
|
||||||
"förenade konungariket storbritannien och nordirlands ekonomi": "GBP",
|
|
||||||
"förenade kungarikets ekonomi": "GBP",
|
|
||||||
"franak cfp": "XPF",
|
"franak cfp": "XPF",
|
||||||
"franc": [
|
"franc": [
|
||||||
"XPF",
|
"XPF",
|
||||||
@@ -8997,9 +8948,6 @@
|
|||||||
"gold als kapitalanlage": "XAU",
|
"gold als kapitalanlage": "XAU",
|
||||||
"gold as an investment": "XAU",
|
"gold as an investment": "XAU",
|
||||||
"gold as currency": "XAU",
|
"gold as currency": "XAU",
|
||||||
"gospodarka wielkiej brytanii": "GBP",
|
|
||||||
"gospodarstvo ujedinjenog kraljevstva": "GBP",
|
|
||||||
"gospodarstvo združenega kraljestva": "GBP",
|
|
||||||
"gourde": "HTG",
|
"gourde": "HTG",
|
||||||
"gourde haiti": "HTG",
|
"gourde haiti": "HTG",
|
||||||
"gourde haitiano": "HTG",
|
"gourde haitiano": "HTG",
|
||||||
@@ -9419,7 +9367,6 @@
|
|||||||
"juaņs": "CNY",
|
"juaņs": "CNY",
|
||||||
"juhokoréjsky won": "KRW",
|
"juhokoréjsky won": "KRW",
|
||||||
"juhosudánska libra": "SSP",
|
"juhosudánska libra": "SSP",
|
||||||
"jungtinės karalystės ekonomika": "GBP",
|
|
||||||
"jungtinių arabų emyratų dirhamas": "AED",
|
"jungtinių arabų emyratų dirhamas": "AED",
|
||||||
"jungtinių valstijų doleris": "USD",
|
"jungtinių valstijų doleris": "USD",
|
||||||
"južnoafrički rand": "ZAR",
|
"južnoafrički rand": "ZAR",
|
||||||
@@ -9484,7 +9431,6 @@
|
|||||||
"karibi forint": "XCG",
|
"karibi forint": "XCG",
|
||||||
"karibia guldeno": "XCG",
|
"karibia guldeno": "XCG",
|
||||||
"karibischer gulden": "XCG",
|
"karibischer gulden": "XCG",
|
||||||
"karibisk gulden": "XCG",
|
|
||||||
"karibski goldinar": "XCG",
|
"karibski goldinar": "XCG",
|
||||||
"karibský gulden": "XCG",
|
"karibský gulden": "XCG",
|
||||||
"karipski gulden": "XCG",
|
"karipski gulden": "XCG",
|
||||||
@@ -9545,9 +9491,6 @@
|
|||||||
"kina papua nugini": "PGK",
|
"kina papua nugini": "PGK",
|
||||||
"kina papuana": "PGK",
|
"kina papuana": "PGK",
|
||||||
"kina papuásia": "PGK",
|
"kina papuásia": "PGK",
|
||||||
"kinh tế anh": "GBP",
|
|
||||||
"kinh tế vương quốc anh": "GBP",
|
|
||||||
"kinh tế vương quốc liên hiệp anh và bắc ireland": "GBP",
|
|
||||||
"kip": "LAK",
|
"kip": "LAK",
|
||||||
"kip laos": "LAK",
|
"kip laos": "LAK",
|
||||||
"kip laosiano": "LAK",
|
"kip laosiano": "LAK",
|
||||||
@@ -9575,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",
|
||||||
@@ -10082,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",
|
||||||
@@ -11192,7 +11132,6 @@
|
|||||||
"põhja korea won": "KPW",
|
"põhja korea won": "KPW",
|
||||||
"põhja makedoonia denaar": "MKD",
|
"põhja makedoonia denaar": "MKD",
|
||||||
"prata como investimento": "XAG",
|
"prata como investimento": "XAG",
|
||||||
"produits agricole de l'angleterre": "GBP",
|
|
||||||
"pula": "BWP",
|
"pula": "BWP",
|
||||||
"pula botswana": "BWP",
|
"pula botswana": "BWP",
|
||||||
"pula botswanais": "BWP",
|
"pula botswanais": "BWP",
|
||||||
@@ -11243,7 +11182,6 @@
|
|||||||
"qatarisk rial": "QAR",
|
"qatarisk rial": "QAR",
|
||||||
"qäpik": "AZN",
|
"qäpik": "AZN",
|
||||||
"qindarka": "ALL",
|
"qindarka": "ALL",
|
||||||
"quanza": "AOA",
|
|
||||||
"quetzal": "GTQ",
|
"quetzal": "GTQ",
|
||||||
"quetzal guatemala": "GTQ",
|
"quetzal guatemala": "GTQ",
|
||||||
"quetzal guatemalteco": "GTQ",
|
"quetzal guatemalteco": "GTQ",
|
||||||
@@ -11375,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",
|
||||||
@@ -11569,7 +11508,6 @@
|
|||||||
"rupia del pakistan": "PKR",
|
"rupia del pakistan": "PKR",
|
||||||
"rupia dell'india": "INR",
|
"rupia dell'india": "INR",
|
||||||
"rupia delle seychelles": "SCR",
|
"rupia delle seychelles": "SCR",
|
||||||
"rupia din seychelles": "SCR",
|
|
||||||
"rupia do nepal": "NPR",
|
"rupia do nepal": "NPR",
|
||||||
"rupia do paquistão": "PKR",
|
"rupia do paquistão": "PKR",
|
||||||
"rupia do seri lanca": "LKR",
|
"rupia do seri lanca": "LKR",
|
||||||
@@ -11625,7 +11563,6 @@
|
|||||||
],
|
],
|
||||||
"rupie indiană": "INR",
|
"rupie indiană": "INR",
|
||||||
"rupie indiane": "INR",
|
"rupie indiane": "INR",
|
||||||
"rupie seychelloză": "SCR",
|
|
||||||
"rupies índies": "INR",
|
"rupies índies": "INR",
|
||||||
"rupija": [
|
"rupija": [
|
||||||
"NPR",
|
"NPR",
|
||||||
@@ -12055,10 +11992,6 @@
|
|||||||
"sterliņu mārciņa": "GBP",
|
"sterliņu mārciņa": "GBP",
|
||||||
"stērliņu mārciņa": "GBP",
|
"stērliņu mārciņa": "GBP",
|
||||||
"stn": "STN",
|
"stn": "STN",
|
||||||
"storbritannien och irlands ekonomi": "GBP",
|
|
||||||
"storbritannien och nordirlands ekonomi": "GBP",
|
|
||||||
"storbritanniens ekonomi": "GBP",
|
|
||||||
"storbritanniens økonomi": "GBP",
|
|
||||||
"stredoafrický frank": "XAF",
|
"stredoafrický frank": "XAF",
|
||||||
"středoafrický frank": "XAF",
|
"středoafrický frank": "XAF",
|
||||||
"sucre": "XSU",
|
"sucre": "XSU",
|
||||||
@@ -12108,7 +12041,6 @@
|
|||||||
"suriye lirası": "SYP",
|
"suriye lirası": "SYP",
|
||||||
"suudi arabistan riyali": "SAR",
|
"suudi arabistan riyali": "SAR",
|
||||||
"suudi riyali": "SAR",
|
"suudi riyali": "SAR",
|
||||||
"suurbritannia majandus": "GBP",
|
|
||||||
"suurbritannia nael": "GBP",
|
"suurbritannia nael": "GBP",
|
||||||
"suurbritannia naelsterling": "GBP",
|
"suurbritannia naelsterling": "GBP",
|
||||||
"suvereni bolivar": "VES",
|
"suvereni bolivar": "VES",
|
||||||
@@ -12215,7 +12147,6 @@
|
|||||||
"švicarski frank": "CHF",
|
"švicarski frank": "CHF",
|
||||||
"švýcarský frank": "CHF",
|
"švýcarský frank": "CHF",
|
||||||
"șekel nou": "ILS",
|
"șekel nou": "ILS",
|
||||||
"șiling somalez": "SOS",
|
|
||||||
"şekel": "ILS",
|
"şekel": "ILS",
|
||||||
"şili pesosu": "CLP",
|
"şili pesosu": "CLP",
|
||||||
"s₣": "CHF",
|
"s₣": "CHF",
|
||||||
@@ -12326,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",
|
||||||
@@ -12558,8 +12488,6 @@
|
|||||||
"uguiya": "MRU",
|
"uguiya": "MRU",
|
||||||
"ugx": "UGX",
|
"ugx": "UGX",
|
||||||
"ui": "UYI",
|
"ui": "UYI",
|
||||||
"uk economy": "GBP",
|
|
||||||
"uk's economy": "GBP",
|
|
||||||
"ukl": "GBP",
|
"ukl": "GBP",
|
||||||
"ukraina grivna": "UAH",
|
"ukraina grivna": "UAH",
|
||||||
"ukraina hrivno": "UAH",
|
"ukraina hrivno": "UAH",
|
||||||
@@ -12600,8 +12528,6 @@
|
|||||||
"unidades de inversion": "MXV",
|
"unidades de inversion": "MXV",
|
||||||
"unidades de inversión": "MXV",
|
"unidades de inversión": "MXV",
|
||||||
"united arab emirates dirham": "AED",
|
"united arab emirates dirham": "AED",
|
||||||
"united kingdom economy": "GBP",
|
|
||||||
"united kingdom's economy": "GBP",
|
|
||||||
"united states dollar": [
|
"united states dollar": [
|
||||||
"USN",
|
"USN",
|
||||||
"USD"
|
"USD"
|
||||||
@@ -12703,7 +12629,6 @@
|
|||||||
"venemaa rubla": "RUB",
|
"venemaa rubla": "RUB",
|
||||||
"venezuelai bolívar": "VES",
|
"venezuelai bolívar": "VES",
|
||||||
"venezuelan digital bolívar": "VED",
|
"venezuelan digital bolívar": "VED",
|
||||||
"verenigd koninkrijk economie": "GBP",
|
|
||||||
"verenigde arabiese emirate dirham": "AED",
|
"verenigde arabiese emirate dirham": "AED",
|
||||||
"verenigde arabische emiraten dirham": "AED",
|
"verenigde arabische emiraten dirham": "AED",
|
||||||
"ves": "VES",
|
"ves": "VES",
|
||||||
@@ -12735,12 +12660,6 @@
|
|||||||
"wir euro": "CHE",
|
"wir euro": "CHE",
|
||||||
"wir franc": "CHW",
|
"wir franc": "CHW",
|
||||||
"wir franken": "CHW",
|
"wir franken": "CHW",
|
||||||
"wirtschaft": "GBP",
|
|
||||||
"wirtschaft des vereinigten königreichs": "GBP",
|
|
||||||
"wirtschaft im vereinigten königreich": "GBP",
|
|
||||||
"wirtschaft in dem vereinigten königreich": "GBP",
|
|
||||||
"wirtschaft vom vereinigten königreich": "GBP",
|
|
||||||
"wirtschaft von dem vereinigten königreich": "GBP",
|
|
||||||
"wit russische roebel": "BYN",
|
"wit russische roebel": "BYN",
|
||||||
"won": "KRW",
|
"won": "KRW",
|
||||||
"won bắc triều tiên": "KPW",
|
"won bắc triều tiên": "KPW",
|
||||||
@@ -12821,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",
|
||||||
@@ -12834,7 +12752,6 @@
|
|||||||
"yeşil burun adaları eskudosu": "CVE",
|
"yeşil burun adaları eskudosu": "CVE",
|
||||||
"yên nhật": "JPY",
|
"yên nhật": "JPY",
|
||||||
"yhdistyneen kuningaskunnan punta": "GBP",
|
"yhdistyneen kuningaskunnan punta": "GBP",
|
||||||
"yhdistyneen kuningaskunnan talous": "GBP",
|
|
||||||
"yhdistyneiden arabiemiraattien dirhami": "AED",
|
"yhdistyneiden arabiemiraattien dirhami": "AED",
|
||||||
"yhdysvaltain dollari": "USD",
|
"yhdysvaltain dollari": "USD",
|
||||||
"ytl": "TRY",
|
"ytl": "TRY",
|
||||||
@@ -12889,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",
|
||||||
@@ -13047,7 +12965,6 @@
|
|||||||
"FKP",
|
"FKP",
|
||||||
"EGP"
|
"EGP"
|
||||||
],
|
],
|
||||||
"£S": "SYP",
|
|
||||||
"£e": "EGP",
|
"£e": "EGP",
|
||||||
"£s": "SYP",
|
"£s": "SYP",
|
||||||
"¥": [
|
"¥": [
|
||||||
@@ -13584,8 +13501,6 @@
|
|||||||
"египетский фунт": "EGP",
|
"египетский фунт": "EGP",
|
||||||
"единая система региональных взаиморасчётов": "XSU",
|
"единая система региональных взаиморасчётов": "XSU",
|
||||||
"единая система региональных взаиморасчетов": "XSU",
|
"единая система региональных взаиморасчетов": "XSU",
|
||||||
"економіка великобританії": "GBP",
|
|
||||||
"економіка великої британії": "GBP",
|
|
||||||
"енглеска фунта": "GBP",
|
"енглеска фунта": "GBP",
|
||||||
"еритрейська накфа": "ERN",
|
"еритрейська накфа": "ERN",
|
||||||
"еритрејска накфа": "ERN",
|
"еритрејска накфа": "ERN",
|
||||||
@@ -13642,8 +13557,6 @@
|
|||||||
"израелски шекел": "ILS",
|
"израелски шекел": "ILS",
|
||||||
"израильский новый шекель": "ILS",
|
"израильский новый шекель": "ILS",
|
||||||
"източнокарибски долар": "XCD",
|
"източнокарибски долар": "XCD",
|
||||||
"икономика на великобритания": "GBP",
|
|
||||||
"икономика на обединеното кралство": "GBP",
|
|
||||||
"индийска рупия": "INR",
|
"индийска рупия": "INR",
|
||||||
"индийская рупия": "INR",
|
"индийская рупия": "INR",
|
||||||
"индијска рупија": "INR",
|
"индијска рупија": "INR",
|
||||||
@@ -14057,7 +13970,6 @@
|
|||||||
"PLZ",
|
"PLZ",
|
||||||
"PLN"
|
"PLN"
|
||||||
],
|
],
|
||||||
"привреда уједињеног краљевства": "GBP",
|
|
||||||
"пула": "BWP",
|
"пула": "BWP",
|
||||||
"південно африканський ранд": "ZAR",
|
"південно африканський ранд": "ZAR",
|
||||||
"південнокорейська вона": "KRW",
|
"південнокорейська вона": "KRW",
|
||||||
@@ -14145,7 +14057,6 @@
|
|||||||
"севернокорејски вон": "KPW",
|
"севернокорејски вон": "KPW",
|
||||||
"северо корейская вона": "KPW",
|
"северо корейская вона": "KPW",
|
||||||
"северокорейская вона": "KPW",
|
"северокорейская вона": "KPW",
|
||||||
"седі": "GHS",
|
|
||||||
"сейшел рупиясе": "SCR",
|
"сейшел рупиясе": "SCR",
|
||||||
"сейшелска рупия": "SCR",
|
"сейшелска рупия": "SCR",
|
||||||
"сейшельская рупия": "SCR",
|
"сейшельская рупия": "SCR",
|
||||||
@@ -14198,8 +14109,6 @@
|
|||||||
"старый румынский лей": "RON",
|
"старый румынский лей": "RON",
|
||||||
"стерлинг фунты": "GBP",
|
"стерлинг фунты": "GBP",
|
||||||
"стерлиң фунты": "GBP",
|
"стерлиң фунты": "GBP",
|
||||||
"стопанство на великобритания": "GBP",
|
|
||||||
"стопанство на обединеното кралство": "GBP",
|
|
||||||
"суверен боливар": "VES",
|
"суверен боливар": "VES",
|
||||||
"суверенний болівар": "VES",
|
"суверенний болівар": "VES",
|
||||||
"суверенный боливар": "VES",
|
"суверенный боливар": "VES",
|
||||||
@@ -14450,7 +14359,6 @@
|
|||||||
"шриланкийска рупия": "LKR",
|
"шриланкийска рупия": "LKR",
|
||||||
"шриланчанска рупија": "LKR",
|
"шриланчанска рупија": "LKR",
|
||||||
"щатски долар": "USD",
|
"щатски долар": "USD",
|
||||||
"экономика великобритании": "GBP",
|
|
||||||
"эритрейская накфа": "ERN",
|
"эритрейская накфа": "ERN",
|
||||||
"эритрея накфасы": "ERN",
|
"эритрея накфасы": "ERN",
|
||||||
"эсватини лилангение": "SZL",
|
"эсватини лилангение": "SZL",
|
||||||
@@ -14600,8 +14508,6 @@
|
|||||||
"יואן סיני": "CNY",
|
"יואן סיני": "CNY",
|
||||||
"ין יפני": "JPY",
|
"ין יפני": "JPY",
|
||||||
"כארתולי לארי": "GEL",
|
"כארתולי לארי": "GEL",
|
||||||
"כלכלת בריטניה": "GBP",
|
|
||||||
"כלכלת הממלכה המאוחדת": "GBP",
|
|
||||||
"כתר דני": "DKK",
|
"כתר דני": "DKK",
|
||||||
"כתר נורבגי": "NOK",
|
"כתר נורבגי": "NOK",
|
||||||
"כתר נורווגי": "NOK",
|
"כתר נורווגי": "NOK",
|
||||||
@@ -14749,7 +14655,6 @@
|
|||||||
"استثمار البلاتين": "XPT",
|
"استثمار البلاتين": "XPT",
|
||||||
"استثمار الذهب": "XAU",
|
"استثمار الذهب": "XAU",
|
||||||
"استثمار الفضة": "XAG",
|
"استثمار الفضة": "XAG",
|
||||||
"اقتصاد المملكة المتحدة": "GBP",
|
|
||||||
"الاستثمار في الذهب": "XAU",
|
"الاستثمار في الذهب": "XAU",
|
||||||
"الأوقية الموريتانية": "MRU",
|
"الأوقية الموريتانية": "MRU",
|
||||||
"البات": "THB",
|
"البات": "THB",
|
||||||
@@ -14803,7 +14708,6 @@
|
|||||||
"أوقية": "MRU",
|
"أوقية": "MRU",
|
||||||
"أوقية موريتانية": "MRU",
|
"أوقية موريتانية": "MRU",
|
||||||
"أوقيه موريتانيه": "MRU",
|
"أوقيه موريتانيه": "MRU",
|
||||||
"إقتصاد بريطانى": "GBP",
|
|
||||||
"إيسكودو جزر الرأس الأخضر": "CVE",
|
"إيسكودو جزر الرأس الأخضر": "CVE",
|
||||||
"بات": "THB",
|
"بات": "THB",
|
||||||
"بات تايلاندي": "THB",
|
"بات تايلاندي": "THB",
|
||||||
@@ -15055,8 +14959,6 @@
|
|||||||
"فورنت مجري": "HUF",
|
"فورنت مجري": "HUF",
|
||||||
"فورينت مجري": "HUF",
|
"فورينت مجري": "HUF",
|
||||||
"فِرَنْكٌ رُوَنْدِيٌّ": "RWF",
|
"فِرَنْكٌ رُوَنْدِيٌّ": "RWF",
|
||||||
"قائمة النقود المعدنية السورية": "SYP",
|
|
||||||
"قائمة عملات سوريا المعدنية": "SYP",
|
|
||||||
"ك": "KWD",
|
"ك": "KWD",
|
||||||
"كتزال غواتيمالي": "GTQ",
|
"كتزال غواتيمالي": "GTQ",
|
||||||
"كرونة آيسلندية": "ISK",
|
"كرونة آيسلندية": "ISK",
|
||||||
@@ -15186,7 +15088,6 @@
|
|||||||
"মালদ্বীপীয় রুফিয়াহ": "MVR",
|
"মালদ্বীপীয় রুফিয়াহ": "MVR",
|
||||||
"মিয়ানমার ক্যত": "MMK",
|
"মিয়ানমার ক্যত": "MMK",
|
||||||
"মিশরীয় পাউন্ড": "EGP",
|
"মিশরীয় পাউন্ড": "EGP",
|
||||||
"যুক্তরাজ্যের অর্থনীতি": "GBP",
|
|
||||||
"রুশ রুবল": "RUB",
|
"রুশ রুবল": "RUB",
|
||||||
"রেনমিনবি": "CNY",
|
"রেনমিনবি": "CNY",
|
||||||
"রেন্মিন্বি": "CNY",
|
"রেন্মিন্বি": "CNY",
|
||||||
@@ -15818,7 +15719,6 @@
|
|||||||
"엔": "JPY",
|
"엔": "JPY",
|
||||||
"엔화": "JPY",
|
"엔화": "JPY",
|
||||||
"영국 파운드": "GBP",
|
"영국 파운드": "GBP",
|
||||||
"영국의 경제": "GBP",
|
|
||||||
"예멘 리알": "YER",
|
"예멘 리알": "YER",
|
||||||
"예멘 리얄": "YER",
|
"예멘 리얄": "YER",
|
||||||
"예멘리얄": "YER",
|
"예멘리얄": "YER",
|
||||||
@@ -16026,11 +15926,9 @@
|
|||||||
"イエメン・リアル": "YER",
|
"イエメン・リアル": "YER",
|
||||||
"イエメン・リヤル": "YER",
|
"イエメン・リヤル": "YER",
|
||||||
"イエメン・リヤール": "YER",
|
"イエメン・リヤール": "YER",
|
||||||
"イギリスの経済": "GBP",
|
|
||||||
"イギリスの通貨": "GBP",
|
"イギリスの通貨": "GBP",
|
||||||
"イギリスポンド": "GBP",
|
"イギリスポンド": "GBP",
|
||||||
"イギリス・ポンド": "GBP",
|
"イギリス・ポンド": "GBP",
|
||||||
"イギリス経済": "GBP",
|
|
||||||
"イラクの通貨": "IQD",
|
"イラクの通貨": "IQD",
|
||||||
"イラク・ディナール": "IQD",
|
"イラク・ディナール": "IQD",
|
||||||
"イランの通貨": "IRR",
|
"イランの通貨": "IRR",
|
||||||
@@ -16332,7 +16230,6 @@
|
|||||||
"英ポンド": "GBP",
|
"英ポンド": "GBP",
|
||||||
"西アフリカcfaフラン": "XOF",
|
"西アフリカcfaフラン": "XOF",
|
||||||
"豪ドル": "AUD",
|
"豪ドル": "AUD",
|
||||||
"財政・経済政策": "GBP",
|
|
||||||
"越南銅": "VND",
|
"越南銅": "VND",
|
||||||
"金投資": "XAU",
|
"金投資": "XAU",
|
||||||
"韓国ウォン": "KRW",
|
"韓国ウォン": "KRW",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4502
searx/data/gsa_useragents.txt
Normal file
4502
searx/data/gsa_useragents.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ import re
|
|||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
||||||
|
|
||||||
from curl_cffi.requests.exceptions import RequestException
|
from httpx import HTTPError
|
||||||
|
|
||||||
from searx.data.core import get_cache, log
|
from searx.data.core import get_cache, log
|
||||||
from searx.network import get as http_get
|
from searx.network import get as http_get
|
||||||
@@ -28,11 +28,11 @@ class TrackerPatternsDB:
|
|||||||
|
|
||||||
ctx_name = "data_tracker_patterns"
|
ctx_name = "data_tracker_patterns"
|
||||||
|
|
||||||
# ClearURL rule lists, the first one that responds HTTP 200 is used
|
|
||||||
CLEAR_LIST_URL = [
|
CLEAR_LIST_URL = [
|
||||||
"https://cdn.jsdelivr.net/gh/clearurls/rules@refs/heads/gh-pages/data.minify.json",
|
# ClearURL rule lists, the first one that responds HTTP 200 is used
|
||||||
"https://rules2.clearurls.xyz/data.minify.json",
|
|
||||||
"https://rules1.clearurls.xyz/data.minify.json",
|
"https://rules1.clearurls.xyz/data.minify.json",
|
||||||
|
"https://rules2.clearurls.xyz/data.minify.json",
|
||||||
|
"https://raw.githubusercontent.com/ClearURLs/Rules/refs/heads/master/data.min.json",
|
||||||
]
|
]
|
||||||
|
|
||||||
class Fields:
|
class Fields:
|
||||||
@@ -87,8 +87,8 @@ class TrackerPatternsDB:
|
|||||||
try:
|
try:
|
||||||
resp = http_get(url, timeout=3)
|
resp = http_get(url, timeout=3)
|
||||||
|
|
||||||
except RequestException as exc:
|
except HTTPError as exc:
|
||||||
log.warning("TRACKER_PATTERNS: RequestException while fetching %s: %s", url, exc)
|
log.warning("TRACKER_PATTERNS: HTTPError (%s) occured while fetching %s", url, exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
],
|
],
|
||||||
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
||||||
"versions": [
|
"versions": [
|
||||||
"154.0",
|
"151.0",
|
||||||
"153.0"
|
"150.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||||
@@ -3474,6 +3474,11 @@
|
|||||||
"symbol": "mm⁻²",
|
"symbol": "mm⁻²",
|
||||||
"to_si_factor": 1e-06
|
"to_si_factor": 1e-06
|
||||||
},
|
},
|
||||||
|
"Q136039973": {
|
||||||
|
"si_name": "Q6137407",
|
||||||
|
"symbol": "FPS",
|
||||||
|
"to_si_factor": 1.0
|
||||||
|
},
|
||||||
"Q1361854": {
|
"Q1361854": {
|
||||||
"si_name": "Q11570",
|
"si_name": "Q11570",
|
||||||
"symbol": "dwt",
|
"symbol": "dwt",
|
||||||
@@ -3516,7 +3521,7 @@
|
|||||||
},
|
},
|
||||||
"Q1377741": {
|
"Q1377741": {
|
||||||
"si_name": "Q25250",
|
"si_name": "Q25250",
|
||||||
"symbol": "V<sub>P</sub>",
|
"symbol": "V_P",
|
||||||
"to_si_factor": 1.0429e+27
|
"to_si_factor": 1.0429e+27
|
||||||
},
|
},
|
||||||
"Q1386162": {
|
"Q1386162": {
|
||||||
@@ -3689,11 +3694,6 @@
|
|||||||
"symbol": "apc",
|
"symbol": "apc",
|
||||||
"to_si_factor": 0.0308568
|
"to_si_factor": 0.0308568
|
||||||
},
|
},
|
||||||
"Q16068": {
|
|
||||||
"si_name": null,
|
|
||||||
"symbol": "DM",
|
|
||||||
"to_si_factor": null
|
|
||||||
},
|
|
||||||
"Q160857": {
|
"Q160857": {
|
||||||
"si_name": "Q25236",
|
"si_name": "Q25236",
|
||||||
"symbol": "hp",
|
"symbol": "hp",
|
||||||
@@ -3872,11 +3872,11 @@
|
|||||||
"Q180892": {
|
"Q180892": {
|
||||||
"si_name": "Q11570",
|
"si_name": "Q11570",
|
||||||
"symbol": "M☉",
|
"symbol": "M☉",
|
||||||
"to_si_factor": 1.988416e+30
|
"to_si_factor": 1.9884e+30
|
||||||
},
|
},
|
||||||
"Q1811": {
|
"Q1811": {
|
||||||
"si_name": "Q11573",
|
"si_name": "Q11573",
|
||||||
"symbol": "au",
|
"symbol": "AU",
|
||||||
"to_si_factor": 149597870700.0
|
"to_si_factor": 149597870700.0
|
||||||
},
|
},
|
||||||
"Q1815100": {
|
"Q1815100": {
|
||||||
@@ -4454,11 +4454,6 @@
|
|||||||
"symbol": "ng",
|
"symbol": "ng",
|
||||||
"to_si_factor": 1e-12
|
"to_si_factor": 1e-12
|
||||||
},
|
},
|
||||||
"Q2285395": {
|
|
||||||
"si_name": null,
|
|
||||||
"symbol": "dBW",
|
|
||||||
"to_si_factor": null
|
|
||||||
},
|
|
||||||
"Q22934083": {
|
"Q22934083": {
|
||||||
"si_name": "Q25406",
|
"si_name": "Q25406",
|
||||||
"symbol": "nC",
|
"symbol": "nC",
|
||||||
@@ -5249,11 +5244,6 @@
|
|||||||
"symbol": "μA",
|
"symbol": "μA",
|
||||||
"to_si_factor": 1e-06
|
"to_si_factor": 1e-06
|
||||||
},
|
},
|
||||||
"Q31274648": {
|
|
||||||
"si_name": "Q6137407",
|
|
||||||
"symbol": "FPS",
|
|
||||||
"to_si_factor": 1.0
|
|
||||||
},
|
|
||||||
"Q3186734": {
|
"Q3186734": {
|
||||||
"si_name": "Q3186734",
|
"si_name": "Q3186734",
|
||||||
"symbol": "J/(m³ K)",
|
"symbol": "J/(m³ K)",
|
||||||
@@ -6326,7 +6316,7 @@
|
|||||||
},
|
},
|
||||||
"Q536785": {
|
"Q536785": {
|
||||||
"si_name": "Q844211",
|
"si_name": "Q844211",
|
||||||
"symbol": "ρ<sub>P</sub>",
|
"symbol": "ρ_P",
|
||||||
"to_si_factor": 5.155e+96
|
"to_si_factor": 5.155e+96
|
||||||
},
|
},
|
||||||
"Q53679433": {
|
"Q53679433": {
|
||||||
@@ -6981,7 +6971,7 @@
|
|||||||
},
|
},
|
||||||
"Q685662": {
|
"Q685662": {
|
||||||
"si_name": "Q44395",
|
"si_name": "Q44395",
|
||||||
"symbol": "p<sub>P</sub>",
|
"symbol": "p_P",
|
||||||
"to_si_factor": 4.633e+113
|
"to_si_factor": 4.633e+113
|
||||||
},
|
},
|
||||||
"Q686163": {
|
"Q686163": {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ ENGINES_CACHE: ExpireCacheSQLite = ExpireCacheSQLite.build_cache(
|
|||||||
ExpireCacheCfg(
|
ExpireCacheCfg(
|
||||||
name="ENGINES_CACHE",
|
name="ENGINES_CACHE",
|
||||||
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
|
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
|
||||||
MAINTENANCE_PERIOD=60 * 60, # 1h
|
MAINTENANCE_PERIOD=60 * 60, # 2h
|
||||||
MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB
|
MAX_VALUE_LEN=1024 * 1024 * 1024, # 1MB
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -305,7 +305,7 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
|
|||||||
|
|
||||||
region: str = ""
|
region: str = ""
|
||||||
"""For an engine, when there is ``region: ...`` in the YAML settings the engine
|
"""For an engine, when there is ``region: ...`` in the YAML settings the engine
|
||||||
does support only this one region:
|
does support only this one region::
|
||||||
|
|
||||||
.. code:: yaml
|
.. code:: yaml
|
||||||
|
|
||||||
@@ -317,9 +317,6 @@ class Engine(abc.ABC): # pylint: disable=too-few-public-methods
|
|||||||
enable_http: bool
|
enable_http: bool
|
||||||
"""Enable HTTP (by default only HTTPS is enabled)."""
|
"""Enable HTTP (by default only HTTPS is enabled)."""
|
||||||
|
|
||||||
enable_http3: bool = False
|
|
||||||
"""Enables the use of HTTP/3 if available"""
|
|
||||||
|
|
||||||
shortcut: str
|
shortcut: str
|
||||||
"""Code used to execute bang requests (``!foo``)"""
|
"""Code used to execute bang requests (``!foo``)"""
|
||||||
|
|
||||||
@@ -364,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
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ from urllib.parse import urlencode
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
from searx.exceptions import SearxEngineAPIException
|
||||||
from searx.result_types import EngineResults
|
from searx.utils import html_to_text, get_embeded_stream_url
|
||||||
from searx.utils import html_to_text
|
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": "https://tv.360kan.com/",
|
"website": "https://tv.360kan.com/",
|
||||||
@@ -30,12 +29,12 @@ def request(query, params):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def response(resp) -> EngineResults:
|
def response(resp):
|
||||||
try:
|
try:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise SearxEngineAPIException(f"Invalid response: {e}") from e
|
raise SearxEngineAPIException(f"Invalid response: {e}") from e
|
||||||
res = EngineResults()
|
results = []
|
||||||
|
|
||||||
if "data" not in data or "result" not in data["data"]:
|
if "data" not in data or "result" not in data["data"]:
|
||||||
raise SearxEngineAPIException("Invalid response")
|
raise SearxEngineAPIException("Invalid response")
|
||||||
@@ -51,15 +50,16 @@ def response(resp) -> EngineResults:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
published_date = None
|
published_date = None
|
||||||
|
|
||||||
res.add(
|
results.append(
|
||||||
res.types.LegacyResult(
|
{
|
||||||
url=entry["play_url"],
|
'url': entry["play_url"],
|
||||||
title=html_to_text(entry["title"]),
|
'title': html_to_text(entry["title"]),
|
||||||
content=html_to_text(entry["description"]),
|
'content': html_to_text(entry["description"]),
|
||||||
template='videos.html',
|
'template': 'videos.html',
|
||||||
publishedDate=published_date,
|
'publishedDate': published_date,
|
||||||
thumbnail=entry["cover_img"],
|
'thumbnail': entry["cover_img"],
|
||||||
)
|
"iframe_src": get_embeded_stream_url(entry["play_url"]),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return res
|
return results
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ fragment SXNG_query on Query {
|
|||||||
|
|
||||||
def setup(_) -> bool:
|
def setup(_) -> bool:
|
||||||
global SXNG_query # pylint: disable=global-statement
|
global SXNG_query # pylint: disable=global-statement
|
||||||
rand_str: str = "".join(random.choices(string.ascii_letters, k=5))
|
rand_str: str = "".join(random.choice(string.ascii_letters) for _ in range(5))
|
||||||
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
|
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ categories: list[str]
|
|||||||
disabled: bool
|
disabled: bool
|
||||||
display_error_messages: bool
|
display_error_messages: bool
|
||||||
enable_http: bool
|
enable_http: bool
|
||||||
enable_http3: bool
|
|
||||||
engine_type: str
|
engine_type: str
|
||||||
inactive: bool
|
inactive: bool
|
||||||
max_page: int
|
max_page: int
|
||||||
|
|||||||
@@ -187,9 +187,8 @@ def set_loggers(engine: "Engine|types.ModuleType", engine_name: str):
|
|||||||
def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]):
|
def update_engine_attributes(engine: "Engine | types.ModuleType", engine_data: dict[str, t.Any]):
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
|
|
||||||
# set / update engine attributes from engine_data
|
# set engine attributes from engine_data
|
||||||
kvargs: dict[str, t.Any]
|
kvargs: dict[str, t.Any]
|
||||||
engine.about = getattr(engine, "about", EngineAbout())
|
|
||||||
if isinstance(engine.about, EngineAbout):
|
if isinstance(engine.about, EngineAbout):
|
||||||
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
|
kvargs = {**msgspec.to_builtins(engine.about), **engine_data.get("about", {})}
|
||||||
else:
|
else:
|
||||||
@@ -270,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
|
||||||
|
|
||||||
|
|
||||||
@@ -318,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Implementation
|
|||||||
==============
|
==============
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ Additional subcategories:
|
|||||||
# Do we need support for "free_collection" and "include_stock_enterprise"?
|
# Do we need support for "free_collection" and "include_stock_enterprise"?
|
||||||
|
|
||||||
|
|
||||||
def setup(_: dict[str, t.Any]) -> bool | None:
|
def init(_):
|
||||||
if not categories:
|
if not categories:
|
||||||
raise ValueError("adobe_stock engine: categories is unset")
|
raise ValueError("adobe_stock engine: categories is unset")
|
||||||
|
|
||||||
@@ -100,9 +100,9 @@ def setup(_: dict[str, t.Any]) -> bool | None:
|
|||||||
raise ValueError("adobe_stock engine: adobe_content_types is unset")
|
raise ValueError("adobe_stock engine: adobe_content_types is unset")
|
||||||
|
|
||||||
if isinstance(adobe_content_types, list):
|
if isinstance(adobe_content_types, list):
|
||||||
for content_type in adobe_content_types:
|
for t in adobe_content_types:
|
||||||
if content_type not in ADOBE_VALID_TYPES:
|
if t not in ADOBE_VALID_TYPES:
|
||||||
raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % content_type)
|
raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % t)
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"adobe_stock engine: adobe_content_types must be a list of strings not %s" % type(adobe_content_types)
|
"adobe_stock engine: adobe_content_types must be a list of strings not %s" % type(adobe_content_types)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,34 +35,11 @@ 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"
|
def init(_):
|
||||||
COOKIE_CACHE_EXPIRATION_SECONDS = 3600
|
|
||||||
|
|
||||||
CACHE: EngineCache
|
|
||||||
"""Stores cookies from Baidu image search warmup."""
|
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
|
||||||
if baidu_category not in ('general', 'images', 'it'):
|
if baidu_category not in ('general', 'images', 'it'):
|
||||||
raise SearxEngineAPIException(f"Unsupported category: {baidu_category}")
|
raise SearxEngineAPIException(f"Unsupported category: {baidu_category}")
|
||||||
|
|
||||||
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 request(query, params):
|
def request(query, params):
|
||||||
page_num = params["pageno"]
|
page_num = params["pageno"]
|
||||||
@@ -115,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
|
||||||
@@ -133,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)
|
||||||
@@ -184,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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ base_url = "https://api.bilibili.com/x/web-interface/search/type"
|
|||||||
|
|
||||||
cookie = {
|
cookie = {
|
||||||
"innersign": "0",
|
"innersign": "0",
|
||||||
"buvid3": "".join(random.choices(string.hexdigits, k=16)) + "infoc",
|
"buvid3": "".join(random.choice(string.hexdigits) for _ in range(16)) + "infoc",
|
||||||
"i-wanna-go-back": "-1",
|
"i-wanna-go-back": "-1",
|
||||||
"b_ut": "7",
|
"b_ut": "7",
|
||||||
"FEED_LIVE_VERSION": "V8",
|
"FEED_LIVE_VERSION": "V8",
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ about: dict[str, t.Any] = {
|
|||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["general", "web"]
|
categories = ["general", "web"]
|
||||||
safesearch = True
|
safesearch = True
|
||||||
enable_http3 = True
|
|
||||||
_safesearch_map: dict[int, str] = {
|
_safesearch_map: dict[int, str] = {
|
||||||
0: "off",
|
0: "off",
|
||||||
1: "moderate",
|
1: "moderate",
|
||||||
@@ -72,21 +71,43 @@ def get_locale_params(engine_region: str | None) -> dict[str, str] | None:
|
|||||||
return {"mkt": engine_region}
|
return {"mkt": engine_region}
|
||||||
|
|
||||||
|
|
||||||
|
def override_accept_language(params: "OnlineParams", engine_region: str | None) -> None:
|
||||||
|
"""Override the ``Accept-Language`` header.
|
||||||
|
|
||||||
|
The default header built by :py:class:`~searx.search.processors.online.OnlineProcessor`
|
||||||
|
appends ``en;q=0.3`` as a fallback language::
|
||||||
|
|
||||||
|
Accept-Language: de,de-DE;q=0.7,en;q=0.3
|
||||||
|
|
||||||
|
Bing seems to better select the results locale based on the
|
||||||
|
``Accept-Language`` value header.
|
||||||
|
|
||||||
|
This function is shared with :py:mod:`searx.engines.bing_images`,
|
||||||
|
:py:mod:`searx.engines.bing_news`, and :py:mod:`searx.engines.bing_videos`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not engine_region or engine_region == "clear":
|
||||||
|
return
|
||||||
|
|
||||||
|
lang = engine_region.split("-")[0]
|
||||||
|
params["headers"]["Accept-Language"] = f"{engine_region},{lang};q=0.9"
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
def request(query: str, params: "OnlineParams"):
|
||||||
"""Assemble a Bing-Web request."""
|
"""Assemble a Bing-Web request."""
|
||||||
|
|
||||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||||
|
|
||||||
|
override_accept_language(params, engine_region)
|
||||||
|
|
||||||
query_params: dict[str, str | int] = {
|
query_params: dict[str, str | int] = {
|
||||||
"q": query,
|
"q": query,
|
||||||
"adlt": _safesearch_map.get(params.get("safesearch", 0), "off"),
|
"adlt": _safesearch_map.get(params.get("safesearch", 0), "off"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if engine_region and engine_region != "clear":
|
locale_params = get_locale_params(engine_region)
|
||||||
lang, _, cc = engine_region.partition("-")
|
if locale_params:
|
||||||
query_params["setlang"] = lang
|
query_params.update(locale_params)
|
||||||
if cc and cc not in ("us", "cn", "ru"): # bing just sends junk for these
|
|
||||||
query_params["cc"] = cc
|
|
||||||
|
|
||||||
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
|
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from lxml import html
|
|||||||
from searx.engines.bing import ( # pylint: disable=unused-import
|
from searx.engines.bing import ( # pylint: disable=unused-import
|
||||||
fetch_traits,
|
fetch_traits,
|
||||||
get_locale_params,
|
get_locale_params,
|
||||||
|
override_accept_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
# about
|
# about
|
||||||
@@ -24,7 +25,6 @@ about = {
|
|||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["images", "web"]
|
categories = ["images", "web"]
|
||||||
paging = True
|
paging = True
|
||||||
enable_http3 = True
|
|
||||||
safesearch = True
|
safesearch = True
|
||||||
time_range_support = True
|
time_range_support = True
|
||||||
time_map = {
|
time_map = {
|
||||||
@@ -43,6 +43,8 @@ def request(query, params):
|
|||||||
|
|
||||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||||
|
|
||||||
|
override_accept_language(params, engine_region)
|
||||||
|
|
||||||
# build URL query
|
# build URL query
|
||||||
# - example: https://www.bing.com/images/async?q=foo&async=1&first=1&count=35
|
# - example: https://www.bing.com/images/async?q=foo&async=1&first=1&count=35
|
||||||
query_params = {
|
query_params = {
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ from urllib.parse import urlencode
|
|||||||
from lxml import html
|
from lxml import html
|
||||||
|
|
||||||
from searx.enginelib.traits import EngineTraits
|
from searx.enginelib.traits import EngineTraits
|
||||||
from searx.engines.bing import get_locale_params
|
from searx.engines.bing import (
|
||||||
|
get_locale_params,
|
||||||
|
override_accept_language,
|
||||||
|
)
|
||||||
from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text
|
from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text
|
||||||
|
|
||||||
# about
|
# about
|
||||||
@@ -30,7 +33,6 @@ categories = ["news"]
|
|||||||
paging = True
|
paging = True
|
||||||
"""If go through the pages and there are actually no new results for another
|
"""If go through the pages and there are actually no new results for another
|
||||||
page, then bing returns the results from the last page again."""
|
page, then bing returns the results from the last page again."""
|
||||||
enable_http3 = True
|
|
||||||
|
|
||||||
time_range_support = True
|
time_range_support = True
|
||||||
time_map = {
|
time_map = {
|
||||||
@@ -51,6 +53,8 @@ def request(query, params):
|
|||||||
|
|
||||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||||
|
|
||||||
|
override_accept_language(params, engine_region)
|
||||||
|
|
||||||
# build URL query
|
# build URL query
|
||||||
# - example: https://www.bing.com/news/infinitescrollajax?q=london&first=1
|
# - example: https://www.bing.com/news/infinitescrollajax?q=london&first=1
|
||||||
page = int(params.get("pageno", 1)) - 1
|
page = int(params.get("pageno", 1)) - 1
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from lxml import html
|
|||||||
from searx.engines.bing import ( # pylint: disable=unused-import
|
from searx.engines.bing import ( # pylint: disable=unused-import
|
||||||
fetch_traits,
|
fetch_traits,
|
||||||
get_locale_params,
|
get_locale_params,
|
||||||
|
override_accept_language,
|
||||||
)
|
)
|
||||||
from searx.engines.bing_images import time_map
|
from searx.engines.bing_images import time_map
|
||||||
from searx.utils import eval_xpath, eval_xpath_getindex
|
from searx.utils import eval_xpath, eval_xpath_getindex
|
||||||
@@ -25,7 +26,6 @@ about = {
|
|||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["videos", "web"]
|
categories = ["videos", "web"]
|
||||||
paging = True
|
paging = True
|
||||||
enable_http3 = True
|
|
||||||
safesearch = True
|
safesearch = True
|
||||||
time_range_support = True
|
time_range_support = True
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ def request(query, params):
|
|||||||
|
|
||||||
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
|
||||||
|
|
||||||
|
override_accept_language(params, engine_region)
|
||||||
|
|
||||||
# build URL query
|
# build URL query
|
||||||
# - example: https://www.bing.com/videos/asyncv2?q=foo&async=content&first=1&count=35
|
# - example: https://www.bing.com/videos/asyncv2?q=foo&async=content&first=1&count=35
|
||||||
query_params = {
|
query_params = {
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ CACHE_SESSION_ID_KEY = "session_id_key"
|
|||||||
KEYWORD_RE = re.compile(r"\[\/?Keyword\]")
|
KEYWORD_RE = re.compile(r"\[\/?Keyword\]")
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
def init(engine_settings: dict[str, t.Any]) -> bool:
|
||||||
global CACHE # pylint: disable=global-statement
|
global CACHE # pylint: disable=global-statement
|
||||||
CACHE = EngineCache(engine_name=engine_settings["name"])
|
CACHE = EngineCache(engine_name=engine_settings["name"])
|
||||||
return True
|
return True
|
||||||
@@ -104,7 +104,7 @@ def response(resp: "SXNG_Response") -> EngineResults:
|
|||||||
title=_remove_keyword_marker(result["Subject"]),
|
title=_remove_keyword_marker(result["Subject"]),
|
||||||
content=_remove_keyword_marker(result["Text"]),
|
content=_remove_keyword_marker(result["Text"]),
|
||||||
url=result["Url"],
|
url=result["Url"],
|
||||||
publishedDate=datetime.fromisoformat(result["Published"]),
|
publishedDate=datetime.strptime(result["Published"], "%Y-%m-%d %H:%M:%S"),
|
||||||
metadata=gettext.gettext("Posted by {author}").format(author=result["Author"]),
|
metadata=gettext.gettext("Posted by {author}").format(author=result["Author"]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ from searx.utils import (
|
|||||||
eval_xpath_getindex,
|
eval_xpath_getindex,
|
||||||
eval_xpath_list,
|
eval_xpath_list,
|
||||||
extract_text,
|
extract_text,
|
||||||
get_embedded_stream_url,
|
get_embeded_stream_url,
|
||||||
js_obj_str_to_json_str,
|
js_obj_str_to_json_str,
|
||||||
js_obj_str_to_python,
|
js_obj_str_to_python,
|
||||||
)
|
)
|
||||||
@@ -151,7 +151,6 @@ about = {
|
|||||||
|
|
||||||
base_url = "https://search.brave.com/"
|
base_url = "https://search.brave.com/"
|
||||||
categories = []
|
categories = []
|
||||||
enable_http3 = True
|
|
||||||
brave_category: t.Literal["search", "videos", "images", "news", "goggles"] = "search"
|
brave_category: t.Literal["search", "videos", "images", "news", "goggles"] = "search"
|
||||||
"""Brave supports common web-search, videos, images, news, and goggles search.
|
"""Brave supports common web-search, videos, images, news, and goggles search.
|
||||||
|
|
||||||
@@ -248,13 +247,13 @@ def extract_json_data(text: str) -> dict[str, t.Any]:
|
|||||||
# node_ids: [0, 19],
|
# node_ids: [0, 19],
|
||||||
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
|
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
|
||||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
# form: null,
|
text = text[text.index("<script") : text.index("</script")]
|
||||||
# error: null
|
if not text:
|
||||||
# });
|
raise ValueError("can't find JS/JSON data in the given text")
|
||||||
start = text.index("data: [{")
|
start = text.index("data: [{")
|
||||||
newline = text.index("\n", start)
|
end = text.rindex("}}]")
|
||||||
end = text.rindex("}}]", start, newline)
|
js_obj_str = text[start:end]
|
||||||
js_obj_str = "{" + text[start:end] + "}}]}"
|
js_obj_str = "{" + js_obj_str + "}}]}"
|
||||||
# js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for
|
# js_obj_str = js_obj_str.replace("\xa0", "") # remove ASCII for
|
||||||
# js_obj_str = js_obj_str.replace(r"\u003C", "<").replace(r"\u003c", "<") # fix broken HTML tags in strings
|
# js_obj_str = js_obj_str.replace(r"\u003C", "<").replace(r"\u003c", "<") # fix broken HTML tags in strings
|
||||||
json_str = js_obj_str_to_json_str(js_obj_str)
|
json_str = js_obj_str_to_json_str(js_obj_str)
|
||||||
@@ -339,7 +338,7 @@ def _parse_search(resp: SXNG_Response) -> EngineResults:
|
|||||||
if len(video_tag):
|
if len(video_tag):
|
||||||
# In my tests a video tag in the WEB search was most often not a
|
# In my tests a video tag in the WEB search was most often not a
|
||||||
# video, except the ones from youtube ..
|
# video, except the ones from youtube ..
|
||||||
iframe_src = get_embedded_stream_url(url)
|
iframe_src = get_embeded_stream_url(url)
|
||||||
if iframe_src:
|
if iframe_src:
|
||||||
item["iframe_src"] = iframe_src
|
item["iframe_src"] = iframe_src
|
||||||
item["template"] = "videos.html"
|
item["template"] = "videos.html"
|
||||||
@@ -354,14 +353,14 @@ def _parse_news(resp: SXNG_Response) -> EngineResults:
|
|||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
dom = html.fromstring(resp.text)
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
for result in eval_xpath_list(dom, "//div[@data-type='news']"):
|
for result in eval_xpath_list(dom, "//div[contains(@class, 'results')]//div[@data-type='news']"):
|
||||||
url = eval_xpath_getindex(result, ".//a/@href", 0, default=None)
|
url = eval_xpath_getindex(result, ".//a[contains(@class, 'result-header')]/@href", 0, default=None)
|
||||||
if url is None:
|
if url is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
title = eval_xpath_list(result, ".//div[contains(@class, 'title')]")
|
title = eval_xpath_list(result, ".//span[contains(@class, 'snippet-title')]")
|
||||||
content = eval_xpath_list(result, ".//div[contains(@class, 'description')]")
|
content = eval_xpath_list(result, ".//p[contains(@class, 'desc')]")
|
||||||
thumbnail = eval_xpath_getindex(result, ".//a[contains(@class, 'thumbnail')]//img/@src", 0, default="")
|
thumbnail = eval_xpath_getindex(result, ".//div[contains(@class, 'image-wrapper')]//img/@src", 0, default="")
|
||||||
|
|
||||||
item = res.types.LegacyResult(
|
item = res.types.LegacyResult(
|
||||||
template="default.html",
|
template="default.html",
|
||||||
@@ -407,6 +406,9 @@ def _parse_videos(json_resp: dict[str, t.Any]) -> EngineResults:
|
|||||||
)
|
)
|
||||||
if result["thumbnail"] is not None:
|
if result["thumbnail"] is not None:
|
||||||
item["thumbnail"] = result["thumbnail"]["src"]
|
item["thumbnail"] = result["thumbnail"]["src"]
|
||||||
|
iframe_src = get_embeded_stream_url(result["url"])
|
||||||
|
if iframe_src:
|
||||||
|
item["iframe_src"] = iframe_src
|
||||||
|
|
||||||
res.add(item)
|
res.add(item)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -40,7 +39,7 @@ if t.TYPE_CHECKING:
|
|||||||
about = {
|
about = {
|
||||||
"website": "https://api.search.brave.com/",
|
"website": "https://api.search.brave.com/",
|
||||||
"wikidata_id": None,
|
"wikidata_id": None,
|
||||||
"official_api_documentation": "https://api-dashboard.search.brave.com/api-reference/web/search/get",
|
"official_api_documentation": "https://api-dashboard.search.brave.com/documentation",
|
||||||
"use_official_api": True,
|
"use_official_api": True,
|
||||||
"require_api_key": True,
|
"require_api_key": True,
|
||||||
"results": "JSON",
|
"results": "JSON",
|
||||||
@@ -63,10 +62,8 @@ base_url = "https://api.search.brave.com/res/v1/web/search"
|
|||||||
time_range_map = {"day": "past_day", "week": "past_week", "month": "past_month", "year": "past_year"}
|
time_range_map = {"day": "past_day", "week": "past_week", "month": "past_month", "year": "past_year"}
|
||||||
"""Mapping of SearXNG time ranges to Brave API time ranges."""
|
"""Mapping of SearXNG time ranges to Brave API time ranges."""
|
||||||
|
|
||||||
max_page = 10
|
|
||||||
|
|
||||||
|
def init(_):
|
||||||
def setup(_: dict[str, t.Any]) -> bool | None:
|
|
||||||
"""Initialize the engine."""
|
"""Initialize the engine."""
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise SearxEngineAPIException("No API key provided")
|
raise SearxEngineAPIException("No API key provided")
|
||||||
@@ -77,8 +74,7 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
search_args: dict[str, str | int | None] = {
|
search_args: dict[str, str | int | None] = {
|
||||||
"q": query,
|
"q": query,
|
||||||
"count": results_per_page,
|
"count": results_per_page,
|
||||||
"offset": params["pageno"] - 1,
|
"offset": (params["pageno"] - 1) * results_per_page,
|
||||||
"text_decorations": False,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply time filter if specified
|
# Apply time filter if specified
|
||||||
@@ -91,7 +87,6 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
|
|
||||||
params["url"] = f"{base_url}?{urlencode(search_args)}"
|
params["url"] = f"{base_url}?{urlencode(search_args)}"
|
||||||
params["headers"]["X-Subscription-Token"] = api_key
|
params["headers"]["X-Subscription-Token"] = api_key
|
||||||
params["headers"]["Accept"] = "application/json"
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_published_date(published_date_raw: str):
|
def _extract_published_date(published_date_raw: str):
|
||||||
@@ -117,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"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
85
searx/engines/cara.py
Normal file
85
searx/engines/cara.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
"""Cara_ is a social media and portfolio-sharing platform for artists and art
|
||||||
|
enthusiasts.
|
||||||
|
|
||||||
|
With the widespread use of generative AI, Cara_ decided to build a place that
|
||||||
|
filters out gen AI images so that people searching for authentic creatives and
|
||||||
|
images can do so easily.
|
||||||
|
|
||||||
|
.. _Cara: https://cara.app/about
|
||||||
|
"""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
|
|
||||||
|
about = {
|
||||||
|
"website": "https://cara.app",
|
||||||
|
"official_api_documentation": None,
|
||||||
|
"use_official_api": False,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "JSON",
|
||||||
|
}
|
||||||
|
|
||||||
|
base_url = "https://cara.app"
|
||||||
|
images_url = "https://images.cara.app"
|
||||||
|
|
||||||
|
categories = ["images"]
|
||||||
|
paging = True
|
||||||
|
results_per_page = 24
|
||||||
|
|
||||||
|
# if using HTTP2, we get blocked immediately
|
||||||
|
enable_http2 = False
|
||||||
|
|
||||||
|
|
||||||
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
|
args = {
|
||||||
|
"q": query,
|
||||||
|
"sortBy": "Top",
|
||||||
|
"take": results_per_page,
|
||||||
|
"skip": (params["pageno"] - 1) * results_per_page,
|
||||||
|
}
|
||||||
|
params["url"] = f"{base_url}/api/search/portfolio-posts?{urlencode(args)}"
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp: "SXNG_Response"):
|
||||||
|
res = EngineResults()
|
||||||
|
json_data: list[dict[str, t.Any]] = resp.json()
|
||||||
|
|
||||||
|
for result in json_data:
|
||||||
|
thumbnail, img = None, None
|
||||||
|
|
||||||
|
i: dict[str, str]
|
||||||
|
for i in result["images"]:
|
||||||
|
if thumbnail is None or i["isCoverImg"]:
|
||||||
|
thumbnail = i
|
||||||
|
|
||||||
|
if img is None or not i["isCoverImg"]:
|
||||||
|
img = i
|
||||||
|
|
||||||
|
if not thumbnail or not img:
|
||||||
|
continue
|
||||||
|
|
||||||
|
res.add(
|
||||||
|
res.types.LegacyResult(
|
||||||
|
{
|
||||||
|
"template": "images.html",
|
||||||
|
"url": f"{base_url}/post/{result['id']}",
|
||||||
|
"thumbnail_src": f"{images_url}/{thumbnail['src']}?height=256",
|
||||||
|
"img_src": f"{images_url}/{img['src']}",
|
||||||
|
"title": result["title"],
|
||||||
|
"content": result["content"],
|
||||||
|
"author": result["name"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
@@ -41,7 +41,7 @@ search_index = "cw22"
|
|||||||
<https://www.chatnoir.eu/docs/api-general>`_ for a full list."""
|
<https://www.chatnoir.eu/docs/api-general>`_ for a full list."""
|
||||||
|
|
||||||
|
|
||||||
def _obtain_api_key() -> tuple[str, str]:
|
def _obtain_api_key() -> tuple[str, str, str]:
|
||||||
home_resp = get(base_url)
|
home_resp = get(base_url)
|
||||||
if not home_resp.ok:
|
if not home_resp.ok:
|
||||||
raise SearxEngineAPIException("failed to obtain api key")
|
raise SearxEngineAPIException("failed to obtain api key")
|
||||||
@@ -58,9 +58,10 @@ def _obtain_api_key() -> tuple[str, str]:
|
|||||||
)
|
)
|
||||||
if not token_resp.ok:
|
if not token_resp.ok:
|
||||||
raise SearxEngineAPIException("failed to obtain api key")
|
raise SearxEngineAPIException("failed to obtain api key")
|
||||||
|
session_id = token_resp.cookies["sessionid"]
|
||||||
scraped_api_key = token_resp.json()["token"]["token"]
|
scraped_api_key = token_resp.json()["token"]["token"]
|
||||||
|
|
||||||
return csrf_token, scraped_api_key
|
return csrf_token, session_id, scraped_api_key
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
def request(query: str, params: "OnlineParams"):
|
||||||
@@ -72,7 +73,7 @@ def request(query: str, params: "OnlineParams"):
|
|||||||
|
|
||||||
params["headers"].update(headers)
|
params["headers"].update(headers)
|
||||||
else:
|
else:
|
||||||
csrf_token, scraped_api_key = _obtain_api_key()
|
csrf_token, session_id, scraped_api_key = _obtain_api_key()
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {scraped_api_key}",
|
"Authorization": f"Bearer {scraped_api_key}",
|
||||||
@@ -80,11 +81,10 @@ def request(query: str, params: "OnlineParams"):
|
|||||||
}
|
}
|
||||||
|
|
||||||
params["headers"].update(headers)
|
params["headers"].update(headers)
|
||||||
params["cookies"] = {"csrftoken": csrf_token}
|
params["cookies"] = {"csrftoken": session_id, "sessionid": session_id}
|
||||||
|
|
||||||
params["url"] = f"{base_url}/api/v1/_search"
|
params["url"] = f"{base_url}/api/v1/_search"
|
||||||
params["method"] = "POST"
|
params["method"] = "POST"
|
||||||
params["impersonate"] = "none"
|
|
||||||
|
|
||||||
json_data = {
|
json_data = {
|
||||||
"query": query,
|
"query": query,
|
||||||
|
|||||||
@@ -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']}",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ time_range_dict = {'day': '24h', 'week': '1w', 'month': '1m', 'year': '1y'}
|
|||||||
base_url = "https://www.chinaso.com"
|
base_url = "https://www.chinaso.com"
|
||||||
|
|
||||||
|
|
||||||
def setup(_: dict[str, t.Any]) -> bool | None:
|
def init(_):
|
||||||
if chinaso_news_source not in t.get_args(ChinasoNewsSourceType):
|
if chinaso_news_source not in t.get_args(ChinasoNewsSourceType):
|
||||||
raise ValueError(f"Unsupported news source: {chinaso_news_source}")
|
raise ValueError(f"Unsupported news source: {chinaso_news_source}")
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ Implementations
|
|||||||
===============
|
===============
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import typing as t
|
|
||||||
import re
|
import re
|
||||||
from os.path import expanduser, isabs, realpath, commonprefix
|
from os.path import expanduser, isabs, realpath, commonprefix
|
||||||
from shlex import split as shlex_split
|
from shlex import split as shlex_split
|
||||||
@@ -101,7 +100,7 @@ _command_logger = logger.getChild('command')
|
|||||||
_compiled_parse_regex = {}
|
_compiled_parse_regex = {}
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]) -> bool | None:
|
def init(engine_settings):
|
||||||
check_parsing_options(engine_settings)
|
check_parsing_options(engine_settings)
|
||||||
|
|
||||||
if 'command' not in engine_settings:
|
if 'command' not in engine_settings:
|
||||||
|
|||||||
@@ -141,13 +141,12 @@ def response(resp: "SXNG_Response") -> EngineResults:
|
|||||||
if name:
|
if name:
|
||||||
authors.add(name)
|
authors.add(name)
|
||||||
|
|
||||||
tag = result.get("fieldOfStudy")
|
|
||||||
res.add(
|
res.add(
|
||||||
res.types.Paper(
|
res.types.Paper(
|
||||||
title=result.get("title"),
|
title=result.get("title"),
|
||||||
url=url,
|
url=url,
|
||||||
content=result.get("fullText", "") or "",
|
content=result.get("fullText", "") or "",
|
||||||
tags=[tag] if tag else [],
|
tags=result.get("fieldOfStudy", []),
|
||||||
publishedDate=published_date,
|
publishedDate=published_date,
|
||||||
type=result.get("documentType", "") or "",
|
type=result.get("documentType", "") or "",
|
||||||
authors=authors,
|
authors=authors,
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Deviantart (Images)"""
|
"""Deviantart (Images)"""
|
||||||
|
|
||||||
import typing as t
|
|
||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from lxml import html
|
from lxml import html
|
||||||
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
from searx.utils import extract_text, eval_xpath, eval_xpath_list
|
from searx.utils import extract_text, eval_xpath, eval_xpath_list
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
# about
|
# about
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://www.deviantart.com/',
|
"website": 'https://www.deviantart.com/',
|
||||||
@@ -30,62 +23,63 @@ paging = True
|
|||||||
# search-url
|
# search-url
|
||||||
base_url = 'https://www.deviantart.com'
|
base_url = 'https://www.deviantart.com'
|
||||||
|
|
||||||
results_xpath = '//div[@data-testid="content_row"]//a[.//*[@data-testid="thumb"]]'
|
results_xpath = '//div[@class="V_S0t_"]/div/div/a'
|
||||||
img_src_xpath = './/img/@srcset'
|
url_xpath = './@href'
|
||||||
thumbnail_src_xpath = './/img/@src'
|
thumbnail_src_xpath = './div/img/@src'
|
||||||
author_xpath = './/*[@property="schema:name"]/@content'
|
img_src_xpath = './div/img/@srcset'
|
||||||
cursor_xpath = '//a[contains(@href, "cursor=") and contains(., "Next")]/@href'
|
title_xpath = './@aria-label'
|
||||||
|
premium_xpath = '../div/div/div/text()'
|
||||||
|
premium_keytext = 'Watch the artist to view this deviation'
|
||||||
|
cursor_xpath = '(//a[@class="vQ2brP"]/@href)[last()]'
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
def request(query, params):
|
||||||
|
|
||||||
# https://www.deviantart.com/search?q=foo
|
# https://www.deviantart.com/search?q=foo
|
||||||
|
|
||||||
args = {'q': query}
|
nextpage_url = params['engine_data'].get('nextpage')
|
||||||
if params['pageno'] > 1:
|
# don't use nextpage when user selected to jump back to page 1
|
||||||
cursor = params['engine_data'].get('cursor')
|
if params['pageno'] > 1 and nextpage_url is not None:
|
||||||
if cursor:
|
params['url'] = nextpage_url
|
||||||
args['cursor'] = cursor
|
else:
|
||||||
|
params['url'] = f"{base_url}/search?{urllib.parse.urlencode({'q': query})}"
|
||||||
|
|
||||||
params['url'] = f"{base_url}/search?{urllib.parse.urlencode(args)}"
|
return params
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
def response(resp):
|
||||||
|
|
||||||
res = EngineResults()
|
results = []
|
||||||
dom = html.fromstring(resp.text)
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
for result in eval_xpath_list(dom, results_xpath):
|
for result in eval_xpath_list(dom, results_xpath):
|
||||||
thumbnail_src = extract_text(eval_xpath(result, thumbnail_src_xpath))
|
# skip images that are blurred
|
||||||
img_src = extract_text(eval_xpath(result, img_src_xpath))
|
_text = extract_text(eval_xpath(result, premium_xpath))
|
||||||
# mature locked thumbs have blur transform (blur_15, blur_30 etc..)
|
if _text and premium_keytext in _text:
|
||||||
if ',blur_' in f'{thumbnail_src}{img_src}':
|
|
||||||
continue
|
continue
|
||||||
|
img_src = extract_text(eval_xpath(result, img_src_xpath))
|
||||||
if img_src:
|
if img_src:
|
||||||
img_src = img_src.split(' ')[0]
|
img_src = img_src.split(' ')[0]
|
||||||
parsed_url = urllib.parse.urlparse(img_src)
|
parsed_url = urllib.parse.urlparse(img_src)
|
||||||
img_src = parsed_url._replace(path=parsed_url.path.split('/v1')[0]).geturl()
|
img_src = parsed_url._replace(path=parsed_url.path.split('/v1')[0]).geturl()
|
||||||
|
|
||||||
author = extract_text(eval_xpath(result, author_xpath))
|
results.append(
|
||||||
|
{
|
||||||
res.add(
|
'template': 'images.html',
|
||||||
res.types.Image(
|
'url': extract_text(eval_xpath(result, url_xpath)),
|
||||||
template='images.html',
|
'img_src': img_src,
|
||||||
url=result.get('href'),
|
'thumbnail_src': extract_text(eval_xpath(result, thumbnail_src_xpath)),
|
||||||
img_src=img_src or "",
|
'title': extract_text(eval_xpath(result, title_xpath)),
|
||||||
thumbnail_src=thumbnail_src or "",
|
}
|
||||||
title=result.get('aria-label'),
|
|
||||||
author=author or "",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
nextpage_url = extract_text(eval_xpath(dom, cursor_xpath))
|
nextpage_url = extract_text(eval_xpath(dom, cursor_xpath))
|
||||||
cursor = urllib.parse.parse_qs(urllib.parse.urlparse(nextpage_url or '').query).get('cursor', [None])[0]
|
if nextpage_url:
|
||||||
if cursor:
|
results.append(
|
||||||
res.add(
|
{
|
||||||
res.types.LegacyResult(
|
'engine_data': nextpage_url.replace("http://", "https://"),
|
||||||
engine_data=cursor,
|
'key': 'nextpage',
|
||||||
key='cursor',
|
}
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return res
|
return results
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import typing as t
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import html
|
import html
|
||||||
|
|
||||||
from searx.enginelib import EngineCache
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
|
||||||
from searx.network import post
|
|
||||||
from searx.utils import format_duration, html_to_text, humanize_number
|
from searx.utils import format_duration, html_to_text, humanize_number
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
@@ -38,39 +35,17 @@ dogpile_categ = "search"
|
|||||||
base_url = "https://www.dogpile.com"
|
base_url = "https://www.dogpile.com"
|
||||||
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
|
safe_search_map = {0: "none", 1: "moderate", 2: "heavy"}
|
||||||
|
|
||||||
CACHE: EngineCache
|
|
||||||
"""Cache for the API token from dogpile"""
|
|
||||||
|
|
||||||
|
def init(_):
|
||||||
def setup(_: dict[str, t.Any]) -> bool | None:
|
|
||||||
if dogpile_categ not in ("search", "images", "videos", "news"):
|
if dogpile_categ not in ("search", "images", "videos", "news"):
|
||||||
raise ValueError("invalid search type: %s" % dogpile_categ)
|
raise ValueError("invalid search type: %s" % dogpile_categ)
|
||||||
global CACHE # pylint: disable=global-statement
|
|
||||||
CACHE = EngineCache("dogpile") # one token for images/videos/news
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _obtain_token() -> str:
|
|
||||||
token = CACHE.get("token")
|
|
||||||
if token:
|
|
||||||
return token
|
|
||||||
resp = post(f"{base_url}/api/token/refresh", headers={"Origin": base_url}, cookies={"dp_api_token": "1"})
|
|
||||||
if not resp.ok:
|
|
||||||
raise SearxEngineAPIException("failed to obtain dogpile token")
|
|
||||||
token = resp.json()["token"]
|
|
||||||
CACHE.set("token", token, expire=240) # 300s ttl
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
def request(query: str, params: "OnlineParams"):
|
||||||
params["url"] = f"{base_url}/api/{dogpile_categ}"
|
params["url"] = f"{base_url}/api/{dogpile_categ}"
|
||||||
params["headers"]["Origin"] = base_url
|
|
||||||
params["cookies"]["dp_api_token"] = "1"
|
|
||||||
if dogpile_categ != "search": # web doesnt need token
|
|
||||||
params["headers"]["x-dogpile-token"] = _obtain_token()
|
|
||||||
|
|
||||||
params["method"] = "POST"
|
params["method"] = "POST"
|
||||||
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
|
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response"):
|
def response(resp: "SXNG_Response"):
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from datetime import datetime
|
|||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from searx.result_types import EngineResults, MainResult, LegacyResult, Image
|
from searx.utils import get_embeded_stream_url, html_to_text, gen_useragent, extr
|
||||||
from searx.utils import html_to_text, gen_useragent, extr
|
|
||||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||||
|
|
||||||
from searx.engines.duckduckgo import fetch_traits # pylint: disable=unused-import
|
from searx.engines.duckduckgo import fetch_traits # pylint: disable=unused-import
|
||||||
@@ -48,7 +47,7 @@ _HTTP_User_Agent: str = gen_useragent()
|
|||||||
send_accept_language_header = False
|
send_accept_language_header = False
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]) -> bool | None:
|
def init(engine_settings: dict[str, t.Any]):
|
||||||
|
|
||||||
if engine_settings["ddg_category"] not in ["images", "videos", "news"]:
|
if engine_settings["ddg_category"] not in ["images", "videos", "news"]:
|
||||||
raise ValueError(f"Unsupported DuckDuckGo category: {engine_settings['ddg_category']}")
|
raise ValueError(f"Unsupported DuckDuckGo category: {engine_settings['ddg_category']}")
|
||||||
@@ -98,7 +97,6 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
# The vqd value is generated from the query and the UA header. To be able to
|
# The vqd value is generated from the query and the UA header. To be able to
|
||||||
# reuse the vqd value, the UA header must be static.
|
# reuse the vqd value, the UA header must be static.
|
||||||
headers["User-Agent"] = _HTTP_User_Agent
|
headers["User-Agent"] = _HTTP_User_Agent
|
||||||
params["impersonate"] = "none"
|
|
||||||
vqd = get_vqd(query=query, params=params) or fetch_vqd(query=query, params=params)
|
vqd = get_vqd(query=query, params=params) or fetch_vqd(query=query, params=params)
|
||||||
|
|
||||||
headers["Accept"] = "*/*"
|
headers["Accept"] = "*/*"
|
||||||
@@ -150,51 +148,54 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _image_result(result):
|
def _image_result(result):
|
||||||
return Image(
|
return {
|
||||||
url=result['url'],
|
'template': 'images.html',
|
||||||
title=result['title'],
|
'url': result['url'],
|
||||||
content='',
|
'title': result['title'],
|
||||||
thumbnail_src=result['thumbnail'],
|
'content': '',
|
||||||
img_src=result['image'],
|
'thumbnail_src': result['thumbnail'],
|
||||||
resolution='%s x %s' % (result['width'], result['height']),
|
'img_src': result['image'],
|
||||||
source=result['source'],
|
'resolution': '%s x %s' % (result['width'], result['height']),
|
||||||
)
|
'source': result['source'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _video_result(result):
|
def _video_result(result):
|
||||||
return LegacyResult(
|
return {
|
||||||
template='videos.html',
|
'template': 'videos.html',
|
||||||
url=result['content'],
|
'url': result['content'],
|
||||||
title=result['title'],
|
'title': result['title'],
|
||||||
content=result['description'],
|
'content': result['description'],
|
||||||
thumbnail=result['images'].get('small') or result['images'].get('medium'),
|
'thumbnail': result['images'].get('small') or result['images'].get('medium'),
|
||||||
source=result['provider'],
|
'iframe_src': get_embeded_stream_url(result['content']),
|
||||||
length=result['duration'],
|
'source': result['provider'],
|
||||||
metadata=result.get('uploader'),
|
'length': result['duration'],
|
||||||
)
|
'metadata': result.get('uploader'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _news_result(result):
|
def _news_result(result):
|
||||||
return MainResult(
|
return {
|
||||||
url=result['url'],
|
'url': result['url'],
|
||||||
title=result['title'],
|
'title': result['title'],
|
||||||
content=html_to_text(result['excerpt']),
|
'content': html_to_text(result['excerpt']),
|
||||||
publishedDate=datetime.fromtimestamp(result['date']),
|
'source': result['source'],
|
||||||
)
|
'publishedDate': datetime.fromtimestamp(result['date']),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
def response(resp):
|
||||||
res = EngineResults()
|
results = []
|
||||||
res_json = resp.json()
|
res_json = resp.json()
|
||||||
|
|
||||||
for result in res_json['results']:
|
for result in res_json['results']:
|
||||||
if ddg_category == 'images':
|
if ddg_category == 'images':
|
||||||
res.add(_image_result(result))
|
results.append(_image_result(result))
|
||||||
elif ddg_category == 'videos':
|
elif ddg_category == 'videos':
|
||||||
res.add(_video_result(result))
|
results.append(_video_result(result))
|
||||||
elif ddg_category == 'news':
|
elif ddg_category == 'news':
|
||||||
res.add(_news_result(result))
|
results.append(_news_result(result))
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid duckduckgo category: {ddg_category}")
|
raise ValueError(f"Invalid duckduckgo category: {ddg_category}")
|
||||||
|
|
||||||
return res
|
return results
|
||||||
|
|||||||
@@ -109,19 +109,7 @@ def response(resp: SXNG_Response):
|
|||||||
|
|
||||||
json_data = loads(resp.text[resp.text.find('\n') + 1 : resp.text.rfind('\n') - 2])
|
json_data = loads(resp.text[resp.text.find('\n') + 1 : resp.text.rfind('\n') - 2])
|
||||||
|
|
||||||
location = json_data.get("location")
|
geoloc = weather.GeoLocation.by_query(resp.search_params["query"])
|
||||||
if not location:
|
|
||||||
return res
|
|
||||||
|
|
||||||
metadata = json_data.get("weatherAlerts", {}).get("metadata", {})
|
|
||||||
geoloc = weather.GeoLocation(
|
|
||||||
name=location,
|
|
||||||
latitude=metadata.get("latitude"),
|
|
||||||
longitude=metadata.get("longitude"),
|
|
||||||
elevation=0,
|
|
||||||
country_code=metadata.get("language").split("-")[-1],
|
|
||||||
timezone=json_data.get("location"),
|
|
||||||
)
|
|
||||||
|
|
||||||
weather_answer = EngineResults.types.WeatherAnswer(
|
weather_answer = EngineResults.types.WeatherAnswer(
|
||||||
current=_weather_data(geoloc, json_data["currentWeather"]),
|
current=_weather_data(geoloc, json_data["currentWeather"]),
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ can't build it ourselves and must scrape it from the HTML pages.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import typing as t
|
import typing as t
|
||||||
import re
|
|
||||||
|
|
||||||
from urllib.parse import quote_plus, urljoin
|
from urllib.parse import quote_plus
|
||||||
from lxml import html
|
from lxml import html
|
||||||
|
|
||||||
from searx.utils import html_to_text, extract_text, eval_xpath
|
from searx.utils import html_to_text, gen_useragent, extract_text, eval_xpath
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
from searx.enginelib import EngineCache
|
from searx.enginelib import EngineCache
|
||||||
from searx.network import get
|
from searx.network import get
|
||||||
@@ -39,6 +38,7 @@ about = {
|
|||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["general"]
|
categories = ["general"]
|
||||||
paging = True
|
paging = True
|
||||||
|
_HTTP_User_Agent: str = gen_useragent()
|
||||||
|
|
||||||
base_url = "https://duckduckgo.com"
|
base_url = "https://duckduckgo.com"
|
||||||
|
|
||||||
@@ -73,8 +73,6 @@ def _fetch_first_page_link(
|
|||||||
resp = get(
|
resp = get(
|
||||||
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
|
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
impersonate="firefox",
|
|
||||||
default_headers=False,
|
|
||||||
timeout=2,
|
timeout=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,43 +96,6 @@ def _cache_key(query: str, pageno: int) -> str:
|
|||||||
return f"nextpage_url|{query}|{pageno}"
|
return f"nextpage_url|{query}|{pageno}"
|
||||||
|
|
||||||
|
|
||||||
def _solve_jsa(resp: "SXNG_Response") -> "SXNG_Response":
|
|
||||||
"""Duckduckgo sometimes issues a challenge instead of json."""
|
|
||||||
|
|
||||||
# length that a real browser would report for where the broken snippet is
|
|
||||||
html_len = {
|
|
||||||
"<p><div></p><p></div": 32,
|
|
||||||
"<li><div></li><li></div": 29,
|
|
||||||
"<div><div></div><div></div": 33,
|
|
||||||
"<br><div></br><br></div": 23,
|
|
||||||
}
|
|
||||||
|
|
||||||
js = resp.text or ""
|
|
||||||
jsa_match = re.search(r"let jsa = (\d+);.*?DDG\.deep\.initialize\('([^']+)'", js, re.S)
|
|
||||||
if not jsa_match:
|
|
||||||
return resp
|
|
||||||
|
|
||||||
js_functions = dict(re.findall(r"let (\w+) = function\(num\) \{([^}]*)\};", js))
|
|
||||||
jsa = int(jsa_match.group(1))
|
|
||||||
try:
|
|
||||||
for name in re.findall(r"jsa = (\w+)\(jsa\);", js):
|
|
||||||
body = js_functions[name]
|
|
||||||
mul = re.search(r"num \* (\d+)", body)
|
|
||||||
jsa = jsa * int(mul.group(1)) if mul else jsa + html_len[re.search(r"`([^`]+)`", body).group(1)]
|
|
||||||
except (KeyError, AttributeError):
|
|
||||||
return resp
|
|
||||||
|
|
||||||
params = resp.search_params
|
|
||||||
follow = get(
|
|
||||||
urljoin("https://links.duckduckgo.com", jsa_match.group(2) + str(jsa)),
|
|
||||||
headers=params["headers"],
|
|
||||||
impersonate="firefox",
|
|
||||||
default_headers=False,
|
|
||||||
)
|
|
||||||
follow.search_params = params
|
|
||||||
return follow
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
|
|
||||||
if len(query) >= 500:
|
if len(query) >= 500:
|
||||||
@@ -142,15 +103,25 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
params["url"] = None
|
params["url"] = None
|
||||||
return
|
return
|
||||||
|
|
||||||
# firefox TLS only
|
headers = params["headers"]
|
||||||
params["impersonate"] = "firefox"
|
|
||||||
params["default_headers"] = False
|
# The vqd value is generated from the query and the UA header. To be able
|
||||||
|
# to reuse the vqd value, the UA header must be static.
|
||||||
|
headers["User-Agent"] = _HTTP_User_Agent
|
||||||
|
headers["Accept"] = "*/*"
|
||||||
|
headers["Referer"] = f"{base_url}/"
|
||||||
|
headers["Host"] = "duckduckgo.com"
|
||||||
|
|
||||||
|
# Sec-Fetch headers are required to not get blocked when sending a Firefox user agent
|
||||||
|
headers["Sec-Fetch-Dest"] = "script"
|
||||||
|
headers["Sec-Fetch-Mode"] = "no-cors"
|
||||||
|
headers["Sec-Fetch-Site"] = "same-site"
|
||||||
|
|
||||||
api_url = ""
|
api_url = ""
|
||||||
if params["pageno"] > 1:
|
if params["pageno"] > 1:
|
||||||
api_url = CACHE.get(_cache_key(query, params["pageno"]))
|
api_url = CACHE.get(_cache_key(query, params["pageno"]))
|
||||||
else:
|
else:
|
||||||
api_url = _fetch_first_page_link(query, params["headers"])
|
api_url = _fetch_first_page_link(query, headers)
|
||||||
|
|
||||||
if not api_url:
|
if not api_url:
|
||||||
params["url"] = None
|
params["url"] = None
|
||||||
@@ -158,27 +129,14 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
|
|
||||||
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
|
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
|
||||||
|
|
||||||
# loads as a script
|
|
||||||
headers = params["headers"]
|
|
||||||
headers["Accept"] = "*/*"
|
|
||||||
headers["Sec-Fetch-Dest"] = "script"
|
|
||||||
headers["Sec-Fetch-Mode"] = "no-cors"
|
|
||||||
headers["Sec-Fetch-Site"] = "same-site"
|
|
||||||
headers["Referer"] = f"{base_url}/"
|
|
||||||
|
|
||||||
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
|
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response"):
|
def response(resp: "SXNG_Response"):
|
||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
|
res_json = resp.json()
|
||||||
|
|
||||||
# check if ddg returns a challenge
|
for result in res_json["results"]:
|
||||||
# e.g. 'site:github.com searxng'
|
|
||||||
if "let jsa =" in (resp.text or ""):
|
|
||||||
resp = _solve_jsa(resp)
|
|
||||||
|
|
||||||
results = resp.json()["results"]
|
|
||||||
for result in results:
|
|
||||||
if "u" not in result:
|
if "u" not in result:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -186,8 +144,8 @@ def response(resp: "SXNG_Response"):
|
|||||||
res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"]))
|
res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"]))
|
||||||
)
|
)
|
||||||
|
|
||||||
if results:
|
# link to next page
|
||||||
next_page_path = results[-1].get("n")
|
next_page_path = res_json["results"][-1].get("n")
|
||||||
if next_page_path:
|
if next_page_path:
|
||||||
CACHE.set(
|
CACHE.set(
|
||||||
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
|
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ authentication configured to read from ``my-index`` index.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import typing as t
|
|
||||||
from json import loads, dumps
|
from json import loads, dumps
|
||||||
from searx.exceptions import SearxEngineAPIException
|
from searx.exceptions import SearxEngineAPIException
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
@@ -69,7 +68,7 @@ show_metadata = False
|
|||||||
page_size = 10
|
page_size = 10
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]) -> bool | None:
|
def init(engine_settings):
|
||||||
if 'query_type' in engine_settings and engine_settings['query_type'] not in _available_query_types:
|
if 'query_type' in engine_settings and engine_settings['query_type'] not in _available_query_types:
|
||||||
raise ValueError('unsupported query type', engine_settings['query_type'])
|
raise ValueError('unsupported query type', engine_settings['query_type'])
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
"""`Europe PMC`_ provides comprehensive access to life sciences literature from
|
|
||||||
trusted sources. With Europe PMC you can search and read millions of
|
|
||||||
publications, preprints and other documents enriched with links to supporting
|
|
||||||
data, reviews, protocols, and other relevant resources.
|
|
||||||
|
|
||||||
.. _Europe PMC: https://europepmc.org/
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
- name: europepmc
|
|
||||||
engine: europepmc
|
|
||||||
shortcut: epmc
|
|
||||||
|
|
||||||
Implementations
|
|
||||||
===============
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from dateutil.parser import isoparse
|
|
||||||
|
|
||||||
from searx.enginelib import EngineCache
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
from searx.utils import html_to_text
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
|
|
||||||
about = {
|
|
||||||
"website": "https://europepmc.org/",
|
|
||||||
"wikidata_id": "Q5412157",
|
|
||||||
"official_api_documentation": "https://europepmc.org/RestfulWebService",
|
|
||||||
"use_official_api": True,
|
|
||||||
"require_api_key": False,
|
|
||||||
"results": "JSON",
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = ["science", "scientific publications"]
|
|
||||||
paging = True
|
|
||||||
|
|
||||||
# engine dependent config
|
|
||||||
search_url = "https://www.ebi.ac.uk/europepmc/webservices/rest/search"
|
|
||||||
article_url = "https://europepmc.org/article/"
|
|
||||||
|
|
||||||
page_size = 20
|
|
||||||
|
|
||||||
CACHE: EngineCache
|
|
||||||
"""Cache for storing the pagination cursor."""
|
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]):
|
|
||||||
global CACHE # pylint: disable=global-statement
|
|
||||||
CACHE = EngineCache(engine_settings["name"])
|
|
||||||
|
|
||||||
|
|
||||||
def _cache_key(query: str, page: int) -> str:
|
|
||||||
return f"{query}|{page}"
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
|
||||||
args = {
|
|
||||||
"query": query,
|
|
||||||
"format": "json",
|
|
||||||
"resultType": "core",
|
|
||||||
"pageSize": page_size,
|
|
||||||
}
|
|
||||||
|
|
||||||
if params["pageno"] > 1:
|
|
||||||
if cursor := CACHE.get(_cache_key(query, params["pageno"])):
|
|
||||||
args["cursorMark"] = cursor
|
|
||||||
else:
|
|
||||||
# no cached cursor for that page
|
|
||||||
params["url"] = None
|
|
||||||
return
|
|
||||||
|
|
||||||
params["url"] = f"{search_url}?{urlencode(args)}"
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
|
||||||
res = EngineResults()
|
|
||||||
|
|
||||||
json_resp = resp.json()
|
|
||||||
|
|
||||||
# store pagination cursor for loading next pages in cache
|
|
||||||
if next_cursor := json_resp.get("nextCursorMark"):
|
|
||||||
next_page = resp.search_params["pageno"] + 1
|
|
||||||
query = resp.search_params["query"]
|
|
||||||
CACHE.set(_cache_key(query, next_page), next_cursor)
|
|
||||||
|
|
||||||
all_results = json_resp.get("resultList", {}).get("result", [])
|
|
||||||
|
|
||||||
for item in all_results:
|
|
||||||
source = item.get("source", "")
|
|
||||||
identifier = item.get("id", "")
|
|
||||||
url = f"{article_url}{source}/{identifier}" if source and identifier else ""
|
|
||||||
|
|
||||||
journal_info: dict[str, t.Any] = item.get("journalInfo", {})
|
|
||||||
journal: dict[str, t.Any] = journal_info.get("journal", {})
|
|
||||||
|
|
||||||
res.add(
|
|
||||||
res.types.Paper(
|
|
||||||
url=url,
|
|
||||||
title=html_to_text(item.get("title", "")),
|
|
||||||
content=html_to_text(item.get("abstractText", "")),
|
|
||||||
journal=journal.get("title", ""),
|
|
||||||
issn=[journal.get("issn", "")],
|
|
||||||
authors=_get_authors(item),
|
|
||||||
doi=item.get("doi", ""),
|
|
||||||
publishedDate=_get_published_date(item),
|
|
||||||
type=", ".join((item.get("pubTypeList", {})).get("pubType", [])),
|
|
||||||
pdf_url=_get_pdf_url(item),
|
|
||||||
html_url=url,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def _get_authors(item: dict[str, t.Any]) -> list:
|
|
||||||
"""Extract the list of authors from the item."""
|
|
||||||
if authors := item.get("authorString", None):
|
|
||||||
authors = [author.strip().rstrip(".") for author in authors.split(",") if author.strip()]
|
|
||||||
else:
|
|
||||||
authors = []
|
|
||||||
return authors
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pdf_url(item: dict[str, t.Any]) -> str:
|
|
||||||
"""Extract the PDF URL in case it is open access."""
|
|
||||||
for url_info in (item.get("fullTextUrlList", {})).get("fullTextUrl", []):
|
|
||||||
if url_info.get("documentStyle") == "pdf" and url_info.get("availabilityCode") == "OA":
|
|
||||||
return url_info.get("url", "")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _get_published_date(item: dict[str, t.Any]) -> datetime | None:
|
|
||||||
"""Extract the published date from the item and convert it to a datetime object."""
|
|
||||||
if unformatted_date := item.get("firstPublicationDate"):
|
|
||||||
return isoparse(unformatted_date)
|
|
||||||
return None
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
"""Engine to search using the official `Exa Search API`_. Exa is a search engine for AI agents.
|
|
||||||
|
|
||||||
.. _Exa Search API: https://exa.ai/docs/reference/search
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
The engine has the following mandatory setting:
|
|
||||||
|
|
||||||
- :py:obj:`api_key`
|
|
||||||
|
|
||||||
You can obtain an API key from the `API Key section <https://dashboard.exa.ai/api-keys>`_ in the Exa dashboard.
|
|
||||||
|
|
||||||
Optional settings are:
|
|
||||||
|
|
||||||
- :py:obj:`results_per_page`
|
|
||||||
- :py:obj:`search_type`
|
|
||||||
- :py:obj:`content_mode`
|
|
||||||
- :py:obj:`content_max_characters`
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
- name: exaapi
|
|
||||||
engine: exaapi
|
|
||||||
shortcut: exa
|
|
||||||
api_key: "..."
|
|
||||||
results_per_page: 10
|
|
||||||
search_type: auto
|
|
||||||
content_mode: highlights
|
|
||||||
inactive: false
|
|
||||||
|
|
||||||
The API supports SafeSearch and region-aware results.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from dateutil import parser
|
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
from searx.utils import html_to_text
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
|
|
||||||
SearchType = t.Literal["fast", "auto", "instant", "deep", "deep-lite", "deep-reasoning"]
|
|
||||||
ContentMode = t.Literal["highlights", "text"]
|
|
||||||
|
|
||||||
about = {
|
|
||||||
"website": "https://exa.ai",
|
|
||||||
"wikidata_id": None,
|
|
||||||
"official_api_documentation": "https://exa.ai/docs/reference/search",
|
|
||||||
"use_official_api": True,
|
|
||||||
"require_api_key": True,
|
|
||||||
"results": "JSON",
|
|
||||||
}
|
|
||||||
|
|
||||||
api_key: str = ""
|
|
||||||
"""API key for Exa Search API (required)."""
|
|
||||||
|
|
||||||
categories = ["general", "web"]
|
|
||||||
safesearch = True
|
|
||||||
|
|
||||||
base_url = "https://api.exa.ai/search"
|
|
||||||
results_per_page: int = 10
|
|
||||||
"""Maximum number of results per request. Value must be between 1 and 100, default is 10."""
|
|
||||||
|
|
||||||
search_type: SearchType = "auto"
|
|
||||||
"""Search type. Default is auto, see documentation for more information."""
|
|
||||||
|
|
||||||
content_mode: ContentMode = "highlights"
|
|
||||||
"""Content to request from the API: ``highlights`` (excerpts) or ``text`` (page text)."""
|
|
||||||
|
|
||||||
content_max_characters: int = 500
|
|
||||||
"""Maximum characters for the requested content."""
|
|
||||||
|
|
||||||
|
|
||||||
def setup(_: dict[str, t.Any]) -> bool | None:
|
|
||||||
if not api_key:
|
|
||||||
raise SearxEngineAPIException("No API key provided")
|
|
||||||
if not 1 <= results_per_page <= 100:
|
|
||||||
raise ValueError("results_per_page must be between 1 and 100")
|
|
||||||
if search_type not in t.get_args(SearchType):
|
|
||||||
raise ValueError(f"Unsupported search type: {search_type}")
|
|
||||||
if content_mode not in t.get_args(ContentMode):
|
|
||||||
raise ValueError(f"Unsupported content mode: {content_mode}")
|
|
||||||
if content_max_characters < 1:
|
|
||||||
raise ValueError("content_max_characters must be at least 1")
|
|
||||||
|
|
||||||
|
|
||||||
def _contents_payload() -> dict[str, t.Any]:
|
|
||||||
if content_mode == "text":
|
|
||||||
return {"text": {"maxCharacters": content_max_characters, "stripLinks": True}}
|
|
||||||
return {"highlights": {"maxCharacters": content_max_characters}}
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_content(result: dict[str, t.Any]) -> str:
|
|
||||||
if content_mode == "text":
|
|
||||||
return html_to_text(result.get("text") or "")
|
|
||||||
return html_to_text(" ".join(result.get("highlights") or []))
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
|
||||||
"""Create the API request."""
|
|
||||||
body: dict[str, t.Any] = {
|
|
||||||
"query": query,
|
|
||||||
"type": search_type,
|
|
||||||
"numResults": results_per_page,
|
|
||||||
"contents": _contents_payload(),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply SafeSearch if enabled
|
|
||||||
if params["safesearch"]:
|
|
||||||
body["moderation"] = True
|
|
||||||
|
|
||||||
# Apply region-aware results if specified
|
|
||||||
locale_parts = params["searxng_locale"].split("-")
|
|
||||||
region = locale_parts[-1]
|
|
||||||
if len(locale_parts) > 1:
|
|
||||||
body["userLocation"] = region.upper()
|
|
||||||
|
|
||||||
params["url"] = base_url
|
|
||||||
params["method"] = "POST"
|
|
||||||
params["headers"]["x-api-key"] = api_key
|
|
||||||
params["json"] = body
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_published_date(value: str | None):
|
|
||||||
"""Extract and parse the published date from the API response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: Raw date string from the API
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parsed datetime object or None if parsing fails
|
|
||||||
"""
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return parser.parse(value)
|
|
||||||
except (parser.ParserError, TypeError, OverflowError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
|
||||||
"""Process the API response and return results."""
|
|
||||||
res = EngineResults()
|
|
||||||
|
|
||||||
for result in resp.json().get("results", []):
|
|
||||||
url = result.get("url")
|
|
||||||
if not url:
|
|
||||||
continue
|
|
||||||
|
|
||||||
res.add(
|
|
||||||
res.types.MainResult(
|
|
||||||
url=url,
|
|
||||||
title=html_to_text(result.get("title") or url),
|
|
||||||
content=_extract_content(result),
|
|
||||||
thumbnail=result.get("image") or "",
|
|
||||||
publishedDate=_extract_published_date(result.get("publishedDate")),
|
|
||||||
author=result.get("author") or "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ CACHE_VALID_DURATION = 30 * 24 * 3600 # one month, same as website
|
|||||||
"""Duration how long settings cookies are valid."""
|
"""Duration how long settings cookies are valid."""
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]) -> bool | None:
|
def init(engine_settings: dict[str, t.Any]):
|
||||||
global CACHE # pylint: disable=global-statement
|
global CACHE # pylint: disable=global-statement
|
||||||
CACHE = EngineCache(engine_settings["name"])
|
CACHE = EngineCache(engine_settings["name"])
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ Implementation
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import typing as t
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ page_size: int = 10
|
|||||||
"""Maximum number of results per page (default 10)."""
|
"""Maximum number of results per page (default 10)."""
|
||||||
|
|
||||||
|
|
||||||
def setup(_: dict[str, t.Any]) -> bool | None:
|
def init(_):
|
||||||
if not base_url:
|
if not base_url:
|
||||||
raise ValueError('gitea engine: base_url is unset')
|
raise ValueError('gitea engine: base_url is unset')
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,12 @@ engines:
|
|||||||
- :ref:`google scholar engine`
|
- :ref:`google scholar engine`
|
||||||
- :ref:`google autocomplete`
|
- :ref:`google autocomplete`
|
||||||
|
|
||||||
This implementation uses Nokia user agents to request an XML layout from Google.
|
|
||||||
The normal web version requires executing JavaScript to load the results and
|
|
||||||
therefore is currently not used here. See `Google discussion`_ for more
|
|
||||||
information on that topic.
|
|
||||||
|
|
||||||
.. _Google discussion: https://github.com/searxng/searxng/issues/6359
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import time
|
||||||
import typing as t
|
import typing as t
|
||||||
from urllib.parse import unquote, urlencode
|
from urllib.parse import unquote, urlencode
|
||||||
|
|
||||||
@@ -35,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:
|
||||||
@@ -47,16 +45,16 @@ about = {
|
|||||||
"official_api_documentation": "https://developers.google.com/custom-search/",
|
"official_api_documentation": "https://developers.google.com/custom-search/",
|
||||||
"use_official_api": False,
|
"use_official_api": False,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": "XML",
|
"results": "HTML",
|
||||||
}
|
}
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["general", "web"]
|
categories = ["general", "web"]
|
||||||
paging = True
|
paging = True
|
||||||
max_page = 50
|
max_page = 50
|
||||||
"""Google supports up to 50 pages of results, see the `Google max_page discussion`_.
|
"""`Google max 50 pages`_
|
||||||
|
|
||||||
.. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
|
.. _Google max 50 pages: https://github.com/searxng/searxng/issues/2982
|
||||||
"""
|
"""
|
||||||
time_range_support = True
|
time_range_support = True
|
||||||
language_support = True
|
language_support = True
|
||||||
@@ -67,23 +65,38 @@ time_range_dict = {"day": "d", "week": "w", "month": "m", "year": "y"}
|
|||||||
# Filter results. 0: None, 1: Moderate, 2: Strict
|
# Filter results. 0: None, 1: Moderate, 2: Strict
|
||||||
filter_mapping = {0: "off", 1: "medium", 2: "high"}
|
filter_mapping = {0: "off", 1: "medium", 2: "high"}
|
||||||
|
|
||||||
# https://github.com/searxng/searxng/issues/6359
|
|
||||||
nokia_useragents = (
|
|
||||||
"Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0",
|
|
||||||
"Nokia7610/2.0 (7.0642.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0",
|
|
||||||
"Nokia6230/2.0 (05.50) Profile/MIDP-2.0 Configuration/CLDC-1.1",
|
|
||||||
"Nokia6230i/2.0 (03.80) Profile/MIDP-2.0 Configuration/CLDC-1.1",
|
|
||||||
"Nokia6280/2.0 (03.60) Profile/MIDP-2.0 Configuration/CLDC-1.1",
|
|
||||||
"NokiaN72/2.0617.1.0.3 Series60/2.8 Profile/MIDP-2.0 Configuration/CLDC-1.1",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# specific xpath variables
|
# specific xpath variables
|
||||||
# ------------------------
|
# ------------------------
|
||||||
|
|
||||||
# Suggestions are links placed in a *card-section*, we extract only the text
|
# Suggestions are links placed in a *card-section*, we extract only the text
|
||||||
# from the links not the links itself.
|
# from the links not the links itself.
|
||||||
suggestion_xpath = '//table[contains(@class, "HExoMb")]//a[contains(@class, "ZWRArf")]'
|
suggestion_xpath = '//div[contains(@class, "gGQDvd iIWm4b")]//a'
|
||||||
|
|
||||||
|
|
||||||
|
_arcid_range = string.ascii_letters + string.digits + "_-"
|
||||||
|
_arcid_random: tuple[str, int] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def ui_async(start: int) -> str:
|
||||||
|
"""Format of the response from UI's async request.
|
||||||
|
|
||||||
|
- ``arc_id:<...>,use_ac:true,_fmt:prog``
|
||||||
|
|
||||||
|
The arc_id is random generated every hour.
|
||||||
|
"""
|
||||||
|
global _arcid_random # pylint: disable=global-statement
|
||||||
|
|
||||||
|
use_ac = "use_ac:true"
|
||||||
|
# _fmt:html returns a HTTP 500 when user search for celebrities like
|
||||||
|
# '!google natasha allegri' or '!google chris evans'
|
||||||
|
_fmt = "_fmt:prog"
|
||||||
|
|
||||||
|
# create a new random arc_id every hour
|
||||||
|
if not _arcid_random or (int(time.time()) - _arcid_random[1]) > 3600:
|
||||||
|
_arcid_random = ("".join(random.choices(_arcid_range, k=23)), int(time.time()))
|
||||||
|
arc_id = f"arc_id:srp_{_arcid_random[0]}_1{start:02}"
|
||||||
|
|
||||||
|
return ",".join([arc_id, use_ac, _fmt])
|
||||||
|
|
||||||
|
|
||||||
def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[str, t.Any]:
|
def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[str, t.Any]:
|
||||||
@@ -115,11 +128,19 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
|
|||||||
A instance of :py:obj:`babel.core.Locale` build from the
|
A instance of :py:obj:`babel.core.Locale` build from the
|
||||||
``searxng_locale`` value.
|
``searxng_locale`` value.
|
||||||
|
|
||||||
|
subdomain:
|
||||||
|
Google subdomain :py:obj:`google_domains` that fits to the country
|
||||||
|
code.
|
||||||
|
|
||||||
params:
|
params:
|
||||||
Py-Dictionary with additional request arguments (can be passed to
|
Py-Dictionary with additional request arguments (can be passed to
|
||||||
:py:func:`urllib.parse.urlencode`).
|
:py:func:`urllib.parse.urlencode`).
|
||||||
|
|
||||||
- ``hl`` parameter: specifies the interface language of user interface.
|
- ``hl`` parameter: specifies the interface language of user interface.
|
||||||
|
- ``lr`` parameter: restricts search results to documents written in
|
||||||
|
a particular language.
|
||||||
|
- ``cr`` parameter: restricts search results to documents
|
||||||
|
originating in a particular country.
|
||||||
- ``ie`` parameter: sets the character encoding scheme that should
|
- ``ie`` parameter: sets the character encoding scheme that should
|
||||||
be used to interpret the query string ('utf8').
|
be used to interpret the query string ('utf8').
|
||||||
- ``oe`` parameter: sets the character encoding scheme that should
|
- ``oe`` parameter: sets the character encoding scheme that should
|
||||||
@@ -136,6 +157,7 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
|
|||||||
ret_val: dict[str, t.Any] = {
|
ret_val: dict[str, t.Any] = {
|
||||||
"language": None,
|
"language": None,
|
||||||
"country": None,
|
"country": None,
|
||||||
|
"subdomain": None,
|
||||||
"params": {},
|
"params": {},
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"cookies": {},
|
"cookies": {},
|
||||||
@@ -148,7 +170,7 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
|
|||||||
except babel.core.UnknownLocaleError:
|
except babel.core.UnknownLocaleError:
|
||||||
locale = None
|
locale = None
|
||||||
|
|
||||||
eng_lang = eng_traits.get_language(sxng_locale) or "lang_en"
|
eng_lang = eng_traits.get_language(sxng_locale, "lang_en")
|
||||||
lang_code = eng_lang.split("_")[-1] # lang_zh-TW --> zh-TW / lang_en --> en
|
lang_code = eng_lang.split("_")[-1] # lang_zh-TW --> zh-TW / lang_en --> en
|
||||||
country = eng_traits.get_region(sxng_locale, eng_traits.all_locale)
|
country = eng_traits.get_region(sxng_locale, eng_traits.all_locale)
|
||||||
|
|
||||||
@@ -163,6 +185,7 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
|
|||||||
ret_val["language"] = eng_lang
|
ret_val["language"] = eng_lang
|
||||||
ret_val["country"] = country
|
ret_val["country"] = country
|
||||||
ret_val["locale"] = locale
|
ret_val["locale"] = locale
|
||||||
|
ret_val["subdomain"] = eng_traits.custom["supported_domains"].get(country.upper(), "www.google.com")
|
||||||
|
|
||||||
# hl parameter:
|
# hl parameter:
|
||||||
# The hl parameter specifies the interface language (host language) of
|
# The hl parameter specifies the interface language (host language) of
|
||||||
@@ -174,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
|
||||||
@@ -201,8 +224,6 @@ def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[st
|
|||||||
|
|
||||||
# specify a region (country) only if a region is given in the selected
|
# specify a region (country) only if a region is given in the selected
|
||||||
# locale --> https://github.com/searxng/searxng/issues/2672
|
# locale --> https://github.com/searxng/searxng/issues/2672
|
||||||
|
|
||||||
if country is not None:
|
|
||||||
ret_val["params"]["cr"] = ""
|
ret_val["params"]["cr"] = ""
|
||||||
if len(sxng_locale.split("-")) > 1:
|
if len(sxng_locale.split("-")) > 1:
|
||||||
ret_val["params"]["cr"] = "country" + country
|
ret_val["params"]["cr"] = "country" + country
|
||||||
@@ -247,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
|
||||||
|
|
||||||
@@ -280,78 +302,88 @@ def detect_google_sorry(resp: "SXNG_Response"):
|
|||||||
raise SearxEngineCaptchaException()
|
raise SearxEngineCaptchaException()
|
||||||
|
|
||||||
|
|
||||||
def unwrap_google_url(raw_url: str) -> str:
|
|
||||||
# remove redirector from url
|
|
||||||
if raw_url.startswith("/url?q="):
|
|
||||||
return unquote(raw_url[7:].split("&sa=U")[0])
|
|
||||||
return raw_url
|
|
||||||
|
|
||||||
|
|
||||||
def wml_dom(resp: "SXNG_Response"):
|
|
||||||
detect_google_sorry(resp)
|
|
||||||
text = resp.text
|
|
||||||
if text.lstrip().startswith("<?xml"):
|
|
||||||
text = text.split("?>", 1)[-1]
|
|
||||||
return html.fromstring(text)
|
|
||||||
|
|
||||||
|
|
||||||
def google_request(
|
|
||||||
query: str,
|
|
||||||
params: "OnlineParams",
|
|
||||||
extra_args: dict[str, t.Any] | None = None,
|
|
||||||
*,
|
|
||||||
eng_traits: EngineTraits | None = None,
|
|
||||||
use_time_range: bool = True,
|
|
||||||
use_safesearch: bool = True,
|
|
||||||
safesearch_map: dict[int, str] | None = None,
|
|
||||||
use_locales: bool = True,
|
|
||||||
) -> None:
|
|
||||||
google_info = get_google_info(params, eng_traits or traits)
|
|
||||||
if not use_locales:
|
|
||||||
google_info["params"].pop("lr")
|
|
||||||
google_info["params"].pop("cr")
|
|
||||||
|
|
||||||
start = (params["pageno"] - 1) * 10
|
|
||||||
args: dict[str, t.Any] = {
|
|
||||||
"q": query,
|
|
||||||
"sca_esv": "1",
|
|
||||||
**google_info["params"],
|
|
||||||
**(extra_args or {}),
|
|
||||||
}
|
|
||||||
if start:
|
|
||||||
args["start"] = start
|
|
||||||
if use_time_range and params["time_range"] in time_range_dict:
|
|
||||||
args["tbs"] = "qdr:" + time_range_dict[params["time_range"]]
|
|
||||||
if use_safesearch and params["safesearch"]:
|
|
||||||
args["safe"] = (safesearch_map or filter_mapping)[params["safesearch"]]
|
|
||||||
|
|
||||||
params["url"] = f"https://www.google.com/wml/search?{urlencode(args)}"
|
|
||||||
params["headers"]["User-Agent"] = random.choice(nokia_useragents)
|
|
||||||
params["impersonate"] = "chrome99_android"
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
google_request(query, params)
|
"""Google search request"""
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
start = (params["pageno"] - 1) * 10
|
||||||
|
google_info = get_google_info(params, traits)
|
||||||
|
|
||||||
|
# https://www.google.de/search?q=corona&hl=de&lr=lang_de&start=0&tbs=qdr%3Ad&safe=medium
|
||||||
|
query_url = (
|
||||||
|
"https://"
|
||||||
|
+ google_info["subdomain"]
|
||||||
|
+ "/search"
|
||||||
|
+ "?"
|
||||||
|
+ urlencode(
|
||||||
|
{
|
||||||
|
"q": query,
|
||||||
|
**google_info["params"],
|
||||||
|
"filter": "0",
|
||||||
|
"start": start,
|
||||||
|
# 'vet': '12ahUKEwik3ZbIzfn7AhXMX_EDHbUDBh0QxK8CegQIARAC..i',
|
||||||
|
# 'ved': '2ahUKEwik3ZbIzfn7AhXMX_EDHbUDBh0Q_skCegQIARAG',
|
||||||
|
# 'cs' : 1,
|
||||||
|
# 'sa': 'N',
|
||||||
|
# 'yv': 3,
|
||||||
|
# 'prmd': 'vin',
|
||||||
|
# 'ei': 'GASaY6TxOcy_xc8PtYeY6AE',
|
||||||
|
# 'sa': 'N',
|
||||||
|
# 'sstk': 'AcOHfVkD7sWCSAheZi-0tx_09XDO55gTWY0JNq3_V26cNN-c8lfD45aZYPI8s_Bqp8s57AHz5pxchDtAGCA_cikAWSjy9kw3kgg'
|
||||||
|
# formally known as use_mobile_ui
|
||||||
|
# "asearch": "arc",
|
||||||
|
# "async": str_async,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if params["time_range"] in time_range_dict:
|
||||||
|
query_url += "&" + urlencode({"tbs": "qdr:" + time_range_dict[params["time_range"]]})
|
||||||
|
if params["safesearch"]:
|
||||||
|
query_url += "&" + urlencode({"safe": filter_mapping[params["safesearch"]]})
|
||||||
|
params["url"] = query_url
|
||||||
|
|
||||||
|
params["cookies"] = google_info["cookies"]
|
||||||
|
params["headers"].update(google_info["headers"])
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
# regex match to get image map that is found inside the returned javascript:
|
||||||
|
# (function(){var s='...';var i=['...'] ...}
|
||||||
|
RE_DATA_IMAGE = re.compile(r"(data:image[^']*?)'[^']*?'((?:dimg|pimg|tsuid)[^']*)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_url_images(text: str):
|
||||||
|
data_image_map = {}
|
||||||
|
|
||||||
|
for image_url, img_id in RE_DATA_IMAGE.findall(text):
|
||||||
|
data_image_map[img_id] = image_url.encode('utf-8').decode("unicode-escape")
|
||||||
|
logger.debug("data:image objects --> %s", list(data_image_map.keys()))
|
||||||
|
return data_image_map
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp: "SXNG_Response"):
|
||||||
|
"""Get response from google's search request"""
|
||||||
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
|
detect_google_sorry(resp)
|
||||||
|
data_image_map = parse_url_images(resp.text)
|
||||||
|
|
||||||
results = EngineResults()
|
results = EngineResults()
|
||||||
dom = wml_dom(resp)
|
|
||||||
|
# convert the text to dom
|
||||||
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
# parse results
|
# parse results
|
||||||
for result in eval_xpath_list(dom, '//div[contains(@class, "zMzFAb")]'):
|
for result in eval_xpath_list(dom, '//a[@data-ved and not(@class)]'):
|
||||||
|
# pylint: disable=too-many-nested-blocks
|
||||||
|
|
||||||
try:
|
try:
|
||||||
title_tag = eval_xpath_getindex(
|
title_tag = eval_xpath_getindex(result, './/div[@style]', 0, default=None)
|
||||||
result, './/a[contains(@class, "fuLhoc")]//span[contains(@class, "CVA68e")]', 0, default=None
|
|
||||||
)
|
|
||||||
if title_tag is None:
|
if title_tag is None:
|
||||||
# this not one of the common google results *section*
|
# this not one of the common google results *section*
|
||||||
logger.debug("ignoring item from the result_xpath list: missing title")
|
logger.debug("ignoring item from the result_xpath list: missing title")
|
||||||
continue
|
continue
|
||||||
title = extract_text(title_tag)
|
title = extract_text(title_tag)
|
||||||
|
|
||||||
raw_url = eval_xpath_getindex(result, './/a[contains(@class, "fuLhoc")]/@href', 0, default=None)
|
raw_url = result.get("href")
|
||||||
if raw_url is None:
|
if raw_url is None:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'ignoring item from the result_xpath list: missing url of title "%s"',
|
'ignoring item from the result_xpath list: missing url of title "%s"',
|
||||||
@@ -359,19 +391,30 @@ def response(resp: "SXNG_Response") -> EngineResults:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
url = unwrap_google_url(raw_url)
|
if raw_url.startswith('/url?q='):
|
||||||
content = extract_text(
|
url = unquote(raw_url[7:].split("&sa=U")[0]) # remove the google redirector
|
||||||
eval_xpath(result, './/div[contains(@class, "taTFJ")]//span[contains(@class, "FrIlee")]')
|
else:
|
||||||
)
|
url = raw_url
|
||||||
thumbnail = eval_xpath_getindex(result, './/img[contains(@src, "encrypted-tbn")]/@src', 0, default=None)
|
|
||||||
results.add(
|
content_nodes = eval_xpath(result, '../..//div[contains(@class, "ilUpNd H66NU aSRlid")]')
|
||||||
results.types.MainResult(
|
for item in content_nodes:
|
||||||
url=url,
|
for script in item.xpath(".//script"):
|
||||||
title=title or "",
|
script.getparent().remove(script)
|
||||||
content=content or "",
|
|
||||||
thumbnail=thumbnail or "",
|
content = extract_text(content_nodes[0])
|
||||||
)
|
|
||||||
)
|
# Images that are NOT the favicon
|
||||||
|
xpath_image = eval_xpath_getindex(result, './/img', index=0, default=None)
|
||||||
|
|
||||||
|
thumbnail = None
|
||||||
|
if xpath_image is not None:
|
||||||
|
thumbnail = xpath_image.get("src")
|
||||||
|
if thumbnail.startswith("data:image"):
|
||||||
|
img_id = xpath_image.get("id")
|
||||||
|
if img_id:
|
||||||
|
thumbnail = data_image_map.get(img_id)
|
||||||
|
|
||||||
|
results.append({"url": url, "title": title, "content": content or '', "thumbnail": thumbnail})
|
||||||
|
|
||||||
except Exception as e: # pylint: disable=broad-except
|
except Exception as e: # pylint: disable=broad-except
|
||||||
logger.error(e, exc_info=True)
|
logger.error(e, exc_info=True)
|
||||||
@@ -379,8 +422,10 @@ def response(resp: "SXNG_Response") -> EngineResults:
|
|||||||
|
|
||||||
# parse suggestion
|
# parse suggestion
|
||||||
for suggestion in eval_xpath_list(dom, suggestion_xpath):
|
for suggestion in eval_xpath_list(dom, suggestion_xpath):
|
||||||
results.add(results.types.LegacyResult(suggestion=extract_text(suggestion)))
|
# append suggestion
|
||||||
|
results.append({"suggestion": extract_text(suggestion)})
|
||||||
|
|
||||||
|
# return results
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@@ -413,12 +458,14 @@ skip_countries = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def fetch_traits(engine_traits: EngineTraits):
|
def fetch_traits(engine_traits: EngineTraits, add_domains: bool = True):
|
||||||
"""Fetch languages from Google."""
|
"""Fetch languages from Google."""
|
||||||
# pylint: disable=import-outside-toplevel, too-many-branches
|
# pylint: disable=import-outside-toplevel, too-many-branches
|
||||||
|
|
||||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||||
|
|
||||||
|
engine_traits.custom["supported_domains"] = {}
|
||||||
|
|
||||||
resp = get("https://www.google.com/preferences", timeout=5)
|
resp = get("https://www.google.com/preferences", timeout=5)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
raise RuntimeError("Response from Google preferences is not OK.")
|
raise RuntimeError("Response from Google preferences is not OK.")
|
||||||
@@ -469,3 +516,22 @@ def fetch_traits(engine_traits: EngineTraits):
|
|||||||
|
|
||||||
# alias regions
|
# alias regions
|
||||||
engine_traits.regions["zh-CN"] = "HK"
|
engine_traits.regions["zh-CN"] = "HK"
|
||||||
|
|
||||||
|
# supported domains
|
||||||
|
|
||||||
|
if add_domains:
|
||||||
|
resp = get("https://www.google.com/supported_domains", timeout=5)
|
||||||
|
if not resp.ok:
|
||||||
|
raise RuntimeError("Response from Google supported domains is not OK.")
|
||||||
|
|
||||||
|
for domain in resp.text.split():
|
||||||
|
domain = domain.strip()
|
||||||
|
if not domain or domain in [
|
||||||
|
".google.com",
|
||||||
|
]:
|
||||||
|
continue
|
||||||
|
region = domain.split(".")[-1].upper()
|
||||||
|
engine_traits.custom["supported_domains"][region] = "www" + domain
|
||||||
|
if region == "HK":
|
||||||
|
# There is no google.cn, we use .com.hk for zh-CN
|
||||||
|
engine_traits.custom["supported_domains"]["CN"] = "www" + domain
|
||||||
|
|||||||
@@ -1,186 +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
|
|
||||||
enable_http3 = 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)
|
|
||||||
|
|
||||||
args = {
|
|
||||||
"rsz": "filtered_cse",
|
|
||||||
"num": str(page_size),
|
|
||||||
"hl": google_info["params"]["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 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],
|
|
||||||
)
|
|
||||||
@@ -1,76 +1,122 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Google Images: see :py:obj:`searx.engines.google`."""
|
"""This is the implementation of the Google Images engine using the internal
|
||||||
|
Google API used by the Google Go Android app.
|
||||||
|
|
||||||
import typing as t
|
This internal API offer results in
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
|
||||||
|
- JSON (``_fmt:json``)
|
||||||
|
- Protobuf_ (``_fmt:pb``)
|
||||||
|
- Protobuf_ compressed? (``_fmt:pc``)
|
||||||
|
- HTML (``_fmt:html``)
|
||||||
|
- Protobuf_ encoded in JSON (``_fmt:jspb``).
|
||||||
|
|
||||||
|
.. _Protobuf: https://en.wikipedia.org/wiki/Protocol_Buffers
|
||||||
|
"""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from json import loads
|
||||||
|
|
||||||
from searx.engines.google import fetch_traits # pylint: disable=unused-import
|
from searx.engines.google import fetch_traits # pylint: disable=unused-import
|
||||||
from searx.engines.google import google_request, wml_dom
|
from searx.engines.google import (
|
||||||
from searx.result_types import EngineResults
|
get_google_info,
|
||||||
from searx.utils import eval_xpath_list
|
time_range_dict,
|
||||||
|
detect_google_sorry,
|
||||||
if t.TYPE_CHECKING:
|
)
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
# about
|
# about
|
||||||
about = {
|
about = {
|
||||||
"website": "https://images.google.com",
|
"website": 'https://images.google.com',
|
||||||
"wikidata_id": "Q521550",
|
"wikidata_id": 'Q521550',
|
||||||
"official_api_documentation": "https://developers.google.com/custom-search",
|
"official_api_documentation": 'https://developers.google.com/custom-search',
|
||||||
"use_official_api": False,
|
"use_official_api": False,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": "XML",
|
"results": 'JSON',
|
||||||
}
|
}
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["images", "web"]
|
categories = ['images', 'web']
|
||||||
paging = True
|
paging = True
|
||||||
enable_http3 = True
|
|
||||||
max_page = 50
|
max_page = 50
|
||||||
"""Google supports up to 50 pages of results, see the `Google max_page discussion`_.
|
"""`Google max 50 pages`_
|
||||||
|
|
||||||
.. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
|
.. _Google max 50 pages: https://github.com/searxng/searxng/issues/2982
|
||||||
"""
|
"""
|
||||||
|
|
||||||
time_range_support = True
|
time_range_support = True
|
||||||
language_support = True
|
language_support = True
|
||||||
safesearch = True
|
safesearch = True
|
||||||
|
|
||||||
filter_mapping = {0: "images", 1: "active", 2: "active"}
|
filter_mapping = {0: 'images', 1: 'active', 2: 'active'}
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
def request(query, params):
|
||||||
google_request(
|
"""Google-Image search request"""
|
||||||
query,
|
|
||||||
params,
|
google_info = get_google_info(params, traits)
|
||||||
{"tbm": "isch"},
|
|
||||||
eng_traits=traits,
|
query_url = (
|
||||||
safesearch_map=filter_mapping,
|
'https://'
|
||||||
use_locales=False,
|
+ google_info['subdomain']
|
||||||
|
+ '/search'
|
||||||
|
+ '?'
|
||||||
|
+ urlencode({'q': query, 'tbm': "isch", **google_info['params'], 'asearch': 'isch'})
|
||||||
|
# don't urlencode this because wildly different AND bad results
|
||||||
|
# pagination uses Zero-based numbering
|
||||||
|
+ f'&async=_fmt:json,p:1,ijn:{params["pageno"] - 1}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if params['time_range'] in time_range_dict:
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
query_url += '&' + urlencode({'tbs': 'qdr:' + time_range_dict[params['time_range']]})
|
||||||
results = EngineResults()
|
if params['safesearch']:
|
||||||
dom = wml_dom(resp)
|
query_url += '&' + urlencode({'safe': filter_mapping[params['safesearch']]})
|
||||||
|
params['url'] = query_url
|
||||||
for link in eval_xpath_list(dom, '//a[contains(@href, "/imgres?")]'):
|
params['cookies'] = google_info['cookies']
|
||||||
qs = parse_qs(urlparse(link.get("href", "")).query)
|
params['headers'].update(google_info['headers'])
|
||||||
img_src = qs.get("imgurl", [""])[0]
|
# this ua will allow getting ~50 results instead of 10. #1641
|
||||||
url = qs.get("imgrefurl", [""])[0]
|
params['headers']['User-Agent'] = (
|
||||||
if not img_src or not url:
|
'NSTN/3.60.474802233.release Dalvik/2.1.0 (Linux; U; Android 12;' f' {google_info.get("country", "US")}) gzip'
|
||||||
continue
|
|
||||||
width, height = qs.get("w", [""])[0], qs.get("h", [""])[0]
|
|
||||||
tbnid = qs.get("tbnid", [""])[0]
|
|
||||||
results.add(
|
|
||||||
results.types.Image(
|
|
||||||
url=url,
|
|
||||||
title=unquote(urlparse(img_src).path.rsplit("/", 1)[-1]) or urlparse(url).netloc,
|
|
||||||
img_src=img_src,
|
|
||||||
thumbnail_src=f"https://encrypted-tbn0.gstatic.com/images?q=tbn:{tbnid}",
|
|
||||||
resolution=f"{width} x {height}" if width and height else "",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp):
|
||||||
|
"""Get response from google's search request"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
detect_google_sorry(resp)
|
||||||
|
|
||||||
|
json_start = resp.text.find('{"ischj":')
|
||||||
|
json_data = loads(resp.text[json_start:])
|
||||||
|
|
||||||
|
for item in json_data["ischj"].get("metadata", []):
|
||||||
|
result_item = {
|
||||||
|
'url': item["result"]["referrer_url"],
|
||||||
|
'title': item["result"]["page_title"],
|
||||||
|
'content': item["text_in_grid"]["snippet"],
|
||||||
|
'source': item["result"]["site_title"],
|
||||||
|
'resolution': f'{item["original_image"]["width"]} x {item["original_image"]["height"]}',
|
||||||
|
'img_src': item["original_image"]["url"],
|
||||||
|
'thumbnail_src': item["thumbnail"]["url"],
|
||||||
|
'template': 'images.html',
|
||||||
|
}
|
||||||
|
|
||||||
|
author = item["result"].get('iptc', {}).get('creator')
|
||||||
|
if author:
|
||||||
|
result_item['author'] = ', '.join(author)
|
||||||
|
|
||||||
|
copyright_notice = item["result"].get('iptc', {}).get('copyright_notice')
|
||||||
|
if copyright_notice:
|
||||||
|
result_item['source'] += ' | ' + copyright_notice
|
||||||
|
|
||||||
|
freshness_date = item["result"].get("freshness_date")
|
||||||
|
if freshness_date:
|
||||||
|
result_item['source'] += ' | ' + freshness_date
|
||||||
|
|
||||||
|
file_size = item.get('gsa', {}).get('file_size')
|
||||||
|
if file_size:
|
||||||
|
result_item['source'] += ' (%s)' % file_size
|
||||||
|
|
||||||
|
results.append(result_item)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -1,91 +1,324 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Google News: see :py:obj:`searx.engines.google`."""
|
"""This is the implementation of the Google News engine.
|
||||||
|
|
||||||
|
Google News has a different region handling compared to Google WEB.
|
||||||
|
|
||||||
|
- the ``ceid`` argument has to be set (:py:obj:`ceid_list`)
|
||||||
|
- the hl_ argument has to be set correctly (and different to Google WEB)
|
||||||
|
- the gl_ argument is mandatory
|
||||||
|
|
||||||
|
If one of this argument is not set correctly, the request is redirected to
|
||||||
|
CONSENT dialog::
|
||||||
|
|
||||||
|
https://consent.google.com/m?continue=
|
||||||
|
|
||||||
|
The google news API ignores some parameters from the common :ref:`google API`:
|
||||||
|
|
||||||
|
- num_ : the number of search results is ignored / there is no paging all
|
||||||
|
results for a query term are in the first response.
|
||||||
|
- save_ : is ignored / Google-News results are always *SafeSearch*
|
||||||
|
|
||||||
|
.. _hl: https://developers.google.com/custom-search/docs/xml_results#hlsp
|
||||||
|
.. _gl: https://developers.google.com/custom-search/docs/xml_results#glsp
|
||||||
|
.. _num: https://developers.google.com/custom-search/docs/xml_results#numsp
|
||||||
|
.. _save: https://developers.google.com/custom-search/docs/xml_results#safesp
|
||||||
|
"""
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from searx.engines.google import fetch_traits # pylint: disable=unused-import
|
import json
|
||||||
from searx.engines.google import google_request, unwrap_google_url, wml_dom
|
import base64
|
||||||
from searx.result_types import EngineResults
|
from urllib.parse import urlencode
|
||||||
|
from lxml import html
|
||||||
|
import babel
|
||||||
|
|
||||||
|
from searx import locales
|
||||||
from searx.utils import (
|
from searx.utils import (
|
||||||
eval_xpath_getindex,
|
eval_xpath,
|
||||||
eval_xpath_list,
|
eval_xpath_list,
|
||||||
|
eval_xpath_getindex,
|
||||||
extract_text,
|
extract_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from searx.engines.google import fetch_traits as _fetch_traits # pylint: disable=unused-import
|
||||||
|
from searx.engines.google import (
|
||||||
|
get_google_info,
|
||||||
|
detect_google_sorry,
|
||||||
|
)
|
||||||
|
from searx.enginelib.traits import EngineTraits
|
||||||
|
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from searx.extended_types import SXNG_Response
|
from searx.extended_types import SXNG_Response
|
||||||
from searx.search.processors import OnlineParams
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
# about
|
# about
|
||||||
about = {
|
about = {
|
||||||
"website": "https://www.google.com",
|
"website": "https://news.google.com",
|
||||||
"wikidata_id": "Q12020",
|
"wikidata_id": "Q12020",
|
||||||
"official_api_documentation": "https://developers.google.com/custom-search",
|
"official_api_documentation": "https://developers.google.com/custom-search",
|
||||||
"use_official_api": False,
|
"use_official_api": False,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": "XML",
|
"results": "HTML",
|
||||||
}
|
}
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["news"]
|
categories = ["news"]
|
||||||
paging = True
|
paging = False
|
||||||
max_page = 50
|
|
||||||
"""Google supports up to 50 pages of results, see the `Google max_page discussion`_.
|
|
||||||
|
|
||||||
.. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
|
|
||||||
"""
|
|
||||||
time_range_support = False
|
time_range_support = False
|
||||||
language_support = True
|
language_support = True
|
||||||
safesearch = False
|
|
||||||
|
# Google-News results are always *SafeSearch*. Option 'safesearch' is set to
|
||||||
|
# False here.
|
||||||
|
#
|
||||||
|
# safesearch : results are identical for safesearch=0 and safesearch=2
|
||||||
|
safesearch = True
|
||||||
|
base_url: str = "https://news.google.com"
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
google_request(
|
"""Google-News search request"""
|
||||||
query,
|
|
||||||
params,
|
sxng_locale = params.get("searxng_locale", "en-US")
|
||||||
{"tbm": "nws"},
|
ceid: str = locales.get_engine_locale(
|
||||||
eng_traits=traits,
|
sxng_locale, traits.custom["ceid"], default="US:en"
|
||||||
use_time_range=False,
|
) # pyright: ignore[reportAssignmentType]
|
||||||
use_safesearch=False,
|
google_info = get_google_info(params, traits)
|
||||||
use_locales=False,
|
google_info["subdomain"] = "news.google.com" # google news has only one domain
|
||||||
|
|
||||||
|
ceid_region, ceid_lang = ceid.split(":")
|
||||||
|
ceid_lang, ceid_suffix = (
|
||||||
|
ceid_lang.split(":")
|
||||||
|
+ [
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)[:2]
|
||||||
|
|
||||||
|
google_info["params"]["hl"] = ceid_lang
|
||||||
|
|
||||||
|
if ceid_suffix and ceid_suffix not in ["Hans", "Hant"]:
|
||||||
|
|
||||||
|
if ceid_region.lower() == ceid_lang:
|
||||||
|
google_info["params"]["hl"] = ceid_lang + "-" + ceid_region
|
||||||
|
else:
|
||||||
|
google_info["params"]["hl"] = ceid_lang + "-" + ceid_suffix
|
||||||
|
|
||||||
|
elif ceid_region.lower() != ceid_lang:
|
||||||
|
|
||||||
|
if ceid_region in ["AT", "BE", "CH", "IL", "SA", "IN", "BD", "PT"]:
|
||||||
|
google_info["params"]["hl"] = ceid_lang
|
||||||
|
else:
|
||||||
|
google_info["params"]["hl"] = ceid_lang + "-" + ceid_region
|
||||||
|
|
||||||
|
google_info["params"]["lr"] = "lang_" + ceid_lang.split("-")[0]
|
||||||
|
google_info["params"]["gl"] = ceid_region
|
||||||
|
|
||||||
|
query_url = (
|
||||||
|
"https://"
|
||||||
|
+ google_info["subdomain"]
|
||||||
|
+ "/search?"
|
||||||
|
+ urlencode(
|
||||||
|
{"q": query, **google_info["params"]},
|
||||||
|
)
|
||||||
|
# ceid includes a ':' character which must not be urlencoded
|
||||||
|
+ ("&ceid=%s" % ceid)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
params["url"] = query_url
|
||||||
def _span_text(link, css_class: str):
|
params["cookies"] = google_info["cookies"]
|
||||||
return extract_text(
|
params["headers"].update(google_info["headers"])
|
||||||
eval_xpath_getindex(link, f'.//span[contains(@class, "{css_class}")]', 0, default=None),
|
|
||||||
allow_none=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
results = EngineResults()
|
"""Get response from google's search request"""
|
||||||
seen = set()
|
|
||||||
for link in eval_xpath_list(wml_dom(resp), '//a[contains(@href, "/url?q=")]'):
|
res = EngineResults()
|
||||||
href = link.get("href")
|
|
||||||
if not href:
|
detect_google_sorry(resp)
|
||||||
|
|
||||||
|
# convert the text to dom
|
||||||
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
|
for result in eval_xpath_list(dom, "//div[@jslog and @data-n-tid and @jsdata]"):
|
||||||
|
|
||||||
|
url: str = eval_xpath_getindex(result, "./a[@target='_blank']/@href", 0, default=0)
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
if url.startswith("./"):
|
||||||
|
url = base_url + url[1:]
|
||||||
|
|
||||||
|
# The real URL is often encoded in the "jslog" attribute
|
||||||
|
jslog: str | None = eval_xpath_getindex(result, "./a[@target='_blank']/@jslog", 0, default=None)
|
||||||
|
|
||||||
|
# Try to extract the real URL from jslog
|
||||||
|
real_url: str | None = None
|
||||||
|
if jslog:
|
||||||
|
# jslog format is usually: "95014; 5:<base64>; track:click,vis". We
|
||||||
|
# want the second part (index 1) after splitting by ";"
|
||||||
|
parts: list[str] = jslog.split(";")
|
||||||
|
if len(parts) > 1:
|
||||||
|
b64_data: str = parts[1].split(":")[-1].strip()
|
||||||
|
# Pad base64 if necessary
|
||||||
|
b64_data += "=" * (-len(b64_data) % 4)
|
||||||
|
decoded_data: list[str | None] = json.loads(base64.b64decode(b64_data).decode("utf-8"))
|
||||||
|
# The URL is typically the last element in the decoded array
|
||||||
|
if (
|
||||||
|
isinstance(decoded_data, list)
|
||||||
|
and isinstance(decoded_data[-1], str)
|
||||||
|
and decoded_data[-1].startswith("http")
|
||||||
|
):
|
||||||
|
real_url = decoded_data[-1]
|
||||||
|
if real_url:
|
||||||
|
url = real_url
|
||||||
|
else:
|
||||||
|
logger.error(f"no real-url found: {url}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
url = unwrap_google_url(href)
|
title = extract_text(eval_xpath(result, "./h4")) or ""
|
||||||
if url in seen or "google.com/search" in url:
|
|
||||||
continue
|
|
||||||
|
|
||||||
title = _span_text(link, "M3vVJe") or _span_text(link, "fuLhoc")
|
# The pub_date is mostly a string like 'yesterday', not a real timezone
|
||||||
if not title:
|
# date or time. Therefore we can't use publishedDate and place the
|
||||||
continue
|
# *pub* sting into the content.
|
||||||
|
|
||||||
source = _span_text(link, "dXDvrc")
|
pub_date = extract_text(eval_xpath(result, ".//time"))
|
||||||
pub_date = _span_text(link, "YVIcad")
|
pub_origin = extract_text(eval_xpath(result, ".//div[contains(@class, 'vr1PYe')]"))
|
||||||
thumbnail = eval_xpath_getindex(link, './/img[contains(@src, "encrypted-tbn")]/@src', 0, default=None)
|
content = " / ".join([x for x in [pub_origin, pub_date] if x])
|
||||||
|
|
||||||
seen.add(url)
|
thumbnail: str = eval_xpath_getindex(result, ".//figure/img/@src", 0, default="")
|
||||||
results.add(
|
if thumbnail and thumbnail.startswith("/"):
|
||||||
results.types.MainResult(
|
thumbnail = base_url + thumbnail
|
||||||
|
|
||||||
|
res.add(
|
||||||
|
res.types.MainResult(
|
||||||
url=url,
|
url=url,
|
||||||
title=title,
|
title=title,
|
||||||
content=" / ".join(x for x in [source, pub_date] if x),
|
content=content,
|
||||||
thumbnail=thumbnail or "",
|
thumbnail=thumbnail,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
ceid_list = [
|
||||||
|
"AE:ar",
|
||||||
|
"AR:es-419",
|
||||||
|
"AT:de",
|
||||||
|
"AU:en",
|
||||||
|
"BD:bn",
|
||||||
|
"BE:fr",
|
||||||
|
"BE:nl",
|
||||||
|
"BG:bg",
|
||||||
|
"BR:pt-419",
|
||||||
|
"BW:en",
|
||||||
|
"CA:en",
|
||||||
|
"CA:fr",
|
||||||
|
"CH:de",
|
||||||
|
"CH:fr",
|
||||||
|
"CL:es-419",
|
||||||
|
"CN:zh-Hans",
|
||||||
|
"CO:es-419",
|
||||||
|
"CU:es-419",
|
||||||
|
"CZ:cs",
|
||||||
|
"DE:de",
|
||||||
|
"EE:et",
|
||||||
|
"EG:ar",
|
||||||
|
"ES:ca",
|
||||||
|
"ES:es",
|
||||||
|
"ET:en",
|
||||||
|
"FI:fi",
|
||||||
|
"FR:fr",
|
||||||
|
"GB:en",
|
||||||
|
"GH:en",
|
||||||
|
"GR:el",
|
||||||
|
"HK:zh-Hant",
|
||||||
|
"HU:hu",
|
||||||
|
"ID:en",
|
||||||
|
"ID:id",
|
||||||
|
"IE:en",
|
||||||
|
"IL:en",
|
||||||
|
"IL:he",
|
||||||
|
"IN:bn",
|
||||||
|
"IN:en",
|
||||||
|
"IN:gu",
|
||||||
|
"IN:hi",
|
||||||
|
"IN:ml",
|
||||||
|
"IN:mr",
|
||||||
|
"IN:pa",
|
||||||
|
"IN:ta",
|
||||||
|
"IN:te",
|
||||||
|
"IT:it",
|
||||||
|
"JP:ja",
|
||||||
|
"KE:en",
|
||||||
|
"KR:ko",
|
||||||
|
"LB:ar",
|
||||||
|
"LT:lt",
|
||||||
|
"LV:en",
|
||||||
|
"LV:lv",
|
||||||
|
"MA:fr",
|
||||||
|
"MY:en",
|
||||||
|
"MY:ms",
|
||||||
|
"NA:en",
|
||||||
|
"NG:en",
|
||||||
|
"NL:nl",
|
||||||
|
"NO:no",
|
||||||
|
"NZ:en",
|
||||||
|
"PH:en",
|
||||||
|
"PK:en",
|
||||||
|
"PL:pl",
|
||||||
|
"RO:ro",
|
||||||
|
"RS:sr",
|
||||||
|
"RU:ru",
|
||||||
|
"SA:ar",
|
||||||
|
"SE:sv",
|
||||||
|
"SG:en",
|
||||||
|
"SI:sl",
|
||||||
|
"SK:sk",
|
||||||
|
"SN:fr",
|
||||||
|
"TH:th",
|
||||||
|
"TR:tr",
|
||||||
|
"TZ:en",
|
||||||
|
"UA:ru",
|
||||||
|
"UA:uk",
|
||||||
|
"UG:en",
|
||||||
|
"US:en",
|
||||||
|
"VN:vi",
|
||||||
|
"ZA:en",
|
||||||
|
"ZW:en",
|
||||||
|
]
|
||||||
|
"""List of region/language combinations supported by Google News. Values of the
|
||||||
|
``ceid`` argument of the Google News REST API."""
|
||||||
|
|
||||||
|
|
||||||
|
_skip_values = [
|
||||||
|
"ET:en", # english (ethiopia)
|
||||||
|
"ID:en", # english (indonesia)
|
||||||
|
"LV:en", # english (latvia)
|
||||||
|
]
|
||||||
|
|
||||||
|
_ceid_locale_map = {"NO:no": "nb-NO"}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_traits(engine_traits: EngineTraits):
|
||||||
|
_fetch_traits(engine_traits, add_domains=False)
|
||||||
|
|
||||||
|
engine_traits.custom["ceid"] = {}
|
||||||
|
|
||||||
|
for ceid in ceid_list:
|
||||||
|
if ceid in _skip_values:
|
||||||
|
continue
|
||||||
|
|
||||||
|
region, lang = ceid.split(":")
|
||||||
|
x = lang.split("-")
|
||||||
|
if len(x) > 1:
|
||||||
|
if x[1] not in ["Hant", "Hans"]:
|
||||||
|
lang = x[0]
|
||||||
|
|
||||||
|
sxng_locale = _ceid_locale_map.get(ceid, lang + "-" + region)
|
||||||
|
try:
|
||||||
|
locale = babel.Locale.parse(sxng_locale, sep="-")
|
||||||
|
except babel.UnknownLocaleError:
|
||||||
|
print("ERROR: %s -> %s is unknown by babel" % (ceid, sxng_locale))
|
||||||
|
continue
|
||||||
|
|
||||||
|
engine_traits.custom["ceid"][locales.region_tag(locale)] = ceid
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ about = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
play_categ = None # apps|movies
|
play_categ = None # apps|movies
|
||||||
enable_http3 = True
|
|
||||||
base_url = 'https://play.google.com'
|
base_url = 'https://play.google.com'
|
||||||
search_url = base_url + "/store/search?{query}&c={play_categ}"
|
search_url = base_url + "/store/search?{query}&c={play_categ}"
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import typing as t
|
|||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from lxml import html
|
from lxml import html
|
||||||
from curl_cffi.requests.exceptions import TooManyRedirects
|
import httpx
|
||||||
|
|
||||||
from searx.utils import (
|
from searx.utils import (
|
||||||
eval_xpath,
|
eval_xpath,
|
||||||
@@ -63,7 +63,6 @@ about = {
|
|||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["science", "scientific publications"]
|
categories = ["science", "scientific publications"]
|
||||||
paging = True
|
paging = True
|
||||||
enable_http3 = True
|
|
||||||
max_page = 50
|
max_page = 50
|
||||||
"""`Google max 50 pages`_
|
"""`Google max 50 pages`_
|
||||||
|
|
||||||
@@ -78,6 +77,8 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
"""Google-Scholar search request"""
|
"""Google-Scholar search request"""
|
||||||
|
|
||||||
google_info = get_google_info(params, traits)
|
google_info = get_google_info(params, traits)
|
||||||
|
# subdomain is: scholar.google.xy
|
||||||
|
google_info["subdomain"] = google_info["subdomain"].replace("www.", "scholar.")
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"q": query,
|
"q": query,
|
||||||
@@ -88,7 +89,7 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
}
|
}
|
||||||
args.update(time_range_args(params))
|
args.update(time_range_args(params))
|
||||||
|
|
||||||
params["url"] = "https://scholar.google.com/scholar?" + urlencode(args)
|
params["url"] = "https://" + google_info["subdomain"] + "/scholar?" + urlencode(args)
|
||||||
params["cookies"] = google_info["cookies"]
|
params["cookies"] = google_info["cookies"]
|
||||||
params["headers"].update(google_info["headers"])
|
params["headers"].update(google_info["headers"])
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ def response(resp: "SXNG_Response") -> EngineResults: # pylint: disable=too-man
|
|||||||
raise SearxEngineAccessDeniedException(
|
raise SearxEngineAccessDeniedException(
|
||||||
message="google_scholar: unusual traffic detected",
|
message="google_scholar: unusual traffic detected",
|
||||||
)
|
)
|
||||||
raise TooManyRedirects(f"location {resp.headers['Location'].split('?')[0]}")
|
raise httpx.TooManyRedirects(f"location {resp.headers['Location'].split('?')[0]}")
|
||||||
|
|
||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
dom = html.fromstring(resp.text)
|
dom = html.fromstring(resp.text)
|
||||||
|
|||||||
@@ -1,85 +1,185 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Google Videos: see :py:obj:`searx.engines.google`."""
|
"""This is the implementation of the Google Videos engine.
|
||||||
|
|
||||||
import typing as t
|
.. admonition:: Content-Security-Policy (CSP)
|
||||||
|
|
||||||
|
This engine needs to allow images from the `data URLs`_ (prefixed with the
|
||||||
|
``data:`` scheme)::
|
||||||
|
|
||||||
|
Header set Content-Security-Policy "img-src 'self' data: ;"
|
||||||
|
|
||||||
|
.. _data URLs:
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from urllib.parse import urlencode, urlparse, parse_qs, unquote
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
from searx.engines.google import fetch_traits # pylint: disable=unused-import
|
|
||||||
from searx.engines.google import google_request, unwrap_google_url, wml_dom
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
from searx.utils import (
|
from searx.utils import (
|
||||||
eval_xpath_getindex,
|
|
||||||
eval_xpath_list,
|
eval_xpath_list,
|
||||||
|
eval_xpath_getindex,
|
||||||
extract_text,
|
extract_text,
|
||||||
parse_duration_string,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
from searx.engines.google import fetch_traits # pylint: disable=unused-import
|
||||||
from searx.extended_types import SXNG_Response
|
from searx.engines.google import (
|
||||||
from searx.search.processors import OnlineParams
|
get_google_info,
|
||||||
|
time_range_dict,
|
||||||
|
filter_mapping,
|
||||||
|
suggestion_xpath,
|
||||||
|
detect_google_sorry,
|
||||||
|
ui_async,
|
||||||
|
)
|
||||||
|
from searx.utils import get_embeded_stream_url
|
||||||
|
|
||||||
# about
|
# about
|
||||||
about = {
|
about = {
|
||||||
"website": "https://www.google.com",
|
"website": 'https://www.google.com',
|
||||||
"wikidata_id": "Q219885",
|
"wikidata_id": 'Q219885',
|
||||||
"official_api_documentation": "https://developers.google.com/custom-search",
|
"official_api_documentation": 'https://developers.google.com/custom-search',
|
||||||
"use_official_api": False,
|
"use_official_api": False,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": "XML",
|
"results": 'HTML',
|
||||||
}
|
}
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["videos", "web"]
|
categories = ['videos', 'web']
|
||||||
paging = True
|
paging = True
|
||||||
max_page = 50
|
max_page = 50
|
||||||
"""Google supports up to 50 pages of results, see the `Google max_page discussion`_.
|
|
||||||
|
|
||||||
.. _Google max_page discussion: https://github.com/searxng/searxng/issues/2982
|
|
||||||
"""
|
|
||||||
language_support = True
|
language_support = True
|
||||||
time_range_support = True
|
time_range_support = True
|
||||||
safesearch = True
|
safesearch = True
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
# =26;[3,"dimg_ZNMiZPCqE4apxc8P3a2tuAQ_137"]a87;data:image/jpeg;base64,/9j/4AAQSkZJRgABA
|
||||||
google_request(
|
# ...6T+9Nl4cnD+gr9OK8I56/tX3l86nWYw//2Q==26;
|
||||||
query,
|
RE_DATA_IMAGE = re.compile(r'"(dimg_[^"]*)"[^;]*;(data:image[^;]*;[^;]*);?')
|
||||||
params,
|
|
||||||
{"tbm": "vid"},
|
|
||||||
eng_traits=traits,
|
def parse_data_images(text: str):
|
||||||
use_locales=False,
|
data_image_map = {}
|
||||||
|
|
||||||
|
for img_id, data_image in RE_DATA_IMAGE.findall(text):
|
||||||
|
end_pos = data_image.rfind("=")
|
||||||
|
if end_pos > 0:
|
||||||
|
data_image = data_image[: end_pos + 1]
|
||||||
|
data_image_map[img_id] = data_image
|
||||||
|
logger.debug("data:image objects --> %s", list(data_image_map.keys()))
|
||||||
|
return data_image_map
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
"""Google-Video search request"""
|
||||||
|
google_info = get_google_info(params, traits)
|
||||||
|
start = (params['pageno'] - 1) * 10
|
||||||
|
|
||||||
|
query_url = (
|
||||||
|
'https://'
|
||||||
|
+ google_info['subdomain']
|
||||||
|
+ '/search'
|
||||||
|
+ "?"
|
||||||
|
+ urlencode(
|
||||||
|
{
|
||||||
|
'q': query,
|
||||||
|
'tbm': "vid",
|
||||||
|
'start': start,
|
||||||
|
**google_info['params'],
|
||||||
|
'asearch': 'arc',
|
||||||
|
'async': ui_async(start),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if params['time_range'] in time_range_dict:
|
||||||
|
query_url += '&' + urlencode({'tbs': 'qdr:' + time_range_dict[params['time_range']]})
|
||||||
|
if 'safesearch' in params:
|
||||||
|
query_url += '&' + urlencode({'safe': filter_mapping[params['safesearch']]})
|
||||||
|
params['url'] = query_url
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
params['cookies'] = google_info['cookies']
|
||||||
results = EngineResults()
|
params['headers'].update(google_info['headers'])
|
||||||
|
return params
|
||||||
|
|
||||||
for result in eval_xpath_list(wml_dom(resp), '//div[contains(@class, "zMzFAb")]'):
|
|
||||||
|
def response(resp):
|
||||||
|
"""Get response from google's search request"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
detect_google_sorry(resp)
|
||||||
|
data_image_map = parse_data_images(resp.text)
|
||||||
|
|
||||||
|
# convert the text to dom
|
||||||
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
|
result_divs = eval_xpath_list(dom, '//div[contains(@class, "MjjYud")]')
|
||||||
|
|
||||||
|
# parse results
|
||||||
|
for result in result_divs:
|
||||||
title = extract_text(
|
title = extract_text(
|
||||||
eval_xpath_getindex(result, './/span[contains(@class, "CVA68e")]', 0, default=None),
|
eval_xpath_getindex(result, './/h3[contains(@class, "LC20lb")] | .//div[@role="heading"]', 0, default=None),
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
)
|
)
|
||||||
raw_url = eval_xpath_getindex(result, './/a[contains(@class, "fuLhoc")]/@href', 0, default=None)
|
url = eval_xpath_getindex(
|
||||||
if not title or not raw_url:
|
result, './/a[@jsname="UWckNb"]/@href | .//a[contains(@href, "/url?q=")]/@href', 0, default=None
|
||||||
continue
|
|
||||||
|
|
||||||
url = unwrap_google_url(raw_url)
|
|
||||||
thumbnail = eval_xpath_getindex(result, './/img[contains(@class, "SygO9d")]/@src', 0, default="")
|
|
||||||
if "/default.jpg" in thumbnail:
|
|
||||||
thumbnail = thumbnail.split("?")[0].replace("/default.jpg", "/hqdefault.jpg")
|
|
||||||
length = None
|
|
||||||
for span in eval_xpath_list(result, './/span[contains(@class, "YVIcad")]'):
|
|
||||||
length = parse_duration_string(extract_text(span) or "")
|
|
||||||
if length:
|
|
||||||
break
|
|
||||||
|
|
||||||
results.add(
|
|
||||||
results.types.MainResult(
|
|
||||||
url=url,
|
|
||||||
title=title,
|
|
||||||
thumbnail=thumbnail,
|
|
||||||
length=length,
|
|
||||||
template="videos.html",
|
|
||||||
)
|
)
|
||||||
|
if url and url.startswith('/url?q='):
|
||||||
|
url = unquote(url[7:].split('&sa=U')[0])
|
||||||
|
|
||||||
|
content = extract_text(
|
||||||
|
eval_xpath_getindex(result, './/div[contains(@class, "ITZIwc")]', 0, default=None), allow_none=True
|
||||||
)
|
)
|
||||||
|
pub_info = extract_text(
|
||||||
|
eval_xpath_getindex(
|
||||||
|
result, './/div[contains(@class, "gqF9jc")] | .//div[contains(@class, "WRu9Cd")]', 0, default=None
|
||||||
|
),
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
# Broader XPath to find any <img> element
|
||||||
|
thumbnail = eval_xpath_getindex(result, './/img/@src', 0, default=None)
|
||||||
|
duration = extract_text(
|
||||||
|
eval_xpath_getindex(result, './/span[contains(@class, "k1U36b")]', 0, default=None), allow_none=True
|
||||||
|
)
|
||||||
|
video_id = eval_xpath_getindex(result, './/div[@jscontroller="rTuANe"]/@data-vid', 0, default=None)
|
||||||
|
|
||||||
|
# Fallback for video_id from URL if not found via XPath
|
||||||
|
if not video_id and url and 'youtube.com' in url:
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
video_id = parse_qs(parsed_url.query).get('v', [None])[0]
|
||||||
|
|
||||||
|
# Handle thumbnail
|
||||||
|
if thumbnail and thumbnail.startswith('data:image'):
|
||||||
|
img_id = eval_xpath_getindex(result, './/img/@id', 0, default=None)
|
||||||
|
if img_id and img_id in data_image_map:
|
||||||
|
thumbnail = data_image_map[img_id]
|
||||||
|
else:
|
||||||
|
thumbnail = None
|
||||||
|
if not thumbnail and video_id:
|
||||||
|
thumbnail = f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
|
||||||
|
|
||||||
|
# Handle video embed URL
|
||||||
|
embed_url = None
|
||||||
|
if video_id:
|
||||||
|
embed_url = get_embeded_stream_url(f"https://www.youtube.com/watch?v={video_id}")
|
||||||
|
elif url:
|
||||||
|
embed_url = get_embeded_stream_url(url)
|
||||||
|
|
||||||
|
# Only append results with valid title and url
|
||||||
|
if title and url:
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
'url': url,
|
||||||
|
'title': title,
|
||||||
|
'content': content or '',
|
||||||
|
'author': pub_info,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'length': duration,
|
||||||
|
'iframe_src': embed_url,
|
||||||
|
'template': 'videos.html',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# parse suggestion
|
||||||
|
for suggestion in eval_xpath_list(dom, suggestion_xpath):
|
||||||
|
results.append({'suggestion': extract_text(suggestion)})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user