Compare commits
90 Commits
2287a6826a
...
48456caeb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48456caeb3 | ||
|
|
ef158ce1f4 | ||
|
|
cbf9ec7bf4 | ||
|
|
409ede1530 | ||
|
|
1326ec7429 | ||
|
|
50406d4b46 | ||
|
|
5ce3aa3acf | ||
|
|
76ebad0b21 | ||
|
|
d76f030cb3 | ||
|
|
b3b15ecc72 | ||
|
|
1319b250af | ||
|
|
198928de05 | ||
|
|
11d9c830b8 | ||
|
|
743f90514b | ||
|
|
48801dbc9a | ||
|
|
5451ab243a | ||
|
|
7ca24eee45 | ||
|
|
c6a70782b2 | ||
|
|
01a07f34b2 | ||
|
|
f32fcb1243 | ||
|
|
bc06b1aece | ||
|
|
ff60fe635f | ||
|
|
6e7119fa4e | ||
|
|
f52cd3f008 | ||
|
|
a2fa7de880 | ||
|
|
0315988f5a | ||
|
|
2e74d86321 | ||
|
|
19b116f1d7 | ||
|
|
fe08bb1d90 | ||
|
|
8ef5fbca4e | ||
|
|
7351c38e6c | ||
|
|
bdfe1c2a15 | ||
|
|
4cbfba9d7b | ||
|
|
4a594f1b53 | ||
|
|
590b211652 | ||
|
|
41e3a0baa7 | ||
|
|
11204459da | ||
|
|
03787b2e5a | ||
|
|
72294a9ffa | ||
|
|
d47cf9db24 | ||
|
|
705ebe64b5 | ||
|
|
b072b0f66b | ||
|
|
4beb5523f5 | ||
|
|
54e6e2a96d | ||
|
|
abcf8ca073 | ||
|
|
1a16281490 | ||
|
|
fd33559cfb | ||
|
|
60e31eacfc | ||
|
|
c45b970546 | ||
|
|
a4251be397 | ||
|
|
c20038e7c3 | ||
|
|
20e40ded6d | ||
|
|
4de0766b76 | ||
|
|
6de83ca47f | ||
|
|
a32bcd54c5 | ||
|
|
c733aa83e8 | ||
|
|
9dfc6f38d5 | ||
|
|
e1076f5c35 | ||
|
|
8489817561 | ||
|
|
38ff1e4a7d | ||
|
|
21cdb0332b | ||
|
|
30330617d9 | ||
|
|
33729439c5 | ||
|
|
20ce88a274 | ||
|
|
8595e467ce | ||
|
|
3392beb914 | ||
|
|
c6c6d3027c | ||
|
|
d19eb3903e | ||
|
|
f45d4145e6 | ||
|
|
937eb907d3 | ||
|
|
851c0e5cc0 | ||
|
|
07a94d4d2e | ||
|
|
e9157b3c1a | ||
|
|
5ae3b3f17e | ||
|
|
50ac76ce94 | ||
|
|
5aea044eb5 | ||
|
|
808dcaf1e2 | ||
|
|
777ba6ddf8 | ||
|
|
8ca57748ff | ||
|
|
30399c50e2 | ||
|
|
e6467bce7c | ||
|
|
8ca2bbc4e9 | ||
|
|
a74593419f | ||
|
|
f94802f2d2 | ||
|
|
d1c584b961 | ||
|
|
81f3d15665 | ||
|
|
99ad69450d | ||
|
|
087da66565 | ||
|
|
b84ae39978 | ||
|
|
391bb1268d |
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
@ -20,3 +20,21 @@ updates:
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] web-client (simple):"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "friday"
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] docker:"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "friday"
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] github-actions:"
|
||||
|
||||
59
.github/workflows/checker.yml
vendored
59
.github/workflows/checker.yml
vendored
@ -1,31 +1,46 @@
|
||||
name: "Checker"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
---
|
||||
name: Checker
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 4 * * 5"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
checker:
|
||||
name: Checker
|
||||
runs-on: ubuntu-24.04
|
||||
search:
|
||||
name: Search
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Ubuntu packages
|
||||
run: |
|
||||
sudo ./utils/searxng.sh install packages
|
||||
|
||||
- name: Set up Python
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
architecture: 'x64'
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
make V=1 install
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Checker
|
||||
run: |
|
||||
make search.checker
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Search checker
|
||||
run: make search.checker
|
||||
|
||||
90
.github/workflows/data-update.yml
vendored
90
.github/workflows/data-update.yml
vendored
@ -1,14 +1,27 @@
|
||||
name: "Update searx.data"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
---
|
||||
name: Update searx.data
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "59 23 28 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
updateData:
|
||||
name: Update data - ${{ matrix.fetch }}
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.repository_owner == 'searxng'}}
|
||||
data:
|
||||
if: github.repository_owner == 'searxng'
|
||||
name: ${{ matrix.fetch }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -20,48 +33,51 @@ jobs:
|
||||
- update_engine_traits.py
|
||||
- update_wikidata_units.py
|
||||
- update_engine_descriptions.py
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Ubuntu packages
|
||||
run: |
|
||||
sudo ./utils/searxng.sh install packages
|
||||
|
||||
- name: Set up Python
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
make V=1 install
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Fetch data
|
||||
env:
|
||||
FETCH_SCRIPT: ./searxng_extra/update/${{ matrix.fetch }}
|
||||
run: |
|
||||
V=1 ./manage pyenv.cmd python "$FETCH_SCRIPT"
|
||||
run: V=1 ./manage pyenv.cmd python "./searxng_extra/update/${{ matrix.fetch }}"
|
||||
|
||||
- name: Create Pull Request
|
||||
- name: Create PR
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: '[data] update searx.data - ${{ matrix.fetch }}'
|
||||
committer: searxng-bot <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: update_data_${{ matrix.fetch }}
|
||||
delete-branch: true
|
||||
draft: false
|
||||
title: '[data] update searx.data - ${{ matrix.fetch }}'
|
||||
body: |
|
||||
update searx.data - ${{ matrix.fetch }}
|
||||
author: "${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>"
|
||||
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
title: "[data] update searx.data - ${{ matrix.fetch }}"
|
||||
commit-message: "[data] update searx.data - ${{ matrix.fetch }}"
|
||||
branch: "update_data_${{ matrix.fetch }}"
|
||||
delete-branch: "true"
|
||||
draft: "false"
|
||||
signoff: "false"
|
||||
labels: |
|
||||
data
|
||||
|
||||
- name: Check outputs
|
||||
- name: Display information
|
||||
run: |
|
||||
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
|
||||
67
.github/workflows/documentation.yml
vendored
Normal file
67
.github/workflows/documentation.yml
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
name: Documentation
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Integration
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
||||
name: Release
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions:
|
||||
# for JamesIves/github-pages-deploy-action to push
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Build documentation
|
||||
run: make V=1 docs.clean docs.html
|
||||
|
||||
- name: Release
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
folder: "dist/docs"
|
||||
branch: "gh-pages"
|
||||
commit-message: "[doc] build from commit ${{ github.sha }}"
|
||||
# Automatically remove deleted files from the deploy branch
|
||||
clean: "true"
|
||||
single-commit: "true"
|
||||
43
.github/workflows/integration.yml
vendored
43
.github/workflows/integration.yml
vendored
@ -47,47 +47,6 @@ jobs:
|
||||
- name: Build themes
|
||||
run: make themes.all
|
||||
|
||||
documentation:
|
||||
name: Documentation
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write # for JamesIves/github-pages-deploy-action to push changes in repo
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
persist-credentials: false
|
||||
- name: Install Ubuntu packages
|
||||
run: sudo ./utils/searxng.sh install buildhost
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
- name: Cache Python dependencies
|
||||
id: cache-python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./local
|
||||
./.nvm
|
||||
./node_modules
|
||||
key: python-ubuntu-24.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||
- name: Build documentation
|
||||
run: |
|
||||
make V=1 docs.clean docs.html
|
||||
- name: Deploy
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: dist/docs
|
||||
CLEAN: true # Automatically remove deleted files from the deploy branch
|
||||
SINGLE_COMMIT: true
|
||||
COMMIT_MESSAGE: '[doc] build from commit ${{ github.sha }}'
|
||||
|
||||
babel:
|
||||
name: Update translations branch
|
||||
runs-on: ubuntu-24.04
|
||||
@ -95,7 +54,6 @@ jobs:
|
||||
needs:
|
||||
- python
|
||||
- themes
|
||||
- documentation
|
||||
permissions:
|
||||
contents: write # for make V=1 weblate.push.translations
|
||||
steps:
|
||||
@ -137,7 +95,6 @@ jobs:
|
||||
needs:
|
||||
- python
|
||||
- themes
|
||||
- documentation
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
51
.github/workflows/security.yml
vendored
51
.github/workflows/security.yml
vendored
@ -1,28 +1,43 @@
|
||||
name: "Security checks"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
---
|
||||
name: Security
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "42 05 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dockers:
|
||||
name: Trivy ${{ matrix.image }}
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
name: Container
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: 'searxng/searxng:latest'
|
||||
ignore-unfixed: false
|
||||
vuln-type: 'os,library'
|
||||
severity: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL'
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
- name: Run Trivy scanner
|
||||
uses: aquasecurity/trivy-action@0.30.0
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
image-ref: "docker.io/searxng/searxng:latest"
|
||||
vuln-type: "os,library"
|
||||
severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
|
||||
ignore-unfixed: "false"
|
||||
format: "sarif"
|
||||
output: "./trivy-results.sarif"
|
||||
|
||||
- name: Upload SARIFs
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: "./trivy-results.sarif"
|
||||
|
||||
179
Dockerfile
179
Dockerfile
@ -1,92 +1,107 @@
|
||||
FROM alpine:3.20
|
||||
ENTRYPOINT ["/sbin/tini","--","/usr/local/searxng/dockerfiles/docker-entrypoint.sh"]
|
||||
EXPOSE 8080
|
||||
VOLUME /etc/searxng
|
||||
FROM docker.io/library/python:3.13-slim AS builder
|
||||
|
||||
ARG SEARXNG_GID=977
|
||||
ARG SEARXNG_UID=977
|
||||
|
||||
RUN addgroup -g ${SEARXNG_GID} searxng && \
|
||||
adduser -u ${SEARXNG_UID} -D -h /usr/local/searxng -s /bin/sh -G searxng searxng
|
||||
|
||||
ENV INSTANCE_NAME=searxng \
|
||||
AUTOCOMPLETE= \
|
||||
BASE_URL= \
|
||||
BIND_ADDRESS=[::]:8080 \
|
||||
MORTY_KEY= \
|
||||
MORTY_URL= \
|
||||
SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml \
|
||||
UWSGI_SETTINGS_PATH=/etc/searxng/uwsgi.ini \
|
||||
UWSGI_WORKERS=%k \
|
||||
UWSGI_THREADS=4
|
||||
|
||||
WORKDIR /usr/local/searxng
|
||||
|
||||
COPY requirements.txt ./requirements.txt
|
||||
|
||||
RUN apk add --no-cache -t build-dependencies \
|
||||
build-base \
|
||||
py3-setuptools \
|
||||
python3-dev \
|
||||
libffi-dev \
|
||||
libxslt-dev \
|
||||
libxml2-dev \
|
||||
openssl-dev \
|
||||
tar \
|
||||
git \
|
||||
&& apk add --no-cache \
|
||||
ca-certificates \
|
||||
python3 \
|
||||
py3-pip \
|
||||
libxml2 \
|
||||
libxslt \
|
||||
openssl \
|
||||
tini \
|
||||
uwsgi \
|
||||
uwsgi-python3 \
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
brotli \
|
||||
&& pip3 install --break-system-packages --no-cache -r requirements.txt \
|
||||
&& apk del build-dependencies \
|
||||
&& rm -rf /root/.cache
|
||||
# lxml
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
zlib1g-dev \
|
||||
# uwsgi
|
||||
libpcre3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --chown=searxng:searxng dockerfiles ./dockerfiles
|
||||
COPY --chown=searxng:searxng searx ./searx
|
||||
WORKDIR /usr/local/searxng/
|
||||
|
||||
COPY ./requirements.txt ./requirements.txt
|
||||
|
||||
# Readd on #4707 "--mount=type=cache,id=pip,target=/root/.cache/pip"
|
||||
RUN python -m venv ./venv \
|
||||
&& . ./venv/bin/activate \
|
||||
&& pip install -r requirements.txt \
|
||||
&& pip install "uwsgi~=2.0"
|
||||
|
||||
COPY ./searx/ ./searx/
|
||||
|
||||
ARG TIMESTAMP_SETTINGS=0
|
||||
ARG TIMESTAMP_UWSGI=0
|
||||
ARG VERSION_GITCOMMIT=unknown
|
||||
|
||||
RUN su searxng -c "/usr/bin/python3 -m compileall -q searx" \
|
||||
&& touch -c --date=@${TIMESTAMP_SETTINGS} searx/settings.yml \
|
||||
&& touch -c --date=@${TIMESTAMP_UWSGI} dockerfiles/uwsgi.ini \
|
||||
&& find /usr/local/searxng/searx/static -a \( -name '*.html' -o -name '*.css' -o -name '*.js' \
|
||||
-o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \
|
||||
-type f -exec gzip -9 -k {} \+ -exec brotli --best {} \+
|
||||
RUN python -m compileall -q searx \
|
||||
&& touch -c --date=@$TIMESTAMP_SETTINGS ./searx/settings.yml \
|
||||
&& touch -c --date=@$TIMESTAMP_UWSGI ./dockerfiles/uwsgi.ini \
|
||||
&& find /usr/local/searxng/searx/static \
|
||||
\( -name '*.html' -o -name '*.css' -o -name '*.js' -o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \
|
||||
-type f -exec gzip -9 -k {} + -exec brotli --best {} +
|
||||
|
||||
ARG SEARXNG_UID=977
|
||||
ARG SEARXNG_GID=977
|
||||
|
||||
RUN grep -m1 root /etc/group > /tmp/.searxng.group \
|
||||
&& grep -m1 root /etc/passwd > /tmp/.searxng.passwd \
|
||||
&& echo "searxng:x:$SEARXNG_GID:" >> /tmp/.searxng.group \
|
||||
&& echo "searxng:x:$SEARXNG_UID:$SEARXNG_GID:searxng:/usr/local/searxng:/bin/bash" >> /tmp/.searxng.passwd
|
||||
|
||||
FROM docker.io/library/python:3.13-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
# healthcheck
|
||||
wget \
|
||||
# lxml (ARMv7)
|
||||
libxslt1.1 \
|
||||
# uwsgi
|
||||
libpcre3 \
|
||||
libxml2 \
|
||||
mailcap \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --chown=root:root --from=builder /tmp/.searxng.passwd /etc/passwd
|
||||
COPY --chown=root:root --from=builder /tmp/.searxng.group /etc/group
|
||||
|
||||
ARG LABEL_DATE="0001-01-01T00:00:00Z"
|
||||
ARG GIT_URL="unspecified"
|
||||
ARG SEARXNG_GIT_VERSION="unspecified"
|
||||
ARG LABEL_VCS_REF="unspecified"
|
||||
ARG LABEL_VCS_URL="unspecified"
|
||||
|
||||
WORKDIR /usr/local/searxng/
|
||||
|
||||
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/venv/ ./venv/
|
||||
COPY --chown=searxng:searxng --from=builder /usr/local/searxng/searx/ ./searx/
|
||||
COPY --chown=searxng:searxng ./dockerfiles/ ./dockerfiles/
|
||||
|
||||
LABEL org.opencontainers.image.authors="searxng <$GIT_URL>" \
|
||||
org.opencontainers.image.created=$LABEL_DATE \
|
||||
org.opencontainers.image.description="A privacy-respecting, hackable metasearch engine" \
|
||||
org.opencontainers.image.documentation="https://github.com/searxng/searxng-docker" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
org.opencontainers.image.revision=$LABEL_VCS_REF \
|
||||
org.opencontainers.image.source=$LABEL_VCS_URL \
|
||||
org.opencontainers.image.title="searxng" \
|
||||
org.opencontainers.image.url=$LABEL_VCS_URL \
|
||||
org.opencontainers.image.version=$SEARXNG_GIT_VERSION
|
||||
|
||||
ENV CONFIG_PATH=/etc/searxng \
|
||||
DATA_PATH=/var/cache/searxng
|
||||
|
||||
ENV SEARXNG_VERSION=$SEARXNG_GIT_VERSION \
|
||||
INSTANCE_NAME=searxng \
|
||||
AUTOCOMPLETE="" \
|
||||
BASE_URL="" \
|
||||
BIND_ADDRESS=[::]:8080 \
|
||||
MORTY_KEY="" \
|
||||
MORTY_URL="" \
|
||||
SEARXNG_SETTINGS_PATH=$CONFIG_PATH/settings.yml \
|
||||
UWSGI_SETTINGS_PATH=$CONFIG_PATH/uwsgi.ini \
|
||||
UWSGI_WORKERS=%k \
|
||||
UWSGI_THREADS=4
|
||||
|
||||
VOLUME $CONFIG_PATH
|
||||
VOLUME $DATA_PATH
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:8080/healthz || exit 1
|
||||
|
||||
# Keep these arguments at the end to prevent redundant layer rebuilds
|
||||
ARG LABEL_DATE=
|
||||
ARG GIT_URL=unknown
|
||||
ARG SEARXNG_GIT_VERSION=unknown
|
||||
ARG SEARXNG_DOCKER_TAG=unknown
|
||||
ARG LABEL_VCS_REF=
|
||||
ARG LABEL_VCS_URL=
|
||||
LABEL maintainer="searxng <${GIT_URL}>" \
|
||||
description="A privacy-respecting, hackable metasearch engine." \
|
||||
version="${SEARXNG_GIT_VERSION}" \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.name="searxng" \
|
||||
org.label-schema.version="${SEARXNG_GIT_VERSION}" \
|
||||
org.label-schema.url="${LABEL_VCS_URL}" \
|
||||
org.label-schema.vcs-ref=${LABEL_VCS_REF} \
|
||||
org.label-schema.vcs-url=${LABEL_VCS_URL} \
|
||||
org.label-schema.build-date="${LABEL_DATE}" \
|
||||
org.label-schema.usage="https://github.com/searxng/searxng-docker" \
|
||||
org.opencontainers.image.title="searxng" \
|
||||
org.opencontainers.image.version="${SEARXNG_DOCKER_TAG}" \
|
||||
org.opencontainers.image.url="${LABEL_VCS_URL}" \
|
||||
org.opencontainers.image.revision=${LABEL_VCS_REF} \
|
||||
org.opencontainers.image.source=${LABEL_VCS_URL} \
|
||||
org.opencontainers.image.created="${LABEL_DATE}" \
|
||||
org.opencontainers.image.documentation="https://github.com/searxng/searxng-docker"
|
||||
ENTRYPOINT ["/usr/local/searxng/dockerfiles/docker-entrypoint.sh"]
|
||||
|
||||
507
client/simple/package-lock.json
generated
507
client/simple/package-lock.json
generated
@ -11,30 +11,30 @@
|
||||
"autocomplete-js": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@eslint/js": "^9.25.1",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"edge.js": "^6.2.1",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint": "^9.25.1",
|
||||
"filemanager-webpack-plugin": "^8.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"ionicons": "^7.4.0",
|
||||
"ionicons": "^8.0.8",
|
||||
"leaflet": "^1.9.4",
|
||||
"less": "^4.3.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"less-loader": "^12.3.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"sharp": "^0.34.1",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^16.17.0",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-standard-less": "^3.0.1",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^3.3.2",
|
||||
"swiped-events": "^1.2.0",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-static-copy": "^2.3.0",
|
||||
"vite": "^6.3.4",
|
||||
"vite-plugin-static-copy": "^2.3.1",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"webpack": "^5.99.5",
|
||||
"webpack": "^5.99.7",
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
},
|
||||
@ -668,9 +668,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz",
|
||||
"integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==",
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
|
||||
"integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@ -678,9 +678,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
|
||||
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -728,9 +728,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz",
|
||||
"integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
|
||||
"version": "9.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
|
||||
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -748,13 +748,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
|
||||
"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.12.0",
|
||||
"@eslint/core": "^0.13.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -1486,9 +1486,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz",
|
||||
"integrity": "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
|
||||
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -1500,9 +1500,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz",
|
||||
"integrity": "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz",
|
||||
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1514,9 +1514,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz",
|
||||
"integrity": "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz",
|
||||
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1528,9 +1528,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz",
|
||||
"integrity": "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz",
|
||||
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1542,9 +1542,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz",
|
||||
"integrity": "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz",
|
||||
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1556,9 +1556,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz",
|
||||
"integrity": "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz",
|
||||
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1570,9 +1570,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz",
|
||||
"integrity": "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz",
|
||||
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -1584,9 +1584,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz",
|
||||
"integrity": "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz",
|
||||
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -1598,9 +1598,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz",
|
||||
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1612,9 +1612,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz",
|
||||
"integrity": "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz",
|
||||
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1626,9 +1626,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz",
|
||||
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -1640,9 +1640,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz",
|
||||
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -1654,9 +1654,23 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz",
|
||||
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz",
|
||||
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -1668,9 +1682,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz",
|
||||
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -1682,9 +1696,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz",
|
||||
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1696,9 +1710,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz",
|
||||
"integrity": "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz",
|
||||
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1710,9 +1724,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz",
|
||||
"integrity": "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz",
|
||||
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1724,9 +1738,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz",
|
||||
"integrity": "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz",
|
||||
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -1738,9 +1752,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz",
|
||||
"integrity": "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz",
|
||||
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1752,9 +1766,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.26.0.tgz",
|
||||
"integrity": "sha512-+0Inu+dJ9/LgWSskcZwx7v17v4GILcwIYxNgD+OuK0U+D5z61WsxWw7yHkYG5OqGPBijsJMVssYRx/Tn+e7F9A==",
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.31.0.tgz",
|
||||
"integrity": "sha512-Ei9MFJ6LPD9BMFs+klkHylbVOOYhG10Jv4bvoFf3GMH15kA41rSYkEdr4DiX84ZdErQE2qtFV/2SUyWoXh0AhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -1763,8 +1777,130 @@
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-darwin-arm64": "4.34.9",
|
||||
"@rollup/rollup-darwin-x64": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-musl": "4.34.9",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.34.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
|
||||
"integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
|
||||
"integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
|
||||
"integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
|
||||
"integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
|
||||
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
|
||||
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@stencil/core/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
@ -1815,9 +1951,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -2507,20 +2643,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable": {
|
||||
"version": "1.8.9",
|
||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.9.tgz",
|
||||
"integrity": "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ==",
|
||||
"version": "1.8.10",
|
||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.10.tgz",
|
||||
"integrity": "sha512-0ZnbicB/N2R6uziva8l6O6BieBklArWyiGx4GkwAhLKhSHyQtRfM9T1nx7HHuHDKkYB/efJQhz3QJ6x/YqoZzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hookified": "^1.7.1",
|
||||
"keyv": "^5.3.1"
|
||||
"hookified": "^1.8.1",
|
||||
"keyv": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable/node_modules/keyv": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.2.tgz",
|
||||
"integrity": "sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz",
|
||||
"integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -3440,20 +3576,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.24.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz",
|
||||
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
|
||||
"version": "9.25.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz",
|
||||
"integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.20.0",
|
||||
"@eslint/config-helpers": "^0.2.0",
|
||||
"@eslint/core": "^0.12.0",
|
||||
"@eslint/config-helpers": "^0.2.1",
|
||||
"@eslint/core": "^0.13.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.24.0",
|
||||
"@eslint/plugin-kit": "^0.2.7",
|
||||
"@eslint/js": "9.25.1",
|
||||
"@eslint/plugin-kit": "^0.2.8",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
@ -4018,9 +4154,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hookified": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.1.tgz",
|
||||
"integrity": "sha512-GrO2l93P8xCWBSTBX9l2BxI78VU/MAAYag+pG8curS3aBGy0++ZlxrQ7PdUOUVMbn5BwkGb6+eRrnf43ipnFEA==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.2.tgz",
|
||||
"integrity": "sha512-5nZbBNP44sFCDjSoB//0N7m508APCgbQ4mGGo1KJGBYyCKNHfry1Pvd0JVHZIxjdnqn8nFRBAN/eFB6Rk/4w5w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -4203,13 +4339,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
|
||||
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
|
||||
"version": "8.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.8.tgz",
|
||||
"integrity": "sha512-CRHhDQA5vsNxC7raeEPqddgO90iR2F13VZS7hB0Vx7JrxJMM2060E3ddgYbpMQNJjzeBsgpkNwdBeK5qQ7RbLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.0.3"
|
||||
"@stencil/core": "^4.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
@ -4472,9 +4608,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/known-css-properties": {
|
||||
"version": "0.35.0",
|
||||
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz",
|
||||
"integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
|
||||
"version": "0.36.0",
|
||||
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz",
|
||||
"integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -4559,9 +4695,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/less-loader": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz",
|
||||
"integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==",
|
||||
"version": "12.3.0",
|
||||
"resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.3.0.tgz",
|
||||
"integrity": "sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -5623,13 +5759,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.7.tgz",
|
||||
"integrity": "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==",
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
|
||||
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.6"
|
||||
"@types/estree": "1.0.7"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@ -5639,25 +5775,26 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.34.7",
|
||||
"@rollup/rollup-android-arm64": "4.34.7",
|
||||
"@rollup/rollup-darwin-arm64": "4.34.7",
|
||||
"@rollup/rollup-darwin-x64": "4.34.7",
|
||||
"@rollup/rollup-freebsd-arm64": "4.34.7",
|
||||
"@rollup/rollup-freebsd-x64": "4.34.7",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.34.7",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.34.7",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.34.7",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-x64-musl": "4.34.7",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.34.7",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.34.7",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.34.7",
|
||||
"@rollup/rollup-android-arm-eabi": "4.40.0",
|
||||
"@rollup/rollup-android-arm64": "4.40.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.40.0",
|
||||
"@rollup/rollup-darwin-x64": "4.40.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.40.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.40.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.40.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.40.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.40.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.40.0",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.40.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.40.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.40.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.40.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.40.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.40.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.40.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.40.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.40.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@ -5733,9 +5870,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
|
||||
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -6083,9 +6220,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint": {
|
||||
"version": "16.18.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.18.0.tgz",
|
||||
"integrity": "sha512-OXb68qzesv7J70BSbFwfK3yTVLEVXiQ/ro6wUE4UrSbKCMjLLA02S8Qq3LC01DxKyVjk7z8xh35aB4JzO3/sNA==",
|
||||
"version": "16.19.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.19.1.tgz",
|
||||
"integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -6112,7 +6249,7 @@
|
||||
"debug": "^4.3.7",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"file-entry-cache": "^10.0.7",
|
||||
"file-entry-cache": "^10.0.8",
|
||||
"global-modules": "^2.0.0",
|
||||
"globby": "^11.1.0",
|
||||
"globjoin": "^0.1.4",
|
||||
@ -6120,7 +6257,7 @@
|
||||
"ignore": "^7.0.3",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"known-css-properties": "^0.35.0",
|
||||
"known-css-properties": "^0.36.0",
|
||||
"mathml-tag-names": "^2.1.3",
|
||||
"meow": "^13.2.0",
|
||||
"micromatch": "^4.0.8",
|
||||
@ -6337,25 +6474,25 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stylelint/node_modules/file-entry-cache": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.7.tgz",
|
||||
"integrity": "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw==",
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.8.tgz",
|
||||
"integrity": "sha512-FGXHpfmI4XyzbLd3HQ8cbUcsFGohJpZtmQRHr8z8FxxtCe2PcpgIlVLwIgunqjvRmXypBETvwhV4ptJizA+Y1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flat-cache": "^6.1.7"
|
||||
"flat-cache": "^6.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/flat-cache": {
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.7.tgz",
|
||||
"integrity": "sha512-qwZ4xf1v1m7Rc9XiORly31YaChvKt6oNVHuqqZcoED/7O+ToyNVGobKsIAopY9ODcWpEDKEBAbrSOCBHtNQvew==",
|
||||
"version": "6.1.8",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.8.tgz",
|
||||
"integrity": "sha512-R6MaD3nrJAtO7C3QOuS79ficm2pEAy++TgEUD8ii1LVlbcgZ9DtASLkt9B+RZSFCzm7QHDMlXPsqqB6W2Pfr1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cacheable": "^1.8.9",
|
||||
"flatted": "^3.3.3",
|
||||
"hookified": "^1.7.1"
|
||||
"hookified": "^1.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/globby": {
|
||||
@ -6642,13 +6779,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
|
||||
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.3",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -6659,9 +6796,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
|
||||
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@ -6792,15 +6929,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
|
||||
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
|
||||
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.30.1"
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@ -6864,9 +7004,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz",
|
||||
"integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.1.tgz",
|
||||
"integrity": "sha512-EfsPcBm3ewg3UMG8RJaC0ADq6/qnUZnokXx4By4+2cAcipjT9i0Y0owIJGqmZI7d6nxk4qB1q5aXOwNuSyPdyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -6943,6 +7083,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||
@ -6958,14 +7126,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.99.5",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz",
|
||||
"integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==",
|
||||
"version": "5.99.7",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz",
|
||||
"integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
@ -6982,7 +7151,7 @@
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.0",
|
||||
"schema-utils": "^4.3.2",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.1",
|
||||
|
||||
@ -9,30 +9,30 @@
|
||||
"icons.html": "node theme_icons.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@eslint/js": "^9.25.1",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"edge.js": "^6.2.1",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint": "^9.25.1",
|
||||
"filemanager-webpack-plugin": "^8.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"ionicons": "^7.4.0",
|
||||
"ionicons": "^8.0.8",
|
||||
"leaflet": "^1.9.4",
|
||||
"less": "^4.3.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"less-loader": "^12.3.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"sharp": "^0.34.1",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^16.17.0",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-standard-less": "^3.0.1",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^3.3.2",
|
||||
"swiped-events": "^1.2.0",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-static-copy": "^2.3.0",
|
||||
"vite": "^6.3.4",
|
||||
"vite-plugin-static-copy": "^2.3.1",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"webpack": "^5.99.5",
|
||||
"webpack": "^5.99.7",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -51,11 +51,13 @@ function jinja_svg_catalog(dest, macros, items) {
|
||||
(item) => {
|
||||
|
||||
/** @type {import("svgo").Config} */
|
||||
// JSON.stringify & JSON.parse are used to create a deep copy of the
|
||||
// item.svgo_opts object
|
||||
const svgo_opts = JSON.parse(JSON.stringify(item.svgo_opts));
|
||||
svgo_opts.plugins.push({
|
||||
name: "addAttributesToSVGElement",
|
||||
name: "addClassesToSVGElement",
|
||||
params: {
|
||||
attributes: [{ "class": __jinja_class_placeholder__, }]
|
||||
classNames: [__jinja_class_placeholder__]
|
||||
}}
|
||||
);
|
||||
|
||||
|
||||
@ -43,15 +43,7 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
get_searxng_version(){
|
||||
su searxng -c \
|
||||
'python3 -c "import six; import searx.version; six.print_(searx.version.VERSION_STRING)"' \
|
||||
2>/dev/null
|
||||
}
|
||||
|
||||
SEARXNG_VERSION="$(get_searxng_version)"
|
||||
export SEARXNG_VERSION
|
||||
echo "SearXNG version ${SEARXNG_VERSION}"
|
||||
echo "SearXNG version $SEARXNG_VERSION"
|
||||
|
||||
# helpers to update the configuration files
|
||||
patch_uwsgi_settings() {
|
||||
@ -76,7 +68,7 @@ patch_searxng_settings() {
|
||||
-e "s|base_url: false|base_url: ${BASE_URL}|g" \
|
||||
-e "s/instance_name: \"SearXNG\"/instance_name: \"${INSTANCE_NAME}\"/g" \
|
||||
-e "s/autocomplete: \"\"/autocomplete: \"${AUTOCOMPLETE}\"/g" \
|
||||
-e "s/ultrasecretkey/$(openssl rand -hex 32)/g" \
|
||||
-e "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" \
|
||||
"${CONF}"
|
||||
|
||||
# Morty configuration
|
||||
@ -172,4 +164,4 @@ printf 'Listen on %s\n' "${BIND_ADDRESS}"
|
||||
|
||||
# Start uwsgi
|
||||
# TODO: "--http-socket" will be removed in the future (see uwsgi.ini.new config file): https://github.com/searxng/searxng/pull/4578
|
||||
exec uwsgi --http-socket "${BIND_ADDRESS}" "${UWSGI_SETTINGS_PATH}"
|
||||
exec /usr/local/searxng/venv/bin/uwsgi --http-socket "${BIND_ADDRESS}" "${UWSGI_SETTINGS_PATH}"
|
||||
|
||||
@ -21,7 +21,6 @@ chmod-socket = 666
|
||||
# Plugin to use and interpreter config
|
||||
single-interpreter = true
|
||||
master = true
|
||||
plugin = python3
|
||||
lazy-apps = true
|
||||
enable-threads = true
|
||||
|
||||
|
||||
@ -96,7 +96,7 @@ Modify the ``/etc/searxng/settings.yml`` to your needs:
|
||||
|
||||
.. literalinclude:: ../../utils/templates/etc/searxng/settings.yml
|
||||
:language: yaml
|
||||
:end-before: # hostnames:
|
||||
:end-before: # preferences:
|
||||
|
||||
To see the entire file jump to :origin:`utils/templates/etc/searxng/settings.yml`
|
||||
|
||||
|
||||
@ -148,6 +148,8 @@ engine is shown. Most of the options have a default value or even are optional.
|
||||
``display_error_messages`` : default ``true``
|
||||
When an engine returns an error, the message is displayed on the user interface.
|
||||
|
||||
.. _engine network:
|
||||
|
||||
``network`` : optional
|
||||
Use the network configuration from another engine.
|
||||
In addition, there are two default networks:
|
||||
@ -257,4 +259,3 @@ Example configuration in settings.yml for a German and English speaker:
|
||||
|
||||
When searching, the default google engine will return German results and
|
||||
"google english" will return English results.
|
||||
|
||||
|
||||
@ -16,8 +16,14 @@
|
||||
open_metrics: ''
|
||||
|
||||
``debug`` : ``$SEARXNG_DEBUG``
|
||||
Allow a more detailed log if you run SearXNG directly. Display *detailed* error
|
||||
messages in the browser too, so this must be deactivated in production.
|
||||
In debug mode, the server provides an interactive debugger, will reload when
|
||||
code is changed and activates a verbose logging.
|
||||
|
||||
.. attention::
|
||||
|
||||
The debug setting is intended for local development server. Don't
|
||||
activate debug (don't use a development server) when deploying to
|
||||
production.
|
||||
|
||||
``donation_url`` :
|
||||
Set value to ``true`` to use your own donation page written in the
|
||||
|
||||
@ -57,11 +57,11 @@
|
||||
|
||||
``default_lang``:
|
||||
Default search language - leave blank to detect from browser information or
|
||||
use codes from :origin:`searx/languages.py`.
|
||||
use codes from :origin:`searx/sxng_locales.py`.
|
||||
|
||||
``languages``:
|
||||
List of available languages - leave unset to use all codes from
|
||||
:origin:`searx/languages.py`. Otherwise list codes of available languages.
|
||||
:origin:`searx/sxng_locales.py`. Otherwise list codes of available languages.
|
||||
The ``all`` value is shown as the ``Default language`` in the user interface
|
||||
(in most cases, it is meant to send the query without a language parameter ;
|
||||
in some cases, it means the English language) Example:
|
||||
|
||||
@ -14,13 +14,14 @@
|
||||
limiter: false
|
||||
public_instance: false
|
||||
image_proxy: false
|
||||
method: "POST"
|
||||
default_http_headers:
|
||||
X-Content-Type-Options : nosniff
|
||||
X-Download-Options : noopen
|
||||
X-Robots-Tag : noindex, nofollow
|
||||
Referrer-Policy : no-referrer
|
||||
|
||||
``base_url`` : ``$SEARXNG_URL``
|
||||
``base_url`` : ``$SEARXNG_BASE_URL``
|
||||
The base URL where SearXNG is deployed. Used to create correct inbound links.
|
||||
|
||||
``port`` & ``bind_address``: ``$SEARXNG_PORT`` & ``$SEARXNG_BIND_ADDRESS``
|
||||
@ -28,6 +29,8 @@
|
||||
directly using ``python searx/webapp.py``. Doesn't apply to a SearXNG
|
||||
services running behind a proxy and using socket communications.
|
||||
|
||||
.. _server.secret_key:
|
||||
|
||||
``secret_key`` : ``$SEARXNG_SECRET``
|
||||
Used for cryptography purpose.
|
||||
|
||||
@ -50,6 +53,11 @@
|
||||
``image_proxy`` : ``$SEARXNG_IMAGE_PROXY``
|
||||
Allow your instance of SearXNG of being able to proxy images. Uses memory space.
|
||||
|
||||
.. _method:
|
||||
|
||||
``method`` : ``$SEARXNG_METHOD``
|
||||
Whether to use ``GET`` or ``POST`` HTTP method when searching.
|
||||
|
||||
.. _HTTP headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
|
||||
|
||||
``default_http_headers`` :
|
||||
|
||||
@ -27,23 +27,24 @@ Privacy-by-design
|
||||
-----------------
|
||||
|
||||
SearXNG was born out of the need for a **privacy-respecting** search tool which
|
||||
can be extended easily to maximize both, its search and its privacy protecting
|
||||
can be extended easily to maximize both its search and its privacy protecting
|
||||
capabilities.
|
||||
|
||||
A few widely used features work differently or turned off by default or not
|
||||
implemented at all **as a consequence of privacy-by-design**.
|
||||
Some widely used search engine features may work differently,
|
||||
may be turned off by default, or may not be implemented at all in SearXNG
|
||||
**as a consequence of a privacy-by-design approach**.
|
||||
|
||||
If a feature reduces the privacy preserving aspects of searx, it should be
|
||||
switched off by default or should not implemented at all. There are plenty of
|
||||
search engines already providing such features. If a feature reduces the
|
||||
protection of searx, users must be informed about the effect of choosing to
|
||||
enable it. Features that protect privacy but differ from the expectations of
|
||||
the user should also be explained.
|
||||
Following this approach, features reducing the privacy preserving aspects of SearXNG should be
|
||||
switched off by default or should not be implemented at all. There are plenty of
|
||||
search engines already providing such features. If a feature reduces
|
||||
SearXNG's efficacy in protecting a user's privacy, the user must be informed about
|
||||
the effect of choosing to enable it. Features that protect privacy but differ from the
|
||||
expectations of the user should also be carefully explained to them.
|
||||
|
||||
Also, if you think that something works weird with searx, it's might be because
|
||||
of the tool you use is designed in a way to interfere with the privacy respect.
|
||||
Submitting a bugreport to the vendor of the tool that misbehaves might be a good
|
||||
feedback to reconsider the disrespect to its customers (e.g. ``GET`` vs ``POST``
|
||||
Also, if you think that something works weird with SearXNG, it might be because
|
||||
the tool you are using is designed in a way that interferes with SearXNG's privacy aspects.
|
||||
Submitting a bug report to the vendor of the tool that misbehaves might be a good
|
||||
feedback for them to reconsider the disrespect to their customers (e.g., ``GET`` vs ``POST``
|
||||
requests in various browsers).
|
||||
|
||||
Remember the other prime directive of SearXNG is to be hackable, so if the above
|
||||
@ -134,7 +135,7 @@ Here is an example which makes a complete rebuild:
|
||||
|
||||
.. _make docs.live:
|
||||
|
||||
live build
|
||||
Live build
|
||||
----------
|
||||
|
||||
.. _sphinx-autobuild:
|
||||
@ -145,8 +146,8 @@ live build
|
||||
It is recommended to assert a complete rebuild before deploying (use
|
||||
``docs.clean``).
|
||||
|
||||
Live build is like WYSIWYG. If you want to edit the documentation, its
|
||||
recommended to use. The Makefile target ``docs.live`` builds the docs, opens
|
||||
Live build is like WYSIWYG. It's the recommended way to go if you want to edit the documentation.
|
||||
The Makefile target ``docs.live`` builds the docs, opens
|
||||
URL in your favorite browser and rebuilds every time a reST file has been
|
||||
changed (:ref:`make docs.clean`).
|
||||
|
||||
@ -159,9 +160,9 @@ changed (:ref:`make docs.clean`).
|
||||
... Start watching changes
|
||||
|
||||
Live builds are implemented by sphinx-autobuild_. Use environment
|
||||
``$(SPHINXOPTS)`` to pass arguments to the sphinx-autobuild_ command. Except
|
||||
option ``--host`` (which is always set to ``0.0.0.0``) you can pass any
|
||||
argument. E.g to find and use a free port, use:
|
||||
``$(SPHINXOPTS)`` to pass arguments to the sphinx-autobuild_ command. You can
|
||||
pass any argument except for the ``--host`` option (which is always set to ``0.0.0.0``).
|
||||
E.g., to find and use a free port, use:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
|
||||
@ -4,19 +4,13 @@
|
||||
Engine Library
|
||||
==============
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
:local:
|
||||
:backlinks: entry
|
||||
|
||||
.. automodule:: searx.enginelib
|
||||
:members:
|
||||
:members:
|
||||
|
||||
.. _searx.enginelib.traits:
|
||||
|
||||
|
||||
Engine traits
|
||||
=============
|
||||
|
||||
.. automodule:: searx.enginelib.traits
|
||||
:members:
|
||||
:members:
|
||||
|
||||
@ -29,7 +29,7 @@ Programming Interface
|
||||
parameter. This function can be omitted, if there is no need to setup anything
|
||||
in advance.
|
||||
|
||||
:py:func:`search(query, params) <searx.engines.demo_offline.searc>`
|
||||
:py:func:`search(query, params) <searx.engines.demo_offline.search>`
|
||||
Each offline engine has a function named ``search``. This function is
|
||||
responsible to perform a search and return the results in a presentable
|
||||
format. (Where *presentable* means presentable by the selected result
|
||||
|
||||
8
docs/dev/engines/online/chinaso.rst
Normal file
8
docs/dev/engines/online/chinaso.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _chinaso engine:
|
||||
|
||||
=======
|
||||
ChinaSo
|
||||
=======
|
||||
|
||||
.. automodule:: searx.engines.chinaso
|
||||
:members:
|
||||
8
docs/dev/engines/online/huggingface.rst
Normal file
8
docs/dev/engines/online/huggingface.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _huggingface engine:
|
||||
|
||||
============
|
||||
Hugging Face
|
||||
============
|
||||
|
||||
.. automodule:: searx.engines.huggingface
|
||||
:members:
|
||||
@ -4,10 +4,5 @@
|
||||
Mullvad-Leta
|
||||
============
|
||||
|
||||
.. contents:: Contents
|
||||
:depth: 2
|
||||
:local:
|
||||
:backlinks: entry
|
||||
|
||||
.. automodule:: searx.engines.mullvad_leta
|
||||
:members:
|
||||
|
||||
@ -6,7 +6,7 @@ Simple Theme Templates
|
||||
|
||||
The simple template is complex, it consists of many different elements and also
|
||||
uses macros and include statements. The following is a rough overview that we
|
||||
would like to give the developerat hand, details must still be taken from the
|
||||
would like to give the developer at hand, details must still be taken from the
|
||||
:origin:`sources <searx/templates/simple/>`.
|
||||
|
||||
A :ref:`result item <result types>` can be of different media types. The media
|
||||
|
||||
@ -53,6 +53,9 @@ Probe HTTP headers
|
||||
.. automodule:: searx.botdetection.http_user_agent
|
||||
:members:
|
||||
|
||||
.. automodule:: searx.botdetection.http_sec_fetch
|
||||
:members:
|
||||
|
||||
.. _botdetection config:
|
||||
|
||||
Config
|
||||
|
||||
8
docs/src/searx.cache.rst
Normal file
8
docs/src/searx.cache.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _searx.cache:
|
||||
|
||||
======
|
||||
Caches
|
||||
======
|
||||
|
||||
.. automodule:: searx.cache
|
||||
:members:
|
||||
@ -2,7 +2,7 @@ mock==5.2.0
|
||||
nose2[coverage_plugin]==0.15.1
|
||||
cov-core==1.15.0
|
||||
black==24.3.0
|
||||
pylint==3.3.6
|
||||
pylint==3.3.7
|
||||
splinter==0.21.0
|
||||
selenium==4.31.0
|
||||
Pallets-Sphinx-Themes==2.3.0
|
||||
@ -16,7 +16,7 @@ sphinx-notfound-page==1.1.0
|
||||
myst-parser==3.0.1
|
||||
linuxdoc==20240924
|
||||
aiounittest==1.5.0
|
||||
yamllint==1.37.0
|
||||
yamllint==1.37.1
|
||||
wlc==1.15
|
||||
coloredlogs==15.0.1
|
||||
docutils>=0.21.2
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
certifi==2025.1.31
|
||||
certifi==2025.4.26
|
||||
babel==2.17.0
|
||||
flask-babel==4.0.0
|
||||
flask==3.1.0
|
||||
jinja2==3.1.6
|
||||
lxml==5.3.2
|
||||
lxml==5.4.0
|
||||
pygments==2.19.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pyyaml==6.0.2
|
||||
@ -11,11 +11,11 @@ httpx[http2]==0.24.1
|
||||
Brotli==1.1.0
|
||||
uvloop==0.21.0
|
||||
httpx-socks[asyncio]==0.7.7
|
||||
setproctitle==1.3.5
|
||||
setproctitle==1.3.6
|
||||
redis==5.0.8
|
||||
markdown-it-py==3.0.0
|
||||
fasttext-predict==0.9.2.4
|
||||
tomli==2.2.1; python_version < '3.11'
|
||||
msgspec==0.19.0
|
||||
typer-slim==0.15.2
|
||||
typer-slim==0.15.3
|
||||
isodate==0.7.2
|
||||
|
||||
@ -22,18 +22,18 @@ searx_dir = abspath(dirname(__file__))
|
||||
searx_parent_dir = abspath(dirname(dirname(__file__)))
|
||||
|
||||
settings = {}
|
||||
searx_debug = False
|
||||
sxng_debug = False
|
||||
logger = logging.getLogger('searx')
|
||||
|
||||
_unset = object()
|
||||
|
||||
|
||||
def init_settings():
|
||||
"""Initialize global ``settings`` and ``searx_debug`` variables and
|
||||
"""Initialize global ``settings`` and ``sxng_debug`` variables and
|
||||
``logger`` from ``SEARXNG_SETTINGS_PATH``.
|
||||
"""
|
||||
|
||||
global settings, searx_debug # pylint: disable=global-variable-not-assigned
|
||||
global settings, sxng_debug # pylint: disable=global-variable-not-assigned
|
||||
|
||||
cfg, msg = searx.settings_loader.load_settings(load_user_settings=True)
|
||||
cfg = cfg or {}
|
||||
@ -42,8 +42,8 @@ def init_settings():
|
||||
settings.clear()
|
||||
settings.update(cfg)
|
||||
|
||||
searx_debug = settings['general']['debug']
|
||||
if searx_debug:
|
||||
sxng_debug = get_setting("general.debug")
|
||||
if sxng_debug:
|
||||
_logging_config_debug()
|
||||
else:
|
||||
logging.basicConfig(level=LOG_LEVEL_PROD, format=LOG_FORMAT_PROD)
|
||||
|
||||
@ -34,6 +34,9 @@ def dump_request(request: SXNG_Request):
|
||||
+ " || Content-Length: %s" % request.headers.get('Content-Length')
|
||||
+ " || Connection: %s" % request.headers.get('Connection')
|
||||
+ " || User-Agent: %s" % request.headers.get('User-Agent')
|
||||
+ " || Sec-Fetch-Site: %s" % request.headers.get('Sec-Fetch-Site')
|
||||
+ " || Sec-Fetch-Mode: %s" % request.headers.get('Sec-Fetch-Mode')
|
||||
+ " || Sec-Fetch-Dest: %s" % request.headers.get('Sec-Fetch-Dest')
|
||||
)
|
||||
|
||||
|
||||
|
||||
103
searx/botdetection/http_sec_fetch.py
Normal file
103
searx/botdetection/http_sec_fetch.py
Normal file
@ -0,0 +1,103 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Method ``http_sec_fetch``
|
||||
-------------------------
|
||||
|
||||
The ``http_sec_fetch`` method protect resources from web attacks with `Fetch
|
||||
Metadata`_. A request is filtered out in case of:
|
||||
|
||||
- http header Sec-Fetch-Mode_ is invalid
|
||||
- http header Sec-Fetch-Dest_ is invalid
|
||||
|
||||
.. _Fetch Metadata:
|
||||
https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header
|
||||
|
||||
.. _Sec-Fetch-Dest:
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Request/destination
|
||||
|
||||
.. _Sec-Fetch-Mode:
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
|
||||
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from __future__ import annotations
|
||||
from ipaddress import (
|
||||
IPv4Network,
|
||||
IPv6Network,
|
||||
)
|
||||
|
||||
import re
|
||||
import flask
|
||||
import werkzeug
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from . import config
|
||||
from ._helpers import logger
|
||||
|
||||
|
||||
def is_browser_supported(user_agent: str) -> bool:
|
||||
"""Check if the browser supports Sec-Fetch headers.
|
||||
|
||||
https://caniuse.com/mdn-http_headers_sec-fetch-dest
|
||||
https://caniuse.com/mdn-http_headers_sec-fetch-mode
|
||||
https://caniuse.com/mdn-http_headers_sec-fetch-site
|
||||
|
||||
Supported browsers:
|
||||
- Chrome >= 80
|
||||
- Firefox >= 90
|
||||
- Safari >= 16.4
|
||||
- Edge (mirrors Chrome)
|
||||
- Opera (mirrors Chrome)
|
||||
"""
|
||||
user_agent = user_agent.lower()
|
||||
|
||||
# Chrome/Chromium/Edge/Opera
|
||||
chrome_match = re.search(r'chrome/(\d+)', user_agent)
|
||||
if chrome_match:
|
||||
version = int(chrome_match.group(1))
|
||||
return version >= 80
|
||||
|
||||
# Firefox
|
||||
firefox_match = re.search(r'firefox/(\d+)', user_agent)
|
||||
if firefox_match:
|
||||
version = int(firefox_match.group(1))
|
||||
return version >= 90
|
||||
|
||||
# Safari
|
||||
safari_match = re.search(r'version/(\d+)\.(\d+)', user_agent)
|
||||
if safari_match:
|
||||
major = int(safari_match.group(1))
|
||||
minor = int(safari_match.group(2))
|
||||
return major > 16 or (major == 16 and minor >= 4)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def filter_request(
|
||||
network: IPv4Network | IPv6Network,
|
||||
request: SXNG_Request,
|
||||
cfg: config.Config,
|
||||
) -> werkzeug.Response | None:
|
||||
|
||||
# Only check Sec-Fetch headers for supported browsers
|
||||
user_agent = request.headers.get('User-Agent', '')
|
||||
if is_browser_supported(user_agent):
|
||||
val = request.headers.get("Sec-Fetch-Mode", "")
|
||||
if val not in ('navigate', 'cors'):
|
||||
logger.debug("invalid Sec-Fetch-Mode '%s'", val)
|
||||
return flask.redirect(flask.url_for('index'), code=302)
|
||||
|
||||
val = request.headers.get("Sec-Fetch-Site", "")
|
||||
if val not in ('same-origin', 'same-site', 'none'):
|
||||
logger.debug("invalid Sec-Fetch-Site '%s'", val)
|
||||
flask.redirect(flask.url_for('index'), code=302)
|
||||
|
||||
val = request.headers.get("Sec-Fetch-Dest", "")
|
||||
if val not in ('document', 'empty'):
|
||||
logger.debug("invalid Sec-Fetch-Dest '%s'", val)
|
||||
flask.redirect(flask.url_for('index'), code=302)
|
||||
|
||||
return None
|
||||
405
searx/cache.py
Normal file
405
searx/cache.py
Normal file
@ -0,0 +1,405 @@
|
||||
"""Implementation of caching solutions.
|
||||
|
||||
- :py:obj:`searx.cache.ExpireCache` and its :py:obj:`searx.cache.ExpireCacheCfg`
|
||||
|
||||
----
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["ExpireCacheCfg", "ExpireCacheStats", "ExpireCache", "ExpireCacheSQLite"]
|
||||
|
||||
import abc
|
||||
import dataclasses
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import pickle
|
||||
import sqlite3
|
||||
import string
|
||||
import tempfile
|
||||
import time
|
||||
import typing
|
||||
|
||||
import msgspec
|
||||
|
||||
from searx import sqlitedb
|
||||
from searx import logger
|
||||
from searx import get_setting
|
||||
|
||||
log = logger.getChild("cache")
|
||||
|
||||
|
||||
class ExpireCacheCfg(msgspec.Struct): # pylint: disable=too-few-public-methods
|
||||
"""Configuration of a :py:obj:`ExpireCache` cache."""
|
||||
|
||||
name: str
|
||||
"""Name of the cache."""
|
||||
|
||||
db_url: str = ""
|
||||
"""URL of the SQLite DB, the path to the database file. If unset a default
|
||||
DB will be created in `/tmp/sxng_cache_{self.name}.db`"""
|
||||
|
||||
MAX_VALUE_LEN: int = 1024 * 10
|
||||
"""Max lenght of a *serialized* value."""
|
||||
|
||||
MAXHOLD_TIME: int = 60 * 60 * 24 * 7 # 7 days
|
||||
"""Hold time (default in sec.), after which a value is removed from the cache."""
|
||||
|
||||
MAINTENANCE_PERIOD: int = 60 * 60 # 2h
|
||||
"""Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to
|
||||
``auto``."""
|
||||
|
||||
MAINTENANCE_MODE: typing.Literal["auto", "off"] = "auto"
|
||||
"""Type of maintenance mode
|
||||
|
||||
``auto``:
|
||||
Maintenance is carried out automatically as part of the maintenance
|
||||
intervals (:py:obj:`MAINTENANCE_PERIOD`); no external process is required.
|
||||
|
||||
``off``:
|
||||
Maintenance is switched off and must be carried out by an external process
|
||||
if required.
|
||||
"""
|
||||
|
||||
password: bytes = get_setting("server.secret_key").encode() # type: ignore
|
||||
"""Password used by :py:obj:`ExpireCache.secret_hash`.
|
||||
|
||||
The default password is taken from :ref:`secret_key <server.secret_key>`.
|
||||
When the password is changed, the hashed keys in the cache can no longer be
|
||||
used, which is why all values in the cache are deleted when the password is
|
||||
changed.
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
# if db_url is unset, use a default DB in /tmp/sxng_cache_{name}.db
|
||||
if not self.db_url:
|
||||
self.db_url = tempfile.gettempdir() + os.sep + f"sxng_cache_{ExpireCache.normalize_name(self.name)}.db"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ExpireCacheStats:
|
||||
"""Dataclass wich provides information on the status of the cache."""
|
||||
|
||||
cached_items: dict[str, list[tuple[str, typing.Any, int]]]
|
||||
"""Values in the cache mapped by context name.
|
||||
|
||||
.. code: python
|
||||
|
||||
{
|
||||
"context name": [
|
||||
("foo key": "foo value", <expire>),
|
||||
("bar key": "bar value", <expire>),
|
||||
# ...
|
||||
],
|
||||
# ...
|
||||
}
|
||||
"""
|
||||
|
||||
def report(self):
|
||||
c_ctx = 0
|
||||
c_kv = 0
|
||||
lines = []
|
||||
|
||||
for ctx_name, kv_list in self.cached_items.items():
|
||||
c_ctx += 1
|
||||
if not kv_list:
|
||||
lines.append(f"[{ctx_name:20s}] empty")
|
||||
continue
|
||||
|
||||
for key, value, expire in kv_list:
|
||||
valid_until = datetime.datetime.fromtimestamp(expire).strftime("%Y-%m-%d %H:%M:%S")
|
||||
c_kv += 1
|
||||
lines.append(f"[{ctx_name:20s}] {valid_until} {key:12}" f" --> ({type(value).__name__}) {value} ")
|
||||
|
||||
lines.append(f"Number of contexts: {c_ctx}")
|
||||
lines.append(f"number of key/value pairs: {c_kv}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class ExpireCache(abc.ABC):
|
||||
"""Abstract base class for the implementation of a key/value cache
|
||||
with expire date."""
|
||||
|
||||
cfg: ExpireCacheCfg
|
||||
|
||||
hash_token = "hash_token"
|
||||
|
||||
@abc.abstractmethod
|
||||
def set(self, key: str, value: typing.Any, expire: int | None, ctx: str | None = None) -> bool:
|
||||
"""Set *key* to *value*. To set a timeout on key use argument
|
||||
``expire`` (in sec.). If expire is unset the default is taken from
|
||||
:py:obj:`ExpireCacheCfg.MAXHOLD_TIME`. After the timeout has expired,
|
||||
the key will automatically be deleted.
|
||||
|
||||
The ``ctx`` argument specifies the context of the ``key``. A key is
|
||||
only unique in its context.
|
||||
|
||||
The concrete implementations of this abstraction determine how the
|
||||
context is mapped in the connected database. In SQL databases, for
|
||||
example, the context is a DB table or in a Key/Value DB it could be
|
||||
a prefix for the key.
|
||||
|
||||
If the context is not specified (the default is ``None``) then a
|
||||
default context should be used, e.g. a default table for SQL databases
|
||||
or a default prefix in a Key/Value DB.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, key: str, default=None, ctx: str | None = None) -> typing.Any:
|
||||
"""Return *value* of *key*. If key is unset, ``None`` is returned."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def maintenance(self, force: bool = False, truncate: bool = False) -> bool:
|
||||
"""Performs maintenance on the cache.
|
||||
|
||||
``force``:
|
||||
Maintenance should be carried out even if the maintenance interval has
|
||||
not yet been reached.
|
||||
|
||||
``truncate``:
|
||||
Truncate the entire cache, which is necessary, for example, if the
|
||||
password has changed.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def state(self) -> ExpireCacheStats:
|
||||
"""Returns a :py:obj:`ExpireCacheStats`, which provides information
|
||||
about the status of the cache."""
|
||||
|
||||
@staticmethod
|
||||
def build_cache(cfg: ExpireCacheCfg) -> ExpireCache:
|
||||
"""Factory to build a caching instance.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, only the SQLite adapter is available, but other database
|
||||
types could be implemented in the future, e.g. a Valkey (Redis)
|
||||
adapter.
|
||||
"""
|
||||
return ExpireCacheSQLite(cfg)
|
||||
|
||||
@staticmethod
|
||||
def normalize_name(name: str) -> str:
|
||||
"""Returns a normalized name that can be used as a file name or as a SQL
|
||||
table name (is used, for example, to normalize the context name)."""
|
||||
|
||||
_valid = "-_." + string.ascii_letters + string.digits
|
||||
return "".join([c for c in name if c in _valid])
|
||||
|
||||
def serialize(self, value: typing.Any) -> bytes:
|
||||
dump: bytes = pickle.dumps(value)
|
||||
return dump
|
||||
|
||||
def deserialize(self, value: bytes) -> typing.Any:
|
||||
obj = pickle.loads(value)
|
||||
return obj
|
||||
|
||||
def secret_hash(self, name: str | bytes) -> str:
|
||||
"""Creates a hash of the argument ``name``. The hash value is formed
|
||||
from the ``name`` combined with the :py:obj:`password
|
||||
<ExpireCacheCfg.password>`. Can be used, for example, to make the
|
||||
``key`` stored in the DB unreadable for third parties."""
|
||||
|
||||
if isinstance(name, str):
|
||||
name = bytes(name, encoding='utf-8')
|
||||
m = hmac.new(name + self.cfg.password, digestmod='sha256')
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
|
||||
"""Cache that manages key/value pairs in a SQLite DB. The DB model in the
|
||||
SQLite DB is implemented in abstract class :py:obj:`SQLiteAppl
|
||||
<searx.sqlitedb.SQLiteAppl>`.
|
||||
|
||||
The following configurations are required / supported:
|
||||
|
||||
- :py:obj:`ExpireCacheCfg.db_url`
|
||||
- :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`
|
||||
- :py:obj:`ExpireCacheCfg.MAINTENANCE_PERIOD`
|
||||
- :py:obj:`ExpireCacheCfg.MAINTENANCE_MODE`
|
||||
"""
|
||||
|
||||
DB_SCHEMA = 1
|
||||
|
||||
# The key/value tables will be created on demand by self.create_table
|
||||
DDL_CREATE_TABLES = {}
|
||||
|
||||
CACHE_TABLE_PREFIX = "CACHE-TABLE-"
|
||||
|
||||
def __init__(self, cfg: ExpireCacheCfg):
|
||||
"""An instance of the SQLite expire cache is build up from a
|
||||
:py:obj:`config <ExpireCacheCfg>`."""
|
||||
|
||||
self.cfg = cfg
|
||||
if cfg.db_url == ":memory:":
|
||||
log.critical("don't use SQLite DB in :memory: in production!!")
|
||||
super().__init__(cfg.db_url)
|
||||
|
||||
def init(self, conn: sqlite3.Connection) -> bool:
|
||||
ret_val = super().init(conn)
|
||||
if not ret_val:
|
||||
return False
|
||||
|
||||
new = hashlib.sha256(self.cfg.password).hexdigest()
|
||||
old = self.properties(self.hash_token)
|
||||
if old != new:
|
||||
if old is not None:
|
||||
log.warning("[%s] hash token changed: truncate all cache tables", self.cfg.name)
|
||||
self.maintenance(force=True, truncate=True)
|
||||
self.properties.set(self.hash_token, new)
|
||||
|
||||
return True
|
||||
|
||||
def maintenance(self, force: bool = False, truncate: bool = False) -> bool:
|
||||
|
||||
if not force and int(time.time()) < self.next_maintenance_time:
|
||||
# log.debug("no maintenance required yet, next maintenance interval is in the future")
|
||||
return False
|
||||
|
||||
# Prevent parallel DB maintenance cycles from other DB connections
|
||||
# (e.g. in multi thread or process environments).
|
||||
self.properties.set("LAST_MAINTENANCE", "") # hint: this (also) sets the m_time of the property!
|
||||
|
||||
if truncate:
|
||||
self.truncate_tables(self.table_names)
|
||||
return True
|
||||
|
||||
# drop items by expire time stamp ..
|
||||
expire = int(time.time())
|
||||
|
||||
with self.connect() as conn:
|
||||
for table in self.table_names:
|
||||
res = conn.execute(f"DELETE FROM {table} WHERE expire < ?", (expire,))
|
||||
log.debug("deleted %s keys from table %s (expire date reached)", res.rowcount, table)
|
||||
|
||||
# Vacuuming the WALs
|
||||
# https://www.theunterminatedstring.com/sqlite-vacuuming/
|
||||
|
||||
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
def create_table(self, table: str) -> bool:
|
||||
"""Create DB ``table`` if it has not yet been created, no recreates are
|
||||
initiated if the table already exists.
|
||||
"""
|
||||
if table in self.table_names:
|
||||
# log.debug("key/value table %s exists in DB (no need to recreate)", table)
|
||||
return False
|
||||
|
||||
log.info("key/value table '%s' NOT exists in DB -> create DB table ..", table)
|
||||
sql_table = "\n".join(
|
||||
[
|
||||
f"CREATE TABLE IF NOT EXISTS {table} (",
|
||||
" key TEXT,",
|
||||
" value BLOB,",
|
||||
f" expire INTEGER DEFAULT (strftime('%s', 'now') + {self.cfg.MAXHOLD_TIME}),",
|
||||
"PRIMARY KEY (key))",
|
||||
]
|
||||
)
|
||||
sql_index = f"CREATE INDEX IF NOT EXISTS index_expire_{table} ON {table}(expire);"
|
||||
with self.connect() as conn:
|
||||
conn.execute(sql_table)
|
||||
conn.execute(sql_index)
|
||||
conn.close()
|
||||
|
||||
self.properties.set(f"{self.CACHE_TABLE_PREFIX}-{table}", table)
|
||||
return True
|
||||
|
||||
@property
|
||||
def table_names(self) -> list[str]:
|
||||
"""List of key/value tables already created in the DB."""
|
||||
sql = f"SELECT value FROM properties WHERE name LIKE '{self.CACHE_TABLE_PREFIX}%%'"
|
||||
rows = self.DB.execute(sql).fetchall() or []
|
||||
return [r[0] for r in rows]
|
||||
|
||||
def truncate_tables(self, table_names: list[str]):
|
||||
log.debug("truncate table: %s", ",".join(table_names))
|
||||
with self.connect() as conn:
|
||||
for table in table_names:
|
||||
conn.execute(f"DELETE FROM {table}")
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
@property
|
||||
def next_maintenance_time(self) -> int:
|
||||
"""Returns (unix epoch) time of the next maintenance."""
|
||||
|
||||
return self.cfg.MAINTENANCE_PERIOD + self.properties.m_time("LAST_MAINTENANCE", int(time.time()))
|
||||
|
||||
# implement ABC methods of ExpireCache
|
||||
|
||||
def set(self, key: str, value: typing.Any, expire: int | None, ctx: str | None = None) -> bool:
|
||||
"""Set key/value in DB table given by argument ``ctx``. If expire is
|
||||
unset the default is taken from :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`.
|
||||
If ``ctx`` argument is ``None`` (the default), a table name is
|
||||
generated from the :py:obj:`ExpireCacheCfg.name`. If DB table does not
|
||||
exists, it will be created (on demand) by :py:obj:`self.create_table
|
||||
<ExpireCacheSQLite.create_table>`.
|
||||
"""
|
||||
table = ctx
|
||||
self.maintenance()
|
||||
|
||||
value = self.serialize(value=value)
|
||||
if len(value) > self.cfg.MAX_VALUE_LEN:
|
||||
log.warning("ExpireCache.set(): %s.key='%s' - value too big to cache (len: %s) ", table, value, len(value))
|
||||
return False
|
||||
|
||||
if not expire:
|
||||
expire = self.cfg.MAXHOLD_TIME
|
||||
expire = int(time.time()) + expire
|
||||
|
||||
table_name = table
|
||||
if not table_name:
|
||||
table_name = self.normalize_name(self.cfg.name)
|
||||
self.create_table(table_name)
|
||||
|
||||
sql = (
|
||||
f"INSERT INTO {table_name} (key, value, expire) VALUES (?, ?, ?)"
|
||||
f" ON CONFLICT DO "
|
||||
f"UPDATE SET value=?, expire=?"
|
||||
)
|
||||
|
||||
if table:
|
||||
with self.DB:
|
||||
self.DB.execute(sql, (key, value, expire, value, expire))
|
||||
else:
|
||||
with self.connect() as conn:
|
||||
conn.execute(sql, (key, value, expire, value, expire))
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
def get(self, key: str, default=None, ctx: str | None = None) -> typing.Any:
|
||||
"""Get value of ``key`` from table given by argument ``ctx``. If
|
||||
``ctx`` argument is ``None`` (the default), a table name is generated
|
||||
from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists (in
|
||||
table), the ``default`` value is returned.
|
||||
|
||||
"""
|
||||
table = ctx
|
||||
self.maintenance()
|
||||
|
||||
if not table:
|
||||
table = self.normalize_name(self.cfg.name)
|
||||
|
||||
if table not in self.table_names:
|
||||
return default
|
||||
|
||||
sql = f"SELECT value FROM {table} WHERE key = ?"
|
||||
row = self.DB.execute(sql, (key,)).fetchone()
|
||||
if row is None:
|
||||
return default
|
||||
|
||||
return self.deserialize(row[0])
|
||||
|
||||
def state(self) -> ExpireCacheStats:
|
||||
cached_items = {}
|
||||
for table in self.table_names:
|
||||
cached_items[table] = []
|
||||
for row in self.DB.execute(f"SELECT key, value, expire FROM {table}"):
|
||||
cached_items[table].append((row[0], self.deserialize(row[1]), row[2]))
|
||||
return ExpireCacheStats(cached_items=cached_items)
|
||||
File diff suppressed because it is too large
Load Diff
@ -839,6 +839,7 @@
|
||||
"ja": "バハマ・ドル",
|
||||
"ko": "바하마 달러",
|
||||
"lt": "Bahamų doleris",
|
||||
"lv": "Bahamu dolārs",
|
||||
"nl": "Bahamaanse dollar",
|
||||
"oc": "Dolar de las Bahamas",
|
||||
"pa": "ਬਹਾਮਾਸੀ ਡਾਲਰ",
|
||||
@ -2119,7 +2120,7 @@
|
||||
"de": "guatemaltekischer Quetzal",
|
||||
"en": "quetzal",
|
||||
"eo": "gvatemala kecalo",
|
||||
"es": "quetzal",
|
||||
"es": "Quetzal",
|
||||
"eu": "Quetzal",
|
||||
"fi": "Guatemalan quetzal",
|
||||
"fr": "Quetzal",
|
||||
@ -2211,6 +2212,7 @@
|
||||
"ja": "香港ドル",
|
||||
"ko": "홍콩 달러",
|
||||
"lt": "Honkongo doleris",
|
||||
"lv": "Honkongas dolārs",
|
||||
"ml": "ഹോങ്കോങ്ങ് ഡോളർ",
|
||||
"ms": "dolar Hong Kong",
|
||||
"nl": "Hongkongse dollar",
|
||||
@ -2424,7 +2426,7 @@
|
||||
"pa": "ਇਜ਼ਰਾਇਲੀ ਨਵਾਂ ਸ਼ੇਕਲ",
|
||||
"pl": "Nowy izraelski szekel",
|
||||
"pt": "novo shekel israelense",
|
||||
"ro": "Shekel",
|
||||
"ro": "shekel",
|
||||
"ru": "новый израильский шекель",
|
||||
"si": "සෙකල්",
|
||||
"sk": "Nový izraelský šekel",
|
||||
@ -2591,6 +2593,7 @@
|
||||
"ja": "アイスランド・クローナ",
|
||||
"ko": "아이슬란드 크로나",
|
||||
"lt": "Islandijos krona",
|
||||
"lv": "Islandes krona",
|
||||
"nl": "IJslandse kroon",
|
||||
"pa": "ਆਈਸਲੈਂਡੀ ਕਰੋਨਾ",
|
||||
"pl": "Korona islandzka",
|
||||
@ -2628,6 +2631,7 @@
|
||||
"ja": "ジャマイカ・ドル",
|
||||
"ko": "자메이카 달러",
|
||||
"lt": "Jamaikos doleris",
|
||||
"lv": "Jamaikas dolārs",
|
||||
"nl": "Jamaicaanse dollar",
|
||||
"pa": "ਜਮੈਕੀ ਡਾਲਰ",
|
||||
"pl": "Dolar jamajski",
|
||||
@ -3104,6 +3108,7 @@
|
||||
"ja": "キープ",
|
||||
"ko": "라오스 킵",
|
||||
"lt": "Laoso kipas",
|
||||
"lv": "Laosas kips",
|
||||
"ms": "Kip",
|
||||
"nl": "Laotiaanse kip",
|
||||
"oc": "kip laossian",
|
||||
@ -3221,6 +3226,7 @@
|
||||
"ja": "リベリア・ドル",
|
||||
"ko": "라이베리아 달러",
|
||||
"lt": "Liberijos doleris",
|
||||
"lv": "Libērijas dolārs",
|
||||
"ms": "Dolar Liberia",
|
||||
"nl": "Liberiaanse dollar",
|
||||
"oc": "Dolar liberian",
|
||||
@ -3427,7 +3433,7 @@
|
||||
"cy": "denar (Macedonia)",
|
||||
"da": "Makedonske denarer",
|
||||
"de": "mazedonischer Denar",
|
||||
"en": "North Macedonian denar",
|
||||
"en": "Macedonian denar",
|
||||
"eo": "makedona denaro",
|
||||
"es": "denar macedonio",
|
||||
"et": "Makedoonia denaar",
|
||||
@ -3691,7 +3697,7 @@
|
||||
"de": "Malawi-Kwacha",
|
||||
"en": "Malawian kwacha",
|
||||
"eo": "malavia kvaĉo",
|
||||
"es": "kwacha malauí",
|
||||
"es": "kuacha malauí",
|
||||
"et": "Malawi kvatša",
|
||||
"fi": "Malawin kwacha",
|
||||
"fr": "kwacha malawien",
|
||||
@ -3867,6 +3873,7 @@
|
||||
"ja": "ナミビア・ドル",
|
||||
"ko": "나미비아 달러",
|
||||
"lt": "Namibijos doleris",
|
||||
"lv": "Namībijas dolārs",
|
||||
"ms": "Dolar Namibia",
|
||||
"nl": "Namibische dollar",
|
||||
"oc": "Dolar namibian",
|
||||
@ -4874,6 +4881,7 @@
|
||||
"ja": "シンガポールドル",
|
||||
"ko": "싱가포르 달러",
|
||||
"lt": "Singapūro doleris",
|
||||
"lv": "Singapūras dolārs",
|
||||
"ml": "സിംഗപ്പൂർ ഡോളർ",
|
||||
"ms": "Dolar Singapura",
|
||||
"nl": "Singaporese dollar",
|
||||
@ -5245,6 +5253,7 @@
|
||||
"ja": "ソモニ",
|
||||
"ko": "타지키스탄 소모니",
|
||||
"lt": "Somonis",
|
||||
"lv": "somoni",
|
||||
"ms": "Somoni",
|
||||
"nl": "Tadzjiekse somoni",
|
||||
"pa": "ਤਾਜਿਕਿਸਤਾਨੀ ਸੋਮੋਨੀ",
|
||||
@ -5843,7 +5852,7 @@
|
||||
"ca": "tala",
|
||||
"cs": "Samojská tala",
|
||||
"de": "samoanischer Tala",
|
||||
"en": "Samoan Tālā",
|
||||
"en": "Samoan tālā",
|
||||
"eo": "samoa talao",
|
||||
"es": "tālā",
|
||||
"et": "Samoa tala",
|
||||
@ -5998,6 +6007,7 @@
|
||||
"XCG": {
|
||||
"ar": "الجلدر الكاريبي",
|
||||
"ca": "florí caribeny",
|
||||
"cs": "Karibský gulden",
|
||||
"de": "Karibischer Gulden",
|
||||
"en": "Caribbean guilder",
|
||||
"eo": "Karibia guldeno",
|
||||
@ -6006,6 +6016,7 @@
|
||||
"hr": "Karipski gulden",
|
||||
"hu": "karibi forint",
|
||||
"it": "fiorino caraibico",
|
||||
"ja": "カリブ・ギルダー",
|
||||
"nl": "Caribische gulden",
|
||||
"pap": "Florin karibense",
|
||||
"pt": "Florim do Caribe",
|
||||
@ -6251,7 +6262,7 @@
|
||||
"de": "sambischer Kwacha",
|
||||
"en": "Zambian Kwacha",
|
||||
"eo": "zambia kvaĉo",
|
||||
"es": "kwacha zambiano",
|
||||
"es": "kuacha zambiano",
|
||||
"et": "Sambia kvatša",
|
||||
"fi": "Sambian kwacha",
|
||||
"fr": "kwacha zambien",
|
||||
@ -6745,6 +6756,7 @@
|
||||
"bahamski dolar": "BSD",
|
||||
"bahamský dolar": "BSD",
|
||||
"bahamský dolár": "BSD",
|
||||
"bahamu dolārs": "BSD",
|
||||
"bahamų doleris": "BSD",
|
||||
"bahrain dinar": "BHD",
|
||||
"bahraini dinar": "BHD",
|
||||
@ -8011,6 +8023,10 @@
|
||||
"dop": "DOP",
|
||||
"dopene": "DOP",
|
||||
"dòlar australià": "AUD",
|
||||
"dòlar bahamià": "BSD",
|
||||
"dòlar barbadià": "BBD",
|
||||
"dòlar belizià": "BZD",
|
||||
"dòlar bruneiès": "BND",
|
||||
"dòlar canadenc": "CAD",
|
||||
"dòlar de bahames": "BSD",
|
||||
"dòlar de barbados": "BBD",
|
||||
@ -8042,14 +8058,18 @@
|
||||
"dòlar del canadà": "CAD",
|
||||
"dòlar del carib oriental": "XCD",
|
||||
"dòlar dels estats units": "USD",
|
||||
"dòlar estatunidenc": "USD",
|
||||
"dòlar etíop": "ETB",
|
||||
"dòlar fijià": "FJD",
|
||||
"dòlar guyanès": "GYD",
|
||||
"dòlar jamaicà": "JMD",
|
||||
"dòlar liberià": "LRD",
|
||||
"dòlar malai": "MYR",
|
||||
"dòlar namibi": "NAD",
|
||||
"dòlar namibià": "NAD",
|
||||
"dòlar neozelandès": "NZD",
|
||||
"dòlar salomonès": "SBD",
|
||||
"dòlar surinamès": "SRD",
|
||||
"dòlar taiwanès": "TWD",
|
||||
"dòlars canadencs": "CAD",
|
||||
"dòlars neozelandesos": "NZD",
|
||||
@ -8252,6 +8272,7 @@
|
||||
"egyptská libra": "EGP",
|
||||
"eiro": "EUR",
|
||||
"ekialdeko karibeko dolar": "XCD",
|
||||
"el peso": "GTQ",
|
||||
"emalangeni": "SZL",
|
||||
"emas sebagai pelaburan": "XAU",
|
||||
"emirati dirham": "AED",
|
||||
@ -8304,6 +8325,7 @@
|
||||
"escudo tanjung verde": "CVE",
|
||||
"escudo zielonego przylądka": "CVE",
|
||||
"escudos cabo verdianos": "CVE",
|
||||
"escut capverdià": "CVE",
|
||||
"escut de cap verd": "CVE",
|
||||
"esloti": [
|
||||
"PLZ",
|
||||
@ -8490,6 +8512,7 @@
|
||||
"franc an chongó": "CDF",
|
||||
"franc burundais": "BIF",
|
||||
"franc burundez": "BIF",
|
||||
"franc burundès": "BIF",
|
||||
"franc burundi": "BIF",
|
||||
"franc centrafrican cfa": "XAF",
|
||||
"franc central african cfa": "XAF",
|
||||
@ -8514,6 +8537,7 @@
|
||||
"franc cfa vest african": "XOF",
|
||||
"franc cfp": "XPF",
|
||||
"franc comorian": "KMF",
|
||||
"franc comorià": "KMF",
|
||||
"franc comorien": "KMF",
|
||||
"franc comoros": "KMF",
|
||||
"franc congo": "CDF",
|
||||
@ -8530,6 +8554,7 @@
|
||||
"franc del congo belga": "CDF",
|
||||
"franc des collectivités françaises du pacifique": "XPF",
|
||||
"franc djibouti": "DJF",
|
||||
"franc djiboutià": "DJF",
|
||||
"franc djiboutien": "DJF",
|
||||
"franc elvețian": "CHF",
|
||||
"franc elveţian": "CHF",
|
||||
@ -8741,6 +8766,7 @@
|
||||
"greenback": "USD",
|
||||
"grivina": "UAH",
|
||||
"grivna": "UAH",
|
||||
"grivna ucraina": "UAH",
|
||||
"grivna ucraniana": "UAH",
|
||||
"grivnas": "UAH",
|
||||
"grivnă": "UAH",
|
||||
@ -8892,6 +8918,7 @@
|
||||
"hongkonský dolár": "HKD",
|
||||
"hongkonški dolar": "HKD",
|
||||
"honkonga dolaro": "HKD",
|
||||
"honkongas dolārs": "HKD",
|
||||
"honkongo doleris": "HKD",
|
||||
"honkonški dolar": "HKD",
|
||||
"hrivna": "UAH",
|
||||
@ -9013,6 +9040,7 @@
|
||||
"irr": "IRR",
|
||||
"isk": "ISK",
|
||||
"islanda krono": "ISK",
|
||||
"islandes krona": "ISK",
|
||||
"islandi kroon": "ISK",
|
||||
"islandiar koroa": "ISK",
|
||||
"islandijos krona": "ISK",
|
||||
@ -9077,6 +9105,7 @@
|
||||
"jamaika doları": "JMD",
|
||||
"jamaika dollar": "JMD",
|
||||
"jamaikan dollari": "JMD",
|
||||
"jamaikas dolārs": "JMD",
|
||||
"jamaikos doleris": "JMD",
|
||||
"jamajčanski dolar": "JMD",
|
||||
"jamajka dolaro": "JMD",
|
||||
@ -9202,6 +9231,7 @@
|
||||
"karibia guldeno": "XCG",
|
||||
"karibischer gulden": "XCG",
|
||||
"karibski goldinar": "XCG",
|
||||
"karibský gulden": "XCG",
|
||||
"karipski gulden": "XCG",
|
||||
"karod": "NPR",
|
||||
"kartvela lario": "GEL",
|
||||
@ -9395,6 +9425,8 @@
|
||||
"krw": "KRW",
|
||||
"ks": "MMK",
|
||||
"ksh": "KES",
|
||||
"kuacha malauí": "MWK",
|
||||
"kuacha zambiano": "ZMW",
|
||||
"kuanza angoleño": "AOA",
|
||||
"kuba peso": "CUP",
|
||||
"kubai peso": "CUP",
|
||||
@ -9445,10 +9477,8 @@
|
||||
"kwacha do malawi": "MWK",
|
||||
"kwacha do maláui": "MWK",
|
||||
"kwacha do malávi": "MWK",
|
||||
"kwacha malaui": "MWK",
|
||||
"kwacha malauiana": "MWK",
|
||||
"kwacha malauiano": "MWK",
|
||||
"kwacha malauí": "MWK",
|
||||
"kwacha malaviana": "MWK",
|
||||
"kwacha malawi": "MWK",
|
||||
"kwacha malawiana": "MWK",
|
||||
@ -9510,6 +9540,7 @@
|
||||
"lao kip": "LAK",
|
||||
"laos kipi": "LAK",
|
||||
"laosa kipo": "LAK",
|
||||
"laosas kips": "LAK",
|
||||
"laosin kip": "LAK",
|
||||
"laoski kip": "LAK",
|
||||
"laoský kip": "LAK",
|
||||
@ -9671,6 +9702,7 @@
|
||||
"liberisk dollar": "LRD",
|
||||
"liberya doları": "LRD",
|
||||
"libériai dollár": "LRD",
|
||||
"libērijas dolārs": "LRD",
|
||||
"libia dinaro": "LYD",
|
||||
"libijos dinaras": "LYD",
|
||||
"libijski dinar": "LYD",
|
||||
@ -10213,6 +10245,7 @@
|
||||
"namibya doları": "NAD",
|
||||
"namíbiai dollár": "NAD",
|
||||
"namíbijský dolár": "NAD",
|
||||
"namībijas dolārs": "NAD",
|
||||
"naujasis solis": "PEN",
|
||||
"naujasis taivano doleris": "TWD",
|
||||
"naujoji rumunijos lėja": "RON",
|
||||
@ -11075,6 +11108,7 @@
|
||||
"riyal oman": "OMR",
|
||||
"riyal qatar": "QAR",
|
||||
"riyal qatari": "QAR",
|
||||
"riyal qatarià": "QAR",
|
||||
"riyal qatarien": "QAR",
|
||||
"riyal qatariota": "QAR",
|
||||
"riyal qatarita": "QAR",
|
||||
@ -11256,6 +11290,7 @@
|
||||
"rupia índia": "INR",
|
||||
"rupia lankijska": "LKR",
|
||||
"rupia maldiva": "MVR",
|
||||
"rupia maldiviana": "MVR",
|
||||
"rupia maldívia": "MVR",
|
||||
"rupia malediwska": "MVR",
|
||||
"rupia mauricia": "MUR",
|
||||
@ -11276,6 +11311,7 @@
|
||||
"rupia seszelska": "SCR",
|
||||
"rupia seychelense": "SCR",
|
||||
"rupia seychellense": "SCR",
|
||||
"rupia seychellesa": "SCR",
|
||||
"rupia singalesa": "LKR",
|
||||
"rupia singalese": "LKR",
|
||||
"rupia sri lanki": "LKR",
|
||||
@ -11556,6 +11592,7 @@
|
||||
"singapurski dolar": "SGD",
|
||||
"singapurský dolar": "SGD",
|
||||
"singapurský dolár": "SGD",
|
||||
"singapūras dolārs": "SGD",
|
||||
"singapūro doleris": "SGD",
|
||||
"sint heleens pond": "SHP",
|
||||
"siria pundo": "SYP",
|
||||
@ -11870,7 +11907,6 @@
|
||||
"švýcarský frank": "CHF",
|
||||
"șekel nou": "ILS",
|
||||
"şekel": "ILS",
|
||||
"şekel nou": "ILS",
|
||||
"şili pesosu": "CLP",
|
||||
"s₣": "CHF",
|
||||
"t": "TMT",
|
||||
@ -11883,6 +11919,8 @@
|
||||
"tadžikistani somoni": "TJS",
|
||||
"tadžikistanin somoni": "TJS",
|
||||
"tadžikistanski somoni": "TJS",
|
||||
"tadžikistānas somoni": "TJS",
|
||||
"tadžiku somoni": "TJS",
|
||||
"taĝika somonio": "TJS",
|
||||
"tai baat": "THB",
|
||||
"tailando batas": "THB",
|
||||
@ -11918,6 +11956,7 @@
|
||||
"tala samoana": "WST",
|
||||
"tala samoano": "WST",
|
||||
"tala samoà": "WST",
|
||||
"talao": "WST",
|
||||
"tambala": "MWK",
|
||||
"tamil rupee": "LKR",
|
||||
"tamilska rupija": "LKR",
|
||||
@ -14413,6 +14452,9 @@
|
||||
"دوبرا": "STN",
|
||||
"دوبرا ساو تومي وبرينسيب": "STN",
|
||||
"دوبرا ساو تومية وبرينسيبية": "STN",
|
||||
"دولار الامريكي": "USD",
|
||||
"دولار الأمريكي": "USD",
|
||||
"دولار امريكي": "USD",
|
||||
"دولار أسترالي": "AUD",
|
||||
"دولار أمريكي": "USD",
|
||||
"دولار بربادوسي": "BBD",
|
||||
@ -15109,6 +15151,7 @@
|
||||
"เยน": "JPY",
|
||||
"เรนมินบิ": "CNY",
|
||||
"เรอัลบราซิล": "BRL",
|
||||
"เรียล": "KHR",
|
||||
"เรียลกัมพูชา": "KHR",
|
||||
"เรียลบราซิล": "BRL",
|
||||
"เลวูโรมาเนีย": "RON",
|
||||
@ -15479,7 +15522,6 @@
|
||||
"₽": "RUB",
|
||||
"₾": "GEL",
|
||||
"⃀": "KGS",
|
||||
"": "SAR",
|
||||
"〒": "KZT",
|
||||
"アイスランドクローナ": "ISK",
|
||||
"アイスランド・クローナ": "ISK",
|
||||
@ -15535,6 +15577,7 @@
|
||||
"カタール・リヤル": "QAR",
|
||||
"カナダドル": "CAD",
|
||||
"カナダ・ドル": "CAD",
|
||||
"カリブ・ギルダー": "XCG",
|
||||
"カーボベルデ・エスクード": "CVE",
|
||||
"ギニア・フラン": "GNF",
|
||||
"ギニー": "EGP",
|
||||
@ -15635,6 +15678,7 @@
|
||||
"チュニジア・ディナール": "TND",
|
||||
"チリの通貨": "CLP",
|
||||
"チリ・ペソ": "CLP",
|
||||
"デジタルルピー": "INR",
|
||||
"デンマーククローネ": "DKK",
|
||||
"デンマーク・クローネ": "DKK",
|
||||
"テンゲ": "KZT",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1684,8 +1684,10 @@
|
||||
"custom": {
|
||||
"ui_lang": {
|
||||
"bg": "bg",
|
||||
"br": "br",
|
||||
"ca": "ca",
|
||||
"cs": "cs",
|
||||
"cy": "cy",
|
||||
"da": "da",
|
||||
"de-DE": "de-de",
|
||||
"el": "el",
|
||||
@ -1695,9 +1697,11 @@
|
||||
"en-US": "en-us",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"eu": "eu",
|
||||
"fi-FI": "fi-fi",
|
||||
"fr-CA": "fr-ca",
|
||||
"fr-FR": "fr-fr",
|
||||
"gl": "gl",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"id": "id",
|
||||
@ -1786,8 +1790,10 @@
|
||||
"custom": {
|
||||
"ui_lang": {
|
||||
"bg": "bg",
|
||||
"br": "br",
|
||||
"ca": "ca",
|
||||
"cs": "cs",
|
||||
"cy": "cy",
|
||||
"da": "da",
|
||||
"de-DE": "de-de",
|
||||
"el": "el",
|
||||
@ -1797,9 +1803,11 @@
|
||||
"en-US": "en-us",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"eu": "eu",
|
||||
"fi-FI": "fi-fi",
|
||||
"fr-CA": "fr-ca",
|
||||
"fr-FR": "fr-fr",
|
||||
"gl": "gl",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"id": "id",
|
||||
@ -1888,8 +1896,10 @@
|
||||
"custom": {
|
||||
"ui_lang": {
|
||||
"bg": "bg",
|
||||
"br": "br",
|
||||
"ca": "ca",
|
||||
"cs": "cs",
|
||||
"cy": "cy",
|
||||
"da": "da",
|
||||
"de-DE": "de-de",
|
||||
"el": "el",
|
||||
@ -1899,9 +1909,11 @@
|
||||
"en-US": "en-us",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"eu": "eu",
|
||||
"fi-FI": "fi-fi",
|
||||
"fr-CA": "fr-ca",
|
||||
"fr-FR": "fr-fr",
|
||||
"gl": "gl",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"id": "id",
|
||||
@ -1990,8 +2002,10 @@
|
||||
"custom": {
|
||||
"ui_lang": {
|
||||
"bg": "bg",
|
||||
"br": "br",
|
||||
"ca": "ca",
|
||||
"cs": "cs",
|
||||
"cy": "cy",
|
||||
"da": "da",
|
||||
"de-DE": "de-de",
|
||||
"el": "el",
|
||||
@ -2001,9 +2015,11 @@
|
||||
"en-US": "en-us",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"eu": "eu",
|
||||
"fi-FI": "fi-fi",
|
||||
"fr-CA": "fr-ca",
|
||||
"fr-FR": "fr-fr",
|
||||
"gl": "gl",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"id": "id",
|
||||
@ -6628,6 +6644,172 @@
|
||||
"to-TO": "to"
|
||||
}
|
||||
},
|
||||
"mullvadleta": {
|
||||
"all_locale": null,
|
||||
"custom": {},
|
||||
"data_type": "traits_v1",
|
||||
"languages": {
|
||||
"ar": "ar",
|
||||
"bg": "bg",
|
||||
"ca": "ca",
|
||||
"cs": "cs",
|
||||
"da": "da",
|
||||
"de": "de",
|
||||
"en": "en",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"fi": "fi",
|
||||
"fr": "fr",
|
||||
"he": "he",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"is": "is",
|
||||
"it": "it",
|
||||
"ja": "jp",
|
||||
"ko": "ko",
|
||||
"lt": "lt",
|
||||
"lv": "lv",
|
||||
"nb": "nb",
|
||||
"nl": "nl",
|
||||
"pl": "pl",
|
||||
"pt": "pt",
|
||||
"ro": "ro",
|
||||
"ru": "ru",
|
||||
"sk": "sk",
|
||||
"sl": "sl",
|
||||
"sr": "sr",
|
||||
"sv": "sv",
|
||||
"tr": "tr",
|
||||
"zh_Hans": "zh-hant",
|
||||
"zh_Hant": "zh-hans"
|
||||
},
|
||||
"regions": {
|
||||
"ar-SA": "sa",
|
||||
"da-DK": "dk",
|
||||
"de-AT": "at",
|
||||
"de-BE": "be",
|
||||
"de-CH": "ch",
|
||||
"de-DE": "de",
|
||||
"en-AU": "au",
|
||||
"en-CA": "ca",
|
||||
"en-GB": "uk",
|
||||
"en-IN": "in",
|
||||
"en-NZ": "nz",
|
||||
"en-PH": "ph",
|
||||
"en-US": "us",
|
||||
"en-ZA": "za",
|
||||
"es-AR": "ar",
|
||||
"es-CL": "cl",
|
||||
"es-ES": "es",
|
||||
"es-MX": "mx",
|
||||
"fi-FI": "fi",
|
||||
"fr-BE": "be",
|
||||
"fr-CA": "ca",
|
||||
"fr-CH": "ch",
|
||||
"fr-FR": "fr",
|
||||
"id-ID": "id",
|
||||
"it-CH": "ch",
|
||||
"it-IT": "it",
|
||||
"ja-JP": "jp",
|
||||
"ko-KR": "kr",
|
||||
"ms-MY": "my",
|
||||
"nb-NO": "no",
|
||||
"nl-BE": "be",
|
||||
"nl-NL": "nl",
|
||||
"pl-PL": "pl",
|
||||
"pt-BR": "br",
|
||||
"pt-PT": "pt",
|
||||
"ru-RU": "ru",
|
||||
"se-SE": "se",
|
||||
"tr-TR": "tr",
|
||||
"zh-CN": "cn",
|
||||
"zh-HK": "hk",
|
||||
"zh-TW": "tw"
|
||||
}
|
||||
},
|
||||
"mullvadleta brave": {
|
||||
"all_locale": null,
|
||||
"custom": {},
|
||||
"data_type": "traits_v1",
|
||||
"languages": {
|
||||
"ar": "ar",
|
||||
"bg": "bg",
|
||||
"ca": "ca",
|
||||
"cs": "cs",
|
||||
"da": "da",
|
||||
"de": "de",
|
||||
"en": "en",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"fi": "fi",
|
||||
"fr": "fr",
|
||||
"he": "he",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"is": "is",
|
||||
"it": "it",
|
||||
"ja": "jp",
|
||||
"ko": "ko",
|
||||
"lt": "lt",
|
||||
"lv": "lv",
|
||||
"nb": "nb",
|
||||
"nl": "nl",
|
||||
"pl": "pl",
|
||||
"pt": "pt",
|
||||
"ro": "ro",
|
||||
"ru": "ru",
|
||||
"sk": "sk",
|
||||
"sl": "sl",
|
||||
"sr": "sr",
|
||||
"sv": "sv",
|
||||
"tr": "tr",
|
||||
"zh_Hans": "zh-hant",
|
||||
"zh_Hant": "zh-hans"
|
||||
},
|
||||
"regions": {
|
||||
"ar-SA": "sa",
|
||||
"da-DK": "dk",
|
||||
"de-AT": "at",
|
||||
"de-BE": "be",
|
||||
"de-CH": "ch",
|
||||
"de-DE": "de",
|
||||
"en-AU": "au",
|
||||
"en-CA": "ca",
|
||||
"en-GB": "uk",
|
||||
"en-IN": "in",
|
||||
"en-NZ": "nz",
|
||||
"en-PH": "ph",
|
||||
"en-US": "us",
|
||||
"en-ZA": "za",
|
||||
"es-AR": "ar",
|
||||
"es-CL": "cl",
|
||||
"es-ES": "es",
|
||||
"es-MX": "mx",
|
||||
"fi-FI": "fi",
|
||||
"fr-BE": "be",
|
||||
"fr-CA": "ca",
|
||||
"fr-CH": "ch",
|
||||
"fr-FR": "fr",
|
||||
"id-ID": "id",
|
||||
"it-CH": "ch",
|
||||
"it-IT": "it",
|
||||
"ja-JP": "jp",
|
||||
"ko-KR": "kr",
|
||||
"ms-MY": "my",
|
||||
"nb-NO": "no",
|
||||
"nl-BE": "be",
|
||||
"nl-NL": "nl",
|
||||
"pl-PL": "pl",
|
||||
"pt-BR": "br",
|
||||
"pt-PT": "pt",
|
||||
"ru-RU": "ru",
|
||||
"se-SE": "se",
|
||||
"tr-TR": "tr",
|
||||
"zh-CN": "cn",
|
||||
"zh-HK": "hk",
|
||||
"zh-TW": "tw"
|
||||
}
|
||||
},
|
||||
"odysee": {
|
||||
"all_locale": null,
|
||||
"custom": {},
|
||||
@ -6934,6 +7116,7 @@
|
||||
"BY",
|
||||
"BZ",
|
||||
"CA",
|
||||
"CC",
|
||||
"CD",
|
||||
"CF",
|
||||
"CG",
|
||||
@ -8105,6 +8288,7 @@
|
||||
"nr",
|
||||
"nrm",
|
||||
"nso",
|
||||
"nup",
|
||||
"nv",
|
||||
"ny",
|
||||
"oc",
|
||||
@ -8121,7 +8305,6 @@
|
||||
"pdc",
|
||||
"pfl",
|
||||
"pi",
|
||||
"pih",
|
||||
"pl",
|
||||
"pms",
|
||||
"pnb",
|
||||
@ -8410,46 +8593,6 @@
|
||||
"zh-classical": "zh-classical"
|
||||
}
|
||||
},
|
||||
"yahoo": {
|
||||
"all_locale": "any",
|
||||
"custom": {},
|
||||
"data_type": "traits_v1",
|
||||
"languages": {
|
||||
"ar": "ar",
|
||||
"bg": "bg",
|
||||
"cs": "cs",
|
||||
"da": "da",
|
||||
"de": "de",
|
||||
"el": "el",
|
||||
"en": "en",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"fi": "fi",
|
||||
"fr": "fr",
|
||||
"he": "he",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"it": "it",
|
||||
"ja": "ja",
|
||||
"ko": "ko",
|
||||
"lt": "lt",
|
||||
"lv": "lv",
|
||||
"nl": "nl",
|
||||
"no": "no",
|
||||
"pl": "pl",
|
||||
"pt": "pt",
|
||||
"ro": "ro",
|
||||
"ru": "ru",
|
||||
"sk": "sk",
|
||||
"sl": "sl",
|
||||
"sv": "sv",
|
||||
"th": "th",
|
||||
"tr": "tr",
|
||||
"zh_Hans": "zh_chs",
|
||||
"zh_Hant": "zh_cht"
|
||||
},
|
||||
"regions": {}
|
||||
},
|
||||
"z-library": {
|
||||
"all_locale": "",
|
||||
"custom": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
||||
],
|
||||
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
||||
"versions": [
|
||||
"136.0",
|
||||
"135.0"
|
||||
"138.0",
|
||||
"137.0"
|
||||
]
|
||||
}
|
||||
@ -4244,6 +4244,11 @@
|
||||
"symbol": "cm³/mol",
|
||||
"to_si_factor": 1e-06
|
||||
},
|
||||
"Q240468": {
|
||||
"si_name": null,
|
||||
"symbol": "syr£",
|
||||
"to_si_factor": null
|
||||
},
|
||||
"Q242988": {
|
||||
"si_name": null,
|
||||
"symbol": "Lib$",
|
||||
@ -5279,6 +5284,11 @@
|
||||
"symbol": "bhp EDR",
|
||||
"to_si_factor": 12.958174
|
||||
},
|
||||
"Q3984193": {
|
||||
"si_name": "Q25269",
|
||||
"symbol": "TeV",
|
||||
"to_si_factor": 1.602176634e-07
|
||||
},
|
||||
"Q39978339": {
|
||||
"si_name": "Q25377184",
|
||||
"symbol": "kg/cm²",
|
||||
@ -6595,14 +6605,14 @@
|
||||
"to_si_factor": null
|
||||
},
|
||||
"Q68725821": {
|
||||
"si_name": null,
|
||||
"si_name": "Q11579",
|
||||
"symbol": "°Rø",
|
||||
"to_si_factor": null
|
||||
"to_si_factor": 1.90476190476
|
||||
},
|
||||
"Q68726230": {
|
||||
"si_name": null,
|
||||
"si_name": "Q11579",
|
||||
"symbol": "°De",
|
||||
"to_si_factor": null
|
||||
"to_si_factor": 0.66667
|
||||
},
|
||||
"Q68726625": {
|
||||
"si_name": null,
|
||||
@ -6917,7 +6927,7 @@
|
||||
"Q72081071": {
|
||||
"si_name": "Q25269",
|
||||
"symbol": "MeV",
|
||||
"to_si_factor": 1.602176634e-13
|
||||
"to_si_factor": 1.60217656535e-13
|
||||
},
|
||||
"Q7235648": {
|
||||
"si_name": null,
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Implementations of the framework for the SearXNG engines.
|
||||
|
||||
- :py:obj:`searx.enginelib.EngineCache`
|
||||
- :py:obj:`searx.enginelib.Engine`
|
||||
- :py:obj:`searx.enginelib.traits`
|
||||
|
||||
There is a command line for developer purposes and for deeper analysis. Here is
|
||||
an example in which the command line is called in the development environment::
|
||||
|
||||
$ ./manage pyenv.cmd bash --norc --noprofile
|
||||
(py3) python -m searx.enginelib --help
|
||||
|
||||
.. hint::
|
||||
|
||||
The long term goal is to modularize all implementations of the engine
|
||||
@ -9,16 +19,158 @@
|
||||
- move implementations of the :ref:`searx.engines loader` to a new module in
|
||||
the :py:obj:`searx.enginelib` namespace.
|
||||
|
||||
-----
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import List, Callable, TYPE_CHECKING
|
||||
|
||||
__all__ = ["EngineCache", "Engine", "ENGINES_CACHE"]
|
||||
|
||||
from typing import List, Callable, TYPE_CHECKING, Any
|
||||
import string
|
||||
import typer
|
||||
|
||||
from ..cache import ExpireCache, ExpireCacheCfg
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from searx.enginelib import traits
|
||||
|
||||
|
||||
ENGINES_CACHE = ExpireCache.build_cache(
|
||||
ExpireCacheCfg(
|
||||
name="ENGINES_CACHE",
|
||||
MAXHOLD_TIME=60 * 60 * 24 * 7, # 7 days
|
||||
MAINTENANCE_PERIOD=60 * 60, # 2h
|
||||
)
|
||||
)
|
||||
"""Global :py:obj:`searx.cache.ExpireCacheSQLite` instance where the cached
|
||||
values from all engines are stored. The `MAXHOLD_TIME` is 7 days and the
|
||||
`MAINTENANCE_PERIOD` is set to two hours."""
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def state():
|
||||
"""Show state for the caches of the engines."""
|
||||
|
||||
title = "cache tables and key/values"
|
||||
print(title)
|
||||
print("=" * len(title))
|
||||
print(ENGINES_CACHE.state().report())
|
||||
print()
|
||||
title = f"properties of {ENGINES_CACHE.cfg.name}"
|
||||
print(title)
|
||||
print("=" * len(title))
|
||||
print(str(ENGINES_CACHE.properties)) # type: ignore
|
||||
|
||||
|
||||
@app.command()
|
||||
def maintenance(force: bool = True):
|
||||
"""Carry out maintenance on cache of the engines."""
|
||||
ENGINES_CACHE.maintenance(force=force)
|
||||
|
||||
|
||||
class EngineCache:
|
||||
"""Persistent (SQLite) key/value cache that deletes its values again after
|
||||
``expire`` seconds (default/max: :py:obj:`MAXHOLD_TIME
|
||||
<searx.cache.ExpireCacheCfg.MAXHOLD_TIME>`). This class is a wrapper around
|
||||
:py:obj:`ENGINES_CACHE` (:py:obj:`ExpireCacheSQLite
|
||||
<searx.cache.ExpireCacheSQLite>`).
|
||||
|
||||
In the :origin:`searx/engines/demo_offline.py` engine you can find an
|
||||
exemplary implementation of such a cache other exaples are implemeted
|
||||
in:
|
||||
|
||||
- :origin:`searx/engines/radio_browser.py`
|
||||
- :origin:`searx/engines/soundcloud.py`
|
||||
- :origin:`searx/engines/startpage.py`
|
||||
|
||||
.. code: python
|
||||
|
||||
from searx.enginelib import EngineCache
|
||||
CACHE: EngineCache
|
||||
|
||||
def init(engine_settings):
|
||||
global CACHE
|
||||
CACHE = EngineCache(engine_settings["name"])
|
||||
|
||||
def request(query, params):
|
||||
token = CACHE.get(key="token")
|
||||
if token is None:
|
||||
token = get_token()
|
||||
# cache token of this engine for 1h
|
||||
CACHE.set(key="token", value=token, expire=3600)
|
||||
...
|
||||
|
||||
For introspection of the DB, jump into developer environment and run command to
|
||||
show cache state::
|
||||
|
||||
$ ./manage pyenv.cmd bash --norc --noprofile
|
||||
(py3) python -m searx.enginelib cache state
|
||||
|
||||
cache tables and key/values
|
||||
===========================
|
||||
[demo_offline ] 2025-04-22 11:32:50 count --> (int) 4
|
||||
[startpage ] 2025-04-22 12:32:30 SC_CODE --> (str) fSOBnhEMlDfE20
|
||||
[duckduckgo ] 2025-04-22 12:32:31 4dff493e.... --> (str) 4-128634958369380006627592672385352473325
|
||||
[duckduckgo ] 2025-04-22 12:40:06 3e2583e2.... --> (str) 4-263126175288871260472289814259666848451
|
||||
[radio_browser ] 2025-04-23 11:33:08 servers --> (list) ['https://de2.api.radio-browser.info', ...]
|
||||
[soundcloud ] 2025-04-29 11:40:06 guest_client_id --> (str) EjkRJG0BLNEZquRiPZYdNtJdyGtTuHdp
|
||||
[wolframalpha ] 2025-04-22 12:40:06 code --> (str) 5aa79f86205ad26188e0e26e28fb7ae7
|
||||
number of tables: 6
|
||||
number of key/value pairs: 7
|
||||
|
||||
In the "cache tables and key/values" section, the table name (engine name) is at
|
||||
first position on the second there is the calculated expire date and on the
|
||||
third and fourth position the key/value is shown.
|
||||
|
||||
About duckduckgo: The *vqd coode* of ddg depends on the query term and therefore
|
||||
the key is a hash value of the query term (to not to store the raw query term).
|
||||
|
||||
In the "properties of ENGINES_CACHE" section all properties of the SQLiteAppl /
|
||||
ExpireCache and their last modification date are shown::
|
||||
|
||||
properties of ENGINES_CACHE
|
||||
===========================
|
||||
[last modified: 2025-04-22 11:32:27] DB_SCHEMA : 1
|
||||
[last modified: 2025-04-22 11:32:27] LAST_MAINTENANCE :
|
||||
[last modified: 2025-04-22 11:32:27] crypt_hash : ca612e3566fdfd7cf7efe...
|
||||
[last modified: 2025-04-22 11:32:30] CACHE-TABLE--demo_offline: demo_offline
|
||||
[last modified: 2025-04-22 11:32:30] CACHE-TABLE--startpage: startpage
|
||||
[last modified: 2025-04-22 11:32:31] CACHE-TABLE--duckduckgo: duckduckgo
|
||||
[last modified: 2025-04-22 11:33:08] CACHE-TABLE--radio_browser: radio_browser
|
||||
[last modified: 2025-04-22 11:40:06] CACHE-TABLE--soundcloud: soundcloud
|
||||
[last modified: 2025-04-22 11:40:06] CACHE-TABLE--wolframalpha: wolframalpha
|
||||
|
||||
These properties provide information about the state of the ExpireCache and
|
||||
control the behavior. For example, the maintenance intervals are controlled by
|
||||
the last modification date of the LAST_MAINTENANCE property and the hash value
|
||||
of the password can be used to detect whether the password has been changed (in
|
||||
this case the DB entries can no longer be decrypted and the entire cache must be
|
||||
discarded).
|
||||
"""
|
||||
|
||||
def __init__(self, engine_name: str, expire: int | None = None):
|
||||
self.expire = expire or ENGINES_CACHE.cfg.MAXHOLD_TIME
|
||||
_valid = "-_." + string.ascii_letters + string.digits
|
||||
self.table_name = "".join([c if c in _valid else "_" for c in engine_name])
|
||||
|
||||
def set(self, key: str, value: Any, expire: int | None = None) -> bool:
|
||||
return ENGINES_CACHE.set(
|
||||
key=key,
|
||||
value=value,
|
||||
expire=expire or self.expire,
|
||||
ctx=self.table_name,
|
||||
)
|
||||
|
||||
def get(self, key: str, default=None) -> Any:
|
||||
return ENGINES_CACHE.get(key, default=default, ctx=self.table_name)
|
||||
|
||||
def secret_hash(self, name: str | bytes) -> str:
|
||||
return ENGINES_CACHE.secret_hash(name=name)
|
||||
|
||||
|
||||
class Engine: # pylint: disable=too-few-public-methods
|
||||
"""Class of engine instances build from YAML settings.
|
||||
|
||||
|
||||
21
searx/enginelib/__main__.py
Normal file
21
searx/enginelib/__main__.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Implementation of a command line for development purposes. To start a
|
||||
command, switch to the environment and run library module as a script::
|
||||
|
||||
$ ./manage pyenv.cmd bash --norc --noprofile
|
||||
(py3) python -m searx.enginelib --help
|
||||
|
||||
The following commands can be used for maintenance and introspection
|
||||
(development) of the engine cache::
|
||||
|
||||
(py3) python -m searx.enginelib cache state
|
||||
(py3) python -m searx.enginelib cache maintenance
|
||||
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from .. import enginelib
|
||||
|
||||
app = typer.Typer()
|
||||
app.add_typer(enginelib.app, name="cache", help="Commands related to the cache of the engines.")
|
||||
app()
|
||||
81
searx/engines/ansa.py
Normal file
81
searx/engines/ansa.py
Normal file
@ -0,0 +1,81 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Engine for Ansa, Italy's oldest news agency.
|
||||
|
||||
To use this engine add the following entry to your engines
|
||||
list in ``settings.yml``:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: ansa
|
||||
engine: ansa
|
||||
shortcut: ans
|
||||
disabled: false
|
||||
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
from searx.utils import eval_xpath, eval_xpath_list, extract_text
|
||||
|
||||
engine_type = 'online'
|
||||
language_support = False
|
||||
categories = ['news']
|
||||
paging = True
|
||||
page_size = 12
|
||||
base_url = 'https://www.ansa.it'
|
||||
|
||||
time_range_support = True
|
||||
time_range_args = {
|
||||
'day': 1,
|
||||
'week': 7,
|
||||
'month': 31,
|
||||
'year': 365,
|
||||
}
|
||||
# https://www.ansa.it/ricerca/ansait/search.shtml?start=0&any=houthi&periodo=&sort=data%3Adesc
|
||||
search_api = 'https://www.ansa.it/ricerca/ansait/search.shtml?'
|
||||
|
||||
about = {
|
||||
'website': 'https://www.ansa.it',
|
||||
'wikidata_id': 'Q392934',
|
||||
'official_api_documentation': None,
|
||||
'use_official_api': False,
|
||||
'require_api_key': False,
|
||||
'results': 'HTML',
|
||||
'language': 'it',
|
||||
}
|
||||
|
||||
|
||||
def request(query, params):
|
||||
query_params = {
|
||||
'any': query,
|
||||
'start': (params['pageno'] - 1) * page_size,
|
||||
'sort': "data:desc",
|
||||
}
|
||||
|
||||
if params['time_range']:
|
||||
query_params['periodo'] = time_range_args.get(params['time_range'])
|
||||
|
||||
params['url'] = search_api + urlencode(query_params)
|
||||
return params
|
||||
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
res = EngineResults()
|
||||
doc = html.fromstring(resp.text)
|
||||
|
||||
for result in eval_xpath_list(doc, "//div[@class='article']"):
|
||||
|
||||
res_obj = MainResult(
|
||||
title=extract_text(eval_xpath(result, "./div[@class='content']/h2[@class='title']/a")),
|
||||
content=extract_text(eval_xpath(result, "./div[@class='content']/div[@class='text']")),
|
||||
url=base_url + extract_text(eval_xpath(result, "./div[@class='content']/h2[@class='title']/a/@href")),
|
||||
)
|
||||
|
||||
thumbnail = extract_text(eval_xpath(result, "./div[@class='image']/a/img/@src"))
|
||||
if thumbnail:
|
||||
res_obj.thumbnail = base_url + thumbnail
|
||||
|
||||
res.append(res_obj)
|
||||
|
||||
return res
|
||||
@ -129,6 +129,7 @@ from lxml import html
|
||||
|
||||
from searx import locales
|
||||
from searx.utils import (
|
||||
extr,
|
||||
extract_text,
|
||||
eval_xpath,
|
||||
eval_xpath_list,
|
||||
@ -253,33 +254,6 @@ def _extract_published_date(published_date_raw):
|
||||
return None
|
||||
|
||||
|
||||
def parse_data_string(resp):
|
||||
# kit.start(app, element, {
|
||||
# node_ids: [0, 19],
|
||||
# data: [{"type":"data","data" .... ["q","goggles_id"],"route":1,"url":1}}]
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
kit_start = resp.text.index("kit.start(app,")
|
||||
start = resp.text[kit_start:].index('data: [{"type":"data"')
|
||||
start = kit_start + start + len('data: ')
|
||||
|
||||
lev = 0
|
||||
end = start
|
||||
inner = False
|
||||
for c in resp.text[start:]:
|
||||
if inner and lev == 0:
|
||||
break
|
||||
end += 1
|
||||
if c == "[":
|
||||
lev += 1
|
||||
inner = True
|
||||
continue
|
||||
if c == "]":
|
||||
lev -= 1
|
||||
|
||||
json_data = js_variable_to_python(resp.text[start:end])
|
||||
return json_data
|
||||
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
|
||||
if brave_category in ('search', 'goggles'):
|
||||
@ -288,7 +262,15 @@ def response(resp) -> EngineResults:
|
||||
if brave_category in ('news'):
|
||||
return _parse_news(resp)
|
||||
|
||||
json_data = parse_data_string(resp)
|
||||
# Example script source containing the data:
|
||||
#
|
||||
# kit.start(app, element, {
|
||||
# node_ids: [0, 19],
|
||||
# data: [{type:"data",data: .... ["q","goggles_id"],route:1,url:1}}]
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
js_object = "[{" + extr(resp.text, "data: [{", "}}],") + "}}]"
|
||||
json_data = js_variable_to_python(js_object)
|
||||
|
||||
# json_data is a list and at the second position (0,1) in this list we find the "response" data we need ..
|
||||
json_resp = json_data[1]['data']['body']['response']
|
||||
|
||||
|
||||
@ -1,5 +1,60 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""ChinaSo: A search engine from ChinaSo."""
|
||||
"""ChinaSo_, a search engine for the chinese language area.
|
||||
|
||||
.. attention::
|
||||
|
||||
ChinaSo engine does not return real URL, the links from these search
|
||||
engines violate the privacy of the users!!
|
||||
|
||||
We try to find a solution for this problem, please follow `issue #4694`_.
|
||||
|
||||
As long as the problem has not been resolved, these engines are
|
||||
not active in a standard setup (``inactive: true``).
|
||||
|
||||
.. _ChinaSo: https://www.chinaso.com/
|
||||
.. _issue #4694: https://github.com/searxng/searxng/issues/4694
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The engine has the following additional settings:
|
||||
|
||||
- :py:obj:`chinaso_category` (:py:obj:`ChinasoCategoryType`)
|
||||
- :py:obj:`chinaso_news_source` (:py:obj:`ChinasoNewsSourceType`)
|
||||
|
||||
In the example below, all three ChinaSO engines are using the :ref:`network
|
||||
<engine network>` from the ``chinaso news`` engine.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: chinaso news
|
||||
engine: chinaso
|
||||
shortcut: chinaso
|
||||
categories: [news]
|
||||
chinaso_category: news
|
||||
chinaso_news_source: all
|
||||
|
||||
- name: chinaso images
|
||||
engine: chinaso
|
||||
network: chinaso news
|
||||
shortcut: chinasoi
|
||||
categories: [images]
|
||||
chinaso_category: images
|
||||
|
||||
- name: chinaso videos
|
||||
engine: chinaso
|
||||
network: chinaso news
|
||||
shortcut: chinasov
|
||||
categories: [videos]
|
||||
chinaso_category: videos
|
||||
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
|
||||
import typing
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from datetime import datetime
|
||||
@ -20,13 +75,31 @@ paging = True
|
||||
time_range_support = True
|
||||
results_per_page = 10
|
||||
categories = []
|
||||
chinaso_category = 'news'
|
||||
|
||||
ChinasoCategoryType = typing.Literal['news', 'videos', 'images']
|
||||
"""ChinaSo supports news, videos, images search.
|
||||
|
||||
- ``news``: search for news
|
||||
- ``videos``: search for videos
|
||||
- ``images``: search for images
|
||||
|
||||
In the category ``news`` you can additionally filter by option
|
||||
:py:obj:`chinaso_news_source`.
|
||||
"""
|
||||
chinaso_category = 'news'
|
||||
"""Configure ChinaSo category (:py:obj:`ChinasoCategoryType`)."""
|
||||
|
||||
ChinasoNewsSourceType = typing.Literal['CENTRAL', 'LOCAL', 'BUSINESS', 'EPAPER', 'all']
|
||||
"""Filtering ChinaSo-News results by source:
|
||||
|
||||
- ``CENTRAL``: central publication
|
||||
- ``LOCAL``: local publication
|
||||
- ``BUSINESS``: business publication
|
||||
- ``EPAPER``: E-Paper
|
||||
- ``all``: all sources
|
||||
"""
|
||||
chinaso_news_source: ChinasoNewsSourceType = 'all'
|
||||
"""Configure ChinaSo-News type (:py:obj:`ChinasoNewsSourceType`)."""
|
||||
|
||||
time_range_dict = {'day': '24h', 'week': '1w', 'month': '1m', 'year': '1y'}
|
||||
|
||||
@ -35,7 +108,9 @@ base_url = "https://www.chinaso.com"
|
||||
|
||||
def init(_):
|
||||
if chinaso_category not in ('news', 'videos', 'images'):
|
||||
raise SearxEngineAPIException(f"Unsupported category: {chinaso_category}")
|
||||
raise ValueError(f"Unsupported category: {chinaso_category}")
|
||||
if chinaso_category == 'news' and chinaso_news_source not in typing.get_args(ChinasoNewsSourceType):
|
||||
raise ValueError(f"Unsupported news source: {chinaso_news_source}")
|
||||
|
||||
|
||||
def request(query, params):
|
||||
@ -56,6 +131,11 @@ def request(query, params):
|
||||
'params': {'start_index': (params["pageno"] - 1) * results_per_page, 'rn': results_per_page},
|
||||
},
|
||||
}
|
||||
if chinaso_news_source != 'all':
|
||||
if chinaso_news_source == 'EPAPER':
|
||||
category_config['news']['params']["type"] = 'EPAPER'
|
||||
else:
|
||||
category_config['news']['params']["cate"] = chinaso_news_source
|
||||
|
||||
query_params.update(category_config[chinaso_category]['params'])
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ close to the implementation, its just a simple example. To get in use of this
|
||||
import json
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
from searx.enginelib import EngineCache
|
||||
|
||||
engine_type = 'offline'
|
||||
categories = ['general']
|
||||
@ -32,14 +33,18 @@ about = {
|
||||
# if there is a need for globals, use a leading underline
|
||||
_my_offline_engine: str = ""
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||
seconds."""
|
||||
|
||||
def init(engine_settings=None):
|
||||
|
||||
def init(engine_settings):
|
||||
"""Initialization of the (offline) engine. The origin of this demo engine is a
|
||||
simple json string which is loaded in this example while the engine is
|
||||
initialized.
|
||||
initialized."""
|
||||
global _my_offline_engine, CACHE # pylint: disable=global-statement
|
||||
|
||||
"""
|
||||
global _my_offline_engine # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"]) # type:ignore
|
||||
|
||||
_my_offline_engine = (
|
||||
'[ {"value": "%s"}'
|
||||
@ -57,8 +62,8 @@ def search(query, request_params) -> EngineResults:
|
||||
results.
|
||||
"""
|
||||
res = EngineResults()
|
||||
count = CACHE.get("count", 0)
|
||||
|
||||
count = 0
|
||||
for row in json.loads(_my_offline_engine):
|
||||
count += 1
|
||||
kvmap = {
|
||||
@ -75,4 +80,7 @@ def search(query, request_params) -> EngineResults:
|
||||
)
|
||||
)
|
||||
res.add(res.types.LegacyResult(number_of_results=count))
|
||||
|
||||
# cache counter value for 20sec
|
||||
CACHE.set("count", count, expire=20)
|
||||
return res
|
||||
|
||||
@ -6,16 +6,17 @@ DuckDuckGo WEB
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import re
|
||||
from urllib.parse import quote_plus
|
||||
import json
|
||||
import re
|
||||
import typing
|
||||
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import babel
|
||||
import lxml.html
|
||||
|
||||
from searx import (
|
||||
locales,
|
||||
redislib,
|
||||
external_bang,
|
||||
)
|
||||
from searx.utils import (
|
||||
@ -25,12 +26,12 @@ from searx.utils import (
|
||||
extract_text,
|
||||
)
|
||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||
from searx import redisdb
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.exceptions import SearxEngineCaptchaException
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if typing.TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
logger: logging.Logger
|
||||
@ -61,28 +62,18 @@ url = "https://html.duckduckgo.com/html"
|
||||
|
||||
time_range_dict = {'day': 'd', 'week': 'w', 'month': 'm', 'year': 'y'}
|
||||
form_data = {'v': 'l', 'api': 'd.js', 'o': 'json'}
|
||||
__CACHE = []
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||
seconds."""
|
||||
|
||||
|
||||
def _cache_key(query: str, region: str):
|
||||
return 'SearXNG_ddg_web_vqd' + redislib.secret_hash(f"{query}//{region}")
|
||||
def init(_): # pylint: disable=unused-argument
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache("duckduckgo") # type:ignore
|
||||
|
||||
|
||||
def cache_vqd(query: str, region: str, value: str):
|
||||
"""Caches a ``vqd`` value from a query."""
|
||||
c = redisdb.client()
|
||||
if c:
|
||||
logger.debug("VALKEY cache vqd value: %s (%s)", value, region)
|
||||
c.set(_cache_key(query, region), value, ex=600)
|
||||
|
||||
else:
|
||||
logger.debug("MEM cache vqd value: %s (%s)", value, region)
|
||||
if len(__CACHE) > 100: # cache vqd from last 100 queries
|
||||
__CACHE.pop(0)
|
||||
__CACHE.append((_cache_key(query, region), value))
|
||||
|
||||
|
||||
def get_vqd(query: str, region: str, force_request: bool = False):
|
||||
def get_vqd(query: str, region: str, force_request: bool = False) -> str:
|
||||
"""Returns the ``vqd`` that fits to the *query*.
|
||||
|
||||
:param query: The query term
|
||||
@ -114,31 +105,34 @@ def get_vqd(query: str, region: str, force_request: bool = False):
|
||||
seems the block list is a sliding window: to get my IP rid from the bot list
|
||||
I had to cool down my IP for 1h (send no requests from that IP to DDG).
|
||||
"""
|
||||
key = _cache_key(query, region)
|
||||
key = CACHE.secret_hash(f"{query}//{region}")
|
||||
value = CACHE.get(key=key)
|
||||
if value is not None and not force_request:
|
||||
logger.debug("vqd: re-use cached value: %s", value)
|
||||
return value
|
||||
|
||||
c = redisdb.client()
|
||||
if c:
|
||||
value = c.get(key)
|
||||
if value or value == b'':
|
||||
value = value.decode('utf-8') # type: ignore
|
||||
logger.debug("re-use CACHED vqd value: %s", value)
|
||||
return value
|
||||
logger.debug("vqd: request value from from duckduckgo.com")
|
||||
resp = get(f'https://duckduckgo.com/?q={quote_plus(query)}')
|
||||
if resp.status_code == 200: # type: ignore
|
||||
value = extr(resp.text, 'vqd="', '"') # type: ignore
|
||||
if value:
|
||||
logger.debug("vqd value from duckduckgo.com request: '%s'", value)
|
||||
else:
|
||||
logger.error("vqd: can't parse value from ddg response (return empty string)")
|
||||
return ""
|
||||
else:
|
||||
logger.error("vqd: got HTTP %s from duckduckgo.com", resp.status_code)
|
||||
|
||||
for k, value in __CACHE:
|
||||
if k == key:
|
||||
logger.debug("MEM re-use CACHED vqd value: %s", value)
|
||||
return value
|
||||
if value:
|
||||
CACHE.set(key=key, value=value)
|
||||
else:
|
||||
logger.error("vqd value from duckduckgo.com ", resp.status_code)
|
||||
return value
|
||||
|
||||
if force_request:
|
||||
resp = get(f'https://duckduckgo.com/?q={quote_plus(query)}')
|
||||
if resp.status_code == 200: # type: ignore
|
||||
value = extr(resp.text, 'vqd="', '"') # type: ignore
|
||||
if value:
|
||||
logger.debug("vqd value from DDG request: %s", value)
|
||||
cache_vqd(query, region, value)
|
||||
return value
|
||||
|
||||
return None
|
||||
def set_vqd(query: str, region: str, value: str):
|
||||
key = CACHE.secret_hash(f"{query}//{region}")
|
||||
CACHE.set(key=key, value=value, expire=3600)
|
||||
|
||||
|
||||
def get_ddg_lang(eng_traits: EngineTraits, sxng_locale, default='en_US'):
|
||||
@ -373,8 +367,11 @@ def response(resp) -> EngineResults:
|
||||
# some locales (at least China) does not have a "next page" button
|
||||
form = form[0]
|
||||
form_vqd = eval_xpath(form, '//input[@name="vqd"]/@value')[0]
|
||||
|
||||
cache_vqd(resp.search_params['data']['q'], resp.search_params['data']['kl'], form_vqd)
|
||||
set_vqd(
|
||||
query=resp.search_params['data']['q'],
|
||||
region=resp.search_params['data']['kl'],
|
||||
value=str(form_vqd),
|
||||
)
|
||||
|
||||
# just select "web-result" and ignore results of class "result--ad result--ad--small"
|
||||
for div_result in eval_xpath(doc, '//div[@id="links"]/div[contains(@class, "web-result")]'):
|
||||
@ -401,7 +398,7 @@ def response(resp) -> EngineResults:
|
||||
results.add(
|
||||
results.types.Answer(
|
||||
answer=zero_click,
|
||||
url=eval_xpath_getindex(doc, '//div[@id="zero_click_abstract"]/a/@href', 0),
|
||||
url=eval_xpath_getindex(doc, '//div[@id="zero_click_abstract"]/a/@href', 0), # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
116
searx/engines/huggingface.py
Normal file
116
searx/engines/huggingface.py
Normal file
@ -0,0 +1,116 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""`Hugging Face`_ search engine for SearXNG.
|
||||
|
||||
.. _Hugging Face: https://huggingface.co
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The engine has the following additional settings:
|
||||
|
||||
- :py:obj:`huggingface_endpoint`
|
||||
|
||||
Configurations for endpoints:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: huggingface
|
||||
engine: huggingface
|
||||
shortcut: hf
|
||||
|
||||
- name: huggingface datasets
|
||||
huggingface_endpoint: datasets
|
||||
engine: huggingface
|
||||
shortcut: hfd
|
||||
|
||||
- name: huggingface spaces
|
||||
huggingface_endpoint: spaces
|
||||
engine: huggingface
|
||||
shortcut: hfs
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from datetime import datetime
|
||||
|
||||
from searx.exceptions import SearxEngineAPIException
|
||||
from searx.utils import html_to_text
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
|
||||
about = {
|
||||
"website": "https://huggingface.co/",
|
||||
"wikidata_id": "Q108943604",
|
||||
"official_api_documentation": "https://huggingface.co/docs/hub/en/api",
|
||||
"use_official_api": True,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
categories = ['it', 'repos']
|
||||
|
||||
base_url = "https://huggingface.co"
|
||||
|
||||
huggingface_endpoint = 'models'
|
||||
"""Hugging Face supports datasets, models, spaces as search endpoint.
|
||||
|
||||
- ``datasets``: search for datasets
|
||||
- ``models``: search for models
|
||||
- ``spaces``: search for spaces
|
||||
"""
|
||||
|
||||
|
||||
def init(_):
|
||||
if huggingface_endpoint not in ('datasets', 'models', 'spaces'):
|
||||
raise SearxEngineAPIException(f"Unsupported Hugging Face endpoint: {huggingface_endpoint}")
|
||||
|
||||
|
||||
def request(query, params):
|
||||
query_params = {
|
||||
"direction": -1,
|
||||
"search": query,
|
||||
}
|
||||
|
||||
params["url"] = f"{base_url}/api/{huggingface_endpoint}?{urlencode(query_params)}"
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
data = resp.json()
|
||||
|
||||
for entry in data:
|
||||
if huggingface_endpoint != 'models':
|
||||
url = f"{base_url}/{huggingface_endpoint}/{entry['id']}"
|
||||
else:
|
||||
url = f"{base_url}/{entry['id']}"
|
||||
|
||||
published_date = None
|
||||
try:
|
||||
published_date = datetime.strptime(entry["createdAt"], "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
contents = []
|
||||
if entry.get("likes"):
|
||||
contents.append(f"Likes: {entry['likes']}")
|
||||
if entry.get("downloads"):
|
||||
contents.append(f"Downloads: {entry['downloads']:,}")
|
||||
if entry.get("tags"):
|
||||
contents.append(f"Tags: {', '.join(entry['tags'])}")
|
||||
if entry.get("description"):
|
||||
contents.append(f"Description: {entry['description']}")
|
||||
|
||||
item = MainResult(
|
||||
title=entry["id"],
|
||||
content=html_to_text(" | ".join(contents)),
|
||||
url=url,
|
||||
publishedDate=published_date,
|
||||
)
|
||||
results.add(item)
|
||||
|
||||
return results
|
||||
68
searx/engines/microsoft_learn.py
Normal file
68
searx/engines/microsoft_learn.py
Normal file
@ -0,0 +1,68 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Engine for Microsoft Learn, Microsoft's technical knowledge base.
|
||||
|
||||
To use this engine add the following entry to your engines list
|
||||
in ``settings.yml``:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: microsoft learn
|
||||
engine: microsoft_learn
|
||||
shortcut: msl
|
||||
disabled: false
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
engine_type = "online"
|
||||
language_support = True
|
||||
categories = ["it"]
|
||||
paging = True
|
||||
page_size = 10
|
||||
time_range_support = False
|
||||
|
||||
search_api = "https://learn.microsoft.com/api/search?"
|
||||
|
||||
about = {
|
||||
"website": "https://learn.microsoft.com",
|
||||
"wikidata_id": "Q123663245",
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
|
||||
def request(query, params):
|
||||
|
||||
if params['language'] == 'all':
|
||||
params['language'] = 'en-us'
|
||||
|
||||
query_params = [
|
||||
("search", query),
|
||||
("locale", params["language"]),
|
||||
("scoringprofile", "semantic-answers"),
|
||||
("facet", "category"),
|
||||
("facet", "products"),
|
||||
("facet", "tags"),
|
||||
("$top", "10"),
|
||||
("$skip", (params["pageno"] - 1) * page_size),
|
||||
("expandScope", "true"),
|
||||
("includeQuestion", "false"),
|
||||
("applyOperator", "false"),
|
||||
("partnerId", "LearnSite"),
|
||||
]
|
||||
|
||||
params["url"] = search_api + urlencode(query_params)
|
||||
return params
|
||||
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
res = EngineResults()
|
||||
json_data = resp.json()
|
||||
|
||||
for result in json_data["results"]:
|
||||
res.add(res.types.MainResult(url=result["url"], title=result["title"], content=result.get("description", "")))
|
||||
|
||||
return res
|
||||
@ -1,46 +1,61 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
"""This is the implementation of the Mullvad-Leta meta-search engine.
|
||||
|
||||
This engine **REQUIRES** that searxng operate within a Mullvad VPN
|
||||
|
||||
If using docker, consider using gluetun for easily connecting to the Mullvad
|
||||
|
||||
- https://github.com/qdm12/gluetun
|
||||
|
||||
Otherwise, follow instructions provided by Mullvad for enabling the VPN on Linux
|
||||
|
||||
- https://mullvad.net/en/help/install-mullvad-app-linux
|
||||
"""Mullvad Leta is a search engine proxy. Currently Leta only offers text
|
||||
search results not image, news or any other types of search result. Leta acts
|
||||
as a proxy to Google and Brave search results. You can select which backend
|
||||
search engine you wish to use, see (:py:obj:`leta_engine`).
|
||||
|
||||
.. hint::
|
||||
|
||||
The :py:obj:`EngineTraits` is empty by default. Maintainers have to run
|
||||
``make data.traits`` (in the Mullvad VPN / :py:obj:`fetch_traits`) and rebase
|
||||
the modified JSON file ``searx/data/engine_traits.json`` on every single
|
||||
update of SearXNG!
|
||||
Leta caches each search for up to 30 days. For example, if you use search
|
||||
terms like ``news``, contrary to your intention you'll get very old results!
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The engine has the following additional settings:
|
||||
|
||||
- :py:obj:`leta_engine` (:py:obj:`LetaEnginesType`)
|
||||
|
||||
You can configure one Leta engine for Google and one for Brave:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: mullvadleta
|
||||
engine: mullvad_leta
|
||||
leta_engine: google
|
||||
shortcut: ml
|
||||
|
||||
- name: mullvadleta brave
|
||||
engine: mullvad_leta
|
||||
network: mullvadleta # use network from engine "mullvadleta" configured above
|
||||
leta_engine: brave
|
||||
shortcut: mlb
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import typing
|
||||
from urllib.parse import urlencode
|
||||
import babel
|
||||
from httpx import Response
|
||||
from lxml import html
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.locales import region_tag, get_official_locales
|
||||
from searx.utils import eval_xpath, extract_text, eval_xpath_list
|
||||
from searx.exceptions import SearxEngineResponseException
|
||||
from searx.locales import get_official_locales, language_tag, region_tag
|
||||
from searx.utils import eval_xpath_list
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if typing.TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
traits: EngineTraits
|
||||
|
||||
use_cache: bool = True # non-cache use only has 100 searches per day!
|
||||
|
||||
leta_engine: str = 'google'
|
||||
|
||||
search_url = "https://leta.mullvad.net"
|
||||
|
||||
# about
|
||||
@ -54,154 +69,205 @@ about = {
|
||||
}
|
||||
|
||||
# engine dependent config
|
||||
categories = ['general', 'web']
|
||||
categories = ["general", "web"]
|
||||
paging = True
|
||||
max_page = 50
|
||||
max_page = 10
|
||||
time_range_support = True
|
||||
time_range_dict = {
|
||||
"day": "d1",
|
||||
"week": "w1",
|
||||
"month": "m1",
|
||||
"year": "y1",
|
||||
"day": "d",
|
||||
"week": "w",
|
||||
"month": "m",
|
||||
"year": "y",
|
||||
}
|
||||
|
||||
available_leta_engines = [
|
||||
'google', # first will be default if provided engine is invalid
|
||||
'brave',
|
||||
]
|
||||
LetaEnginesType = typing.Literal["google", "brave"]
|
||||
"""Engine types supported by mullvadleta."""
|
||||
|
||||
leta_engine: LetaEnginesType = "google"
|
||||
"""Select Leta's engine type from :py:obj:`LetaEnginesType`."""
|
||||
|
||||
|
||||
def is_vpn_connected(dom: html.HtmlElement) -> bool:
|
||||
"""Returns true if the VPN is connected, False otherwise"""
|
||||
connected_text = extract_text(eval_xpath(dom, '//main/div/p[1]'))
|
||||
return connected_text != 'You are not connected to Mullvad VPN.'
|
||||
def init(_):
|
||||
l = typing.get_args(LetaEnginesType)
|
||||
if leta_engine not in l:
|
||||
raise ValueError(f"leta_engine '{leta_engine}' is invalid, use one of {', '.join(l)}")
|
||||
|
||||
|
||||
def assign_headers(headers: dict) -> dict:
|
||||
"""Assigns the headers to make a request to Mullvad Leta"""
|
||||
headers['Accept'] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
|
||||
headers['Content-Type'] = "application/x-www-form-urlencoded"
|
||||
headers['Host'] = "leta.mullvad.net"
|
||||
headers['Origin'] = "https://leta.mullvad.net"
|
||||
return headers
|
||||
class DataNodeQueryMetaDataIndices(typing.TypedDict):
|
||||
"""Indices into query metadata."""
|
||||
|
||||
success: int
|
||||
q: int # pylint: disable=invalid-name
|
||||
country: int
|
||||
language: int
|
||||
lastUpdated: int
|
||||
engine: int
|
||||
items: int
|
||||
infobox: int
|
||||
news: int
|
||||
timestamp: int
|
||||
altered: int
|
||||
page: int
|
||||
next: int # if -1, there no more results are available
|
||||
previous: int
|
||||
|
||||
|
||||
class DataNodeResultIndices(typing.TypedDict):
|
||||
"""Indices into query resultsdata."""
|
||||
|
||||
link: int
|
||||
snippet: int
|
||||
title: int
|
||||
favicon: int
|
||||
|
||||
|
||||
def request(query: str, params: dict):
|
||||
country = traits.get_region(params.get('searxng_locale', 'all'), traits.all_locale) # type: ignore
|
||||
|
||||
result_engine = leta_engine
|
||||
if leta_engine not in available_leta_engines:
|
||||
result_engine = available_leta_engines[0]
|
||||
logger.warning(
|
||||
'Configured engine "%s" not one of the available engines %s, defaulting to "%s"',
|
||||
leta_engine,
|
||||
available_leta_engines,
|
||||
result_engine,
|
||||
)
|
||||
|
||||
params['url'] = search_url
|
||||
params['method'] = 'POST'
|
||||
params['data'] = {
|
||||
params["method"] = "GET"
|
||||
args = {
|
||||
"q": query,
|
||||
"gl": country if country is str else '',
|
||||
'engine': result_engine,
|
||||
"engine": leta_engine,
|
||||
"x-sveltekit-invalidated": "001", # hardcoded from all requests seen
|
||||
}
|
||||
# pylint: disable=undefined-variable
|
||||
if use_cache:
|
||||
params['data']['oc'] = "on"
|
||||
# pylint: enable=undefined-variable
|
||||
|
||||
if params['time_range'] in time_range_dict:
|
||||
params['dateRestrict'] = time_range_dict[params['time_range']]
|
||||
else:
|
||||
params['dateRestrict'] = ''
|
||||
country = traits.get_region(params.get("searxng_locale"), traits.all_locale) # type: ignore
|
||||
if country:
|
||||
args["country"] = country
|
||||
|
||||
if params['pageno'] > 1:
|
||||
# Page 1 is n/a, Page 2 is 11, page 3 is 21, ...
|
||||
params['data']['start'] = ''.join([str(params['pageno'] - 1), "1"])
|
||||
language = traits.get_language(params.get("searxng_locale"), traits.all_locale) # type: ignore
|
||||
if language:
|
||||
args["language"] = language
|
||||
|
||||
if params['headers'] is None:
|
||||
params['headers'] = {}
|
||||
if params["time_range"] in time_range_dict:
|
||||
args["lastUpdated"] = time_range_dict[params["time_range"]]
|
||||
|
||||
if params["pageno"] > 1:
|
||||
args["page"] = params["pageno"]
|
||||
|
||||
params["url"] = f"{search_url}/search/__data.json?{urlencode(args)}"
|
||||
|
||||
assign_headers(params['headers'])
|
||||
return params
|
||||
|
||||
|
||||
def extract_result(dom_result: list[html.HtmlElement]):
|
||||
# Infoboxes sometimes appear in the beginning and will have a length of 0
|
||||
if len(dom_result) == 3:
|
||||
[a_elem, h3_elem, p_elem] = dom_result
|
||||
elif len(dom_result) == 4:
|
||||
[_, a_elem, h3_elem, p_elem] = dom_result
|
||||
else:
|
||||
return None
|
||||
def response(resp: Response) -> EngineResults:
|
||||
json_response = resp.json()
|
||||
|
||||
return {
|
||||
'url': extract_text(a_elem.text),
|
||||
'title': extract_text(h3_elem),
|
||||
'content': extract_text(p_elem),
|
||||
}
|
||||
nodes = json_response["nodes"]
|
||||
# 0: is None
|
||||
# 1: has "connected=True", not useful
|
||||
# 2: query results within "data"
|
||||
|
||||
data_nodes = nodes[2]["data"]
|
||||
# Instead of nested object structure, all objects are flattened into a
|
||||
# list. Rather, the first object in data_node provides indices into the
|
||||
# "data_nodes" to access each searchresult (which is an object of more
|
||||
# indices)
|
||||
#
|
||||
# Read the relative TypedDict definitions for details
|
||||
|
||||
query_meta_data: DataNodeQueryMetaDataIndices = data_nodes[0]
|
||||
|
||||
query_items_indices = query_meta_data["items"]
|
||||
|
||||
results = EngineResults()
|
||||
for idx in data_nodes[query_items_indices]:
|
||||
query_item_indices: DataNodeResultIndices = data_nodes[idx]
|
||||
results.add(
|
||||
MainResult(
|
||||
url=data_nodes[query_item_indices["link"]],
|
||||
title=data_nodes[query_item_indices["title"]],
|
||||
content=data_nodes[query_item_indices["snippet"]],
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def extract_results(search_results: html.HtmlElement):
|
||||
for search_result in search_results:
|
||||
dom_result = eval_xpath_list(search_result, 'div/div/*')
|
||||
result = extract_result(dom_result)
|
||||
if result is not None:
|
||||
yield result
|
||||
def fetch_traits(engine_traits: EngineTraits) -> None:
|
||||
"""Fetch languages and regions from Mullvad-Leta"""
|
||||
|
||||
def extract_table_data(table):
|
||||
for row in table.xpath(".//tr")[2:]:
|
||||
cells = row.xpath(".//td | .//th") # includes headers and data
|
||||
if len(cells) > 1: # ensure the column exists
|
||||
cell0 = cells[0].text_content().strip()
|
||||
cell1 = cells[1].text_content().strip()
|
||||
yield [cell0, cell1]
|
||||
|
||||
def response(resp: Response):
|
||||
"""Checks if connected to Mullvad VPN, then extracts the search results from
|
||||
the DOM resp: requests response object"""
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
if not is_vpn_connected(dom):
|
||||
raise SearxEngineResponseException('Not connected to Mullvad VPN')
|
||||
search_results = eval_xpath(dom.body, '//main/div[2]/div')
|
||||
return list(extract_results(search_results))
|
||||
|
||||
|
||||
def fetch_traits(engine_traits: EngineTraits):
|
||||
"""Fetch languages and regions from Mullvad-Leta
|
||||
|
||||
.. warning::
|
||||
|
||||
Fetching the engine traits also requires a Mullvad VPN connection. If
|
||||
not connected, then an error message will print and no traits will be
|
||||
updated.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
# see https://github.com/searxng/searxng/issues/762
|
||||
from searx.network import post as http_post
|
||||
from searx.network import get as http_get
|
||||
|
||||
# pylint: enable=import-outside-toplevel
|
||||
resp = http_post(search_url, headers=assign_headers({}))
|
||||
|
||||
resp = http_get(f"{search_url}/documentation")
|
||||
if not isinstance(resp, Response):
|
||||
print("ERROR: failed to get response from mullvad-leta. Are you connected to the VPN?")
|
||||
return
|
||||
if not resp.ok:
|
||||
print("ERROR: response from mullvad-leta is not OK. Are you connected to the VPN?")
|
||||
return
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
if not is_vpn_connected(dom):
|
||||
print('ERROR: Not connected to Mullvad VPN')
|
||||
return
|
||||
# supported region codes
|
||||
options = eval_xpath_list(dom.body, '//main/div/form/div[2]/div/select[1]/option')
|
||||
if options is None or len(options) <= 0:
|
||||
print('ERROR: could not find any results. Are you connected to the VPN?')
|
||||
for x in options:
|
||||
eng_country = x.get("value")
|
||||
|
||||
sxng_locales = get_official_locales(eng_country, engine_traits.languages.keys(), regional=True)
|
||||
# There are 4 HTML tables on the documentation page for extracting information:
|
||||
# 0. Keyboard Shortcuts
|
||||
# 1. Query Parameters (shoutout to Mullvad for accessible docs for integration)
|
||||
# 2. Country Codes [Country, Code]
|
||||
# 3. Language Codes [Language, Code]
|
||||
tables = eval_xpath_list(dom.body, "//table")
|
||||
if tables is None or len(tables) <= 0:
|
||||
print("ERROR: could not find any tables. Was the page updated?")
|
||||
|
||||
if not sxng_locales:
|
||||
print(
|
||||
"ERROR: can't map from Mullvad-Leta country %s (%s) to a babel region."
|
||||
% (x.get('data-name'), eng_country)
|
||||
)
|
||||
language_table = tables[3]
|
||||
lang_map = {
|
||||
"zh-hant": "zh_Hans",
|
||||
"zh-hans": "zh_Hant",
|
||||
"jp": "ja",
|
||||
}
|
||||
|
||||
for language, code in extract_table_data(language_table):
|
||||
|
||||
locale_tag = lang_map.get(code, code).replace("-", "_") # type: ignore
|
||||
try:
|
||||
locale = babel.Locale.parse(locale_tag)
|
||||
except babel.UnknownLocaleError:
|
||||
print(f"ERROR: Mullvad-Leta language {language} ({code}) is unknown by babel")
|
||||
continue
|
||||
|
||||
for sxng_locale in sxng_locales:
|
||||
engine_traits.regions[region_tag(sxng_locale)] = eng_country
|
||||
sxng_tag = language_tag(locale)
|
||||
engine_traits.languages[sxng_tag] = code
|
||||
|
||||
country_table = tables[2]
|
||||
country_map = {
|
||||
"cn": "zh-CN",
|
||||
"hk": "zh-HK",
|
||||
"jp": "ja-JP",
|
||||
"my": "ms-MY",
|
||||
"tw": "zh-TW",
|
||||
"uk": "en-GB",
|
||||
"us": "en-US",
|
||||
}
|
||||
|
||||
for country, code in extract_table_data(country_table):
|
||||
|
||||
sxng_tag = country_map.get(code)
|
||||
if sxng_tag:
|
||||
engine_traits.regions[sxng_tag] = code
|
||||
continue
|
||||
|
||||
try:
|
||||
locale = babel.Locale.parse(f"{code.lower()}_{code.upper()}")
|
||||
except babel.UnknownLocaleError:
|
||||
locale = None
|
||||
|
||||
if locale:
|
||||
engine_traits.regions[region_tag(locale)] = code
|
||||
continue
|
||||
|
||||
official_locales = get_official_locales(code, engine_traits.languages.keys(), regional=True)
|
||||
if not official_locales:
|
||||
print(f"ERROR: Mullvad-Leta country '{code}' ({country}) could not be mapped as expected.")
|
||||
continue
|
||||
|
||||
for locale in official_locales:
|
||||
engine_traits.regions[region_tag(locale)] = code
|
||||
|
||||
@ -40,7 +40,10 @@ about = {
|
||||
}
|
||||
|
||||
base_url = 'https://oqi2j6v4iz-dsn.algolia.net'
|
||||
pdia_config_url = 'https://pdimagearchive.org/_astro/config.BiNvrvzG.js'
|
||||
pdia_base_url = 'https://pdimagearchive.org'
|
||||
pdia_search_url = pdia_base_url + '/search/?q='
|
||||
pdia_config_start = "/_astro/InfiniteSearch."
|
||||
pdia_config_end = ".js"
|
||||
categories = ['images']
|
||||
page_size = 20
|
||||
paging = True
|
||||
@ -62,11 +65,17 @@ def _get_algolia_api_key():
|
||||
if __CACHED_API_KEY:
|
||||
return __CACHED_API_KEY
|
||||
|
||||
resp = get(pdia_search_url)
|
||||
if resp.status_code != 200:
|
||||
raise LookupError("Failed to fetch config location (and as such the API key) for PDImageArchive")
|
||||
pdia_config_filepart = extr(resp.text, pdia_config_start, pdia_config_end)
|
||||
pdia_config_url = pdia_base_url + pdia_config_start + pdia_config_filepart + pdia_config_end
|
||||
|
||||
resp = get(pdia_config_url)
|
||||
if resp.status_code != 200:
|
||||
raise LookupError("Failed to obtain Algolia API key for PDImageArchive")
|
||||
|
||||
api_key = extr(resp.text, 'r="', '"', default=None)
|
||||
api_key = extr(resp.text, 'const r="', '"', default=None)
|
||||
|
||||
if api_key is None:
|
||||
raise LookupError("Couldn't obtain Algolia API key for PDImageArchive")
|
||||
|
||||
@ -11,7 +11,7 @@ from searx.exceptions import SearxEngineAPIException, SearxEngineCaptchaExceptio
|
||||
|
||||
# Metadata
|
||||
about = {
|
||||
"website": "https://m.quark.cn/",
|
||||
"website": "https://quark.sm.cn/",
|
||||
"wikidata_id": "Q48816502",
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
@ -53,7 +53,7 @@ def request(query, params):
|
||||
|
||||
category_config = {
|
||||
'general': {
|
||||
'endpoint': 'https://m.quark.cn/s',
|
||||
'endpoint': 'https://quark.sm.cn/s',
|
||||
'params': {
|
||||
"q": query,
|
||||
"layout": "html",
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
https://de1.api.radio-browser.info/#Advanced_station_search
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import random
|
||||
import socket
|
||||
from urllib.parse import urlencode
|
||||
@ -13,9 +15,15 @@ import babel
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.network import get
|
||||
from searx.enginelib import EngineCache
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.locales import language_tag
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
traits: EngineTraits
|
||||
|
||||
about = {
|
||||
@ -52,11 +60,24 @@ none filters are applied. Valid filters are:
|
||||
|
||||
"""
|
||||
|
||||
servers = []
|
||||
CACHE: EngineCache
|
||||
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||
seconds."""
|
||||
|
||||
|
||||
def init(_):
|
||||
# see https://api.radio-browser.info
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache("radio_browser")
|
||||
server_list()
|
||||
|
||||
|
||||
def server_list() -> list[str]:
|
||||
|
||||
servers = CACHE.get("servers", [])
|
||||
if servers:
|
||||
return servers
|
||||
|
||||
# hint: can take up to 40sec!
|
||||
ips = socket.getaddrinfo("all.api.radio-browser.info", 80, 0, 0, socket.IPPROTO_TCP)
|
||||
for ip_tuple in ips:
|
||||
_ip: str = ip_tuple[4][0] # type: ignore
|
||||
@ -65,8 +86,22 @@ def init(_):
|
||||
if srv not in servers:
|
||||
servers.append(srv)
|
||||
|
||||
# update server list once in 24h
|
||||
CACHE.set(key="servers", value=servers, expire=60 * 60 * 24)
|
||||
|
||||
return servers
|
||||
|
||||
|
||||
def request(query, params):
|
||||
|
||||
servers = server_list()
|
||||
if not servers:
|
||||
logger.error("Fetched server list is empty!")
|
||||
params["url"] = None
|
||||
return
|
||||
|
||||
server = random.choice(servers)
|
||||
|
||||
args = {
|
||||
'name': query,
|
||||
'order': 'votes',
|
||||
@ -87,8 +122,7 @@ def request(query, params):
|
||||
if countrycode in traits.custom['countrycodes']: # type: ignore
|
||||
args['countrycode'] = countrycode
|
||||
|
||||
params['url'] = f"{random.choice(servers)}/json/stations/search?{urlencode(args)}"
|
||||
return params
|
||||
params['url'] = f"{server}/json/stations/search?{urlencode(args)}"
|
||||
|
||||
|
||||
def response(resp):
|
||||
@ -154,8 +188,9 @@ def fetch_traits(engine_traits: EngineTraits):
|
||||
|
||||
babel_reg_list = get_global("territory_languages").keys()
|
||||
|
||||
language_list = get(f'{servers[0]}/json/languages').json() # type: ignore
|
||||
country_list = get(f'{servers[0]}/json/countries').json() # type: ignore
|
||||
server = server_list()[0]
|
||||
language_list = get(f'{server}/json/languages').json() # type: ignore
|
||||
country_list = get(f'{server}/json/countries').json() # type: ignore
|
||||
|
||||
for lang in language_list:
|
||||
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Semantic Scholar (Science)
|
||||
"""
|
||||
"""Semantic Scholar (Science)"""
|
||||
|
||||
from json import dumps, loads
|
||||
from json import dumps
|
||||
from datetime import datetime
|
||||
from lxml import html
|
||||
|
||||
from flask_babel import gettext
|
||||
from searx.network import get
|
||||
from searx.utils import eval_xpath_getindex, gen_useragent, html_to_text
|
||||
|
||||
|
||||
about = {
|
||||
"website": 'https://www.semanticscholar.org/',
|
||||
@ -19,13 +22,31 @@ about = {
|
||||
categories = ['science', 'scientific publications']
|
||||
paging = True
|
||||
search_url = 'https://www.semanticscholar.org/api/1/search'
|
||||
paper_url = 'https://www.semanticscholar.org/paper'
|
||||
base_url = 'https://www.semanticscholar.org'
|
||||
|
||||
|
||||
def _get_ui_version():
|
||||
resp = get(base_url)
|
||||
if not resp.ok:
|
||||
raise RuntimeError("Can't determine Semantic Scholar UI version")
|
||||
|
||||
doc = html.fromstring(resp.text)
|
||||
ui_version = eval_xpath_getindex(doc, "//meta[@name='s2-ui-version']/@content", 0)
|
||||
if not ui_version:
|
||||
raise RuntimeError("Can't determine Semantic Scholar UI version")
|
||||
|
||||
return ui_version
|
||||
|
||||
|
||||
def request(query, params):
|
||||
params['url'] = search_url
|
||||
params['method'] = 'POST'
|
||||
params['headers']['content-type'] = 'application/json'
|
||||
params['headers'] = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-S2-UI-Version': _get_ui_version(),
|
||||
'X-S2-Client': "webapp-browser",
|
||||
'User-Agent': gen_useragent(),
|
||||
}
|
||||
params['data'] = dumps(
|
||||
{
|
||||
"queryString": query,
|
||||
@ -43,7 +64,8 @@ def request(query, params):
|
||||
|
||||
|
||||
def response(resp):
|
||||
res = loads(resp.text)
|
||||
res = resp.json()
|
||||
|
||||
results = []
|
||||
for result in res['results']:
|
||||
url = result.get('primaryPaperLink', {}).get('url')
|
||||
@ -54,7 +76,7 @@ def response(resp):
|
||||
if alternatePaperLinks:
|
||||
url = alternatePaperLinks[0].get('url')
|
||||
if not url:
|
||||
url = paper_url + '/%s' % result['id']
|
||||
url = base_url + '/paper/%s' % result['id']
|
||||
|
||||
# publishedDate
|
||||
if 'pubDate' in result:
|
||||
@ -88,7 +110,7 @@ def response(resp):
|
||||
'template': 'paper.html',
|
||||
'url': url,
|
||||
'title': result['title']['text'],
|
||||
'content': result['paperAbstract']['text'],
|
||||
'content': html_to_text(result['paperAbstract']['text']),
|
||||
'journal': result.get('venue', {}).get('text') or result.get('journal', {}).get('name'),
|
||||
'doi': result.get('doiInfo', {}).get('doi'),
|
||||
'tags': result.get('fieldsOfStudy'),
|
||||
|
||||
151
searx/engines/senscritique.py
Normal file
151
searx/engines/senscritique.py
Normal file
@ -0,0 +1,151 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""SensCritique (movies)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps, loads
|
||||
from typing import Any, Optional
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
|
||||
about = {
|
||||
"website": 'https://www.senscritique.com/',
|
||||
"wikidata_id": 'Q16676060',
|
||||
"official_api_documentation": None,
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
'language': 'fr',
|
||||
}
|
||||
|
||||
categories = ['movies']
|
||||
paging = True
|
||||
page_size = 16
|
||||
graphql_url = 'https://apollo.senscritique.com/'
|
||||
|
||||
graphql_query = """query SearchProductExplorer($query: String, $offset: Int, $limit: Int,
|
||||
$sortBy: SearchProductExplorerSort) {
|
||||
searchProductExplorer(
|
||||
query: $query
|
||||
filters: []
|
||||
sortBy: $sortBy
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
) {
|
||||
items {
|
||||
category
|
||||
dateRelease
|
||||
duration
|
||||
id
|
||||
originalTitle
|
||||
rating
|
||||
title
|
||||
url
|
||||
yearOfProduction
|
||||
medias {
|
||||
picture
|
||||
}
|
||||
countries {
|
||||
name
|
||||
}
|
||||
genresInfos {
|
||||
label
|
||||
}
|
||||
directors {
|
||||
name
|
||||
}
|
||||
stats {
|
||||
ratingCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
def request(query: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
offset = (params['pageno'] - 1) * page_size
|
||||
|
||||
data = {
|
||||
"operationName": "SearchProductExplorer",
|
||||
"variables": {"offset": offset, "limit": page_size, "query": query, "sortBy": "RELEVANCE"},
|
||||
"query": graphql_query,
|
||||
}
|
||||
|
||||
params['url'] = graphql_url
|
||||
params['method'] = 'POST'
|
||||
params['headers']['Content-Type'] = 'application/json'
|
||||
params['data'] = dumps(data)
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
res = EngineResults()
|
||||
response_data = loads(resp.text)
|
||||
|
||||
items = response_data.get('data', {}).get('searchProductExplorer', {}).get('items', [])
|
||||
if not items:
|
||||
return res
|
||||
|
||||
for item in items:
|
||||
result = parse_item(item)
|
||||
if not result:
|
||||
continue
|
||||
res.add(result=result)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def parse_item(item: dict[str, Any]) -> MainResult | None:
|
||||
"""Parse a single item from the SensCritique API response"""
|
||||
title = item.get('title', '')
|
||||
if not title:
|
||||
return None
|
||||
year = item.get('yearOfProduction')
|
||||
original_title = item.get('originalTitle')
|
||||
|
||||
thumbnail: str = ""
|
||||
if item.get('medias', {}) and item['medias'].get('picture'):
|
||||
thumbnail = item['medias']['picture']
|
||||
|
||||
content_parts = build_content_parts(item, title, original_title)
|
||||
url = f"https://www.senscritique.com{item['url']}"
|
||||
|
||||
return MainResult(
|
||||
url=url,
|
||||
title=title + (f' ({year})' if year else ''),
|
||||
content=' | '.join(content_parts),
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
|
||||
|
||||
def build_content_parts(item: dict[str, Any], title: str, original_title: Optional[str]) -> list[str]:
|
||||
"""Build the content parts for an item"""
|
||||
content_parts = []
|
||||
|
||||
if item.get('category'):
|
||||
content_parts.append(item['category'])
|
||||
|
||||
if original_title and original_title != title:
|
||||
content_parts.append(f"Original title: {original_title}")
|
||||
|
||||
if item.get('directors'):
|
||||
directors = [director['name'] for director in item['directors']]
|
||||
content_parts.append(f"Director(s): {', '.join(directors)}")
|
||||
|
||||
if item.get('countries'):
|
||||
countries = [country['name'] for country in item['countries']]
|
||||
content_parts.append(f"Country: {', '.join(countries)}")
|
||||
|
||||
if item.get('genresInfos'):
|
||||
genres = [genre['label'] for genre in item['genresInfos']]
|
||||
content_parts.append(f"Genre(s): {', '.join(genres)}")
|
||||
|
||||
if item.get('duration'):
|
||||
minutes = item['duration'] // 60
|
||||
if minutes > 0:
|
||||
content_parts.append(f"Duration: {minutes} min")
|
||||
|
||||
if item.get('rating') and item.get('stats', {}).get('ratingCount'):
|
||||
content_parts.append(f"Rating: {item['rating']}/10 ({item['stats']['ratingCount']} votes)")
|
||||
|
||||
return content_parts
|
||||
@ -1,17 +1,26 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""SoundCloud is a German audio streaming service."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
|
||||
from dateutil import parser
|
||||
from lxml import html
|
||||
|
||||
from searx.network import get as http_get
|
||||
from searx.enginelib import EngineCache
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
logger: logging.Logger
|
||||
|
||||
about = {
|
||||
"website": "ttps://soundcloud.com",
|
||||
"website": "https://soundcloud.com",
|
||||
"wikidata_id": "Q568769",
|
||||
"official_api_documentation": "https://developers.soundcloud.com/docs/api/guide",
|
||||
"use_official_api": False,
|
||||
@ -28,7 +37,6 @@ HTML frontend of the common WEB site.
|
||||
"""
|
||||
|
||||
cid_re = re.compile(r'client_id:"([^"]*)"', re.I | re.U)
|
||||
guest_client_id = ""
|
||||
results_per_page = 10
|
||||
|
||||
soundcloud_facet = "model"
|
||||
@ -48,6 +56,10 @@ app_locale_map = {
|
||||
"sv": "sv",
|
||||
}
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||
seconds."""
|
||||
|
||||
|
||||
def request(query, params):
|
||||
|
||||
@ -55,6 +67,12 @@ def request(query, params):
|
||||
# - user_id=451561-497874-703312-310156
|
||||
# - app_version=1740727428
|
||||
|
||||
guest_client_id = CACHE.get("guest_client_id")
|
||||
if guest_client_id is None:
|
||||
guest_client_id = get_client_id()
|
||||
if guest_client_id:
|
||||
CACHE.set(key="guest_client_id", value=guest_client_id)
|
||||
|
||||
args = {
|
||||
"q": query,
|
||||
"offset": (params['pageno'] - 1) * results_per_page,
|
||||
@ -104,12 +122,12 @@ def response(resp):
|
||||
return results
|
||||
|
||||
|
||||
def init(engine_settings=None): # pylint: disable=unused-argument
|
||||
global guest_client_id # pylint: disable=global-statement
|
||||
guest_client_id = get_client_id()
|
||||
def init(engine_settings): # pylint: disable=unused-argument
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"]) # type:ignore
|
||||
|
||||
|
||||
def get_client_id() -> str:
|
||||
def get_client_id() -> str | None:
|
||||
|
||||
client_id = ""
|
||||
url = "https://soundcloud.com"
|
||||
@ -143,4 +161,4 @@ def get_client_id() -> str:
|
||||
logger.info("using client_id '%s' for soundclud queries", client_id)
|
||||
else:
|
||||
logger.warning("missing valid client_id for soundclud queries")
|
||||
return client_id
|
||||
return client_id or None
|
||||
|
||||
@ -84,7 +84,6 @@ from typing import TYPE_CHECKING, Any
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
from unicodedata import normalize, combining
|
||||
from time import time
|
||||
from datetime import datetime, timedelta
|
||||
from json import loads
|
||||
|
||||
@ -97,6 +96,7 @@ from searx.network import get # see https://github.com/searxng/searxng/issues/7
|
||||
from searx.exceptions import SearxEngineCaptchaException
|
||||
from searx.locales import region_tag
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.enginelib import EngineCache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
@ -159,10 +159,21 @@ search_form_xpath = '//form[@id="search"]'
|
||||
</form>
|
||||
"""
|
||||
|
||||
# timestamp of the last fetch of 'sc' code
|
||||
sc_code_ts = 0
|
||||
sc_code = ''
|
||||
sc_code_cache_sec = 30
|
||||
|
||||
CACHE: EngineCache
|
||||
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||
seconds."""
|
||||
|
||||
|
||||
def init(_):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
|
||||
# hint: all three startpage engines (WEB, Images & News) can/should use the
|
||||
# same sc_code ..
|
||||
CACHE = EngineCache("startpage") # type:ignore
|
||||
|
||||
|
||||
sc_code_cache_sec = 3600
|
||||
"""Time in seconds the sc-code is cached in memory :py:obj:`get_sc_code`."""
|
||||
|
||||
|
||||
@ -176,14 +187,10 @@ def get_sc_code(searxng_locale, params):
|
||||
|
||||
Startpage's search form generates a new sc-code on each request. This
|
||||
function scrap a new sc-code from Startpage's home page every
|
||||
:py:obj:`sc_code_cache_sec` seconds.
|
||||
:py:obj:`sc_code_cache_sec` seconds."""
|
||||
|
||||
"""
|
||||
|
||||
global sc_code_ts, sc_code # pylint: disable=global-statement
|
||||
|
||||
if sc_code and (time() < (sc_code_ts + sc_code_cache_sec)):
|
||||
logger.debug("get_sc_code: reuse '%s'", sc_code)
|
||||
sc_code = CACHE.get("SC_CODE", "")
|
||||
if sc_code:
|
||||
return sc_code
|
||||
|
||||
headers = {**params['headers']}
|
||||
@ -233,8 +240,9 @@ def get_sc_code(searxng_locale, params):
|
||||
message="get_sc_code: [PR-695] query new sc time-stamp failed! (%s)" % resp.url, # type: ignore
|
||||
) from exc
|
||||
|
||||
sc_code_ts = time()
|
||||
sc_code = str(sc_code)
|
||||
logger.debug("get_sc_code: new value is: %s", sc_code)
|
||||
CACHE.set(key="SC_CODE", value=sc_code, expire=sc_code_cache_sec)
|
||||
return sc_code
|
||||
|
||||
|
||||
|
||||
51
searx/engines/steam.py
Normal file
51
searx/engines/steam.py
Normal file
@ -0,0 +1,51 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Steam (store) for SearXNG."""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from searx.utils import html_to_text
|
||||
from searx.result_types import EngineResults, MainResult
|
||||
|
||||
about = {
|
||||
"website": 'https://store.steampowered.com/',
|
||||
"wikidata_id": 'Q337535',
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
}
|
||||
|
||||
categories = []
|
||||
|
||||
base_url = "https://store.steampowered.com"
|
||||
|
||||
|
||||
def request(query, params):
|
||||
query_params = {"term": query, "cc": "us", "l": "en"}
|
||||
params['url'] = f'{base_url}/api/storesearch/?{urlencode(query_params)}'
|
||||
return params
|
||||
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
search_results = resp.json()
|
||||
|
||||
for item in search_results.get('items', []):
|
||||
app_id = item.get('id')
|
||||
|
||||
currency = item.get('price', {}).get('currency', 'USD')
|
||||
price = item.get('price', {}).get('final', 0) / 100
|
||||
|
||||
platforms = ', '.join([platform for platform, supported in item.get('platforms', {}).items() if supported])
|
||||
|
||||
content = [f'Price: {price:.2f} {currency}', f'Platforms: {platforms}']
|
||||
|
||||
results.add(
|
||||
MainResult(
|
||||
title=item.get('name'),
|
||||
content=html_to_text(' | '.join(content)),
|
||||
url=f'{base_url}/app/{app_id}',
|
||||
thumbnail=item.get('tiny_image', ''),
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from lxml import etree
|
||||
import lxml.etree
|
||||
|
||||
# about
|
||||
about = {
|
||||
@ -72,7 +72,7 @@ def replace_pua_chars(text):
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
search_results = etree.XML(resp.content)
|
||||
search_results = lxml.etree.XML(resp.content)
|
||||
|
||||
# return empty array if there are no results
|
||||
if search_results.xpath(failure_xpath):
|
||||
|
||||
@ -3,11 +3,13 @@
|
||||
Wolfram|Alpha (Science)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from json import loads
|
||||
from time import time
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from searx.network import get as http_get
|
||||
from searx.enginelib import EngineCache
|
||||
|
||||
# about
|
||||
about = {
|
||||
@ -40,41 +42,39 @@ search_url = (
|
||||
|
||||
referer_url = url + 'input/?{query}'
|
||||
|
||||
token = {'value': '', 'last_updated': None}
|
||||
|
||||
# pods to display as image in infobox
|
||||
# this pods do return a plaintext, but they look better and are more useful as images
|
||||
image_pods = {'VisualRepresentation', 'Illustration', 'Symbol'}
|
||||
|
||||
|
||||
# seems, wolframalpha resets its token in every hour
|
||||
def obtain_token():
|
||||
update_time = time() - (time() % 3600)
|
||||
try:
|
||||
token_response = http_get('https://www.wolframalpha.com/input/api/v1/code?ts=9999999999999999999', timeout=2.0)
|
||||
token['value'] = loads(token_response.text)['code']
|
||||
token['last_updated'] = update_time
|
||||
except: # pylint: disable=bare-except
|
||||
pass
|
||||
CACHE: EngineCache
|
||||
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||
seconds."""
|
||||
|
||||
|
||||
def init(engine_settings):
|
||||
global CACHE # pylint: disable=global-statement
|
||||
CACHE = EngineCache(engine_settings["name"]) # type:ignore
|
||||
|
||||
|
||||
def obtain_token() -> str:
|
||||
token = CACHE.get(key="token")
|
||||
if token is None:
|
||||
resp = http_get('https://www.wolframalpha.com/input/api/v1/code?ts=9999999999999999999', timeout=2.0)
|
||||
token = resp.json()["code"]
|
||||
# seems, wolframalpha resets its token in every hour
|
||||
CACHE.set(key="code", value=token, expire=3600)
|
||||
return token
|
||||
|
||||
|
||||
def init(engine_settings=None): # pylint: disable=unused-argument
|
||||
obtain_token()
|
||||
|
||||
|
||||
# do search-request
|
||||
def request(query, params):
|
||||
# obtain token if last update was more than an hour
|
||||
if time() - (token['last_updated'] or 0) > 3600:
|
||||
obtain_token()
|
||||
params['url'] = search_url.format(query=urlencode({'input': query}), token=token['value'])
|
||||
token = obtain_token()
|
||||
params['url'] = search_url.format(query=urlencode({'input': query}), token=token)
|
||||
params['headers']['Referer'] = referer_url.format(query=urlencode({'i': query}))
|
||||
|
||||
return params
|
||||
|
||||
|
||||
# get response from search-request
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
|
||||
@ -63,21 +63,52 @@ lang2domain = {
|
||||
}
|
||||
"""Map language to domain"""
|
||||
|
||||
locale_aliases = {
|
||||
'zh': 'zh_Hans',
|
||||
'zh-HK': 'zh_Hans',
|
||||
'zh-CN': 'zh_Hans', # dead since 2015 / routed to hk.search.yahoo.com
|
||||
'zh-TW': 'zh_Hant',
|
||||
yahoo_languages = {
|
||||
"all": "any",
|
||||
"ar": "ar",
|
||||
"bg": "bg",
|
||||
"cs": "cs",
|
||||
"da": "da",
|
||||
"de": "de",
|
||||
"el": "el",
|
||||
"en": "en",
|
||||
"es": "es",
|
||||
"et": "et",
|
||||
"fi": "fi",
|
||||
"fr": "fr",
|
||||
"he": "he",
|
||||
"hr": "hr",
|
||||
"hu": "hu",
|
||||
"it": "it",
|
||||
"ja": "ja",
|
||||
"ko": "ko",
|
||||
"lt": "lt",
|
||||
"lv": "lv",
|
||||
"nl": "nl",
|
||||
"no": "no",
|
||||
"pl": "pl",
|
||||
"pt": "pt",
|
||||
"ro": "ro",
|
||||
"ru": "ru",
|
||||
"sk": "sk",
|
||||
"sl": "sl",
|
||||
"sv": "sv",
|
||||
"th": "th",
|
||||
"tr": "tr",
|
||||
"zh": "zh_chs",
|
||||
"zh_Hans": "zh_chs",
|
||||
'zh-CN': "zh_chs",
|
||||
"zh_Hant": "zh_cht",
|
||||
"zh-HK": "zh_cht",
|
||||
'zh-TW': "zh_cht",
|
||||
}
|
||||
|
||||
|
||||
def request(query, params):
|
||||
"""build request"""
|
||||
|
||||
lang = locale_aliases.get(params['language'], None)
|
||||
if not lang:
|
||||
lang = params['language'].split('-')[0]
|
||||
lang = traits.get_language(lang, traits.all_locale)
|
||||
lang = params["language"].split("-")[0]
|
||||
lang = yahoo_languages.get(lang, "any")
|
||||
|
||||
offset = (params['pageno'] - 1) * 7 + 1
|
||||
age, btf = time_range_dict.get(params['time_range'], ('', ''))
|
||||
@ -154,39 +185,3 @@ def response(resp):
|
||||
results.append({'suggestion': extract_text(suggestion)})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def fetch_traits(engine_traits: EngineTraits):
|
||||
"""Fetch languages from yahoo"""
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import babel
|
||||
from searx import network
|
||||
from searx.locales import language_tag
|
||||
|
||||
engine_traits.all_locale = 'any'
|
||||
|
||||
resp = network.get('https://search.yahoo.com/preferences/languages')
|
||||
if not resp.ok:
|
||||
print("ERROR: response from yahoo is not OK.")
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
offset = len('lang_')
|
||||
|
||||
eng2sxng = {'zh_chs': 'zh_Hans', 'zh_cht': 'zh_Hant'}
|
||||
|
||||
for val in eval_xpath_list(dom, '//div[contains(@class, "lang-item")]/input/@value'):
|
||||
eng_tag = val[offset:]
|
||||
|
||||
try:
|
||||
sxng_tag = language_tag(babel.Locale.parse(eng2sxng.get(eng_tag, eng_tag)))
|
||||
except babel.UnknownLocaleError:
|
||||
print('ERROR: unknown language --> %s' % eng_tag)
|
||||
continue
|
||||
|
||||
conflict = engine_traits.languages.get(sxng_tag)
|
||||
if conflict:
|
||||
if conflict != eng_tag:
|
||||
print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, eng_tag))
|
||||
continue
|
||||
engine_traits.languages[sxng_tag] = eng_tag
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Implementations for providing the favicons in SearXNG"""
|
||||
"""Implementations for providing the favicons in SearXNG.
|
||||
|
||||
There is a command line for developer purposes and for deeper analysis. Here is
|
||||
an example in which the command line is called in the development environment::
|
||||
|
||||
$ ./manage pyenv.cmd bash --norc --noprofile
|
||||
(py3) python -m searx.favicons --help
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@ -236,6 +236,12 @@ class FaviconCacheSQLite(sqlitedb.SQLiteAppl, FaviconCache):
|
||||
model in the SQLite DB is implemented using the abstract class
|
||||
:py:obj:`sqlitedb.SQLiteAppl`.
|
||||
|
||||
For introspection of the DB, jump into developer environment and run command
|
||||
to show cache state::
|
||||
|
||||
$ ./manage pyenv.cmd bash --norc --noprofile
|
||||
(py3) python -m searx.favicons cache state
|
||||
|
||||
The following configurations are required / supported:
|
||||
|
||||
- :py:obj:`FaviconCacheConfig.db_url`
|
||||
@ -357,6 +363,10 @@ CREATE TABLE IF NOT EXISTS blob_map (
|
||||
if sha256 != FALLBACK_ICON:
|
||||
conn.execute(self.SQL_INSERT_BLOBS, (sha256, bytes_c, mime, data))
|
||||
conn.execute(self.SQL_INSERT_BLOB_MAP, (sha256, resolver, authority))
|
||||
# hint: the with context of the connection object closes the transaction
|
||||
# but not the DB connection. The connection has to be closed by the
|
||||
# caller of self.connect()!
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
@ -376,7 +386,8 @@ CREATE TABLE IF NOT EXISTS blob_map (
|
||||
return
|
||||
self.properties.set("LAST_MAINTENANCE", "") # hint: this (also) sets the m_time of the property!
|
||||
|
||||
# do maintenance tasks
|
||||
# Do maintenance tasks. This can be take a little more time, to avoid
|
||||
# DB locks, etablish a new DB connecton.
|
||||
|
||||
with self.connect() as conn:
|
||||
|
||||
@ -407,6 +418,12 @@ CREATE TABLE IF NOT EXISTS blob_map (
|
||||
conn.execute("DELETE FROM blob_map WHERE sha256 IN ('%s')" % "','".join(sha_list))
|
||||
logger.debug("dropped %s blobs with total size of %s bytes", len(sha_list), c)
|
||||
|
||||
# Vacuuming the WALs
|
||||
# https://www.theunterminatedstring.com/sqlite-vacuuming/
|
||||
|
||||
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
conn.close()
|
||||
|
||||
def _query_val(self, sql, default=None):
|
||||
val = self.DB.execute(sql).fetchone()
|
||||
if val is not None:
|
||||
|
||||
@ -112,6 +112,7 @@ from searx.botdetection import (
|
||||
http_accept_encoding,
|
||||
http_accept_language,
|
||||
http_user_agent,
|
||||
http_sec_fetch,
|
||||
ip_limit,
|
||||
ip_lists,
|
||||
get_network,
|
||||
@ -179,16 +180,17 @@ def filter_request(request: SXNG_Request) -> werkzeug.Response | None:
|
||||
logger.error("BLOCK %s: matched BLOCKLIST - %s", network.compressed, msg)
|
||||
return flask.make_response(('IP is on BLOCKLIST - %s' % msg, 429))
|
||||
|
||||
# methods applied on /
|
||||
# methods applied on all requests
|
||||
|
||||
for func in [
|
||||
http_user_agent,
|
||||
]:
|
||||
val = func.filter_request(network, request, cfg)
|
||||
if val is not None:
|
||||
logger.debug(f"NOT OK ({func.__name__}): {network}: %s", dump_request(sxng_request))
|
||||
return val
|
||||
|
||||
# methods applied on /search
|
||||
# methods applied on /search requests
|
||||
|
||||
if request.path == '/search':
|
||||
|
||||
@ -197,11 +199,14 @@ def filter_request(request: SXNG_Request) -> werkzeug.Response | None:
|
||||
http_accept_encoding,
|
||||
http_accept_language,
|
||||
http_user_agent,
|
||||
http_sec_fetch,
|
||||
ip_limit,
|
||||
]:
|
||||
val = func.filter_request(network, request, cfg)
|
||||
if val is not None:
|
||||
logger.debug(f"NOT OK ({func.__name__}): {network}: %s", dump_request(sxng_request))
|
||||
return val
|
||||
|
||||
logger.debug(f"OK {network}: %s", dump_request(sxng_request))
|
||||
return None
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ from typing import Dict
|
||||
|
||||
import httpx
|
||||
|
||||
from searx import logger, searx_debug
|
||||
from searx import logger, sxng_debug
|
||||
from searx.extended_types import SXNG_Response
|
||||
from .client import new_client, get_loop, AsyncHTTPTransportNoHttp
|
||||
from .raise_for_httperror import raise_for_httperror
|
||||
@ -186,7 +186,7 @@ class Network:
|
||||
local_address = next(self._local_addresses_cycle)
|
||||
proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key
|
||||
key = (verify, max_redirects, local_address, proxies)
|
||||
hook_log_response = self.log_response if searx_debug else None
|
||||
hook_log_response = self.log_response if sxng_debug else None
|
||||
if key not in self._clients or self._clients[key].is_closed:
|
||||
client = new_client(
|
||||
self.enable_http,
|
||||
|
||||
@ -33,7 +33,7 @@ class SXNGPlugin(Plugin):
|
||||
"""Rewrite hostnames, remove results or prioritize them."""
|
||||
|
||||
id = "tor_check"
|
||||
keywords = ["tor-check"]
|
||||
keywords = ["tor-check", "tor_check", "torcheck", "tor", "tor check"]
|
||||
|
||||
def __init__(self, plg_cfg: "PluginCfg") -> None:
|
||||
super().__init__(plg_cfg)
|
||||
@ -53,7 +53,7 @@ class SXNGPlugin(Plugin):
|
||||
if search.search_query.pageno > 1:
|
||||
return results
|
||||
|
||||
if search.search_query.query.lower() == "tor-check":
|
||||
if search.search_query.query.lower() in self.keywords:
|
||||
|
||||
# Request the list of tor exit nodes.
|
||||
try:
|
||||
|
||||
@ -155,7 +155,7 @@ class ExternalBangParser(QueryPartParser):
|
||||
return raw_value.startswith('!!') and len(raw_value) > 2
|
||||
|
||||
def __call__(self, raw_value):
|
||||
value = raw_value[2:]
|
||||
value = raw_value[2:].lower()
|
||||
found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, [])
|
||||
if self.enable_autocomplete:
|
||||
self._autocomplete(bang_ac_list)
|
||||
@ -183,7 +183,7 @@ class BangParser(QueryPartParser):
|
||||
return raw_value[0] == '!' and (len(raw_value) < 2 or raw_value[1] != '!')
|
||||
|
||||
def __call__(self, raw_value):
|
||||
value = raw_value[1:].replace('-', ' ').replace('_', ' ')
|
||||
value = raw_value[1:].replace('-', ' ').replace('_', ' ').lower()
|
||||
found = self._parse(value) if len(value) > 0 else False
|
||||
if found and raw_value[0] == '!':
|
||||
self.raw_text_query.specific = True
|
||||
|
||||
@ -55,8 +55,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
|
||||
result.parsed_url = result.parsed_url._replace(
|
||||
# if the result has no scheme, use http as default
|
||||
scheme=result.parsed_url.scheme or "http",
|
||||
# normalize ``example.com/path/`` to ``example.com/path``
|
||||
path=result.parsed_url.path.rstrip("/"),
|
||||
path=result.parsed_url.path,
|
||||
)
|
||||
result.url = result.parsed_url.geturl()
|
||||
|
||||
@ -73,7 +72,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
|
||||
item["url"] = _url._replace(
|
||||
scheme=_url.scheme or "http",
|
||||
# netloc=_url.netloc.replace("www.", ""),
|
||||
path=_url.path.rstrip("/"),
|
||||
path=_url.path,
|
||||
).geturl()
|
||||
|
||||
infobox_id = getattr(result, "id", None)
|
||||
@ -82,7 +81,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
|
||||
result.id = _url._replace(
|
||||
scheme=_url.scheme or "http",
|
||||
# netloc=_url.netloc.replace("www.", ""),
|
||||
path=_url.path.rstrip("/"),
|
||||
path=_url.path,
|
||||
).geturl()
|
||||
|
||||
|
||||
@ -259,9 +258,6 @@ class Result(msgspec.Struct, kw_only=True):
|
||||
``parse_url`` from field ``url``. The ``url`` field is initialized
|
||||
with the resulting value in ``parse_url``, if ``url`` and
|
||||
``parse_url`` are not equal.
|
||||
|
||||
- ``example.com/path/`` and ``example.com/path`` are equivalent and are
|
||||
normalized to ``example.com/path``.
|
||||
"""
|
||||
_normalize_url_fields(self)
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
|
||||
|
||||
import redis.exceptions
|
||||
|
||||
from searx import logger, settings, searx_debug
|
||||
from searx import logger, settings, sxng_debug
|
||||
from searx.redisdb import client as get_redis_client
|
||||
from searx.exceptions import SearxSettingsException
|
||||
from searx.search.processors import PROCESSORS
|
||||
@ -139,7 +139,7 @@ def initialize():
|
||||
signal.signal(signal.SIGUSR1, _signal_handler)
|
||||
|
||||
# special case when debug is activate
|
||||
if searx_debug and settings['checker']['off_when_debug']:
|
||||
if sxng_debug and settings['checker']['off_when_debug']:
|
||||
logger.info('debug mode: checker is disabled')
|
||||
return
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ server:
|
||||
port: 8888
|
||||
bind_address: "127.0.0.1"
|
||||
# public URL of the instance, to ensure correct inbound links. Is overwritten
|
||||
# by ${SEARXNG_URL}.
|
||||
# by ${SEARXNG_BASE_URL}.
|
||||
base_url: false # "http://example.com/location"
|
||||
# rate limit the number of request on the instance, block some bots.
|
||||
# Is overwritten by ${SEARXNG_LIMITER}
|
||||
@ -101,7 +101,8 @@ server:
|
||||
# 1.0 and 1.1 are supported
|
||||
http_protocol_version: "1.0"
|
||||
# POST queries are more secure as they don't show up in history but may cause
|
||||
# problems when using Firefox containers
|
||||
# problems when using Firefox containers.
|
||||
# Is overwritten by ${SEARXNG_METHOD}
|
||||
method: "POST"
|
||||
default_http_headers:
|
||||
X-Content-Type-Options: nosniff
|
||||
@ -433,6 +434,11 @@ engines:
|
||||
disabled: true
|
||||
shortcut: aa
|
||||
|
||||
- name: ansa
|
||||
engine: ansa
|
||||
shortcut: ans
|
||||
disabled: true
|
||||
|
||||
# - name: annas articles
|
||||
# engine: annas_archive
|
||||
# shortcut: aaa
|
||||
@ -613,23 +619,37 @@ engines:
|
||||
# to show premium or plus results too:
|
||||
# skip_premium: false
|
||||
|
||||
# WARNING: links from chinaso.com voilate users privacy
|
||||
# Before activate these engines its mandatory to read
|
||||
# - https://github.com/searxng/searxng/issues/4694
|
||||
# - https://docs.searxng.org/dev/engines/online/chinaso.html
|
||||
|
||||
- name: chinaso news
|
||||
chinaso_category: news
|
||||
engine: chinaso
|
||||
shortcut: chinaso
|
||||
categories: [news]
|
||||
chinaso_category: news
|
||||
chinaso_news_source: all
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: chinaso images
|
||||
chinaso_category: images
|
||||
engine: chinaso
|
||||
network: chinaso news
|
||||
shortcut: chinasoi
|
||||
categories: [images]
|
||||
chinaso_category: images
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: chinaso videos
|
||||
chinaso_category: videos
|
||||
engine: chinaso
|
||||
network: chinaso news
|
||||
shortcut: chinasov
|
||||
categories: [videos]
|
||||
chinaso_category: videos
|
||||
disabled: true
|
||||
inactive: true
|
||||
|
||||
- name: cloudflareai
|
||||
engine: cloudflareai
|
||||
@ -705,26 +725,6 @@ engines:
|
||||
search_type: news
|
||||
disabled: true
|
||||
|
||||
- name: curlie
|
||||
engine: xpath
|
||||
shortcut: cl
|
||||
categories: general
|
||||
disabled: true
|
||||
paging: true
|
||||
lang_all: ''
|
||||
search_url: https://curlie.org/search?q={query}&lang={lang}&start={pageno}&stime=92452189
|
||||
page_size: 20
|
||||
results_xpath: //div[@id="site-list-content"]/div[@class="site-item"]
|
||||
url_xpath: ./div[@class="title-and-desc"]/a/@href
|
||||
title_xpath: ./div[@class="title-and-desc"]/a/div
|
||||
content_xpath: ./div[@class="title-and-desc"]/div[@class="site-descr"]
|
||||
about:
|
||||
website: https://curlie.org/
|
||||
wikidata_id: Q60715723
|
||||
use_official_api: false
|
||||
require_api_key: false
|
||||
results: HTML
|
||||
|
||||
- name: currency
|
||||
engine: currency_convert
|
||||
categories: general
|
||||
@ -1129,6 +1129,22 @@ engines:
|
||||
- name: il post
|
||||
engine: il_post
|
||||
shortcut: pst
|
||||
|
||||
- name: huggingface
|
||||
engine: huggingface
|
||||
shortcut: hf
|
||||
disabled: true
|
||||
|
||||
- name: huggingface datasets
|
||||
huggingface_endpoint: datasets
|
||||
engine: huggingface
|
||||
shortcut: hfd
|
||||
disabled: true
|
||||
|
||||
- name: huggingface spaces
|
||||
huggingface_endpoint: spaces
|
||||
engine: huggingface
|
||||
shortcut: hfs
|
||||
disabled: true
|
||||
|
||||
- name: imdb
|
||||
@ -1326,6 +1342,11 @@ engines:
|
||||
# index: my-index
|
||||
# auth_key: Bearer XXXX
|
||||
|
||||
- name: microsoft learn
|
||||
engine: microsoft_learn
|
||||
shortcut: msl
|
||||
disabled: true
|
||||
|
||||
- name: mixcloud
|
||||
engine: mixcloud
|
||||
shortcut: mc
|
||||
@ -1393,14 +1414,21 @@ engines:
|
||||
require_api_key: false
|
||||
results: JSON
|
||||
|
||||
# read https://docs.searxng.org/dev/engines/online/mullvad_leta.html
|
||||
# - name: mullvadleta
|
||||
# engine: mullvad_leta
|
||||
# leta_engine: google # choose one of the following: google, brave
|
||||
# use_cache: true # Only 100 non-cache searches per day, suggested only for private instances
|
||||
# search_url: https://leta.mullvad.net
|
||||
# categories: [general, web]
|
||||
# shortcut: ml
|
||||
# https://docs.searxng.org/dev/engines/online/mullvad_leta.html
|
||||
- name: mullvadleta
|
||||
engine: mullvad_leta
|
||||
disabled: true
|
||||
leta_engine: google
|
||||
categories: [general, web]
|
||||
shortcut: ml
|
||||
|
||||
- name: mullvadleta brave
|
||||
engine: mullvad_leta
|
||||
network: mullvadleta
|
||||
disabled: true
|
||||
leta_engine: brave
|
||||
categories: [general, web]
|
||||
shortcut: mlb
|
||||
|
||||
- name: odysee
|
||||
engine: odysee
|
||||
@ -1945,6 +1973,11 @@ engines:
|
||||
categories: [images, web]
|
||||
shortcut: spi
|
||||
|
||||
- name: steam
|
||||
engine: steam
|
||||
shortcut: stm
|
||||
disabled: true
|
||||
|
||||
- name: tokyotoshokan
|
||||
engine: tokyotoshokan
|
||||
shortcut: tt
|
||||
@ -2637,6 +2670,12 @@ engines:
|
||||
shortcut: pgo
|
||||
disabled: true
|
||||
|
||||
- name: senscritique
|
||||
engine: senscritique
|
||||
shortcut: scr
|
||||
timeout: 4.0
|
||||
disabled: true
|
||||
|
||||
# Doku engine lets you access to any Doku wiki instance:
|
||||
# A public one or a privete/corporate one.
|
||||
# - name: ubuntuwiki
|
||||
|
||||
@ -182,7 +182,7 @@ SCHEMA = {
|
||||
'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'),
|
||||
'image_proxy': SettingsValue(bool, False, 'SEARXNG_IMAGE_PROXY'),
|
||||
'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),
|
||||
'method': SettingsValue(('POST', 'GET'), 'POST'),
|
||||
'method': SettingsValue(('POST', 'GET'), 'POST', 'SEARXNG_METHOD'),
|
||||
'default_http_headers': SettingsValue(dict, {}),
|
||||
},
|
||||
'redis': {
|
||||
|
||||
@ -7,20 +7,83 @@
|
||||
:py:obj:`SQLiteProperties`:
|
||||
Class to manage properties stored in a database.
|
||||
|
||||
----
|
||||
Examplarical implementations based on :py:obj:`SQLiteAppl`:
|
||||
|
||||
:py:obj:`searx.cache.ExpireCacheSQLite` :
|
||||
Cache that manages key/value pairs in a SQLite DB, in which the key/value
|
||||
pairs are deleted after an "expire" time. This type of cache is used, for
|
||||
example, for the engines, see :py:obj:`searx.enginelib.EngineCache`.
|
||||
|
||||
:py:obj:`searx.favicons.cache.FaviconCacheSQLite` :
|
||||
Favicon cache that manages the favicon BLOBs in a SQLite DB.
|
||||
|
||||
----
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import abc
|
||||
import datetime
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
import threading
|
||||
import abc
|
||||
import uuid
|
||||
|
||||
from searx import logger
|
||||
|
||||
logger = logger.getChild('sqlitedb')
|
||||
logger = logger.getChild("sqlitedb")
|
||||
|
||||
THREAD_LOCAL = threading.local()
|
||||
|
||||
|
||||
class DBSession:
|
||||
"""A *thead-local* DB session"""
|
||||
|
||||
@classmethod
|
||||
def get_connect(cls, app: SQLiteAppl) -> sqlite3.Connection:
|
||||
"""Returns a thread local DB connection. The connection is only
|
||||
established once per thread.
|
||||
"""
|
||||
if getattr(THREAD_LOCAL, "DBSession_map", None) is None:
|
||||
THREAD_LOCAL.DBSession_map = {}
|
||||
|
||||
session = THREAD_LOCAL.DBSession_map.get(app.db_url)
|
||||
if session is None:
|
||||
session = cls(app)
|
||||
return session.conn
|
||||
|
||||
def __init__(self, app: SQLiteAppl):
|
||||
self.uuid = uuid.uuid4()
|
||||
self.app = app
|
||||
self._conn = None
|
||||
# self.__del__ will be called, when thread ends
|
||||
if getattr(THREAD_LOCAL, "DBSession_map", None) is None:
|
||||
THREAD_LOCAL.DBSession_map = {}
|
||||
THREAD_LOCAL.DBSession_map[self.app.db_url] = self
|
||||
|
||||
@property
|
||||
def conn(self) -> sqlite3.Connection:
|
||||
msg = f"[{threading.current_thread().ident}] DBSession: " f"{self.app.__class__.__name__}({self.app.db_url})"
|
||||
if self._conn is None:
|
||||
self._conn = self.app.connect()
|
||||
logger.debug("%s --> created new connection", msg)
|
||||
# else:
|
||||
# logger.debug("%s --> already connected", msg)
|
||||
|
||||
return self._conn
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if self._conn is not None:
|
||||
# HINT: Don't use Python's logging facility in a destructor, it
|
||||
# will produce error reports when python aborts the process or
|
||||
# thread, because at this point objects that the logging module
|
||||
# needs, do not exist anymore.
|
||||
# msg = f"DBSession: close [{self.uuid}] {self.app.__class__.__name__}({self.app.db_url})"
|
||||
# logger.debug(msg)
|
||||
self._conn.close()
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
pass
|
||||
|
||||
|
||||
class SQLiteAppl(abc.ABC):
|
||||
@ -51,13 +114,18 @@ class SQLiteAppl(abc.ABC):
|
||||
"""
|
||||
|
||||
SQLITE_JOURNAL_MODE = "WAL"
|
||||
"""``SQLiteAppl`` applications are optimzed for WAL_ mode, its not recommend
|
||||
to change the journal mode (see :py:obj:`SQLiteAppl.tear_down`).
|
||||
|
||||
.. _WAL: https://sqlite.org/wal.html
|
||||
"""
|
||||
SQLITE_CONNECT_ARGS = {
|
||||
# "timeout": 5.0,
|
||||
# "detect_types": 0,
|
||||
"check_same_thread": bool(SQLITE_THREADING_MODE != "serialized"),
|
||||
"cached_statements": 0, # https://github.com/python/cpython/issues/118172
|
||||
# "uri": False,
|
||||
"autocommit": False,
|
||||
"isolation_level": None,
|
||||
} # fmt:skip
|
||||
"""Connection arguments (:py:obj:`sqlite3.connect`)
|
||||
|
||||
@ -66,10 +134,6 @@ class SQLiteAppl(abc.ABC):
|
||||
``serialized``. The check is more of a hindrance in this case because it
|
||||
would prevent a DB connector from being used in multiple threads.
|
||||
|
||||
``autocommit``:
|
||||
Is disabled by default. Note: autocommit option has been added in Python
|
||||
3.12.
|
||||
|
||||
``cached_statements``:
|
||||
Is set to ``0`` by default. Note: Python 3.12+ fetch result are not
|
||||
consistent in multi-threading application and causing an API misuse error.
|
||||
@ -89,9 +153,17 @@ class SQLiteAppl(abc.ABC):
|
||||
|
||||
self.db_url = db_url
|
||||
self.properties = SQLiteProperties(db_url)
|
||||
self.thread_local = threading.local()
|
||||
self._init_done = False
|
||||
self._compatibility()
|
||||
# atexit.register(self.tear_down)
|
||||
|
||||
# def tear_down(self):
|
||||
# """:ref:`Vacuuming the WALs` upon normal interpreter termination
|
||||
# (:py:obj:`atexit.register`).
|
||||
|
||||
# .. _SQLite: Vacuuming the WALs: https://www.theunterminatedstring.com/sqlite-vacuuming/
|
||||
# """
|
||||
# self.DB.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
|
||||
def _compatibility(self):
|
||||
|
||||
@ -113,19 +185,31 @@ class SQLiteAppl(abc.ABC):
|
||||
"SQLite runtime library version %s is not supported (require >= 3.35)", sqlite3.sqlite_version
|
||||
)
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.Connection(self.db_url, **self.SQLITE_CONNECT_ARGS) # type: ignore
|
||||
conn.execute(f"PRAGMA journal_mode={self.SQLITE_JOURNAL_MODE}")
|
||||
self.register_functions(conn)
|
||||
return conn
|
||||
|
||||
def connect(self) -> sqlite3.Connection:
|
||||
"""Creates a new DB connection (:py:obj:`SQLITE_CONNECT_ARGS`). If not
|
||||
already done, the DB schema is set up
|
||||
already done, the DB schema is set up. The caller must take care of
|
||||
closing the resource. Alternatively, :py:obj:`SQLiteAppl.DB` can also
|
||||
be used (the resource behind `self.DB` is automatically closed when the
|
||||
process or thread is terminated).
|
||||
"""
|
||||
if sys.version_info < (3, 12):
|
||||
# Prior Python 3.12 there is no "autocommit" option
|
||||
self.SQLITE_CONNECT_ARGS.pop("autocommit", None)
|
||||
|
||||
self.init()
|
||||
logger.debug("%s: connect to DB: %s // %s", self.__class__.__name__, self.db_url, self.SQLITE_CONNECT_ARGS)
|
||||
conn = sqlite3.Connection(self.db_url, **self.SQLITE_CONNECT_ARGS) # type: ignore
|
||||
conn.execute(f"PRAGMA journal_mode={self.SQLITE_JOURNAL_MODE}")
|
||||
self.register_functions(conn)
|
||||
msg = (
|
||||
f"[{threading.current_thread().ident}] {self.__class__.__name__}({self.db_url})"
|
||||
f" {self.SQLITE_CONNECT_ARGS} // {self.SQLITE_JOURNAL_MODE}"
|
||||
)
|
||||
logger.debug(msg)
|
||||
|
||||
with self._connect() as conn:
|
||||
self.init(conn)
|
||||
return conn
|
||||
|
||||
def register_functions(self, conn):
|
||||
@ -150,7 +234,7 @@ class SQLiteAppl(abc.ABC):
|
||||
.. _re.search: https://docs.python.org/3/library/re.html#re.search
|
||||
"""
|
||||
|
||||
conn.create_function('regexp', 2, lambda x, y: 1 if re.search(x, y) else 0, deterministic=True)
|
||||
conn.create_function("regexp", 2, lambda x, y: 1 if re.search(x, y) else 0, deterministic=True)
|
||||
|
||||
@property
|
||||
def DB(self) -> sqlite3.Connection:
|
||||
@ -168,57 +252,66 @@ class SQLiteAppl(abc.ABC):
|
||||
https://docs.python.org/3/library/sqlite3.html#sqlite3-controlling-transactions
|
||||
"""
|
||||
|
||||
if getattr(self.thread_local, 'DB', None) is None:
|
||||
self.thread_local.DB = self.connect()
|
||||
conn = None
|
||||
|
||||
# Theoretically it is possible to reuse the DB cursor across threads as
|
||||
# of Python 3.12, in practice the threading of the cursor seems to me to
|
||||
# be so faulty that I prefer to establish one connection per thread
|
||||
if self.SQLITE_THREADING_MODE == "serialized":
|
||||
# Theoretically it is possible to reuse the DB cursor across threads
|
||||
# as of Python 3.12, in practice the threading of the cursor seems
|
||||
# to me a little faulty that I prefer to establish one connection
|
||||
# per thread.
|
||||
#
|
||||
# may we can activate this code one day ..
|
||||
# if self._DB is None:
|
||||
# self._DB = self.connect()
|
||||
# conn = self._DB
|
||||
conn = DBSession.get_connect(self)
|
||||
else:
|
||||
conn = DBSession.get_connect(self)
|
||||
|
||||
self.thread_local.DB.commit()
|
||||
return self.thread_local.DB
|
||||
# Since more than one instance of SQLiteAppl share the same DB
|
||||
# connection, we need to make sure that each SQLiteAppl instance has run
|
||||
# its init method at least once.
|
||||
self.init(conn)
|
||||
|
||||
# In "serialized" mode, SQLite can be safely used by multiple threads
|
||||
# with no restriction.
|
||||
#
|
||||
# if self.SQLITE_THREADING_MODE != "serialized":
|
||||
# if getattr(self.thread_local, 'DB', None) is None:
|
||||
# self.thread_local.DB = self.connect()
|
||||
# return self.thread_local.DB
|
||||
#
|
||||
# if self._DB is None:
|
||||
# self._DB = self.connect() # pylint: disable=attribute-defined-outside-init
|
||||
# return self._DB
|
||||
return conn
|
||||
|
||||
def init(self):
|
||||
def init(self, conn: sqlite3.Connection) -> bool:
|
||||
"""Initializes the DB schema and properties, is only executed once even
|
||||
if called several times."""
|
||||
if called several times.
|
||||
|
||||
If the initialization has not yet taken place, it is carried out and a
|
||||
`True` is returned to the caller at the end. If the initialization has
|
||||
already been carried out in the past, `False` is returned.
|
||||
"""
|
||||
|
||||
if self._init_done:
|
||||
return
|
||||
return False
|
||||
self._init_done = True
|
||||
|
||||
logger.debug("init DB: %s", self.db_url)
|
||||
self.properties.init()
|
||||
self.properties.init(conn)
|
||||
|
||||
ver = self.properties("DB_SCHEMA")
|
||||
if ver is None:
|
||||
with self.properties.DB:
|
||||
self.create_schema(self.properties.DB)
|
||||
with conn:
|
||||
self.create_schema(conn)
|
||||
else:
|
||||
ver = int(ver)
|
||||
if ver != self.DB_SCHEMA:
|
||||
raise sqlite3.DatabaseError("Expected DB schema v%s, DB schema is v%s" % (self.DB_SCHEMA, ver))
|
||||
logger.debug("DB_SCHEMA = %s", ver)
|
||||
|
||||
def create_schema(self, conn):
|
||||
return True
|
||||
|
||||
def create_schema(self, conn: sqlite3.Connection):
|
||||
|
||||
logger.debug("create schema ..")
|
||||
self.properties.set("DB_SCHEMA", self.DB_SCHEMA)
|
||||
self.properties.set("LAST_MAINTENANCE", "")
|
||||
with conn:
|
||||
for table_name, sql in self.DDL_CREATE_TABLES.items():
|
||||
conn.execute(sql)
|
||||
self.properties.set(f"Table {table_name} created", table_name)
|
||||
self.properties.set("DB_SCHEMA", self.DB_SCHEMA)
|
||||
self.properties.set("LAST_MAINTENANCE", "")
|
||||
|
||||
|
||||
class SQLiteProperties(SQLiteAppl):
|
||||
@ -253,33 +346,32 @@ CREATE TABLE IF NOT EXISTS properties (
|
||||
" ON CONFLICT(name) DO UPDATE"
|
||||
" SET value=excluded.value, m_time=strftime('%s', 'now')"
|
||||
)
|
||||
SQL_DELETE = "DELETE FROM properties WHERE name = ?"
|
||||
SQL_TABLE_EXISTS = (
|
||||
"SELECT name FROM sqlite_master"
|
||||
" WHERE type='table' AND name='properties'"
|
||||
) # fmt:skip
|
||||
SQLITE_CONNECT_ARGS = dict(SQLiteAppl.SQLITE_CONNECT_ARGS)
|
||||
SQLITE_CONNECT_ARGS["autocommit"] = True # This option has no effect before Python 3.12
|
||||
|
||||
def __init__(self, db_url: str): # pylint: disable=super-init-not-called
|
||||
|
||||
self.db_url = db_url
|
||||
self.thread_local = threading.local()
|
||||
self._init_done = False
|
||||
self._compatibility()
|
||||
|
||||
def init(self):
|
||||
def init(self, conn: sqlite3.Connection) -> bool:
|
||||
"""Initializes DB schema of the properties in the DB."""
|
||||
|
||||
if self._init_done:
|
||||
return
|
||||
return False
|
||||
self._init_done = True
|
||||
logger.debug("init properties of DB: %s", self.db_url)
|
||||
with self.DB as conn:
|
||||
res = conn.execute(self.SQL_TABLE_EXISTS)
|
||||
if res.fetchone() is None: # DB schema needs to be be created
|
||||
self.create_schema(conn)
|
||||
res = conn.execute(self.SQL_TABLE_EXISTS)
|
||||
if res.fetchone() is None: # DB schema needs to be be created
|
||||
self.create_schema(conn)
|
||||
return True
|
||||
|
||||
def __call__(self, name, default=None):
|
||||
def __call__(self, name: str, default=None):
|
||||
"""Returns the value of the property ``name`` or ``default`` if property
|
||||
not exists in DB."""
|
||||
|
||||
@ -288,36 +380,47 @@ CREATE TABLE IF NOT EXISTS properties (
|
||||
return default
|
||||
return res[0]
|
||||
|
||||
def set(self, name, value):
|
||||
def set(self, name: str, value: str | int):
|
||||
"""Set ``value`` of property ``name`` in DB. If property already
|
||||
exists, update the ``m_time`` (and the value)."""
|
||||
|
||||
self.DB.execute(self.SQL_SET, (name, value))
|
||||
with self.DB:
|
||||
self.DB.execute(self.SQL_SET, (name, value))
|
||||
|
||||
if sys.version_info <= (3, 12):
|
||||
# Prior Python 3.12 there is no "autocommit" option / lets commit
|
||||
# explicitely.
|
||||
self.DB.commit()
|
||||
def delete(self, name: str) -> int:
|
||||
"""Delete of property ``name`` from DB."""
|
||||
with self.DB:
|
||||
cur = self.DB.execute(self.SQL_DELETE, (name,))
|
||||
return cur.rowcount
|
||||
|
||||
def row(self, name, default=None):
|
||||
def row(self, name: str, default=None):
|
||||
"""Returns the DB row of property ``name`` or ``default`` if property
|
||||
not exists in DB."""
|
||||
|
||||
cur = self.DB.cursor()
|
||||
cur.execute("SELECT * FROM properties WHERE name = ?", (name,))
|
||||
res = cur.fetchone()
|
||||
if res is None:
|
||||
res = self.DB.execute("SELECT * FROM properties WHERE name = ?", (name,))
|
||||
row = res.fetchone()
|
||||
if row is None:
|
||||
return default
|
||||
col_names = [column[0] for column in cur.description]
|
||||
return dict(zip(col_names, res))
|
||||
|
||||
def m_time(self, name, default: int = 0) -> int:
|
||||
col_names = [column[0] for column in row.description]
|
||||
return dict(zip(col_names, row))
|
||||
|
||||
def m_time(self, name: str, default: int = 0) -> int:
|
||||
"""Last modification time of this property."""
|
||||
res = self.DB.execute(self.SQL_M_TIME, (name,)).fetchone()
|
||||
if res is None:
|
||||
res = self.DB.execute(self.SQL_M_TIME, (name,))
|
||||
row = res.fetchone()
|
||||
if row is None:
|
||||
return default
|
||||
return int(res[0])
|
||||
return int(row[0])
|
||||
|
||||
def create_schema(self, conn):
|
||||
with conn:
|
||||
conn.execute(self.DDL_PROPERTIES)
|
||||
|
||||
def __str__(self) -> str:
|
||||
lines = []
|
||||
for row in self.DB.execute("SELECT name, value, m_time FROM properties"):
|
||||
name, value, m_time = row
|
||||
m_time = datetime.datetime.fromtimestamp(m_time).strftime("%Y-%m-%d %H:%M:%S")
|
||||
lines.append(f"[last modified: {m_time}] {name:20s}: {value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@ -23,6 +23,7 @@ sxng_locales = (
|
||||
('da-DK', 'Dansk', 'Danmark', 'Danish', '\U0001f1e9\U0001f1f0'),
|
||||
('de', 'Deutsch', '', 'German', '\U0001f310'),
|
||||
('de-AT', 'Deutsch', 'Österreich', 'German', '\U0001f1e6\U0001f1f9'),
|
||||
('de-BE', 'Deutsch', 'Belgien', 'German', '\U0001f1e7\U0001f1ea'),
|
||||
('de-CH', 'Deutsch', 'Schweiz', 'German', '\U0001f1e8\U0001f1ed'),
|
||||
('de-DE', 'Deutsch', 'Deutschland', 'German', '\U0001f1e9\U0001f1ea'),
|
||||
('el', 'Ελληνικά', '', 'Greek', '\U0001f310'),
|
||||
|
||||
@ -7,35 +7,35 @@ template. This file from:
|
||||
|
||||
{%-
|
||||
set catalog = {
|
||||
'alert' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 80c-8.66 0-16.58 7.36-16 16l8 216a8 8 0 008 8h0a8 8 0 008-8l8-216c.58-8.64-7.34-16-16-16z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="256" cy="416" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'appstore' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="64" y="64" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="216" y="64" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="368" y="64" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="64" y="216" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="216" y="216" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="368" y="216" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="64" y="368" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="216" y="368" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><rect x="368" y="368" width="80" height="80" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/></svg>',
|
||||
'book' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 160c16-63.16 76.43-95.41 208-96a15.94 15.94 0 0116 16v288a16 16 0 01-16 16c-128 0-177.45 25.81-208 64-30.37-38-80-64-208-64-9.88 0-16-8.05-16-17.93V80a15.94 15.94 0 0116-16c131.57.59 192 32.84 208 96zM256 160v288" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'close' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368"/></svg>',
|
||||
'download' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M336 176h40a40 40 0 0140 40v208a40 40 0 01-40 40H136a40 40 0 01-40-40V216a40 40 0 0140-40h40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M176 272l80 80 80-80M256 48v288"/></svg>',
|
||||
'ellipsis-vertical' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><circle cx="256" cy="256" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><circle cx="256" cy="416" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><circle cx="256" cy="96" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/></svg>',
|
||||
'file-tray-full' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M384 80H128c-26 0-43 14-48 40L48 272v112a48.14 48.14 0 0048 48h320a48.14 48.14 0 0048-48V272l-32-152c-5-27-23-40-48-40z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M48 272h144M320 272h144M192 272a64 64 0 00128 0M144 144h224M128 208h256"/></svg>',
|
||||
'film' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="48" y="96" width="416" height="320" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="336" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="256" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="176" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="384" y="96" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="336" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="256" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="176" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="48" y="96" width="80" height="80" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="128" y="96" width="256" height="160" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><rect x="128" y="256" width="256" height="160" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'globe' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 48C141.13 48 48 141.13 48 256s93.13 208 208 208 208-93.13 208-208S370.87 48 256 48z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M256 48c-58.07 0-112.67 93.13-112.67 208S197.93 464 256 464s112.67-93.13 112.67-208S314.07 48 256 48z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M117.33 117.33c38.24 27.15 86.38 43.34 138.67 43.34s100.43-16.19 138.67-43.34M394.67 394.67c-38.24-27.15-86.38-43.34-138.67-43.34s-100.43 16.19-138.67 43.34" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32" d="M256 48v416M464 256H48"/></svg>',
|
||||
'heart' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M352.92 80C288 80 256 144 256 144s-32-64-96.92-64c-52.76 0-94.54 44.14-95.08 96.81-1.1 109.33 86.73 187.08 183 252.42a16 16 0 0018 0c96.26-65.34 184.09-143.09 183-252.42-.54-52.67-42.32-96.81-95.08-96.81z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'image' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="48" y="80" width="416" height="352" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><circle cx="336" cy="176" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M304 335.79l-90.66-90.49a32 32 0 00-43.87-1.3L48 352M224 432l123.34-123.34a32 32 0 0143.11-2L464 368" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'layers' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M434.8 137.65l-149.36-68.1c-16.19-7.4-42.69-7.4-58.88 0L77.3 137.65c-17.6 8-17.6 21.09 0 29.09l148 67.5c16.89 7.7 44.69 7.7 61.58 0l148-67.5c17.52-8 17.52-21.1-.08-29.09zM160 308.52l-82.7 37.11c-17.6 8-17.6 21.1 0 29.1l148 67.5c16.89 7.69 44.69 7.69 61.58 0l148-67.5c17.6-8 17.6-21.1 0-29.1l-79.94-38.47" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M160 204.48l-82.8 37.16c-17.6 8-17.6 21.1 0 29.1l148 67.49c16.89 7.7 44.69 7.7 61.58 0l148-67.49c17.7-8 17.7-21.1.1-29.1L352 204.48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'leecher' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 268l144 144 144-144M256 392V100"/></svg>',
|
||||
'location' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M256 48c-79.5 0-144 61.39-144 137 0 87 96 224.87 131.25 272.49a15.77 15.77 0 0025.5 0C304 409.89 400 272.07 400 185c0-75.61-64.5-137-144-137z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="256" cy="192" r="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'magnet' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M421.83 293.82A144 144 0 00218.18 90.17M353.94 225.94a48 48 0 00-67.88-67.88" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M192 464v-48M90.18 421.82l33.94-33.94M48 320h48"/><path d="M286.06 158.06L172.92 271.19a32 32 0 01-45.25 0L105 248.57a32 32 0 010-45.26L218.18 90.17M421.83 293.82L308.69 407a32 32 0 01-45.26 0l-22.62-22.63a32 32 0 010-45.26l113.13-113.17M139.6 169.98l67.88 67.89M275.36 305.75l67.89 67.88" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'musical-notes' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M192 218v-6c0-14.84 10-27 24.24-30.59l174.59-46.68A20 20 0 01416 154v22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M416 295.94v80c0 13.91-8.93 25.59-22 30l-22 8c-25.9 8.72-52-10.42-52-38h0a33.37 33.37 0 0123-32l51-18.15c13.07-4.4 22-15.94 22-29.85V58a10 10 0 00-12.6-9.61L204 102a16.48 16.48 0 00-12 16v226c0 13.91-8.93 25.6-22 30l-52 18c-13.88 4.68-22 17.22-22 32h0c0 27.58 26.52 46.55 52 38l22-8c13.07-4.4 22-16.08 22-30v-80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'navigate-down' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 184l144 144 144-144"/></svg>',
|
||||
'navigate-left' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M328 112L184 256l144 144"/></svg>',
|
||||
'navigate-right' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M184 112l144 144-144 144"/></svg>',
|
||||
'navigate-up' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M112 328l144-144 144 144"/></svg>',
|
||||
'people' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M402 168c-2.93 40.67-33.1 72-66 72s-63.12-31.32-66-72c-3-42.31 26.37-72 66-72s69 30.46 66 72z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M336 304c-65.17 0-127.84 32.37-143.54 95.41-2.08 8.34 3.15 16.59 11.72 16.59h263.65c8.57 0 13.77-8.25 11.72-16.59C463.85 335.36 401.18 304 336 304z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M200 185.94c-2.34 32.48-26.72 58.06-53 58.06s-50.7-25.57-53-58.06C91.61 152.15 115.34 128 147 128s55.39 24.77 53 57.94z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M206 306c-18.05-8.27-37.93-11.45-59-11.45-52 0-102.1 25.85-114.65 76.2-1.65 6.66 2.53 13.25 9.37 13.25H154" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/></svg>',
|
||||
'play' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M112 111v290c0 17.44 17 28.52 31 20.16l247.9-148.37c12.12-7.25 12.12-26.33 0-33.58L143 90.84c-14-8.36-31 2.72-31 20.16z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/></svg>',
|
||||
'radio' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><circle cx="256" cy="256.02" r="32"/><path d="M184.25 192.25a96 96 0 000 127.52M327.77 319.77a96 96 0 000-127.52M133.28 141.28a168 168 0 000 229.44M378.72 370.72a168 168 0 000-229.44M435 416a240.34 240.34 0 000-320M77 96a240.34 240.34 0 000 320" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'save' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M380.93 57.37A32 32 0 00358.3 48H94.22A46.21 46.21 0 0048 94.22v323.56A46.21 46.21 0 0094.22 464h323.56A46.36 46.36 0 00464 417.78V153.7a32 32 0 00-9.37-22.63zM256 416a64 64 0 1164-64 63.92 63.92 0 01-64 64zm48-224H112a16 16 0 01-16-16v-64a16 16 0 0116-16h192a16 16 0 0116 16v64a16 16 0 01-16 16z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'school' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M32 192L256 64l224 128-224 128L32 192z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M112 240v128l144 80 144-80V240M480 368V192M256 320v128"/></svg>',
|
||||
'search' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M221.09 64a157.09 157.09 0 10157.09 157.09A157.1 157.1 0 00221.09 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M338.29 338.29L448 448"/></svg>',
|
||||
'seeder' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M464 208L352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96"/></svg>',
|
||||
'settings' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M262.29 192.31a64 64 0 1057.4 57.4 64.13 64.13 0 00-57.4-57.4zM416.39 256a154.34 154.34 0 01-1.53 20.79l45.21 35.46a10.81 10.81 0 012.45 13.75l-42.77 74a10.81 10.81 0 01-13.14 4.59l-44.9-18.08a16.11 16.11 0 00-15.17 1.75A164.48 164.48 0 01325 400.8a15.94 15.94 0 00-8.82 12.14l-6.73 47.89a11.08 11.08 0 01-10.68 9.17h-85.54a11.11 11.11 0 01-10.69-8.87l-6.72-47.82a16.07 16.07 0 00-9-12.22 155.3 155.3 0 01-21.46-12.57 16 16 0 00-15.11-1.71l-44.89 18.07a10.81 10.81 0 01-13.14-4.58l-42.77-74a10.8 10.8 0 012.45-13.75l38.21-30a16.05 16.05 0 006-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 00-6.07-13.94l-38.19-30A10.81 10.81 0 0149.48 186l42.77-74a10.81 10.81 0 0113.14-4.59l44.9 18.08a16.11 16.11 0 0015.17-1.75A164.48 164.48 0 01187 111.2a15.94 15.94 0 008.82-12.14l6.73-47.89A11.08 11.08 0 01213.23 42h85.54a11.11 11.11 0 0110.69 8.87l6.72 47.82a16.07 16.07 0 009 12.22 155.3 155.3 0 0121.46 12.57 16 16 0 0015.11 1.71l44.89-18.07a10.81 10.81 0 0113.14 4.58l42.77 74a10.8 10.8 0 01-2.45 13.75l-38.21 30a16.05 16.05 0 00-6.05 14.08c.33 4.14.55 8.3.55 12.47z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>',
|
||||
'tv' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><rect x="32" y="96" width="448" height="272" rx="32.14" ry="32.14" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M128 416h256"/></svg>',
|
||||
'alert' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 80c-8.66 0-16.58 7.36-16 16l8 216a8 8 0 0 0 8 8h0a8 8 0 0 0 8-8l8-216c.58-8.64-7.34-16-16-16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><circle cx="256" cy="416" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'appstore' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="80" height="80" x="64" y="64" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="216" y="64" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="368" y="64" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="64" y="216" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="216" y="216" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="368" y="216" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="64" y="368" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="216" y="368" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><rect width="80" height="80" x="368" y="368" rx="40" ry="40" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
|
||||
'book' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 160c16-63.16 76.43-95.41 208-96a15.94 15.94 0 0 1 16 16v288a16 16 0 0 1-16 16c-128 0-177.45 25.81-208 64-30.37-38-80-64-208-64-9.88 0-16-8.05-16-17.93V80a15.94 15.94 0 0 1 16-16c131.57.59 192 32.84 208 96M256 160v288" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'close' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M368 368 144 144M368 144 144 368" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'download' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M336 176h40a40 40 0 0 1 40 40v208a40 40 0 0 1-40 40H136a40 40 0 0 1-40-40V216a40 40 0 0 1 40-40h40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="m176 272 80 80 80-80M256 48v288" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'ellipsis-vertical' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><circle cx="256" cy="256" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><circle cx="256" cy="416" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><circle cx="256" cy="96" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
|
||||
'file-tray-full' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M384 80H128c-26 0-43 14-48 40L48 272v112a48.14 48.14 0 0 0 48 48h320a48.14 48.14 0 0 0 48-48V272l-32-152c-5-27-23-40-48-40Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><path d="M48 272h144M320 272h144M192 272a64 64 0 0 0 128 0M144 144h224M128 208h256" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'film' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="416" height="320" x="48" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="336" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="256" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="176" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="384" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="336" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="256" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="176" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="80" height="80" x="48" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="256" height="160" x="128" y="96" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><rect width="256" height="160" x="128" y="256" rx="28" ry="28" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'globe' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 48C141.13 48 48 141.13 48 256s93.13 208 208 208 208-93.13 208-208S370.87 48 256 48Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M256 48c-58.07 0-112.67 93.13-112.67 208S197.93 464 256 464s112.67-93.13 112.67-208S314.07 48 256 48Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M117.33 117.33c38.24 27.15 86.38 43.34 138.67 43.34s100.43-16.19 138.67-43.34M394.67 394.67c-38.24-27.15-86.38-43.34-138.67-43.34s-100.43 16.19-138.67 43.34" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M256 48v416M464 256H48" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
|
||||
'heart' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M352.92 80C288 80 256 144 256 144s-32-64-96.92-64c-52.76 0-94.54 44.14-95.08 96.81-1.1 109.33 86.73 187.08 183 252.42a16 16 0 0 0 18 0c96.26-65.34 184.09-143.09 183-252.42-.54-52.67-42.32-96.81-95.08-96.81" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'image' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="416" height="352" x="48" y="80" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><circle cx="336" cy="176" r="32" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="m304 335.79-90.66-90.49a32 32 0 0 0-43.87-1.3L48 352M224 432l123.34-123.34a32 32 0 0 1 43.11-2L464 368" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'layers' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m434.8 137.65-149.36-68.1c-16.19-7.4-42.69-7.4-58.88 0L77.3 137.65c-17.6 8-17.6 21.09 0 29.09l148 67.5c16.89 7.7 44.69 7.7 61.58 0l148-67.5c17.52-8 17.52-21.1-.08-29.09M160 308.52l-82.7 37.11c-17.6 8-17.6 21.1 0 29.1l148 67.5c16.89 7.69 44.69 7.69 61.58 0l148-67.5c17.6-8 17.6-21.1 0-29.1l-79.94-38.47" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="m160 204.48-82.8 37.16c-17.6 8-17.6 21.1 0 29.1l148 67.49c16.89 7.7 44.69 7.7 61.58 0l148-67.49c17.7-8 17.7-21.1.1-29.1L352 204.48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'leecher' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m112 268 144 144 144-144M256 392V100" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
|
||||
'location' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M256 48c-79.5 0-144 61.39-144 137 0 87 96 224.87 131.25 272.49a15.77 15.77 0 0 0 25.5 0C304 409.89 400 272.07 400 185c0-75.61-64.5-137-144-137" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><circle cx="256" cy="192" r="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'magnet' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M421.83 293.82A144 144 0 0 0 218.18 90.17" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M353.94 225.94a48 48 0 0 0-67.88-67.88" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M192 464v-48M90.18 421.82l33.94-33.94M48 320h48" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/><path d="M286.06 158.06 172.92 271.19a32 32 0 0 1-45.25 0L105 248.57a32 32 0 0 1 0-45.26L218.18 90.17M421.83 293.82 308.69 407a32 32 0 0 1-45.26 0l-22.62-22.63a32 32 0 0 1 0-45.26l113.13-113.17M139.6 169.98l67.88 67.89M275.36 305.75l67.89 67.88" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'musical-notes' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M192 218v-6c0-14.84 10-27 24.24-30.59l174.59-46.68A20 20 0 0 1 416 154v22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M416 295.94v80c0 13.91-8.93 25.59-22 30l-22 8c-25.9 8.72-52-10.42-52-38h0a33.37 33.37 0 0 1 23-32l51-18.15c13.07-4.4 22-15.94 22-29.85V58a10 10 0 0 0-12.6-9.61L204 102a16.48 16.48 0 0 0-12 16v226c0 13.91-8.93 25.6-22 30l-52 18c-13.88 4.68-22 17.22-22 32h0c0 27.58 26.52 46.55 52 38l22-8c13.07-4.4 22-16.08 22-30v-80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'navigate-down' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m112 184 144 144 144-144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
|
||||
'navigate-left' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M328 112 184 256l144 144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
|
||||
'navigate-right' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m184 112 144 144-144 144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
|
||||
'navigate-up' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="m112 328 144-144 144 144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48px"/></svg>',
|
||||
'people' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M402 168c-2.93 40.67-33.1 72-66 72s-63.12-31.32-66-72c-3-42.31 26.37-72 66-72s69 30.46 66 72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M336 304c-65.17 0-127.84 32.37-143.54 95.41-2.08 8.34 3.15 16.59 11.72 16.59h263.65c8.57 0 13.77-8.25 11.72-16.59C463.85 335.36 401.18 304 336 304Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M200 185.94c-2.34 32.48-26.72 58.06-53 58.06s-50.7-25.57-53-58.06C91.61 152.15 115.34 128 147 128s55.39 24.77 53 57.94" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M206 306c-18.05-8.27-37.93-11.45-59-11.45-52 0-102.1 25.85-114.65 76.2-1.65 6.66 2.53 13.25 9.37 13.25H154" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/></svg>',
|
||||
'play' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M112 111v290c0 17.44 17 28.52 31 20.16l247.9-148.37c12.12-7.25 12.12-26.33 0-33.58L143 90.84c-14-8.36-31 2.72-31 20.16Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/></svg>',
|
||||
'radio' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><circle cx="256" cy="256.02" r="32"/><path d="M184.25 192.25a96 96 0 0 0 0 127.52M327.77 319.77a96 96 0 0 0 0-127.52M133.28 141.28a168 168 0 0 0 0 229.44M378.72 370.72a168 168 0 0 0 0-229.44" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M435 416a240.34 240.34 0 0 0 0-320M77 96a240.34 240.34 0 0 0 0 320" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'save' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M380.93 57.37A32 32 0 0 0 358.3 48H94.22A46.21 46.21 0 0 0 48 94.22v323.56A46.21 46.21 0 0 0 94.22 464h323.56A46.36 46.36 0 0 0 464 417.78V153.7a32 32 0 0 0-9.37-22.63ZM256 416a64 64 0 1 1 64-64 63.92 63.92 0 0 1-64 64m48-224H112a16 16 0 0 1-16-16v-64a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16v64a16 16 0 0 1-16 16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'school' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M32 192 256 64l224 128-224 128z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/><path d="M112 240v128l144 80 144-80V240M480 368V192M256 320v128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'search' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M221.09 64a157.09 157.09 0 1 0 157.09 157.09A157.1 157.1 0 0 0 221.09 64Z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32px"/><path d="M338.29 338.29 448 448" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/></svg>',
|
||||
'seeder' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M464 208 352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'settings' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><path d="M262.29 192.31a64 64 0 1 0 57.4 57.4 64.13 64.13 0 0 0-57.4-57.4M416.39 256a154 154 0 0 1-1.53 20.79l45.21 35.46a10.81 10.81 0 0 1 2.45 13.75l-42.77 74a10.81 10.81 0 0 1-13.14 4.59l-44.9-18.08a16.11 16.11 0 0 0-15.17 1.75A164.5 164.5 0 0 1 325 400.8a15.94 15.94 0 0 0-8.82 12.14l-6.73 47.89a11.08 11.08 0 0 1-10.68 9.17h-85.54a11.11 11.11 0 0 1-10.69-8.87l-6.72-47.82a16.07 16.07 0 0 0-9-12.22 155 155 0 0 1-21.46-12.57 16 16 0 0 0-15.11-1.71l-44.89 18.07a10.81 10.81 0 0 1-13.14-4.58l-42.77-74a10.8 10.8 0 0 1 2.45-13.75l38.21-30a16.05 16.05 0 0 0 6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 0 0-6.07-13.94l-38.19-30A10.81 10.81 0 0 1 49.48 186l42.77-74a10.81 10.81 0 0 1 13.14-4.59l44.9 18.08a16.11 16.11 0 0 0 15.17-1.75A164.5 164.5 0 0 1 187 111.2a15.94 15.94 0 0 0 8.82-12.14l6.73-47.89A11.08 11.08 0 0 1 213.23 42h85.54a11.11 11.11 0 0 1 10.69 8.87l6.72 47.82a16.07 16.07 0 0 0 9 12.22 155 155 0 0 1 21.46 12.57 16 16 0 0 0 15.11 1.71l44.89-18.07a10.81 10.81 0 0 1 13.14 4.58l42.77 74a10.8 10.8 0 0 1-2.45 13.75l-38.21 30a16.05 16.05 0 0 0-6.05 14.08c.33 4.14.55 8.3.55 12.47" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32px"/></svg>',
|
||||
'tv' : '<svg viewBox="0 0 512 512" class="ionicon __jinja_class_placeholder__" aria-hidden="true"><rect width="448" height="272" x="32" y="96" rx="32.14" ry="32.14" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32px"/><path d="M128 416h256" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32px"/></svg>',
|
||||
'information-circle' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M248 64C146.39 64 64 146.39 64 248s82.39 184 184 184 184-82.39 184-184S349.61 64 248 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 220h32v116"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M208 340h88"/><path d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z" fill="currentColor" stroke="currentColor" stroke-miterlimit="10" stroke-width="1"/></svg>',
|
||||
'newspaper' : '<svg viewBox="0 0 512 512" aria-hidden="true" class="__jinja_class_placeholder__"><path d="M368 415.86V72a24.07 24.07 0 00-24-24H72a24.07 24.07 0 00-24 24v352a40.12 40.12 0 0040 40h328" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path d="M416 464h0a48 48 0 01-48-48V128h72a24 24 0 0124 24v264a48 48 0 01-48 48z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path d="M240 128h64M240 192h64M112 256h192M112 320h192M112 384h192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M176 208h-64a16 16 0 01-16-16v-64a16 16 0 0116-16h64a16 16 0 0116 16v64a16 16 0 01-16 16z" fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="1"/></svg>',
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -25,13 +25,14 @@
|
||||
# Yahya-Lando <yahya-lando@users.noreply.translate.codeberg.org>, 2025.
|
||||
# curtwheeler <curtwheeler@users.noreply.translate.codeberg.org>, 2025.
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
# DZDevelopers <dzdevelopers@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-31 18:08+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-04-15 10:37+0000\n"
|
||||
"Last-Translator: DZDevelopers <dzdevelopers@noreply.codeberg.org>\n"
|
||||
"Language-Team: Arabic <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/ar/>\n"
|
||||
"Language: ar\n"
|
||||
@ -535,15 +536,15 @@ msgstr "جودة الملف"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "{دالة}"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
msgstr ""
|
||||
msgstr "قم بتصفية نتائج .onion التي تظهر في القائمة السوداء الخاصة بـ Ahmia."
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "آلة حاسبة بسيطة"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
@ -639,7 +640,7 @@ msgstr ""
|
||||
|
||||
#: searx/plugins/unit_converter.py:49
|
||||
msgid "Unit converter plugin"
|
||||
msgstr ""
|
||||
msgstr "إضافة محول الوحدات"
|
||||
|
||||
#: searx/plugins/unit_converter.py:50
|
||||
msgid "Convert between units"
|
||||
@ -1190,6 +1191,8 @@ msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
"رابط يحتوي على تفضيلاتك. يمكن استخدام هذا الرابط لاستعادة إعداداتك على جهاز "
|
||||
"مختلف."
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
|
||||
Binary file not shown.
@ -18,21 +18,22 @@
|
||||
# Kjev <Kjev@users.noreply.translate.codeberg.org>, 2025.
|
||||
# KinoCineaste <kinocineaste@users.noreply.translate.codeberg.org>, 2025.
|
||||
# AlanBacker <alanbacker@users.noreply.translate.codeberg.org>, 2025.
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-02-17 13:01+0000\n"
|
||||
"Last-Translator: AlanBacker "
|
||||
"<alanbacker@users.noreply.translate.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-05-09 07:09+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"Language-Team: Esperanto <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/eo/>\n"
|
||||
"Language: eo\n"
|
||||
"Language-Team: Esperanto "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/eo/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -260,7 +261,7 @@ msgstr ""
|
||||
#: searx/engines/duckduckgo_weather.py:81 searx/engines/wttr.py:36
|
||||
#: searx/searxng.msg
|
||||
msgid "Sunrise"
|
||||
msgstr ""
|
||||
msgstr "Sunleviĝo"
|
||||
|
||||
#. WEATHER_TERMS['SUNSET']
|
||||
#: searx/engines/duckduckgo_weather.py:82 searx/engines/wttr.py:37
|
||||
@ -2067,4 +2068,3 @@ msgstr "kaŝi videojn"
|
||||
#~ "Specifante kutimajn agordojn en la URL"
|
||||
#~ " de preferoj povas esti uzata por "
|
||||
#~ "sinkronigi preferojn tra aparatoj."
|
||||
|
||||
|
||||
Binary file not shown.
@ -43,13 +43,14 @@
|
||||
# pxrb <pxrb@users.noreply.translate.codeberg.org>, 2025.
|
||||
# curtwheeler <curtwheeler@users.noreply.translate.codeberg.org>, 2025.
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
# Atul_Eterno <atul_eterno@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-04-08 11:34+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-04-24 14:06+0000\n"
|
||||
"Last-Translator: Atul_Eterno <atul_eterno@noreply.codeberg.org>\n"
|
||||
"Language-Team: Spanish <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/es/>\n"
|
||||
"Language: es\n"
|
||||
@ -553,7 +554,7 @@ msgstr "Calidad del archivo"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "Lista negra de Ahmia"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
@ -1210,6 +1211,8 @@ msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
"Una URL conteniendo sus preferencias. Esta URL puede ser usada para "
|
||||
"restaurar sus ajustes en un dispositivo diferente."
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
|
||||
Binary file not shown.
@ -23,13 +23,14 @@
|
||||
# Parsa Ranjbar <parsa@users.noreply.translate.codeberg.org>, 2025.
|
||||
# arashe22 <arashe22@users.noreply.translate.codeberg.org>, 2025.
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
# ehsanrs2 <ehsanrs2@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-31 18:08+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-05-06 12:53+0000\n"
|
||||
"Last-Translator: ehsanrs2 <ehsanrs2@noreply.codeberg.org>\n"
|
||||
"Language-Team: Persian <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/fa/>\n"
|
||||
"Language: fa_IR\n"
|
||||
@ -533,15 +534,15 @@ msgstr "کیفیت فایل"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "لیست سیاه Ahmia"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
msgstr ""
|
||||
msgstr "نتایج onion که در لیست سیاه Ahmia ظاهر میشوند را فیلتر کنید."
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "ماشین حساب اولیه"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
@ -614,7 +615,7 @@ msgstr ""
|
||||
|
||||
#: searx/plugins/tor_check.py:65
|
||||
msgid "Could not download the list of Tor exit-nodes from"
|
||||
msgstr "دانلود لیست گرههای خروجی تور از این مسیر ممکن نیست:"
|
||||
msgstr "نتوانستم لیست گرههای خروجی Tor را از اینجا دانلود کنم"
|
||||
|
||||
#: searx/plugins/tor_check.py:72
|
||||
msgid "You are using Tor and it looks like you have the external IP address"
|
||||
@ -634,7 +635,7 @@ msgstr "آرگومان های ردیاب ها را از URL برگشتی حذف
|
||||
|
||||
#: searx/plugins/unit_converter.py:49
|
||||
msgid "Unit converter plugin"
|
||||
msgstr ""
|
||||
msgstr "افزونه تبدیل واحد"
|
||||
|
||||
#: searx/plugins/unit_converter.py:50
|
||||
msgid "Convert between units"
|
||||
@ -681,7 +682,7 @@ msgstr "ردیاب مشکل"
|
||||
|
||||
#: searx/templates/simple/base.html:70 searx/templates/simple/stats.html:18
|
||||
msgid "Engine stats"
|
||||
msgstr "آمار موتور"
|
||||
msgstr "وضعیت موتور"
|
||||
|
||||
#: searx/templates/simple/base.html:72
|
||||
msgid "Public instances"
|
||||
@ -693,7 +694,7 @@ msgstr "سیاست حفظ حریم خصوصی"
|
||||
|
||||
#: searx/templates/simple/base.html:78
|
||||
msgid "Contact instance maintainer"
|
||||
msgstr "تماس با مسئولنگهداری نمونه"
|
||||
msgstr "تماس با نگهدارنده نمونه"
|
||||
|
||||
#: searx/templates/simple/categories.html:30
|
||||
msgid "Click on the magnifier to perform search"
|
||||
@ -1185,6 +1186,8 @@ msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
"یک URL حاوی تنظیمات برگزیده شما. از این URL میتوان برای بازیابی تنظیمات شما "
|
||||
"در دستگاه دیگری استفاده کرد."
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
@ -1244,7 +1247,7 @@ msgstr "زمان بیشینه"
|
||||
|
||||
#: searx/templates/simple/preferences/favicon.html:2
|
||||
msgid "Favicon Resolver"
|
||||
msgstr ""
|
||||
msgstr "حل کننده فاویکون"
|
||||
|
||||
#: searx/templates/simple/preferences/favicon.html:15
|
||||
msgid "Display favicons near search results"
|
||||
|
||||
Binary file not shown.
@ -3,22 +3,23 @@
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
|
||||
# return42 <return42@users.noreply.translate.codeberg.org>, 2025.
|
||||
# aindriu80 <aindriu80@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-01-21 19:34+0000\n"
|
||||
"Last-Translator: return42 <return42@users.noreply.translate.codeberg.org>"
|
||||
"\n"
|
||||
"PO-Revision-Date: 2025-04-14 09:17+0000\n"
|
||||
"Last-Translator: aindriu80 <aindriu80@noreply.codeberg.org>\n"
|
||||
"Language-Team: Irish <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/ga/>\n"
|
||||
"Language: ga\n"
|
||||
"Language-Team: Irish "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/ga/>\n"
|
||||
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 "
|
||||
":(n>6 && n<11) ? 3 : 4;\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :("
|
||||
"n>6 && n<11) ? 3 : 4;\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -437,11 +438,11 @@ msgstr "Cruthaigh luachanna randamacha éag"
|
||||
#: searx/answerers/statistics.py:36
|
||||
#, python-brace-format
|
||||
msgid "Compute {func} of the arguments"
|
||||
msgstr ""
|
||||
msgstr "Ríomh {func} na n-argóintí"
|
||||
|
||||
#: searx/engines/openstreetmap.py:158
|
||||
msgid "Show route in map .."
|
||||
msgstr ""
|
||||
msgstr "Taispeáin an bealach ar an léarscáil .."
|
||||
|
||||
#: searx/engines/pdbe.py:96
|
||||
#, python-brace-format
|
||||
@ -514,15 +515,15 @@ msgstr "Cáilíocht comhad"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "Liosta dubh Ahmia"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
msgstr ""
|
||||
msgstr "Scag amach torthaí oinniún atá le feiceáil ar liosta dubh Ahmia."
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "Áireamhán Bunúsach"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
@ -530,7 +531,7 @@ msgstr "Ríomh nathanna matamaiticiúla tríd an mbarra cu"
|
||||
|
||||
#: searx/plugins/hash_plugin.py:34
|
||||
msgid "Hash plugin"
|
||||
msgstr ""
|
||||
msgstr "Breiseán hais"
|
||||
|
||||
#: searx/plugins/hash_plugin.py:35
|
||||
msgid "Converts strings to different hash digests."
|
||||
@ -571,6 +572,8 @@ msgid ""
|
||||
"Displays your IP if the query is \"ip\" and your user agent if the query "
|
||||
"is \"user-agent\"."
|
||||
msgstr ""
|
||||
"Taispeáin do IP más \"ip\" an cheist agus do ghníomhaire úsáideora más "
|
||||
"\"úsáideoir-gníomhaire\" an cheist."
|
||||
|
||||
#: searx/plugins/self_info.py:52
|
||||
msgid "Your IP is: "
|
||||
@ -595,15 +598,17 @@ msgstr ""
|
||||
|
||||
#: searx/plugins/tor_check.py:65
|
||||
msgid "Could not download the list of Tor exit-nodes from"
|
||||
msgstr ""
|
||||
msgstr "Níorbh fhéidir liosta na nóid scoir Tor a íoslódáil ó"
|
||||
|
||||
#: searx/plugins/tor_check.py:72
|
||||
msgid "You are using Tor and it looks like you have the external IP address"
|
||||
msgstr ""
|
||||
"Tá tú ag úsáid Tor agus tá an chuma ar an scéal go bhfuil an seoladh IP "
|
||||
"seachtrach agat"
|
||||
|
||||
#: searx/plugins/tor_check.py:76
|
||||
msgid "You are not using Tor and you have the external IP address"
|
||||
msgstr ""
|
||||
msgstr "Níl tú ag úsáid Tor agus tá an seoladh IP seachtrach agat"
|
||||
|
||||
#: searx/plugins/tracker_url_remover.py:37
|
||||
msgid "Tracker URL remover"
|
||||
@ -615,7 +620,7 @@ msgstr "Bain argóintí rianaithe ón URL ar ais"
|
||||
|
||||
#: searx/plugins/unit_converter.py:49
|
||||
msgid "Unit converter plugin"
|
||||
msgstr ""
|
||||
msgstr "Breiseán tiontaire aonad"
|
||||
|
||||
#: searx/plugins/unit_converter.py:50
|
||||
msgid "Convert between units"
|
||||
@ -933,7 +938,7 @@ msgstr "Samplaí"
|
||||
|
||||
#: searx/templates/simple/answer/translations.html:21
|
||||
msgid "Definitions"
|
||||
msgstr ""
|
||||
msgstr "Sainmhínithe"
|
||||
|
||||
#: searx/templates/simple/answer/translations.html:30
|
||||
msgid "Synonyms"
|
||||
@ -1094,7 +1099,7 @@ msgstr "Ceadaigh"
|
||||
|
||||
#: searx/templates/simple/preferences/answerers.html:5
|
||||
msgid "Keywords (first word in query)"
|
||||
msgstr ""
|
||||
msgstr "Eochairfhocail (an chéad fhocal sa cheist)"
|
||||
|
||||
#: searx/templates/simple/preferences/answerers.html:6
|
||||
#: searx/templates/simple/result_templates/packages.html:7
|
||||
@ -1171,6 +1176,8 @@ msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
"URL ina bhfuil do shainroghanna. Is féidir an URL seo a úsáid chun do "
|
||||
"shocruithe a chur ar ais ar ghléas eile."
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
@ -1186,7 +1193,7 @@ msgstr "Roghanna hais"
|
||||
|
||||
#: searx/templates/simple/preferences/doi_resolver.html:1
|
||||
msgid "Digital Object Identifier (DOI)"
|
||||
msgstr ""
|
||||
msgstr "Aitheantóir Oibiachta Digiteach (DOI)"
|
||||
|
||||
#: searx/templates/simple/preferences/doi_resolver.html:6
|
||||
msgid "Open Access DOI resolver"
|
||||
@ -1631,4 +1638,3 @@ msgstr "físeán a cheilt"
|
||||
#~ "shonrú sna roghanna URL a úsáid "
|
||||
#~ "chun roghanna a shioncronú ar fud "
|
||||
#~ "feistí."
|
||||
|
||||
|
||||
Binary file not shown.
@ -26,20 +26,22 @@
|
||||
# nogb <nogb@users.noreply.translate.codeberg.org>, 2025.
|
||||
# syobon <syobon@users.noreply.translate.codeberg.org>, 2025.
|
||||
# cc5efd7b0 <cc5efd7b0@noreply.codeberg.org>, 2025.
|
||||
# ayame30 <ayame30@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-15 14:07+0000\n"
|
||||
"Last-Translator: cc5efd7b0 <cc5efd7b0@noreply.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-04-11 15:12+0000\n"
|
||||
"Last-Translator: ayame30 <ayame30@noreply.codeberg.org>\n"
|
||||
"Language-Team: Japanese <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/ja/>\n"
|
||||
"Language: ja\n"
|
||||
"Language-Team: Japanese "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/ja/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -530,15 +532,15 @@ msgstr "ファイル品質"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "Ahmiaのブラックリスト"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
msgstr ""
|
||||
msgstr "Ahmiaのブラックリストに表示されるオニオン結果を除外します。"
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "基本的な計算機"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
@ -624,7 +626,7 @@ msgstr "返された URL からトラッカー引数を消去する"
|
||||
|
||||
#: searx/plugins/unit_converter.py:49
|
||||
msgid "Unit converter plugin"
|
||||
msgstr ""
|
||||
msgstr "単位変換プラグイン"
|
||||
|
||||
#: searx/plugins/unit_converter.py:50
|
||||
msgid "Convert between units"
|
||||
@ -1170,7 +1172,8 @@ msgstr "このURLで違うブラウザに設定を復活"
|
||||
msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
msgstr "設定内容が保存されたURLです。このURLを使用すると、別のデバイスで設定を復元で"
|
||||
"きます。"
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
@ -2019,4 +2022,3 @@ msgstr "動画を隠す"
|
||||
#~ "preferences URL can be used to "
|
||||
#~ "sync preferences across devices."
|
||||
#~ msgstr "初期設定URLを使うことで、特別な設定をデバイスをまたいで同期できる。"
|
||||
|
||||
|
||||
Binary file not shown.
@ -16,20 +16,21 @@
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-27 13:44+0000\n"
|
||||
"PO-Revision-Date: 2025-04-20 12:41+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"Language-Team: Lithuanian <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/lt/>\n"
|
||||
"Language: lt\n"
|
||||
"Language-Team: Lithuanian "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/lt/>\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100"
|
||||
" < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < "
|
||||
"11) ? 1 : n % 1 != 0 ? 2: 3);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < "
|
||||
"11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 :"
|
||||
" n % 1 != 0 ? 2: 3);\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -525,15 +526,15 @@ msgstr "Failo kokybė"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "Ahmia juodasis sąrašas"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
msgstr ""
|
||||
msgstr "Išfiltruoti onion rezultatus esančius Ahmia juodajame sąraše."
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "Bazinis skaičiuotuvas"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
@ -582,6 +583,8 @@ msgid ""
|
||||
"Displays your IP if the query is \"ip\" and your user agent if the query "
|
||||
"is \"user-agent\"."
|
||||
msgstr ""
|
||||
"Rodo tavo IP jei užklausa yra „ip“ ir tavo naudotojo agentą jei užklausa yra "
|
||||
"„user-agent“."
|
||||
|
||||
#: searx/plugins/self_info.py:52
|
||||
msgid "Your IP is: "
|
||||
@ -589,7 +592,7 @@ msgstr "Jūsų IP adresas: "
|
||||
|
||||
#: searx/plugins/self_info.py:55
|
||||
msgid "Your user-agent is: "
|
||||
msgstr ""
|
||||
msgstr "Tavo naudotojo agentas (user-agent) yra: "
|
||||
|
||||
#: searx/plugins/tor_check.py:42
|
||||
msgid "Tor check plugin"
|
||||
@ -606,15 +609,15 @@ msgstr ""
|
||||
|
||||
#: searx/plugins/tor_check.py:65
|
||||
msgid "Could not download the list of Tor exit-nodes from"
|
||||
msgstr ""
|
||||
msgstr "Nepavyko atsisiųsti Tor išėjimo mazgų iš"
|
||||
|
||||
#: searx/plugins/tor_check.py:72
|
||||
msgid "You are using Tor and it looks like you have the external IP address"
|
||||
msgstr ""
|
||||
msgstr "Jūs naudojate Tor ir atrodo, kad turite išorinį IP adresą"
|
||||
|
||||
#: searx/plugins/tor_check.py:76
|
||||
msgid "You are not using Tor and you have the external IP address"
|
||||
msgstr ""
|
||||
msgstr "Jūs nenaudojate Tor, tačiau atrodo, kad turite išorinį IP adresą"
|
||||
|
||||
#: searx/plugins/tracker_url_remover.py:37
|
||||
msgid "Tracker URL remover"
|
||||
@ -626,11 +629,11 @@ msgstr "Šalinti seklių argumentus iš grąžinamų URL"
|
||||
|
||||
#: searx/plugins/unit_converter.py:49
|
||||
msgid "Unit converter plugin"
|
||||
msgstr ""
|
||||
msgstr "Matavimo vienetų konvertavimo papildinys"
|
||||
|
||||
#: searx/plugins/unit_converter.py:50
|
||||
msgid "Convert between units"
|
||||
msgstr ""
|
||||
msgstr "Konvertuoti tarp matavimo vienetų"
|
||||
|
||||
#: searx/templates/simple/404.html:4
|
||||
msgid "Page not found"
|
||||
@ -942,7 +945,7 @@ msgstr "Pavyzdžiai"
|
||||
|
||||
#: searx/templates/simple/answer/translations.html:21
|
||||
msgid "Definitions"
|
||||
msgstr ""
|
||||
msgstr "Apibrėžimai"
|
||||
|
||||
#: searx/templates/simple/answer/translations.html:30
|
||||
msgid "Synonyms"
|
||||
@ -966,7 +969,7 @@ msgstr "Pranešimai iš paieškos sistemų"
|
||||
|
||||
#: searx/templates/simple/elements/engines_msg.html:7
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
msgstr "sekundės"
|
||||
|
||||
#: searx/templates/simple/elements/search_url.html:3
|
||||
msgid "Search URL"
|
||||
@ -1084,11 +1087,11 @@ msgstr "Pakeiskite nuostatose naudojamą paieškos variklį:"
|
||||
|
||||
#: searx/templates/simple/messages/no_results.html:22
|
||||
msgid "Switch to another instance:"
|
||||
msgstr ""
|
||||
msgstr "Pakeisti instanciją:"
|
||||
|
||||
#: searx/templates/simple/messages/no_results.html:24
|
||||
msgid "Search for another query or select another category."
|
||||
msgstr ""
|
||||
msgstr "Ieškoti kitos užklausos arba pasirinkti kitą kategoriją."
|
||||
|
||||
#: searx/templates/simple/messages/no_results.html:25
|
||||
msgid "Go back to the previous page using the previous page button."
|
||||
@ -1101,7 +1104,7 @@ msgstr "Leisti"
|
||||
|
||||
#: searx/templates/simple/preferences/answerers.html:5
|
||||
msgid "Keywords (first word in query)"
|
||||
msgstr ""
|
||||
msgstr "Raktažodžiai (pirmasis užklausos žodis)"
|
||||
|
||||
#: searx/templates/simple/preferences/answerers.html:6
|
||||
#: searx/templates/simple/result_templates/packages.html:7
|
||||
@ -1178,6 +1181,8 @@ msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
"URL adresas su jūsų nustatymais. Šis URL adresas gali būti panaudotas "
|
||||
"atstatyti jūsų nustatymus kitame įrenginyje."
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
@ -1193,7 +1198,7 @@ msgstr ""
|
||||
|
||||
#: searx/templates/simple/preferences/doi_resolver.html:1
|
||||
msgid "Digital Object Identifier (DOI)"
|
||||
msgstr ""
|
||||
msgstr "Skaitmeninis objekto identifikatorius (DOI)"
|
||||
|
||||
#: searx/templates/simple/preferences/doi_resolver.html:6
|
||||
msgid "Open Access DOI resolver"
|
||||
@ -2058,4 +2063,3 @@ msgstr "slėpti vaizdo įrašą"
|
||||
#~ "Nurodant tinkintus nustatymus nuostatų URL,"
|
||||
#~ " jūs galite susinchronizuoti nuostatas tarp"
|
||||
#~ " prietaisų."
|
||||
|
||||
|
||||
Binary file not shown.
@ -28,22 +28,24 @@
|
||||
# Bubowny <bubowny@users.noreply.translate.codeberg.org>, 2025.
|
||||
# matsob0123 <matsob0123@users.noreply.translate.codeberg.org>, 2025.
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
# polskiecus <polskiecus@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-14 07:09+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-05-09 07:09+0000\n"
|
||||
"Last-Translator: polskiecus <polskiecus@noreply.codeberg.org>\n"
|
||||
"Language-Team: Polish <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/pl/>\n"
|
||||
"Language: pl\n"
|
||||
"Language-Team: Polish "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/pl/>\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && "
|
||||
"(n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && "
|
||||
"n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && ("
|
||||
"n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && "
|
||||
"n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -539,15 +541,17 @@ msgstr "Jakość pliku"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "Czarna lista wyszukiwarki Ahmia"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
msgstr ""
|
||||
"Pomiń serwisy .onion, które znajdują się na czarnej liście wyszkukiwarki "
|
||||
"Ahmia"
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "Kalkulator Prosty"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
@ -642,7 +646,7 @@ msgstr "Usuń argumenty elementów śledzących ze zwróconego adresu URL"
|
||||
|
||||
#: searx/plugins/unit_converter.py:49
|
||||
msgid "Unit converter plugin"
|
||||
msgstr ""
|
||||
msgstr "Plugin do konwersji jednostek"
|
||||
|
||||
#: searx/plugins/unit_converter.py:50
|
||||
msgid "Convert between units"
|
||||
@ -1196,6 +1200,8 @@ msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
"Adres URL zawierający twoje preferencje/ustawienia. Ten Adres URL pozwala na "
|
||||
"odzyskanie/przeniesienie swoich ustawień na inne urządzenie"
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
@ -2103,4 +2109,3 @@ msgstr "ukryj wideo"
|
||||
#~ "Określanie własnych ustawień w adresie "
|
||||
#~ "URL preferencji może służyć do "
|
||||
#~ "synchronizowania preferencji między urządzeniami."
|
||||
|
||||
|
||||
Binary file not shown.
@ -30,7 +30,7 @@ msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-31 18:08+0000\n"
|
||||
"PO-Revision-Date: 2025-04-30 15:18+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"Language-Team: Romanian <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/ro/>\n"
|
||||
@ -544,7 +544,7 @@ msgstr ""
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "Calculator de bază"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
|
||||
Binary file not shown.
@ -14,21 +14,23 @@
|
||||
# Vision <vision@users.noreply.translate.codeberg.org>, 2025.
|
||||
# curtwheeler <curtwheeler@users.noreply.translate.codeberg.org>, 2025.
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
# whytf <whytf@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-27 13:44+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-04-23 19:16+0000\n"
|
||||
"Last-Translator: whytf <whytf@noreply.codeberg.org>\n"
|
||||
"Language-Team: Slovak <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/sk/>\n"
|
||||
"Language: sk\n"
|
||||
"Language-Team: Slovak "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/sk/>\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 "
|
||||
"&& n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
|
||||
">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -433,12 +435,12 @@ msgstr "Pozastavené"
|
||||
#: searx/webutils.py:314
|
||||
#, python-brace-format
|
||||
msgid "{minutes} minute(s) ago"
|
||||
msgstr "pred {minutes} min."
|
||||
msgstr "pred {minutes} minútami"
|
||||
|
||||
#: searx/webutils.py:315
|
||||
#, python-brace-format
|
||||
msgid "{hours} hour(s), {minutes} minute(s) ago"
|
||||
msgstr "pred {hours} hod., {minutes} min."
|
||||
msgstr "pred {hours} hodinami, {minutes} minútami"
|
||||
|
||||
#: searx/answerers/random.py:69
|
||||
msgid "Generate different random values"
|
||||
@ -524,15 +526,15 @@ msgstr "Kvalita súboru"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:32
|
||||
msgid "Ahmia blacklist"
|
||||
msgstr ""
|
||||
msgstr "Čierna listina Ahmia"
|
||||
|
||||
#: searx/plugins/ahmia_filter.py:33
|
||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||
msgstr ""
|
||||
msgstr "Odfiltruj výsledky onion, ktoré sa zobrazujú na čiernej listine Ahmia."
|
||||
|
||||
#: searx/plugins/calculator.py:38
|
||||
msgid "Basic Calculator"
|
||||
msgstr ""
|
||||
msgstr "Základná Kalkulačka"
|
||||
|
||||
#: searx/plugins/calculator.py:39
|
||||
msgid "Calculate mathematical expressions via the search bar"
|
||||
@ -626,7 +628,7 @@ msgstr "Odstrániť sledovacie argumenty z vrátenej URL"
|
||||
|
||||
#: searx/plugins/unit_converter.py:49
|
||||
msgid "Unit converter plugin"
|
||||
msgstr ""
|
||||
msgstr "Modul konvertora jednotiek"
|
||||
|
||||
#: searx/plugins/unit_converter.py:50
|
||||
msgid "Convert between units"
|
||||
@ -1182,6 +1184,8 @@ msgid ""
|
||||
"A URL containing your preferences. This URL can be used to restore your "
|
||||
"settings on a different device."
|
||||
msgstr ""
|
||||
"Adresa URL obsahujúca vaše preferencie. Túto adresu URL môžete použiť na "
|
||||
"obnovenie nastavení v inom zariadení."
|
||||
|
||||
#: searx/templates/simple/preferences/cookies.html:46
|
||||
msgid "Copy preferences hash"
|
||||
@ -2079,4 +2083,3 @@ msgstr "skryť video"
|
||||
#~ "Zadaním osobitých nastavení v adrese "
|
||||
#~ "(URL) nastavení je možné synchronizovať "
|
||||
#~ "nastavenia do iných zariadení."
|
||||
|
||||
|
||||
Binary file not shown.
@ -24,21 +24,22 @@
|
||||
# return42 <return42@users.noreply.translate.codeberg.org>, 2025.
|
||||
# Eshan-K-I <eshan-k-i@users.noreply.translate.codeberg.org>, 2025.
|
||||
# rajeeban <rajeeban@users.noreply.translate.codeberg.org>, 2025.
|
||||
# prashere <prashere@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-01-20 03:36+0000\n"
|
||||
"Last-Translator: rajeeban <rajeeban@users.noreply.translate.codeberg.org>"
|
||||
"\n"
|
||||
"PO-Revision-Date: 2025-04-22 10:45+0000\n"
|
||||
"Last-Translator: prashere <prashere@noreply.codeberg.org>\n"
|
||||
"Language-Team: Tamil <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/ta/>\n"
|
||||
"Language: ta\n"
|
||||
"Language-Team: Tamil "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/ta/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -149,7 +150,7 @@ msgstr "கிடங்கு"
|
||||
#. CATEGORY_GROUPS['SOFTWARE_WIKIS']
|
||||
#: searx/searxng.msg
|
||||
msgid "software wikis"
|
||||
msgstr "மென்பொருள் விகி"
|
||||
msgstr "மென்பொருள் விக்கி"
|
||||
|
||||
#. CATEGORY_GROUPS['WEB']
|
||||
#: searx/searxng.msg
|
||||
@ -2064,4 +2065,3 @@ msgstr "காணொளிகளை மறை"
|
||||
#~ "preferences URL can be used to "
|
||||
#~ "sync preferences across devices."
|
||||
#~ msgstr ""
|
||||
|
||||
|
||||
Binary file not shown.
@ -12,21 +12,22 @@
|
||||
# saledai <saledai@users.noreply.translate.codeberg.org>, 2024, 2025.
|
||||
# Anonymous <anonymous@users.noreply.translate.codeberg.org>, 2025.
|
||||
# yuttct <yuttct@users.noreply.translate.codeberg.org>, 2025.
|
||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-01-06 15:53+0000\n"
|
||||
"Last-Translator: sahussawud "
|
||||
"<sahussawud@users.noreply.translate.codeberg.org>\n"
|
||||
"PO-Revision-Date: 2025-04-15 10:37+0000\n"
|
||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||
"Language-Team: Thai <https://translate.codeberg.org/projects/searxng/searxng/"
|
||||
"th/>\n"
|
||||
"Language: th\n"
|
||||
"Language-Team: Thai "
|
||||
"<https://translate.codeberg.org/projects/searxng/searxng/th/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.10.2\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||
@ -382,7 +383,7 @@ msgstr "ข้อผิดพลาดระหว่างแจงโครง
|
||||
|
||||
#: searx/webutils.py:38
|
||||
msgid "HTTP protocol error"
|
||||
msgstr "โปรโตคอล HTTP เกิดข้อผิดพลาดของ"
|
||||
msgstr "เกิดข้อผิดพลาดของโปรโตคอล HTTP"
|
||||
|
||||
#: searx/webutils.py:39
|
||||
msgid "network error"
|
||||
@ -1788,4 +1789,3 @@ msgstr "ซ่อนวิดีโอ"
|
||||
#~ msgstr ""
|
||||
#~ "การระบุการตั้งค่าแบบกำหนดเองใน URL "
|
||||
#~ "ค่ากำหนดสามารถใช้เพื่อซิงค์กับค่ากำหนดในอุปกรณ์ต่างๆได้"
|
||||
|
||||
|
||||
Binary file not shown.
@ -23,7 +23,7 @@ msgstr ""
|
||||
"Project-Id-Version: searx\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-03-29 09:21+0000\n"
|
||||
"PO-Revision-Date: 2025-03-29 12:40+0000\n"
|
||||
"PO-Revision-Date: 2025-05-03 15:52+0000\n"
|
||||
"Last-Translator: SomeTr <sometr@noreply.codeberg.org>\n"
|
||||
"Language-Team: Ukrainian <https://translate.codeberg.org/projects/searxng/"
|
||||
"searxng/uk/>\n"
|
||||
@ -383,7 +383,7 @@ msgstr "помилка пошуку"
|
||||
|
||||
#: searx/webutils.py:36
|
||||
msgid "timeout"
|
||||
msgstr "таймаут"
|
||||
msgstr "тайм-аут"
|
||||
|
||||
#: searx/webutils.py:37
|
||||
msgid "parsing error"
|
||||
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user