mirror of
https://github.com/searxng/searxng.git
synced 2026-09-14 10:16:03 +00:00
Compare commits
19 Commits
0ce0d957b1
...
b7ecc1c240
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ecc1c240 | ||
|
|
164167dea0 | ||
|
|
b3eb7657b9 | ||
|
|
d2b4bff856 | ||
|
|
1520a8d545 | ||
|
|
f8f7adce6b | ||
|
|
4c42704c80 | ||
|
|
4b4bf0ecaf | ||
|
|
bb22bb1831 | ||
|
|
96e63df8ca | ||
|
|
0691e50e13 | ||
|
|
599d9488c5 | ||
|
|
078c9fcb68 | ||
|
|
3ec6d65f9b | ||
|
|
22e73727c0 | ||
|
|
6c3fb9e42b | ||
|
|
f9b4869426 | ||
|
|
7eedd44f5f | ||
|
|
57ef342ad1 |
1
.github/workflows/container.yml
vendored
1
.github/workflows/container.yml
vendored
@@ -134,6 +134,7 @@ jobs:
|
|||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: "false"
|
persist-credentials: "false"
|
||||||
|
fetch-depth: "0"
|
||||||
|
|
||||||
- name: Setup cache Python
|
- name: Setup cache Python
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
|
|||||||
72
client/simple/src/less/result_types/paper.less
Normal file
72
client/simple/src/less/result_types/paper.less
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
/*
|
||||||
|
Layout of the Paper result class
|
||||||
|
*/
|
||||||
|
|
||||||
|
.result-paper {
|
||||||
|
.attributes {
|
||||||
|
display: table;
|
||||||
|
border-spacing: 0.125rem;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: table-row;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
display: table-cell;
|
||||||
|
|
||||||
|
time {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span:first-child {
|
||||||
|
color: var(--color-base-font);
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(2) {
|
||||||
|
color: var(--color-result-publishdate-font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0.25rem 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1.24;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: @phone) {
|
||||||
|
.result-paper {
|
||||||
|
.attributes {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(2) {
|
||||||
|
.ltr-margin-left(0.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -309,11 +309,11 @@ article[data-vim-selected].category-social {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-paper,
|
|
||||||
.result-packages {
|
.result-packages {
|
||||||
.attributes {
|
.attributes {
|
||||||
display: table;
|
display: table;
|
||||||
border-spacing: 0.125rem;
|
border-spacing: 0.125rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
display: table-row;
|
display: table-row;
|
||||||
@@ -353,12 +353,6 @@ article[data-vim-selected].category-social {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-packages {
|
|
||||||
.attributes {
|
|
||||||
margin-top: 0.3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.template_group_images {
|
.template_group_images {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1118,7 +1112,6 @@ summary.title {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-paper,
|
|
||||||
.result-packages {
|
.result-packages {
|
||||||
.attributes {
|
.attributes {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1164,3 +1157,4 @@ pre code {
|
|||||||
|
|
||||||
// import layouts of the Result types
|
// import layouts of the Result types
|
||||||
@import "result_types/keyvalue.less";
|
@import "result_types/keyvalue.less";
|
||||||
|
@import "result_types/paper.less";
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
FROM ghcr.io/searxng/base:searxng-builder AS builder
|
FROM ghcr.io/searxng/base:searxng-builder AS builder
|
||||||
|
|
||||||
COPY ./requirements*.txt ./
|
ARG TIMESTAMP_VENV="0"
|
||||||
|
|
||||||
ARG TIMESTAMP="0"
|
COPY ./requirements.txt ./requirements-server.txt ./
|
||||||
|
|
||||||
RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux; \
|
RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
|
||||||
|
export SOURCE_DATE_EPOCH="$TIMESTAMP_VENV"; \
|
||||||
uv venv; \
|
uv venv; \
|
||||||
uv pip install --no-managed-python --compile-bytecode --requirements ./requirements.txt --requirements ./requirements-server.txt; \
|
uv pip install --no-managed-python --compile-bytecode --requirements ./requirements.txt --requirements ./requirements-server.txt; \
|
||||||
uv cache prune --ci; \
|
uv cache prune --ci; \
|
||||||
find ./.venv/ -exec touch -h -t $TIMESTAMP {} +
|
find ./.venv/lib/python*/site-packages/*.dist-info/ -type f -name "RECORD" -exec sort -t, -k1,1 -o {} {} \;; \
|
||||||
|
find ./.venv/ -exec touch -h --date="@$TIMESTAMP_VENV" {} +; \
|
||||||
|
unset SOURCE_DATE_EPOCH
|
||||||
|
|
||||||
|
# use "--exclude=./searx/version_frozen.py" when actions/runner-images updates to Podman 5.0+
|
||||||
COPY ./searx/ ./searx/
|
COPY ./searx/ ./searx/
|
||||||
|
|
||||||
ARG TIMESTAMP_SETTINGS="0"
|
ARG TIMESTAMP_SETTINGS="0"
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux -o pipefail; \
|
||||||
python -m compileall -q ./searx/; \
|
python -m compileall -q ./searx/; \
|
||||||
touch -c -t $TIMESTAMP_SETTINGS ./searx/settings.yml; \
|
|
||||||
find ./searx/static/ -type f \
|
find ./searx/static/ -type f \
|
||||||
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
|
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
|
||||||
-exec gzip -9 -k {} + \
|
-exec gzip -9 -k {} + \
|
||||||
-exec brotli -9 -k {} + \
|
-exec brotli -9 -k {} + \
|
||||||
-exec gzip --test {}.gz + \
|
-exec gzip --test {}.gz + \
|
||||||
-exec brotli --test {}.br +; \
|
-exec brotli --test {}.br +; \
|
||||||
# Move always changing files to /usr/local/searxng/
|
touch -c --date="@$TIMESTAMP_SETTINGS" ./searx/settings.yml
|
||||||
mv ./searx/version_frozen.py ./
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ARG CONTAINER_IMAGE_NAME="searxng"
|
|||||||
COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/.venv/ ./.venv/
|
COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/.venv/ ./.venv/
|
||||||
COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/searx/ ./searx/
|
COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/searx/ ./searx/
|
||||||
COPY --chown=searxng:searxng ./container/ ./
|
COPY --chown=searxng:searxng ./container/ ./
|
||||||
COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/version_frozen.py ./searx/
|
#COPY --chown=searxng:searxng ./searx/version_frozen.py ./searx/
|
||||||
|
|
||||||
ARG CREATED="0001-01-01T00:00:00Z"
|
ARG CREATED="0001-01-01T00:00:00Z"
|
||||||
ARG VERSION="unknown"
|
ARG VERSION="unknown"
|
||||||
@@ -30,7 +30,6 @@ ENV SEARXNG_VERSION="$VERSION" \
|
|||||||
GRANIAN_HOST="::" \
|
GRANIAN_HOST="::" \
|
||||||
GRANIAN_PORT="8080" \
|
GRANIAN_PORT="8080" \
|
||||||
GRANIAN_WEBSOCKETS="false" \
|
GRANIAN_WEBSOCKETS="false" \
|
||||||
GRANIAN_LOOP="uvloop" \
|
|
||||||
GRANIAN_BLOCKING_THREADS="4" \
|
GRANIAN_BLOCKING_THREADS="4" \
|
||||||
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
||||||
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
|
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
|
||||||
|
|||||||
8
docs/dev/engines/online/arxiv.rst
Normal file
8
docs/dev/engines/online/arxiv.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _arxiv engine:
|
||||||
|
|
||||||
|
=====
|
||||||
|
arXiv
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.arxiv
|
||||||
|
:members:
|
||||||
8
docs/dev/engines/online/astrophysics_data_system.rst
Normal file
8
docs/dev/engines/online/astrophysics_data_system.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _astrophysics_data_system engine:
|
||||||
|
|
||||||
|
==============================
|
||||||
|
Astrophysics Data System (ADS)
|
||||||
|
==============================
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.astrophysics_data_system
|
||||||
|
:members:
|
||||||
8
docs/dev/engines/online/crossref.rst
Normal file
8
docs/dev/engines/online/crossref.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _crossref engine:
|
||||||
|
|
||||||
|
========
|
||||||
|
Crossref
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.crossref
|
||||||
|
:members:
|
||||||
@@ -1,100 +1,8 @@
|
|||||||
.. _openalex engine:
|
.. _openalex engine:
|
||||||
|
|
||||||
=========
|
|
||||||
OpenAlex
|
|
||||||
=========
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
========
|
||||||
|
OpenAlex
|
||||||
The OpenAlex engine integrates the `OpenAlex`_ Works API to return scientific paper
|
========
|
||||||
results using the :origin:`paper.html <searx/templates/simple/result_templates/paper.html>`
|
|
||||||
template. It is an "online" JSON engine that uses the official public API and does
|
|
||||||
not require an API key.
|
|
||||||
|
|
||||||
.. _OpenAlex: https://openalex.org
|
|
||||||
.. _OpenAlex API overview: https://docs.openalex.org/how-to-use-the-api/api-overview
|
|
||||||
|
|
||||||
Key features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- Uses the official Works endpoint (JSON)
|
|
||||||
- Paging support via ``page`` and ``per-page``
|
|
||||||
- Relevance sorting (``sort=relevance_score:desc``)
|
|
||||||
- Language filter support (maps SearXNG language to ``filter=language:<iso2>``)
|
|
||||||
- Maps fields commonly used in scholarly results: title, authors, abstract
|
|
||||||
(reconstructed from inverted index), journal/venue, publisher, DOI, tags
|
|
||||||
(concepts), PDF/HTML links, pages, volume, issue, published date, and a short
|
|
||||||
citations comment
|
|
||||||
- Supports OpenAlex "polite pool" by adding a ``mailto`` parameter
|
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
Minimal example for :origin:`settings.yml <searx/settings.yml>`:
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
- name: openalex
|
|
||||||
engine: openalex
|
|
||||||
shortcut: oa
|
|
||||||
categories: science, scientific publications
|
|
||||||
timeout: 5.0
|
|
||||||
# Recommended by OpenAlex: join the polite pool with an email address
|
|
||||||
mailto: "[email protected]"
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- The ``mailto`` key is optional but recommended by OpenAlex for better service.
|
|
||||||
- Language is inherited from the user's UI language; when it is not ``all``, the
|
|
||||||
engine adds ``filter=language:<iso2>`` (e.g. ``language:fr``). If OpenAlex has
|
|
||||||
few results for that language, you may see fewer items.
|
|
||||||
- Results typically include a main link. When the primary landing page from
|
|
||||||
OpenAlex is a DOI resolver, the engine will use that stable link. When an open
|
|
||||||
access link is available, it is exposed via the ``PDF`` and/or ``HTML`` links
|
|
||||||
in the result footer.
|
|
||||||
|
|
||||||
|
|
||||||
What is returned
|
|
||||||
================
|
|
||||||
|
|
||||||
Each result uses the ``paper.html`` template and may include:
|
|
||||||
|
|
||||||
- ``title`` and ``content`` (abstract; reconstructed from the inverted index)
|
|
||||||
- ``authors`` (display names)
|
|
||||||
- ``journal`` (host venue display name) and ``publisher``
|
|
||||||
- ``doi`` (normalized to the plain DOI, without the ``https://doi.org/`` prefix)
|
|
||||||
- ``tags`` (OpenAlex concepts display names)
|
|
||||||
- ``pdf_url`` (Open access PDF if available) and ``html_url`` (landing page)
|
|
||||||
- ``publishedDate`` (parsed from ``publication_date``)
|
|
||||||
- ``pages``, ``volume``, ``number`` (issue)
|
|
||||||
- ``type`` and a brief ``comments`` string with citation count
|
|
||||||
|
|
||||||
|
|
||||||
Rate limits & polite pool
|
|
||||||
=========================
|
|
||||||
|
|
||||||
OpenAlex offers a free public API with generous daily limits. For extra courtesy
|
|
||||||
and improved service quality, include a contact email in each request via
|
|
||||||
``mailto``. You can set it directly in the engine configuration as shown above.
|
|
||||||
See: `OpenAlex API overview`_.
|
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting
|
|
||||||
===============
|
|
||||||
|
|
||||||
- Few or no results in a non-English UI language:
|
|
||||||
Ensure the selected language has sufficient coverage at OpenAlex, or set the
|
|
||||||
UI language to English and retry.
|
|
||||||
- Preference changes fail while testing locally:
|
|
||||||
Make sure your ``server.secret_key`` and ``server.base_url`` are set in your
|
|
||||||
instance settings so signed cookies work; see :ref:`settings server`.
|
|
||||||
|
|
||||||
|
|
||||||
Implementation
|
|
||||||
===============
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.openalex
|
.. automodule:: searx.engines.openalex
|
||||||
:members:
|
:members:
|
||||||
|
|||||||
8
docs/dev/engines/online/openlibrary.rst
Normal file
8
docs/dev/engines/online/openlibrary.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _openlibrary engine:
|
||||||
|
|
||||||
|
============
|
||||||
|
Open Library
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.openlibrary
|
||||||
|
:members:
|
||||||
8
docs/dev/engines/online/pubmed.rst
Normal file
8
docs/dev/engines/online/pubmed.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _pubmed engine:
|
||||||
|
|
||||||
|
======
|
||||||
|
PubMed
|
||||||
|
======
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.pubmed
|
||||||
|
:members:
|
||||||
8
docs/dev/engines/online/semantic_scholar.rst
Normal file
8
docs/dev/engines/online/semantic_scholar.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _semantic_scholar engine:
|
||||||
|
|
||||||
|
================
|
||||||
|
Semantic Scholar
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.semantic_scholar
|
||||||
|
:members:
|
||||||
8
docs/dev/engines/online/springer.rst
Normal file
8
docs/dev/engines/online/springer.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _springer engine:
|
||||||
|
|
||||||
|
===============
|
||||||
|
Springer Nature
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.springer
|
||||||
|
:members:
|
||||||
7
docs/dev/result_types/main/paper.rst
Normal file
7
docs/dev/result_types/main/paper.rst
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.. _result_types.paper:
|
||||||
|
|
||||||
|
=============
|
||||||
|
Paper Results
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. automodule:: searx.result_types.paper
|
||||||
@@ -16,6 +16,7 @@ following types have been implemented so far ..
|
|||||||
main/mainresult
|
main/mainresult
|
||||||
main/keyvalue
|
main/keyvalue
|
||||||
main/code
|
main/code
|
||||||
|
main/paper
|
||||||
|
|
||||||
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that
|
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that
|
||||||
have not yet been typed. The templates can be used as orientation until the
|
have not yet been typed. The templates can be used as orientation until the
|
||||||
@@ -26,7 +27,6 @@ final typing is complete.
|
|||||||
- :ref:`template videos`
|
- :ref:`template videos`
|
||||||
- :ref:`template torrent`
|
- :ref:`template torrent`
|
||||||
- :ref:`template map`
|
- :ref:`template map`
|
||||||
- :ref:`template paper`
|
|
||||||
- :ref:`template packages`
|
- :ref:`template packages`
|
||||||
- :ref:`template files`
|
- :ref:`template files`
|
||||||
- :ref:`template products`
|
- :ref:`template products`
|
||||||
|
|||||||
1
manage
1
manage
@@ -159,7 +159,6 @@ webapp.run() {
|
|||||||
GRANIAN_HOST="::" \
|
GRANIAN_HOST="::" \
|
||||||
GRANIAN_PORT="8888" \
|
GRANIAN_PORT="8888" \
|
||||||
GRANIAN_WEBSOCKETS="false" \
|
GRANIAN_WEBSOCKETS="false" \
|
||||||
GRANIAN_LOOP="uvloop" \
|
|
||||||
GRANIAN_BLOCKING_THREADS="4" \
|
GRANIAN_BLOCKING_THREADS="4" \
|
||||||
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
||||||
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m" \
|
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m" \
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ pyyaml==6.0.2
|
|||||||
httpx[http2]==0.28.1
|
httpx[http2]==0.28.1
|
||||||
httpx-socks[asyncio]==0.10.0
|
httpx-socks[asyncio]==0.10.0
|
||||||
Brotli==1.1.0
|
Brotli==1.1.0
|
||||||
uvloop==0.21.0
|
|
||||||
setproctitle==1.3.7
|
setproctitle==1.3.7
|
||||||
valkey==6.1.1
|
valkey==6.1.1
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ for *newest* articles and journals (PDF) / by shortcut ``!aaa <search-term>``.
|
|||||||
|
|
||||||
- name: annas articles
|
- name: annas articles
|
||||||
engine: annas_archive
|
engine: annas_archive
|
||||||
|
categories = ["general", "articles"]
|
||||||
shortcut: aaa
|
shortcut: aaa
|
||||||
aa_content: 'magazine'
|
aa_content: "magazine"
|
||||||
aa_ext: 'pdf'
|
aa_ext: "pdf"
|
||||||
aa_sort: 'newest'
|
aa_sort: "newest"
|
||||||
|
|
||||||
|
|
||||||
Implementations
|
Implementations
|
||||||
===============
|
===============
|
||||||
@@ -47,6 +49,7 @@ from searx.result_types import EngineResults
|
|||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from searx.extended_types import SXNG_Response
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
# about
|
# about
|
||||||
about: dict[str, t.Any] = {
|
about: dict[str, t.Any] = {
|
||||||
@@ -59,7 +62,7 @@ about: dict[str, t.Any] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories: list[str] = ["files"]
|
categories = ["files", "books"]
|
||||||
paging: bool = True
|
paging: bool = True
|
||||||
|
|
||||||
# search-url
|
# search-url
|
||||||
@@ -72,14 +75,14 @@ aa_content: str = ""
|
|||||||
|
|
||||||
To not filter use an empty string (default).
|
To not filter use an empty string (default).
|
||||||
"""
|
"""
|
||||||
aa_sort: str = ''
|
aa_sort: str = ""
|
||||||
"""Sort Anna's results, possible values::
|
"""Sort Anna's results, possible values::
|
||||||
|
|
||||||
newest, oldest, largest, smallest
|
newest, oldest, largest, smallest
|
||||||
|
|
||||||
To sort by *most relevant* use an empty string (default)."""
|
To sort by *most relevant* use an empty string (default)."""
|
||||||
|
|
||||||
aa_ext: str = ''
|
aa_ext: str = ""
|
||||||
"""Filter Anna's results by a file ending. Common filters for example are
|
"""Filter Anna's results by a file ending. Common filters for example are
|
||||||
``pdf`` and ``epub``.
|
``pdf`` and ``epub``.
|
||||||
|
|
||||||
@@ -91,29 +94,31 @@ aa_ext: str = ''
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def init(engine_settings: dict[str, t.Any]) -> None: # pylint: disable=unused-argument
|
def setup(engine_settings: dict[str, t.Any]) -> bool: # pylint: disable=unused-argument
|
||||||
"""Check of engine's settings."""
|
"""Check of engine's settings."""
|
||||||
traits = EngineTraits(**ENGINE_TRAITS['annas archive'])
|
traits = EngineTraits(**ENGINE_TRAITS["annas archive"])
|
||||||
|
|
||||||
if aa_content and aa_content not in traits.custom['content']:
|
if aa_content and aa_content not in traits.custom["content"]:
|
||||||
raise ValueError(f'invalid setting content: {aa_content}')
|
raise ValueError(f"invalid setting content: {aa_content}")
|
||||||
|
|
||||||
if aa_sort and aa_sort not in traits.custom['sort']:
|
if aa_sort and aa_sort not in traits.custom["sort"]:
|
||||||
raise ValueError(f'invalid setting sort: {aa_sort}')
|
raise ValueError(f"invalid setting sort: {aa_sort}")
|
||||||
|
|
||||||
if aa_ext and aa_ext not in traits.custom['ext']:
|
if aa_ext and aa_ext not in traits.custom["ext"]:
|
||||||
raise ValueError(f'invalid setting ext: {aa_ext}')
|
raise ValueError(f"invalid setting ext: {aa_ext}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: dict[str, t.Any]) -> None:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
lang = traits.get_language(params["language"], traits.all_locale)
|
lang = traits.get_language(params["searxng_locale"], traits.all_locale)
|
||||||
args = {
|
args = {
|
||||||
'lang': lang,
|
"lang": lang,
|
||||||
'content': aa_content,
|
"content": aa_content,
|
||||||
'ext': aa_ext,
|
"ext": aa_ext,
|
||||||
'sort': aa_sort,
|
"sort": aa_sort,
|
||||||
'q': query,
|
"q": query,
|
||||||
'page': params['pageno'],
|
"page": params["pageno"],
|
||||||
}
|
}
|
||||||
# filter out None and empty values
|
# filter out None and empty values
|
||||||
filtered_args = dict((k, v) for k, v in args.items() if v)
|
filtered_args = dict((k, v) for k, v in args.items() if v)
|
||||||
@@ -126,29 +131,28 @@ def response(resp: "SXNG_Response") -> EngineResults:
|
|||||||
|
|
||||||
# The rendering of the WEB page is strange; positions of Anna's result page
|
# The rendering of the WEB page is strange; positions of Anna's result page
|
||||||
# are enclosed in SGML comments. These comments are *uncommented* by some
|
# are enclosed in SGML comments. These comments are *uncommented* by some
|
||||||
# JS code, see query of class '.js-scroll-hidden' in Anna's HTML template:
|
# JS code, see query of class ".js-scroll-hidden" in Anna's HTML template:
|
||||||
# https://annas-software.org/AnnaArchivist/annas-archive/-/blob/main/allthethings/templates/macros/md5_list.html
|
# https://annas-software.org/AnnaArchivist/annas-archive/-/blob/main/allthethings/templates/macros/md5_list.html
|
||||||
|
|
||||||
for item in eval_xpath_list(dom, '//main//div[contains(@class, "js-aarecord-list-outer")]/div'):
|
for item in eval_xpath_list(dom, "//main//div[contains(@class, 'js-aarecord-list-outer')]/div"):
|
||||||
try:
|
try:
|
||||||
kwargs: dict[str, t.Any] = _get_result(item)
|
kwargs: dict[str, t.Any] = _get_result(item)
|
||||||
except SearxEngineXPathException:
|
except SearxEngineXPathException:
|
||||||
continue
|
continue
|
||||||
res.add(res.types.LegacyResult(**kwargs))
|
res.add(res.types.Paper(**kwargs))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def _get_result(item: ElementBase) -> dict[str, t.Any]:
|
def _get_result(item: ElementBase) -> dict[str, t.Any]:
|
||||||
return {
|
return {
|
||||||
'template': 'paper.html',
|
"url": base_url + eval_xpath_getindex(item, "./a/@href", 0),
|
||||||
'url': base_url + eval_xpath_getindex(item, './a/@href', 0),
|
"title": extract_text(eval_xpath(item, "./div//a[starts-with(@href, '/md5')]")),
|
||||||
'title': extract_text(eval_xpath(item, './div//a[starts-with(@href, "/md5")]')),
|
"authors": [extract_text(eval_xpath_getindex(item, ".//a[starts-with(@href, '/search')]", 0))],
|
||||||
'authors': [extract_text(eval_xpath_getindex(item, './/a[starts-with(@href, "/search")]', 0))],
|
"publisher": extract_text(
|
||||||
'publisher': extract_text(
|
eval_xpath_getindex(item, ".//a[starts-with(@href, '/search')]", 1, default=None), allow_none=True
|
||||||
eval_xpath_getindex(item, './/a[starts-with(@href, "/search")]', 1, default=None), allow_none=True
|
|
||||||
),
|
),
|
||||||
'content': extract_text(eval_xpath(item, './/div[contains(@class, "relative")]')),
|
"content": extract_text(eval_xpath(item, ".//div[contains(@class, 'relative')]")),
|
||||||
'thumbnail': extract_text(eval_xpath_getindex(item, './/img/@src', 0, default=None), allow_none=True),
|
"thumbnail": extract_text(eval_xpath_getindex(item, ".//img/@src", 0, default=None), allow_none=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -160,25 +164,25 @@ def fetch_traits(engine_traits: EngineTraits):
|
|||||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||||
from searx.locales import language_tag
|
from searx.locales import language_tag
|
||||||
|
|
||||||
engine_traits.all_locale = ''
|
engine_traits.all_locale = ""
|
||||||
engine_traits.custom['content'] = []
|
engine_traits.custom["content"] = []
|
||||||
engine_traits.custom['ext'] = []
|
engine_traits.custom["ext"] = []
|
||||||
engine_traits.custom['sort'] = []
|
engine_traits.custom["sort"] = []
|
||||||
|
|
||||||
resp = get(base_url + '/search')
|
resp = get(base_url + "/search")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
raise RuntimeError("Response from Anna's search page is not OK.")
|
raise RuntimeError("Response from Anna's search page is not OK.")
|
||||||
dom = html.fromstring(resp.text)
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
# supported language codes
|
# supported language codes
|
||||||
|
|
||||||
lang_map = {}
|
lang_map: dict[str, str] = {}
|
||||||
for x in eval_xpath_list(dom, "//form//input[@name='lang']"):
|
for x in eval_xpath_list(dom, "//form//input[@name='lang']"):
|
||||||
eng_lang = x.get("value")
|
eng_lang = x.get("value")
|
||||||
if eng_lang in ('', '_empty', 'nl-BE', 'und') or eng_lang.startswith('anti__'):
|
if eng_lang in ("", "_empty", "nl-BE", "und") or eng_lang.startswith("anti__"):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
locale = babel.Locale.parse(lang_map.get(eng_lang, eng_lang), sep='-')
|
locale = babel.Locale.parse(lang_map.get(eng_lang, eng_lang), sep="-")
|
||||||
except babel.UnknownLocaleError:
|
except babel.UnknownLocaleError:
|
||||||
# silently ignore unknown languages
|
# silently ignore unknown languages
|
||||||
# print("ERROR: %s -> %s is unknown by babel" % (x.get("data-name"), eng_lang))
|
# print("ERROR: %s -> %s is unknown by babel" % (x.get("data-name"), eng_lang))
|
||||||
@@ -193,16 +197,16 @@ def fetch_traits(engine_traits: EngineTraits):
|
|||||||
|
|
||||||
for x in eval_xpath_list(dom, "//form//input[@name='content']"):
|
for x in eval_xpath_list(dom, "//form//input[@name='content']"):
|
||||||
if not x.get("value").startswith("anti__"):
|
if not x.get("value").startswith("anti__"):
|
||||||
engine_traits.custom['content'].append(x.get("value"))
|
engine_traits.custom["content"].append(x.get("value"))
|
||||||
|
|
||||||
for x in eval_xpath_list(dom, "//form//input[@name='ext']"):
|
for x in eval_xpath_list(dom, "//form//input[@name='ext']"):
|
||||||
if not x.get("value").startswith("anti__"):
|
if not x.get("value").startswith("anti__"):
|
||||||
engine_traits.custom['ext'].append(x.get("value"))
|
engine_traits.custom["ext"].append(x.get("value"))
|
||||||
|
|
||||||
for x in eval_xpath_list(dom, "//form//select[@name='sort']//option"):
|
for x in eval_xpath_list(dom, "//form//select[@name='sort']//option"):
|
||||||
engine_traits.custom['sort'].append(x.get("value"))
|
engine_traits.custom["sort"].append(x.get("value"))
|
||||||
|
|
||||||
# for better diff; sort the persistence of these traits
|
# for better diff; sort the persistence of these traits
|
||||||
engine_traits.custom['content'].sort()
|
engine_traits.custom["content"].sort()
|
||||||
engine_traits.custom['ext'].sort()
|
engine_traits.custom["ext"].sort()
|
||||||
engine_traits.custom['sort'].sort()
|
engine_traits.custom["sort"].sort()
|
||||||
|
|||||||
@@ -1,110 +1,129 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""ArXiV (Scientific preprints)
|
"""arXiv is a free distribution service and an open-access archive for nearly
|
||||||
|
2.4 million scholarly articles in the fields of physics, mathematics, computer
|
||||||
|
science, quantitative biology, quantitative finance, statistics, electrical
|
||||||
|
engineering and systems science, and economics.
|
||||||
|
|
||||||
|
The engine uses the `arXiv API`_.
|
||||||
|
|
||||||
|
.. _arXiv API: https://info.arxiv.org/help/api/user-manual.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.etree import XPath
|
from lxml.etree import XPath
|
||||||
from searx.utils import eval_xpath, eval_xpath_list, eval_xpath_getindex
|
from searx.utils import eval_xpath, eval_xpath_list, eval_xpath_getindex
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
# about
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://arxiv.org',
|
"website": "https://arxiv.org",
|
||||||
"wikidata_id": 'Q118398',
|
"wikidata_id": "Q118398",
|
||||||
"official_api_documentation": 'https://arxiv.org/help/api',
|
"official_api_documentation": "https://info.arxiv.org/help/api/user-manual.html",
|
||||||
"use_official_api": True,
|
"use_official_api": True,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": 'XML-RSS',
|
"results": "XML-RSS",
|
||||||
}
|
}
|
||||||
|
|
||||||
categories = ['science', 'scientific publications']
|
categories = ["science", "scientific publications"]
|
||||||
paging = True
|
paging = True
|
||||||
|
arxiv_max_results = 10
|
||||||
|
arxiv_search_prefix = "all"
|
||||||
|
"""Search fields, for more details see, `Details of Query Construction`_.
|
||||||
|
|
||||||
base_url = (
|
.. _Details of Query Construction:
|
||||||
'https://export.arxiv.org/api/query?search_query=all:' + '{query}&start={offset}&max_results={number_of_results}'
|
https://info.arxiv.org/help/api/user-manual.html#51-details-of-query-construction
|
||||||
)
|
"""
|
||||||
|
|
||||||
# engine dependent config
|
base_url = "https://export.arxiv.org/api/query"
|
||||||
number_of_results = 10
|
"""`arXiv API`_ URL, for more details see Query-Interface_
|
||||||
|
|
||||||
|
.. _Query-Interface: https://info.arxiv.org/help/api/user-manual.html#_query_interface
|
||||||
|
"""
|
||||||
|
|
||||||
# xpaths
|
|
||||||
arxiv_namespaces = {
|
arxiv_namespaces = {
|
||||||
"atom": "http://www.w3.org/2005/Atom",
|
"atom": "http://www.w3.org/2005/Atom",
|
||||||
"arxiv": "http://arxiv.org/schemas/atom",
|
"arxiv": "http://arxiv.org/schemas/atom",
|
||||||
}
|
}
|
||||||
xpath_entry = XPath('//atom:entry', namespaces=arxiv_namespaces)
|
xpath_entry = XPath("//atom:entry", namespaces=arxiv_namespaces)
|
||||||
xpath_title = XPath('.//atom:title', namespaces=arxiv_namespaces)
|
xpath_title = XPath(".//atom:title", namespaces=arxiv_namespaces)
|
||||||
xpath_id = XPath('.//atom:id', namespaces=arxiv_namespaces)
|
xpath_id = XPath(".//atom:id", namespaces=arxiv_namespaces)
|
||||||
xpath_summary = XPath('.//atom:summary', namespaces=arxiv_namespaces)
|
xpath_summary = XPath(".//atom:summary", namespaces=arxiv_namespaces)
|
||||||
xpath_author_name = XPath('.//atom:author/atom:name', namespaces=arxiv_namespaces)
|
xpath_author_name = XPath(".//atom:author/atom:name", namespaces=arxiv_namespaces)
|
||||||
xpath_doi = XPath('.//arxiv:doi', namespaces=arxiv_namespaces)
|
xpath_doi = XPath(".//arxiv:doi", namespaces=arxiv_namespaces)
|
||||||
xpath_pdf = XPath('.//atom:link[@title="pdf"]', namespaces=arxiv_namespaces)
|
xpath_pdf = XPath(".//atom:link[@title='pdf']", namespaces=arxiv_namespaces)
|
||||||
xpath_published = XPath('.//atom:published', namespaces=arxiv_namespaces)
|
xpath_published = XPath(".//atom:published", namespaces=arxiv_namespaces)
|
||||||
xpath_journal = XPath('.//arxiv:journal_ref', namespaces=arxiv_namespaces)
|
xpath_journal = XPath(".//arxiv:journal_ref", namespaces=arxiv_namespaces)
|
||||||
xpath_category = XPath('.//atom:category/@term', namespaces=arxiv_namespaces)
|
xpath_category = XPath(".//atom:category/@term", namespaces=arxiv_namespaces)
|
||||||
xpath_comment = XPath('./arxiv:comment', namespaces=arxiv_namespaces)
|
xpath_comment = XPath("./arxiv:comment", namespaces=arxiv_namespaces)
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
# basic search
|
|
||||||
offset = (params['pageno'] - 1) * number_of_results
|
|
||||||
|
|
||||||
string_args = {'query': query, 'offset': offset, 'number_of_results': number_of_results}
|
args = {
|
||||||
|
"search_query": f"{arxiv_search_prefix}:{query}",
|
||||||
params['url'] = base_url.format(**string_args)
|
"start": (params["pageno"] - 1) * arxiv_max_results,
|
||||||
|
"max_results": arxiv_max_results,
|
||||||
return params
|
}
|
||||||
|
params["url"] = f"{base_url}?{urlencode(args)}"
|
||||||
|
|
||||||
|
|
||||||
def response(resp):
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
results = []
|
|
||||||
|
res = EngineResults()
|
||||||
|
|
||||||
dom = etree.fromstring(resp.content)
|
dom = etree.fromstring(resp.content)
|
||||||
for entry in eval_xpath_list(dom, xpath_entry):
|
for entry in eval_xpath_list(dom, xpath_entry):
|
||||||
title = eval_xpath_getindex(entry, xpath_title, 0).text
|
|
||||||
|
|
||||||
url = eval_xpath_getindex(entry, xpath_id, 0).text
|
title: str = eval_xpath_getindex(entry, xpath_title, 0).text
|
||||||
abstract = eval_xpath_getindex(entry, xpath_summary, 0).text
|
|
||||||
|
|
||||||
authors = [author.text for author in eval_xpath_list(entry, xpath_author_name)]
|
url: str = eval_xpath_getindex(entry, xpath_id, 0).text
|
||||||
|
abstract: str = eval_xpath_getindex(entry, xpath_summary, 0).text
|
||||||
|
|
||||||
|
authors: list[str] = [author.text for author in eval_xpath_list(entry, xpath_author_name)]
|
||||||
|
|
||||||
# doi
|
# doi
|
||||||
doi_element = eval_xpath_getindex(entry, xpath_doi, 0, default=None)
|
doi_element = eval_xpath_getindex(entry, xpath_doi, 0, default=None)
|
||||||
doi = None if doi_element is None else doi_element.text
|
doi: str = "" if doi_element is None else doi_element.text
|
||||||
|
|
||||||
# pdf
|
# pdf
|
||||||
pdf_element = eval_xpath_getindex(entry, xpath_pdf, 0, default=None)
|
pdf_element = eval_xpath_getindex(entry, xpath_pdf, 0, default=None)
|
||||||
pdf_url = None if pdf_element is None else pdf_element.attrib.get('href')
|
pdf_url: str = "" if pdf_element is None else pdf_element.attrib.get("href")
|
||||||
|
|
||||||
# journal
|
# journal
|
||||||
journal_element = eval_xpath_getindex(entry, xpath_journal, 0, default=None)
|
journal_element = eval_xpath_getindex(entry, xpath_journal, 0, default=None)
|
||||||
journal = None if journal_element is None else journal_element.text
|
journal: str = "" if journal_element is None else journal_element.text
|
||||||
|
|
||||||
# tags
|
# tags
|
||||||
tag_elements = eval_xpath(entry, xpath_category)
|
tag_elements = eval_xpath(entry, xpath_category)
|
||||||
tags = [str(tag) for tag in tag_elements]
|
tags: list[str] = [str(tag) for tag in tag_elements]
|
||||||
|
|
||||||
# comments
|
# comments
|
||||||
comments_elements = eval_xpath_getindex(entry, xpath_comment, 0, default=None)
|
comments_elements = eval_xpath_getindex(entry, xpath_comment, 0, default=None)
|
||||||
comments = None if comments_elements is None else comments_elements.text
|
comments: str = "" if comments_elements is None else comments_elements.text
|
||||||
|
|
||||||
publishedDate = datetime.strptime(eval_xpath_getindex(entry, xpath_published, 0).text, '%Y-%m-%dT%H:%M:%SZ')
|
publishedDate = datetime.strptime(eval_xpath_getindex(entry, xpath_published, 0).text, "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
res_dict = {
|
res.add(
|
||||||
'template': 'paper.html',
|
res.types.Paper(
|
||||||
'url': url,
|
url=url,
|
||||||
'title': title,
|
title=title,
|
||||||
'publishedDate': publishedDate,
|
publishedDate=publishedDate,
|
||||||
'content': abstract,
|
content=abstract,
|
||||||
'doi': doi,
|
doi=doi,
|
||||||
'authors': authors,
|
authors=authors,
|
||||||
'journal': journal,
|
journal=journal,
|
||||||
'tags': tags,
|
tags=tags,
|
||||||
'comments': comments,
|
comments=comments,
|
||||||
'pdf_url': pdf_url,
|
pdf_url=pdf_url,
|
||||||
}
|
)
|
||||||
|
)
|
||||||
|
|
||||||
results.append(res_dict)
|
return res
|
||||||
|
|
||||||
return results
|
|
||||||
|
|||||||
@@ -1,93 +1,163 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
""".. sidebar:: info
|
"""The Astrophysics Data System (ADS_) is a digital library portal for
|
||||||
|
researchers in astronomy and physics, operated by the Smithsonian Astrophysical
|
||||||
|
Observatory (SAO) under a NASA grant. The ADS_ is a solr instance, but not with
|
||||||
|
the standard API paths.
|
||||||
|
|
||||||
The Astrophysics Data System (ADS) is a digital library portal for researchers in astronomy and physics,
|
.. note::
|
||||||
operated by the Smithsonian Astrophysical Observatory (SAO) under a NASA grant.
|
|
||||||
The engine is adapted from the solr engine.
|
|
||||||
|
|
||||||
|
The ADS_ engine requires an :py:obj:`API key <api_key>`.
|
||||||
|
|
||||||
|
This engine uses the `search/query`_ API endpoint. Since the user's search term
|
||||||
|
is passed through, the `search syntax`_ of ADS can be used (at least to some
|
||||||
|
extent).
|
||||||
|
|
||||||
|
.. _ADS: https://ui.adsabs.harvard.edu
|
||||||
|
.. _search/query: https://ui.adsabs.harvard.edu/help/api/api-docs.html#get-/search/query
|
||||||
|
.. _search syntax: https://ui.adsabs.harvard.edu/help/search/search-syntax
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
The engine has the following additional settings:
|
||||||
|
|
||||||
|
- :py:obj:`api_key`
|
||||||
|
- :py:obj:`ads_sort`
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: astrophysics data system
|
||||||
|
api_key: "..."
|
||||||
|
inactive: false
|
||||||
|
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=global-statement
|
import typing as t
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import loads
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from searx.utils import html_to_text
|
||||||
from searx.exceptions import SearxEngineAPIException
|
from searx.exceptions import SearxEngineAPIException
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://ui.adsabs.harvard.edu/',
|
"website": "https://ui.adsabs.harvard.edu/",
|
||||||
"wikidata_id": 'Q752099',
|
"wikidata_id": "Q752099",
|
||||||
"official_api_documentation": 'https://ui.adsabs.harvard.edu/help/api/api-docs.html',
|
"official_api_documentation": "https://ui.adsabs.harvard.edu/help/api/api-docs.html",
|
||||||
"use_official_api": True,
|
"use_official_api": True,
|
||||||
"require_api_key": True,
|
"require_api_key": True,
|
||||||
"results": 'JSON',
|
"results": "JSON",
|
||||||
}
|
}
|
||||||
|
|
||||||
base_url = 'https://api.adsabs.harvard.edu/v1/search'
|
categories = ["science", "scientific publications"]
|
||||||
result_base_url = 'https://ui.adsabs.harvard.edu/abs/'
|
|
||||||
rows = 10
|
|
||||||
sort = '' # sorting: asc or desc
|
|
||||||
field_list = ['bibcode', 'author', 'title', 'abstract', 'doi', 'date'] # list of field names to display on the UI
|
|
||||||
default_fields = '' # default field to query
|
|
||||||
query_fields = '' # query fields
|
|
||||||
paging = True
|
paging = True
|
||||||
api_key = 'unset'
|
base_url = "https://api.adsabs.harvard.edu/v1/search/query"
|
||||||
|
|
||||||
|
api_key = "unset"
|
||||||
|
"""Get an API token as described in https://ui.adsabs.harvard.edu/help/api"""
|
||||||
|
|
||||||
|
ads_field_list = [
|
||||||
|
"abstract",
|
||||||
|
"author",
|
||||||
|
"bibcode",
|
||||||
|
"comment",
|
||||||
|
"date",
|
||||||
|
"doi",
|
||||||
|
"isbn",
|
||||||
|
"issn",
|
||||||
|
"keyword",
|
||||||
|
"page",
|
||||||
|
"page_count",
|
||||||
|
"page_range",
|
||||||
|
"pub",
|
||||||
|
"pubdate",
|
||||||
|
"pubnote",
|
||||||
|
"read_count",
|
||||||
|
"title",
|
||||||
|
"volume",
|
||||||
|
"year",
|
||||||
|
]
|
||||||
|
"""Set of fields to return in the response from ADS."""
|
||||||
|
|
||||||
|
ads_rows = 10
|
||||||
|
"""How many records to return for the ADS request."""
|
||||||
|
|
||||||
|
ads_sort = "read_count desc"
|
||||||
|
"""The format is 'field' + 'direction' where direction is one of 'asc' or 'desc'
|
||||||
|
and field is any of the valid indexes."""
|
||||||
|
|
||||||
|
|
||||||
def init(_):
|
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||||
if api_key == 'unset':
|
"""Initialization of the ADS_ engine, checks whether the :py:obj:`api_key`
|
||||||
raise SearxEngineAPIException('missing ADS API key')
|
is set, otherwise the engine is inactive.
|
||||||
|
"""
|
||||||
|
key: str = engine_settings.get("api_key", "")
|
||||||
|
if key and key not in ("unset", "unknown", "..."):
|
||||||
|
return True
|
||||||
|
logger.error("Astrophysics Data System (ADS) API key is not set or invalid.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
query_params = {'q': query, 'rows': rows}
|
|
||||||
if field_list:
|
|
||||||
query_params['fl'] = ','.join(field_list)
|
|
||||||
if query_fields:
|
|
||||||
query_params['qf'] = ','.join(query_fields)
|
|
||||||
if default_fields:
|
|
||||||
query_params['df'] = default_fields
|
|
||||||
if sort:
|
|
||||||
query_params['sort'] = sort
|
|
||||||
|
|
||||||
query_params['start'] = rows * (params['pageno'] - 1)
|
args: dict[str, str | int] = {
|
||||||
|
"q": query,
|
||||||
|
"fl": ",".join(ads_field_list),
|
||||||
|
"rows": ads_rows,
|
||||||
|
"start": ads_rows * (params["pageno"] - 1),
|
||||||
|
}
|
||||||
|
if ads_sort:
|
||||||
|
args["sort"] = ads_sort
|
||||||
|
|
||||||
params['headers']['Authorization'] = f'Bearer {api_key}'
|
params["headers"]["Authorization"] = f"Bearer {api_key}"
|
||||||
params['url'] = f"{base_url}/query?{urlencode(query_params)}"
|
params["url"] = f"{base_url}?{urlencode(args)}"
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp):
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
try:
|
|
||||||
resp_json = loads(resp.text)
|
|
||||||
except Exception as e:
|
|
||||||
raise SearxEngineAPIException("failed to parse response") from e
|
|
||||||
|
|
||||||
if 'error' in resp_json:
|
res = EngineResults()
|
||||||
raise SearxEngineAPIException(resp_json['error']['msg'])
|
json_data: dict[str, dict[str, t.Any]] = resp.json()
|
||||||
|
|
||||||
resp_json = resp_json["response"]
|
if "error" in json_data:
|
||||||
result_len = resp_json["numFound"]
|
raise SearxEngineAPIException(json_data["error"]["msg"])
|
||||||
results = []
|
|
||||||
|
|
||||||
for res in resp_json["docs"]:
|
def _str(k: str) -> str:
|
||||||
author = res.get("author")
|
return str(doc.get(k, ""))
|
||||||
|
|
||||||
if author:
|
def _list(k: str) -> list[str]:
|
||||||
author = author[0] + ' et al.'
|
return doc.get(k, [])
|
||||||
|
|
||||||
results.append(
|
for doc in json_data["response"]["docs"]:
|
||||||
{
|
authors: list[str] = doc["author"]
|
||||||
'url': result_base_url + res.get("bibcode") + "/",
|
if len(authors) > 15:
|
||||||
'title': res.get("title")[0],
|
# There are articles with hundreds of authors
|
||||||
'author': author,
|
authors = authors[:15] + ["et al."]
|
||||||
'content': res.get("abstract"),
|
|
||||||
'doi': res.get("doi"),
|
paper = res.types.Paper(
|
||||||
'publishedDate': datetime.fromisoformat(res.get("date")),
|
url=f"https://ui.adsabs.harvard.edu/abs/{doc.get('bibcode')}/",
|
||||||
}
|
title=html_to_text(_list("title")[0]),
|
||||||
|
authors=authors,
|
||||||
|
content=html_to_text(_str("abstract")),
|
||||||
|
doi=_list("doi")[0],
|
||||||
|
issn=_list("issn"),
|
||||||
|
isbn=_list("isbn"),
|
||||||
|
tags=_list("keyword"),
|
||||||
|
pages=",".join(_list("page")),
|
||||||
|
publisher=_str("pub") + " " + _str("year"),
|
||||||
|
publishedDate=datetime.fromisoformat(_str("date")),
|
||||||
|
volume=_str("volume"),
|
||||||
|
views=_str("read_count"),
|
||||||
|
comments=" / ".join(_list("pubnote")),
|
||||||
)
|
)
|
||||||
|
res.add(paper)
|
||||||
|
|
||||||
results.append({'number_of_results': result_len})
|
return res
|
||||||
|
|
||||||
return results
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ research from repositories and journals.
|
|||||||
|
|
||||||
.. _CORE: https://core.ac.uk/about
|
.. _CORE: https://core.ac.uk/about
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The CORE engine requires an :py:obj:`API key <api_key>`.
|
||||||
|
|
||||||
.. _core engine config:
|
.. _core engine config:
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
@@ -17,135 +21,141 @@ The engine has the following additional settings:
|
|||||||
.. code:: yaml
|
.. code:: yaml
|
||||||
|
|
||||||
- name: core.ac.uk
|
- name: core.ac.uk
|
||||||
engine: core
|
|
||||||
categories: science
|
|
||||||
shortcut: cor
|
|
||||||
api_key: "..."
|
api_key: "..."
|
||||||
timeout: 5
|
inactive: false
|
||||||
|
|
||||||
Implementations
|
Implementations
|
||||||
===============
|
===============
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://core.ac.uk',
|
"website": "https://core.ac.uk",
|
||||||
"wikidata_id": 'Q22661180',
|
"wikidata_id": "Q22661180",
|
||||||
"official_api_documentation": 'https://api.core.ac.uk/docs/v3',
|
"official_api_documentation": "https://api.core.ac.uk/docs/v3",
|
||||||
"use_official_api": True,
|
"use_official_api": True,
|
||||||
"require_api_key": True,
|
"require_api_key": True,
|
||||||
"results": 'JSON',
|
"results": "JSON",
|
||||||
}
|
}
|
||||||
|
|
||||||
api_key = 'unset'
|
api_key = ""
|
||||||
"""For an API key register at https://core.ac.uk/services/api and insert
|
"""For an API key register at https://core.ac.uk/services/api and insert
|
||||||
the API key in the engine :ref:`core engine config`."""
|
the API key in the engine :ref:`core engine config`."""
|
||||||
|
|
||||||
categories = ['science', 'scientific publications']
|
categories = ["science", "scientific publications"]
|
||||||
paging = True
|
paging = True
|
||||||
nb_per_page = 10
|
nb_per_page = 10
|
||||||
base_url = 'https://api.core.ac.uk/v3/search/works/'
|
base_url = "https://api.core.ac.uk/v3/search/works/"
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||||
if api_key == 'unset':
|
"""Initialization of the CORE_ engine, checks whether the :py:obj:`api_key`
|
||||||
raise SearxEngineAPIException('missing CORE API key')
|
is set, otherwise the engine is inactive.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key: str = engine_settings.get("api_key", "")
|
||||||
|
if key and key not in ("unset", "unknown", "..."):
|
||||||
|
return True
|
||||||
|
logger.error("CORE's API key is not set or invalid.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
|
|
||||||
# API v3 uses different parameters
|
# API v3 uses different parameters
|
||||||
search_params = {
|
search_params = {
|
||||||
'q': query,
|
"q": query,
|
||||||
'offset': (params['pageno'] - 1) * nb_per_page,
|
"offset": (params["pageno"] - 1) * nb_per_page,
|
||||||
'limit': nb_per_page,
|
"limit": nb_per_page,
|
||||||
'sort': 'relevance',
|
"sort": "relevance",
|
||||||
}
|
}
|
||||||
|
|
||||||
params['url'] = base_url + '?' + urlencode(search_params)
|
params["url"] = base_url + "?" + urlencode(search_params)
|
||||||
params['headers'] = {'Authorization': f'Bearer {api_key}'}
|
params["headers"] = {"Authorization": f"Bearer {api_key}"}
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp):
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
results = []
|
# pylint: disable=too-many-branches
|
||||||
|
res = EngineResults()
|
||||||
json_data = resp.json()
|
json_data = resp.json()
|
||||||
|
|
||||||
for result in json_data.get('results', []):
|
for result in json_data.get("results", []):
|
||||||
# Get title
|
# Get title
|
||||||
if not result.get('title'):
|
if not result.get("title"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get URL - try different options
|
# Get URL - try different options
|
||||||
url = None
|
url: str | None = None
|
||||||
|
|
||||||
# Try DOI first
|
# Try DOI first
|
||||||
doi = result.get('doi')
|
doi: str = result.get("doi")
|
||||||
if doi:
|
if doi:
|
||||||
url = f'https://doi.org/{doi}'
|
url = f"https://doi.org/{doi}"
|
||||||
|
|
||||||
if url is None and result.get('doi'):
|
if url is None and result.get("doi"):
|
||||||
# use the DOI reference
|
# use the DOI reference
|
||||||
url = 'https://doi.org/' + str(result['doi'])
|
url = "https://doi.org/" + str(result["doi"])
|
||||||
elif result.get('id'):
|
elif result.get("id"):
|
||||||
url = 'https://core.ac.uk/works/' + str(result['id'])
|
url = "https://core.ac.uk/works/" + str(result["id"])
|
||||||
elif result.get('downloadUrl'):
|
elif result.get("downloadUrl"):
|
||||||
url = result['downloadUrl']
|
url = result["downloadUrl"]
|
||||||
elif result.get('sourceFulltextUrls'):
|
elif result.get("sourceFulltextUrls"):
|
||||||
url = result['sourceFulltextUrls']
|
url = result["sourceFulltextUrls"]
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Published date
|
# Published date
|
||||||
published_date = None
|
published_date = None
|
||||||
|
|
||||||
raw_date = result.get('publishedDate') or result.get('depositedDate')
|
raw_date = result.get("publishedDate") or result.get("depositedDate")
|
||||||
if raw_date:
|
if raw_date:
|
||||||
try:
|
try:
|
||||||
published_date = datetime.fromisoformat(result['publishedDate'].replace('Z', '+00:00'))
|
published_date = datetime.fromisoformat(result["publishedDate"].replace("Z", "+00:00"))
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Handle journals
|
# Handle journals
|
||||||
journals = []
|
journals = []
|
||||||
if result.get('journals'):
|
if result.get("journals"):
|
||||||
journals = [j.get('title') for j in result['journals'] if j.get('title')]
|
journals = [j.get("title") for j in result["journals"] if j.get("title")]
|
||||||
|
|
||||||
# Handle publisher
|
# Handle publisher
|
||||||
publisher = result.get('publisher', '').strip("'")
|
publisher = result.get("publisher", "").strip("'")
|
||||||
if publisher:
|
|
||||||
publisher = publisher.strip("'")
|
|
||||||
|
|
||||||
# Handle authors
|
# Handle authors
|
||||||
authors = set()
|
authors: set[str] = set()
|
||||||
for i in result.get('authors', []):
|
for i in result.get("authors", []):
|
||||||
name = i.get("name")
|
name: str | None = i.get("name")
|
||||||
if name:
|
if name:
|
||||||
authors.add(name)
|
authors.add(name)
|
||||||
|
|
||||||
results.append(
|
res.add(
|
||||||
{
|
res.types.Paper(
|
||||||
'template': 'paper.html',
|
title=result.get("title"),
|
||||||
'title': result.get('title'),
|
url=url,
|
||||||
'url': url,
|
content=result.get("fullText", "") or "",
|
||||||
'content': result.get('fullText', '') or '',
|
tags=result.get("fieldOfStudy", []),
|
||||||
# 'comments': '',
|
publishedDate=published_date,
|
||||||
'tags': result.get('fieldOfStudy', []),
|
type=result.get("documentType", "") or "",
|
||||||
'publishedDate': published_date,
|
authors=authors,
|
||||||
'type': result.get('documentType', '') or '',
|
editor=", ".join(result.get("contributors", [])),
|
||||||
'authors': authors,
|
publisher=publisher,
|
||||||
'editor': ', '.join(result.get('contributors', [])),
|
journal=", ".join(journals),
|
||||||
'publisher': publisher,
|
doi=result.get("doi"),
|
||||||
'journal': ', '.join(journals),
|
pdf_url=result.get("downloadUrl", {}) or result.get("sourceFulltextUrls", {}),
|
||||||
'doi': result.get('doi'),
|
)
|
||||||
# 'issn' : ''
|
|
||||||
# 'isbn' : ''
|
|
||||||
'pdf_url': result.get('downloadUrl', {}) or result.get("sourceFulltextUrls", {}),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return res
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""CrossRef"""
|
"""Crossref_ is the sustainable source of community-owned scholarly metadata and
|
||||||
|
is relied upon by thousands of systems across the research ecosystem and the
|
||||||
|
globe.
|
||||||
|
|
||||||
|
.. _Crossref: https://www.crossref.org/documentation/retrieve-metadata/
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": "https://www.crossref.org/",
|
"website": "https://www.crossref.org/",
|
||||||
"wikidata_id": "Q5188229",
|
"wikidata_id": "Q5188229",
|
||||||
"official_api_documentation": "https://api.crossref.org",
|
"official_api_documentation": "https://api.crossref.org/swagger-ui/",
|
||||||
"use_official_api": False,
|
"use_official_api": True,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": "JSON",
|
"results": "JSON",
|
||||||
}
|
}
|
||||||
@@ -16,48 +29,69 @@ about = {
|
|||||||
categories = ["science", "scientific publications"]
|
categories = ["science", "scientific publications"]
|
||||||
paging = True
|
paging = True
|
||||||
search_url = "https://api.crossref.org/works"
|
search_url = "https://api.crossref.org/works"
|
||||||
|
"""Returns a list of all works (journal articles, conference proceedings, books,
|
||||||
|
components, etc), 20 per page (`Works/get_works`_).
|
||||||
|
|
||||||
|
.. _Works/get_works: https://api.crossref.org/swagger-ui/index.html#/Works/get_works
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
params["url"] = search_url + "?" + urlencode({"query": query, "offset": 20 * (params["pageno"] - 1)})
|
args = {
|
||||||
return params
|
"query": query,
|
||||||
|
"offset": 20 * (params["pageno"] - 1),
|
||||||
|
}
|
||||||
|
params["url"] = f"{search_url}?{urlencode(args)}"
|
||||||
|
|
||||||
|
|
||||||
def response(resp):
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
results = []
|
res = EngineResults()
|
||||||
for record in resp.json()["message"]["items"]:
|
json_data = resp.json()
|
||||||
|
|
||||||
|
def field(k: str) -> str:
|
||||||
|
return str(record.get(k, ""))
|
||||||
|
|
||||||
|
for record in json_data["message"]["items"]:
|
||||||
|
|
||||||
if record["type"] == "component":
|
if record["type"] == "component":
|
||||||
# These seem to be files published along with papers. Not something you'd search for
|
# These seem to be files published along with papers. Not something
|
||||||
|
# you'd search for.
|
||||||
continue
|
continue
|
||||||
result = {
|
title: str = ""
|
||||||
"template": "paper.html",
|
journal: str = ""
|
||||||
"content": record.get("abstract", ""),
|
|
||||||
"doi": record.get("DOI"),
|
|
||||||
"pages": record.get("page"),
|
|
||||||
"publisher": record.get("publisher"),
|
|
||||||
"tags": record.get("subject"),
|
|
||||||
"type": record.get("type"),
|
|
||||||
"url": record.get("URL"),
|
|
||||||
"volume": record.get("volume"),
|
|
||||||
}
|
|
||||||
if record["type"] == "book-chapter":
|
if record["type"] == "book-chapter":
|
||||||
result["title"] = record["container-title"][0]
|
title = record["container-title"][0]
|
||||||
if record["title"][0].lower().strip() != result["title"].lower().strip():
|
if record["title"][0].lower().strip() != title.lower().strip():
|
||||||
result["title"] += f" ({record['title'][0]})"
|
title += f" ({record['title'][0]})"
|
||||||
else:
|
else:
|
||||||
result["title"] = record["title"][0] if "title" in record else record.get("container-title", [None])[0]
|
title = record["title"][0] if "title" in record else record.get("container-title", [None])[0]
|
||||||
result["journal"] = record.get("container-title", [None])[0] if "title" in record else None
|
journal = record.get("container-title", [None])[0] if "title" in record else ""
|
||||||
|
|
||||||
|
item = res.types.Paper(
|
||||||
|
title=title,
|
||||||
|
journal=journal,
|
||||||
|
content=field("abstract"),
|
||||||
|
doi=field("DOI"),
|
||||||
|
pages=field("page"),
|
||||||
|
publisher=field("publisher"),
|
||||||
|
tags=record.get("subject"),
|
||||||
|
type=field("type"),
|
||||||
|
url=field("URL"),
|
||||||
|
volume=field("volume"),
|
||||||
|
)
|
||||||
|
res.add(item)
|
||||||
|
|
||||||
if "resource" in record and "primary" in record["resource"] and "URL" in record["resource"]["primary"]:
|
if "resource" in record and "primary" in record["resource"] and "URL" in record["resource"]["primary"]:
|
||||||
result["url"] = record["resource"]["primary"]["URL"]
|
item.url = record["resource"]["primary"]["URL"]
|
||||||
|
|
||||||
if "published" in record and "date-parts" in record["published"]:
|
if "published" in record and "date-parts" in record["published"]:
|
||||||
result["publishedDate"] = datetime(*(record["published"]["date-parts"][0] + [1, 1][:3]))
|
item.publishedDate = datetime(*(record["published"]["date-parts"][0] + [1, 1][:3]))
|
||||||
result["authors"] = [a.get("given", "") + " " + a.get("family", "") for a in record.get("author", [])]
|
|
||||||
result["isbn"] = record.get("isbn") or [i["value"] for i in record.get("isbn-type", [])]
|
item.authors = [a.get("given", "") + " " + a.get("family", "") for a in record.get("author", [])]
|
||||||
|
item.isbn = record.get("isbn") or [i["value"] for i in record.get("isbn-type", [])]
|
||||||
|
|
||||||
# All the links are not PDFs, even if the URL ends with ".pdf"
|
# All the links are not PDFs, even if the URL ends with ".pdf"
|
||||||
# result["pdf_url"] = record.get("link", [{"URL": None}])[0]["URL"]
|
# item.pdf_url = record.get("link", [{"URL": None}])[0]["URL"]
|
||||||
|
|
||||||
results.append(result)
|
return res
|
||||||
|
|
||||||
return results
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ about = {
|
|||||||
_my_online_engine = None
|
_my_online_engine = None
|
||||||
|
|
||||||
|
|
||||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
def setup(engine_settings: "OnlineParams") -> bool:
|
||||||
"""Dynamic setup of the engine settings.
|
"""Dynamic setup of the engine settings.
|
||||||
|
|
||||||
For more details see :py:obj:`searx.enginelib.Engine.setup`."""
|
For more details see :py:obj:`searx.enginelib.Engine.setup`."""
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ engines:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
@@ -28,8 +30,10 @@ from searx.exceptions import SearxEngineCaptchaException
|
|||||||
from searx.enginelib.traits import EngineTraits
|
from searx.enginelib.traits import EngineTraits
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
# about
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://www.google.com',
|
"website": 'https://www.google.com',
|
||||||
"wikidata_id": 'Q9366',
|
"wikidata_id": 'Q9366',
|
||||||
@@ -89,7 +93,7 @@ def ui_async(start: int) -> str:
|
|||||||
return ",".join([arc_id, use_ac, _fmt])
|
return ",".join([arc_id, use_ac, _fmt])
|
||||||
|
|
||||||
|
|
||||||
def get_google_info(params, eng_traits):
|
def get_google_info(params: "OnlineParams", eng_traits: EngineTraits) -> dict[str, t.Any]:
|
||||||
"""Composing various (language) properties for the google engines (:ref:`google
|
"""Composing various (language) properties for the google engines (:ref:`google
|
||||||
API`).
|
API`).
|
||||||
|
|
||||||
@@ -144,7 +148,7 @@ def get_google_info(params, eng_traits):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ret_val = {
|
ret_val: dict[str, t.Any] = {
|
||||||
'language': None,
|
'language': None,
|
||||||
'country': None,
|
'country': None,
|
||||||
'subdomain': None,
|
'subdomain': None,
|
||||||
@@ -273,7 +277,7 @@ def detect_google_sorry(resp):
|
|||||||
raise SearxEngineCaptchaException()
|
raise SearxEngineCaptchaException()
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
"""Google search request"""
|
"""Google search request"""
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
start = (params['pageno'] - 1) * 10
|
start = (params['pageno'] - 1) * 10
|
||||||
@@ -317,7 +321,6 @@ def request(query, params):
|
|||||||
|
|
||||||
params['cookies'] = google_info['cookies']
|
params['cookies'] = google_info['cookies']
|
||||||
params['headers'].update(google_info['headers'])
|
params['headers'].update(google_info['headers'])
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
# =26;[3,"dimg_ZNMiZPCqE4apxc8P3a2tuAQ_137"]a87;data:image/jpeg;base64,/9j/4AAQSkZJRgABA
|
# =26;[3,"dimg_ZNMiZPCqE4apxc8P3a2tuAQ_137"]a87;data:image/jpeg;base64,/9j/4AAQSkZJRgABA
|
||||||
@@ -341,7 +344,7 @@ def parse_data_images(text: str):
|
|||||||
return data_image_map
|
return data_image_map
|
||||||
|
|
||||||
|
|
||||||
def response(resp) -> EngineResults:
|
def response(resp: "SXNG_Response"):
|
||||||
"""Get response from google's search request"""
|
"""Get response from google's search request"""
|
||||||
# pylint: disable=too-many-branches, too-many-statements
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
detect_google_sorry(resp)
|
detect_google_sorry(resp)
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""This is the implementation of the Google Scholar engine.
|
"""Google Scholar is a freely accessible web search engine that indexes the full
|
||||||
|
text or metadata of scholarly literature across an array of publishing formats
|
||||||
|
and disciplines.
|
||||||
|
|
||||||
Compared to other Google services the Scholar engine has a simple GET REST-API
|
Compared to other Google services the Scholar engine has a simple GET REST-API
|
||||||
and there does not exists `async` API. Even though the API slightly vintage we
|
and there does not exists ``async`` API. Even though the API slightly vintage
|
||||||
can make use of the :ref:`google API` to assemble the arguments of the GET
|
we can make use of the :ref:`google API` to assemble the arguments of the GET
|
||||||
request.
|
request.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: google scholar
|
||||||
|
engine: google_scholar
|
||||||
|
shortcut: gos
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from lxml import html
|
from lxml import html
|
||||||
@@ -16,6 +33,7 @@ from searx.utils import (
|
|||||||
eval_xpath_getindex,
|
eval_xpath_getindex,
|
||||||
eval_xpath_list,
|
eval_xpath_list,
|
||||||
extract_text,
|
extract_text,
|
||||||
|
ElementType,
|
||||||
)
|
)
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineCaptchaException
|
from searx.exceptions import SearxEngineCaptchaException
|
||||||
@@ -26,18 +44,23 @@ from searx.engines.google import (
|
|||||||
time_range_dict,
|
time_range_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
# about
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://scholar.google.com',
|
"website": "https://scholar.google.com",
|
||||||
"wikidata_id": 'Q494817',
|
"wikidata_id": "Q494817",
|
||||||
"official_api_documentation": 'https://developers.google.com/custom-search',
|
"official_api_documentation": "https://developers.google.com/custom-search",
|
||||||
"use_official_api": False,
|
"use_official_api": False,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": 'HTML',
|
"results": "HTML",
|
||||||
}
|
}
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ['science', 'scientific publications']
|
categories = ["science", "scientific publications"]
|
||||||
paging = True
|
paging = True
|
||||||
max_page = 50
|
max_page = 50
|
||||||
"""`Google max 50 pages`_
|
"""`Google max 50 pages`_
|
||||||
@@ -50,9 +73,97 @@ safesearch = False
|
|||||||
send_accept_language_header = True
|
send_accept_language_header = True
|
||||||
|
|
||||||
|
|
||||||
def time_range_args(params):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
|
"""Google-Scholar search request"""
|
||||||
|
|
||||||
|
google_info = get_google_info(params, traits)
|
||||||
|
# subdomain is: scholar.google.xy
|
||||||
|
google_info["subdomain"] = google_info["subdomain"].replace("www.", "scholar.")
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"q": query,
|
||||||
|
**google_info["params"],
|
||||||
|
"start": (params["pageno"] - 1) * 10,
|
||||||
|
"as_sdt": "2007", # include patents / to disable set "0,5"
|
||||||
|
"as_vis": "0", # include citations / to disable set "1"
|
||||||
|
}
|
||||||
|
args.update(time_range_args(params))
|
||||||
|
|
||||||
|
params["url"] = "https://" + google_info["subdomain"] + "/scholar?" + urlencode(args)
|
||||||
|
params["cookies"] = google_info["cookies"]
|
||||||
|
params["headers"].update(google_info["headers"])
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp: "SXNG_Response") -> EngineResults: # pylint: disable=too-many-locals
|
||||||
|
"""Parse response from Google Scholar"""
|
||||||
|
|
||||||
|
res = EngineResults()
|
||||||
|
dom = html.fromstring(resp.text)
|
||||||
|
detect_google_captcha(dom)
|
||||||
|
|
||||||
|
# parse results
|
||||||
|
for result in eval_xpath_list(dom, "//div[@data-rp]"):
|
||||||
|
|
||||||
|
title = extract_text(eval_xpath(result, ".//h3[1]//a"))
|
||||||
|
if not title:
|
||||||
|
# this is a [ZITATION] block
|
||||||
|
continue
|
||||||
|
|
||||||
|
pub_type: str = extract_text(eval_xpath(result, ".//span[@class='gs_ctg2']")) or ""
|
||||||
|
if pub_type:
|
||||||
|
pub_type = pub_type[1:-1].lower()
|
||||||
|
|
||||||
|
url: str = eval_xpath_getindex(result, ".//h3[1]//a/@href", 0)
|
||||||
|
content: str = extract_text(eval_xpath(result, ".//div[@class='gs_rs']")) or ""
|
||||||
|
authors, journal, publisher, publishedDate = parse_gs_a(
|
||||||
|
extract_text(eval_xpath(result, ".//div[@class='gs_a']"))
|
||||||
|
)
|
||||||
|
if publisher in url:
|
||||||
|
publisher = ""
|
||||||
|
|
||||||
|
# cited by
|
||||||
|
comments: str = (
|
||||||
|
extract_text(eval_xpath(result, ".//div[@class='gs_fl']/a[starts-with(@href,'/scholar?cites=')]")) or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# link to the html or pdf document
|
||||||
|
html_url: str = ""
|
||||||
|
pdf_url: str = ""
|
||||||
|
doc_url = eval_xpath_getindex(result, ".//div[@class='gs_or_ggsm']/a/@href", 0, default=None)
|
||||||
|
doc_type = extract_text(eval_xpath(result, ".//span[@class='gs_ctg2']"))
|
||||||
|
if doc_type == "[PDF]":
|
||||||
|
pdf_url = doc_url
|
||||||
|
else:
|
||||||
|
html_url = doc_url
|
||||||
|
|
||||||
|
res.add(
|
||||||
|
res.types.Paper(
|
||||||
|
type=pub_type,
|
||||||
|
url=url,
|
||||||
|
title=title,
|
||||||
|
authors=authors,
|
||||||
|
publisher=publisher,
|
||||||
|
journal=journal,
|
||||||
|
publishedDate=publishedDate,
|
||||||
|
content=content,
|
||||||
|
comments=comments,
|
||||||
|
html_url=html_url,
|
||||||
|
pdf_url=pdf_url,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# parse suggestion
|
||||||
|
for suggestion in eval_xpath(dom, "//div[contains(@class, 'gs_qsuggest_wrap')]//li//a"):
|
||||||
|
res.add(res.types.LegacyResult(suggestion=extract_text(suggestion)))
|
||||||
|
|
||||||
|
for correction in eval_xpath(dom, "//div[@class='gs_r gs_pda']/a"):
|
||||||
|
res.add(res.types.LegacyResult(correction=extract_text(correction)))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def time_range_args(params: "OnlineParams") -> dict[str, int]:
|
||||||
"""Returns a dictionary with a time range arguments based on
|
"""Returns a dictionary with a time range arguments based on
|
||||||
``params['time_range']``.
|
``params["time_range"]``.
|
||||||
|
|
||||||
Google Scholar supports a detailed search by year. Searching by *last
|
Google Scholar supports a detailed search by year. Searching by *last
|
||||||
month* or *last week* (as offered by SearXNG) is uncommon for scientific
|
month* or *last week* (as offered by SearXNG) is uncommon for scientific
|
||||||
@@ -60,21 +171,23 @@ def time_range_args(params):
|
|||||||
|
|
||||||
To limit the result list when the users selects a range, all the SearXNG
|
To limit the result list when the users selects a range, all the SearXNG
|
||||||
ranges (*day*, *week*, *month*, *year*) are mapped to *year*. If no range
|
ranges (*day*, *week*, *month*, *year*) are mapped to *year*. If no range
|
||||||
is set an empty dictionary of arguments is returned. Example; when
|
is set an empty dictionary of arguments is returned.
|
||||||
user selects a time range (current year minus one in 2022):
|
|
||||||
|
Example; when user selects a time range and we find ourselves in the year
|
||||||
|
2025 (current year minus one):
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
{ 'as_ylo' : 2021 }
|
{ "as_ylo" : 2024 }
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ret_val = {}
|
ret_val: dict[str, int] = {}
|
||||||
if params['time_range'] in time_range_dict:
|
if params["time_range"] in time_range_dict:
|
||||||
ret_val['as_ylo'] = datetime.now().year - 1
|
ret_val["as_ylo"] = datetime.now().year - 1
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
|
|
||||||
def detect_google_captcha(dom):
|
def detect_google_captcha(dom: ElementType):
|
||||||
"""In case of CAPTCHA Google Scholar open its own *not a Robot* dialog and is
|
"""In case of CAPTCHA Google Scholar open its own *not a Robot* dialog and is
|
||||||
not redirected to ``sorry.google.com``.
|
not redirected to ``sorry.google.com``.
|
||||||
"""
|
"""
|
||||||
@@ -82,29 +195,7 @@ def detect_google_captcha(dom):
|
|||||||
raise SearxEngineCaptchaException()
|
raise SearxEngineCaptchaException()
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def parse_gs_a(text: str | None) -> tuple[list[str], str, str, datetime | None]:
|
||||||
"""Google-Scholar search request"""
|
|
||||||
|
|
||||||
google_info = get_google_info(params, traits)
|
|
||||||
# subdomain is: scholar.google.xy
|
|
||||||
google_info['subdomain'] = google_info['subdomain'].replace("www.", "scholar.")
|
|
||||||
|
|
||||||
args = {
|
|
||||||
'q': query,
|
|
||||||
**google_info['params'],
|
|
||||||
'start': (params['pageno'] - 1) * 10,
|
|
||||||
'as_sdt': '2007', # include patents / to disable set '0,5'
|
|
||||||
'as_vis': '0', # include citations / to disable set '1'
|
|
||||||
}
|
|
||||||
args.update(time_range_args(params))
|
|
||||||
|
|
||||||
params['url'] = 'https://' + google_info['subdomain'] + '/scholar?' + urlencode(args)
|
|
||||||
params['cookies'] = google_info['cookies']
|
|
||||||
params['headers'].update(google_info['headers'])
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def parse_gs_a(text: str | None):
|
|
||||||
"""Parse the text written in green.
|
"""Parse the text written in green.
|
||||||
|
|
||||||
Possible formats:
|
Possible formats:
|
||||||
@@ -113,98 +204,28 @@ def parse_gs_a(text: str | None):
|
|||||||
* "{authors} - {publisher}"
|
* "{authors} - {publisher}"
|
||||||
"""
|
"""
|
||||||
if text is None or text == "":
|
if text is None or text == "":
|
||||||
return None, None, None, None
|
return [], "", "", None
|
||||||
|
|
||||||
s_text = text.split(' - ')
|
s_text = text.split(" - ")
|
||||||
authors = s_text[0].split(', ')
|
authors: list[str] = s_text[0].split(", ")
|
||||||
publisher = s_text[-1]
|
publisher: str = s_text[-1]
|
||||||
if len(s_text) != 3:
|
if len(s_text) != 3:
|
||||||
return authors, None, publisher, None
|
return authors, "", publisher, None
|
||||||
|
|
||||||
# the format is "{authors} - {journal}, {year} - {publisher}" or "{authors} - {year} - {publisher}"
|
# the format is "{authors} - {journal}, {year} - {publisher}" or "{authors} - {year} - {publisher}"
|
||||||
# get journal and year
|
# get journal and year
|
||||||
journal_year = s_text[1].split(', ')
|
journal_year = s_text[1].split(", ")
|
||||||
# journal is optional and may contains some coma
|
# journal is optional and may contains some coma
|
||||||
if len(journal_year) > 1:
|
if len(journal_year) > 1:
|
||||||
journal = ', '.join(journal_year[0:-1])
|
journal: str = ", ".join(journal_year[0:-1])
|
||||||
if journal == '…':
|
if journal == "…":
|
||||||
journal = None
|
journal = ""
|
||||||
else:
|
else:
|
||||||
journal = None
|
journal = ""
|
||||||
# year
|
# year
|
||||||
year = journal_year[-1]
|
year = journal_year[-1]
|
||||||
try:
|
try:
|
||||||
publishedDate = datetime.strptime(year.strip(), '%Y')
|
publishedDate = datetime.strptime(year.strip(), "%Y")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
publishedDate = None
|
publishedDate = None
|
||||||
return authors, journal, publisher, publishedDate
|
return authors, journal, publisher, publishedDate
|
||||||
|
|
||||||
|
|
||||||
def response(resp): # pylint: disable=too-many-locals
|
|
||||||
"""Parse response from Google Scholar"""
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# convert the text to dom
|
|
||||||
dom = html.fromstring(resp.text)
|
|
||||||
detect_google_captcha(dom)
|
|
||||||
|
|
||||||
# parse results
|
|
||||||
for result in eval_xpath_list(dom, '//div[@data-rp]'):
|
|
||||||
|
|
||||||
title = extract_text(eval_xpath(result, './/h3[1]//a'))
|
|
||||||
|
|
||||||
if not title:
|
|
||||||
# this is a [ZITATION] block
|
|
||||||
continue
|
|
||||||
|
|
||||||
pub_type = extract_text(eval_xpath(result, './/span[@class="gs_ctg2"]'))
|
|
||||||
if pub_type:
|
|
||||||
pub_type = pub_type[1:-1].lower()
|
|
||||||
|
|
||||||
url = eval_xpath_getindex(result, './/h3[1]//a/@href', 0)
|
|
||||||
content = extract_text(eval_xpath(result, './/div[@class="gs_rs"]'))
|
|
||||||
authors, journal, publisher, publishedDate = parse_gs_a(
|
|
||||||
extract_text(eval_xpath(result, './/div[@class="gs_a"]'))
|
|
||||||
)
|
|
||||||
if publisher in url:
|
|
||||||
publisher = None
|
|
||||||
|
|
||||||
# cited by
|
|
||||||
comments = extract_text(eval_xpath(result, './/div[@class="gs_fl"]/a[starts-with(@href,"/scholar?cites=")]'))
|
|
||||||
|
|
||||||
# link to the html or pdf document
|
|
||||||
html_url = None
|
|
||||||
pdf_url = None
|
|
||||||
doc_url = eval_xpath_getindex(result, './/div[@class="gs_or_ggsm"]/a/@href', 0, default=None)
|
|
||||||
doc_type = extract_text(eval_xpath(result, './/span[@class="gs_ctg2"]'))
|
|
||||||
if doc_type == "[PDF]":
|
|
||||||
pdf_url = doc_url
|
|
||||||
else:
|
|
||||||
html_url = doc_url
|
|
||||||
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
'template': 'paper.html',
|
|
||||||
'type': pub_type,
|
|
||||||
'url': url,
|
|
||||||
'title': title,
|
|
||||||
'authors': authors,
|
|
||||||
'publisher': publisher,
|
|
||||||
'journal': journal,
|
|
||||||
'publishedDate': publishedDate,
|
|
||||||
'content': content,
|
|
||||||
'comments': comments,
|
|
||||||
'html_url': html_url,
|
|
||||||
'pdf_url': pdf_url,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# parse suggestion
|
|
||||||
for suggestion in eval_xpath(dom, '//div[contains(@class, "gs_qsuggest_wrap")]//li//a'):
|
|
||||||
# append suggestion
|
|
||||||
results.append({'suggestion': extract_text(suggestion)})
|
|
||||||
|
|
||||||
for correction in eval_xpath(dom, '//div[@class="gs_r gs_pda"]/a'):
|
|
||||||
results.append({'correction': extract_text(correction)})
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|||||||
@@ -1,14 +1,103 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
# pylint: disable=missing-module-docstring
|
"""The OpenAlex engine integrates the `OpenAlex`_ Works API to return scientific
|
||||||
#
|
paper results using the :ref:`result_types.paper` class. It is an "online" JSON
|
||||||
# Engine is documented in: docs/dev/engines/online/openalex.rst
|
engine that uses the official public API and does not require an API key.
|
||||||
|
|
||||||
|
.. _OpenAlex: https://openalex.org
|
||||||
|
.. _OpenAlex API overview: https://docs.openalex.org/how-to-use-the-api/api-overview
|
||||||
|
|
||||||
|
Key features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Uses the official Works endpoint (JSON)
|
||||||
|
- Paging support via ``page`` and ``per-page``
|
||||||
|
- Relevance sorting (``sort=relevance_score:desc``)
|
||||||
|
- Language filter support (maps SearXNG language to ``filter=language:<iso2>``)
|
||||||
|
- Maps fields commonly used in scholarly results: title, authors, abstract
|
||||||
|
(reconstructed from inverted index), journal/venue, publisher, DOI, tags
|
||||||
|
(concepts), PDF/HTML links, pages, volume, issue, published date, and a short
|
||||||
|
citations comment
|
||||||
|
- Supports OpenAlex "polite pool" by adding a ``mailto`` parameter
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Minimal example for :origin:`settings.yml <searx/settings.yml>`:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: openalex
|
||||||
|
engine: openalex
|
||||||
|
shortcut: oa
|
||||||
|
categories: science, scientific publications
|
||||||
|
timeout: 5.0
|
||||||
|
# Recommended by OpenAlex: join the polite pool with an email address
|
||||||
|
mailto: "[email protected]"
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
- The ``mailto`` key is optional but recommended by OpenAlex for better service.
|
||||||
|
- Language is inherited from the user's UI language; when it is not ``all``, the
|
||||||
|
engine adds ``filter=language:<iso2>`` (e.g. ``language:fr``). If OpenAlex has
|
||||||
|
few results for that language, you may see fewer items.
|
||||||
|
- Results typically include a main link. When the primary landing page from
|
||||||
|
OpenAlex is a DOI resolver, the engine will use that stable link. When an open
|
||||||
|
access link is available, it is exposed via the ``PDF`` and/or ``HTML`` links
|
||||||
|
in the result footer.
|
||||||
|
|
||||||
|
|
||||||
|
What is returned
|
||||||
|
================
|
||||||
|
|
||||||
|
Each result uses the :ref:`result_types.paper` class and may include:
|
||||||
|
|
||||||
|
- ``title`` and ``content`` (abstract; reconstructed from the inverted index)
|
||||||
|
- ``authors`` (display names)
|
||||||
|
- ``journal`` (host venue display name) and ``publisher``
|
||||||
|
- ``doi`` (normalized to the plain DOI, without the ``https://doi.org/`` prefix)
|
||||||
|
- ``tags`` (OpenAlex concepts display names)
|
||||||
|
- ``pdf_url`` (Open access PDF if available) and ``html_url`` (landing page)
|
||||||
|
- ``publishedDate`` (parsed from ``publication_date``)
|
||||||
|
- ``pages``, ``volume``, ``number`` (issue)
|
||||||
|
- ``type`` and a brief ``comments`` string with citation count
|
||||||
|
|
||||||
|
|
||||||
|
Rate limits & polite pool
|
||||||
|
=========================
|
||||||
|
|
||||||
|
OpenAlex offers a free public API with generous daily limits. For extra courtesy
|
||||||
|
and improved service quality, include a contact email in each request via
|
||||||
|
``mailto``. You can set it directly in the engine configuration as shown above.
|
||||||
|
See: `OpenAlex API overview`_.
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
===============
|
||||||
|
|
||||||
|
- Few or no results in a non-English UI language:
|
||||||
|
Ensure the selected language has sufficient coverage at OpenAlex, or set the
|
||||||
|
UI language to English and retry.
|
||||||
|
- Preference changes fail while testing locally:
|
||||||
|
Make sure your ``server.secret_key`` and ``server.base_url`` are set in your
|
||||||
|
instance settings so signed cookies work; see :ref:`settings server`.
|
||||||
|
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
===============
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
# about
|
# about
|
||||||
about = {
|
about = {
|
||||||
@@ -31,7 +120,7 @@ search_url = "https://api.openalex.org/works"
|
|||||||
mailto = ""
|
mailto = ""
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: dict[str, t.Any]) -> None:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
# Build OpenAlex query using search parameter and paging
|
# Build OpenAlex query using search parameter and paging
|
||||||
args = {
|
args = {
|
||||||
"search": query,
|
"search": query,
|
||||||
@@ -60,7 +149,7 @@ def request(query: str, params: dict[str, t.Any]) -> None:
|
|||||||
params["url"] = f"{search_url}?{urlencode(args)}"
|
params["url"] = f"{search_url}?{urlencode(args)}"
|
||||||
|
|
||||||
|
|
||||||
def response(resp: SXNG_Response) -> EngineResults:
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
|
|
||||||
@@ -71,12 +160,11 @@ def response(resp: SXNG_Response) -> EngineResults:
|
|||||||
authors = _extract_authors(item)
|
authors = _extract_authors(item)
|
||||||
journal, publisher, pages, volume, number, published_date = _extract_biblio(item)
|
journal, publisher, pages, volume, number, published_date = _extract_biblio(item)
|
||||||
doi = _doi_to_plain(item.get("doi"))
|
doi = _doi_to_plain(item.get("doi"))
|
||||||
tags = _extract_tags(item) or None
|
tags = _extract_tags(item)
|
||||||
comments = _extract_comments(item)
|
comments = _extract_comments(item)
|
||||||
|
|
||||||
res.add(
|
res.add(
|
||||||
res.types.LegacyResult(
|
res.types.Paper(
|
||||||
template="paper.html",
|
|
||||||
url=url,
|
url=url,
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
@@ -99,7 +187,7 @@ def response(resp: SXNG_Response) -> EngineResults:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def _stringify_pages(biblio: dict[str, t.Any]) -> str | None:
|
def _stringify_pages(biblio: dict[str, t.Any]) -> str:
|
||||||
first_page = biblio.get("first_page")
|
first_page = biblio.get("first_page")
|
||||||
last_page = biblio.get("last_page")
|
last_page = biblio.get("last_page")
|
||||||
if first_page and last_page:
|
if first_page and last_page:
|
||||||
@@ -108,7 +196,7 @@ def _stringify_pages(biblio: dict[str, t.Any]) -> str | None:
|
|||||||
return str(first_page)
|
return str(first_page)
|
||||||
if last_page:
|
if last_page:
|
||||||
return str(last_page)
|
return str(last_page)
|
||||||
return None
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(value: str | None) -> datetime | None:
|
def _parse_date(value: str | None) -> datetime | None:
|
||||||
@@ -123,9 +211,9 @@ def _parse_date(value: str | None) -> datetime | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _doi_to_plain(doi_value: str | None) -> str | None:
|
def _doi_to_plain(doi_value: str | None) -> str:
|
||||||
if not doi_value:
|
if not doi_value:
|
||||||
return None
|
return ""
|
||||||
# OpenAlex `doi` field is commonly a full URL like https://doi.org/10.1234/abcd
|
# OpenAlex `doi` field is commonly a full URL like https://doi.org/10.1234/abcd
|
||||||
return doi_value.removeprefix("https://doi.org/")
|
return doi_value.removeprefix("https://doi.org/")
|
||||||
|
|
||||||
@@ -151,14 +239,17 @@ def _reconstruct_abstract(
|
|||||||
return text if text != "" else None
|
return text if text != "" else None
|
||||||
|
|
||||||
|
|
||||||
def _extract_links(item: dict[str, t.Any]) -> tuple[str, str | None, str | None]:
|
def _extract_links(item: dict[str, t.Any]) -> tuple[str, str, str]:
|
||||||
primary_location = item.get("primary_location", {})
|
primary_location: dict[str, str] = item.get("primary_location", {})
|
||||||
landing_page_url: str | None = primary_location.get("landing_page_url")
|
open_access: dict[str, str] = item.get("open_access", {})
|
||||||
|
|
||||||
|
landing_page_url: str = primary_location.get("landing_page_url") or ""
|
||||||
work_url: str = item.get("id", "")
|
work_url: str = item.get("id", "")
|
||||||
|
|
||||||
url: str = landing_page_url or work_url
|
url: str = landing_page_url or work_url
|
||||||
open_access = item.get("open_access", {})
|
html_url: str = landing_page_url
|
||||||
pdf_url: str | None = primary_location.get("pdf_url") or open_access.get("oa_url")
|
pdf_url: str = primary_location.get("pdf_url") or open_access.get("oa_url") or ""
|
||||||
html_url: str | None = landing_page_url
|
|
||||||
return url, html_url, pdf_url
|
return url, html_url, pdf_url
|
||||||
|
|
||||||
|
|
||||||
@@ -185,20 +276,21 @@ def _extract_tags(item: dict[str, t.Any]) -> list[str]:
|
|||||||
|
|
||||||
def _extract_biblio(
|
def _extract_biblio(
|
||||||
item: dict[str, t.Any],
|
item: dict[str, t.Any],
|
||||||
) -> tuple[str | None, str | None, str | None, str | None, str | None, datetime | None]:
|
) -> tuple[str, str, str, str, str, datetime | None]:
|
||||||
host_venue = item.get("host_venue", {})
|
host_venue: dict[str, str] = item.get("host_venue", {})
|
||||||
biblio = item.get("biblio", {})
|
biblio: dict[str, str] = item.get("biblio", {})
|
||||||
journal: str | None = host_venue.get("display_name")
|
|
||||||
publisher: str | None = host_venue.get("publisher")
|
journal: str = host_venue.get("display_name", "")
|
||||||
pages = _stringify_pages(biblio)
|
publisher: str = host_venue.get("publisher", "")
|
||||||
volume = biblio.get("volume")
|
pages: str = _stringify_pages(biblio)
|
||||||
number = biblio.get("issue")
|
volume = biblio.get("volume", "")
|
||||||
|
number = biblio.get("issue", "")
|
||||||
published_date = _parse_date(item.get("publication_date"))
|
published_date = _parse_date(item.get("publication_date"))
|
||||||
return journal, publisher, pages, volume, number, published_date
|
return journal, publisher, pages, volume, number, published_date
|
||||||
|
|
||||||
|
|
||||||
def _extract_comments(item: dict[str, t.Any]) -> str | None:
|
def _extract_comments(item: dict[str, t.Any]) -> str:
|
||||||
cited_by_count = item.get("cited_by_count")
|
cited_by_count = item.get("cited_by_count")
|
||||||
if isinstance(cited_by_count, int):
|
if isinstance(cited_by_count, int):
|
||||||
return f"{cited_by_count} citations"
|
return f"{cited_by_count} citations"
|
||||||
return None
|
return ""
|
||||||
|
|||||||
@@ -1,71 +1,109 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Open library (books)
|
"""`Open Library`_ is an open, editable library catalog, building towards a web
|
||||||
"""
|
page for every book ever published.
|
||||||
from urllib.parse import urlencode
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
.. _Open Library: https://openlibrary.org
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
The service sometimes takes a very long time to respond, the ``timeout`` may
|
||||||
|
need to be adjusted.
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: openlibrary
|
||||||
|
engine: openlibrary
|
||||||
|
shortcut: ol
|
||||||
|
timeout: 10
|
||||||
|
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
'website': 'https://openlibrary.org',
|
"website": "https://openlibrary.org",
|
||||||
'wikidata_id': 'Q1201876',
|
"wikidata_id": "Q1201876",
|
||||||
'require_api_key': False,
|
"require_api_key": False,
|
||||||
'use_official_api': False,
|
"use_official_api": False,
|
||||||
'official_api_documentation': 'https://openlibrary.org/developers/api',
|
"official_api_documentation": "https://openlibrary.org/developers/api",
|
||||||
}
|
}
|
||||||
|
|
||||||
paging = True
|
paging = True
|
||||||
categories = []
|
categories = ["general", "books"]
|
||||||
|
|
||||||
base_url = "https://openlibrary.org"
|
base_url = "https://openlibrary.org"
|
||||||
|
search_api = "https://openlibrary.org/search.json"
|
||||||
|
"""The engine uses the API at the endpoint search.json_.
|
||||||
|
|
||||||
|
.. _search.json: https://openlibrary.org/dev/docs/api/search
|
||||||
|
"""
|
||||||
results_per_page = 10
|
results_per_page = 10
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
args = {
|
args = {
|
||||||
'q': query,
|
"q": query,
|
||||||
'page': params['pageno'],
|
"page": params["pageno"],
|
||||||
'limit': results_per_page,
|
"limit": results_per_page,
|
||||||
|
"fields": "*",
|
||||||
}
|
}
|
||||||
params['url'] = f"{base_url}/search.json?{urlencode(args)}"
|
params["url"] = f"{search_api}?{urlencode(args)}"
|
||||||
return params
|
logger.debug("REST API: %s", params["url"])
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(date):
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
try:
|
res = EngineResults()
|
||||||
return parser.parse(date)
|
json_data = resp.json()
|
||||||
except parser.ParserError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
for item in json_data.get("docs", []):
|
||||||
def response(resp):
|
cover = ""
|
||||||
results = []
|
if "lending_identifier_s" in item:
|
||||||
|
|
||||||
for item in resp.json().get("docs", []):
|
|
||||||
cover = None
|
|
||||||
if 'lending_identifier_s' in item:
|
|
||||||
cover = f"https://archive.org/services/img/{item['lending_identifier_s']}"
|
cover = f"https://archive.org/services/img/{item['lending_identifier_s']}"
|
||||||
|
|
||||||
published = item.get('publish_date')
|
published = item.get("publish_date")
|
||||||
if published:
|
if published:
|
||||||
published_dates = [date for date in map(_parse_date, published) if date]
|
published_dates = [date for date in map(_parse_date, published) if date]
|
||||||
if published_dates:
|
if published_dates:
|
||||||
published = min(published_dates)
|
published = min(published_dates)
|
||||||
|
|
||||||
if not published:
|
if not published:
|
||||||
published = parser.parse(str(item.get('first_published_year')))
|
published = _parse_date(str(item.get("first_publish_year")))
|
||||||
|
|
||||||
result = {
|
content = " / ".join(item.get("first_sentence", []))
|
||||||
'template': 'paper.html',
|
res.add(
|
||||||
'url': f"{base_url}{item['key']}",
|
res.types.Paper(
|
||||||
'title': item['title'],
|
url=f"{base_url}/{item['key']}",
|
||||||
'content': re.sub(r"\{|\}", "", item['first_sentence'][0]) if item.get('first_sentence') else '',
|
title=item["title"],
|
||||||
'isbn': item.get('isbn', [])[:5],
|
content=content,
|
||||||
'authors': item.get('author_name', []),
|
isbn=item.get("isbn", [])[:5],
|
||||||
'thumbnail': cover,
|
authors=item.get("author_name", []),
|
||||||
'publishedDate': published,
|
thumbnail=cover,
|
||||||
'tags': item.get('subject', [])[:10] + item.get('place', [])[:10],
|
publishedDate=published,
|
||||||
}
|
tags=item.get("subject", [])[:10] + item.get("place", [])[:10],
|
||||||
results.append(result)
|
)
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
return results
|
|
||||||
|
def _parse_date(date: str) -> datetime | None:
|
||||||
|
if not date:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return parser.parse(date)
|
||||||
|
except parser.ParserError:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -1,132 +1,151 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""PubMed (Scholar publications)
|
"""PubMed_ comprises more than 39 million citations for biomedical literature
|
||||||
|
from MEDLINE, life science journals, and online books. Citations may include
|
||||||
|
links to full text content from PubMed Central and publisher web sites.
|
||||||
|
|
||||||
|
.. _PubMed: https://pubmed.ncbi.nlm.nih.gov/
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: pubmed
|
||||||
|
engine: pubmed
|
||||||
|
shortcut: pub
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
from searx.result_types import EngineResults
|
||||||
from searx.network import get
|
from searx.network import get
|
||||||
from searx.utils import (
|
from searx.utils import (
|
||||||
eval_xpath_getindex,
|
eval_xpath_getindex,
|
||||||
eval_xpath_list,
|
eval_xpath_list,
|
||||||
extract_text,
|
extract_text,
|
||||||
|
ElementType,
|
||||||
)
|
)
|
||||||
|
|
||||||
# about
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://www.ncbi.nlm.nih.gov/pubmed/',
|
"website": "https://www.ncbi.nlm.nih.gov/pubmed/",
|
||||||
"wikidata_id": 'Q1540899',
|
"wikidata_id": "Q1540899",
|
||||||
"official_api_documentation": {
|
"official_api_documentation": {
|
||||||
'url': 'https://www.ncbi.nlm.nih.gov/home/develop/api/',
|
"url": "https://www.ncbi.nlm.nih.gov/home/develop/api/",
|
||||||
'comment': 'More info on api: https://www.ncbi.nlm.nih.gov/books/NBK25501/',
|
"comment": "More info on api: https://www.ncbi.nlm.nih.gov/books/NBK25501/",
|
||||||
},
|
},
|
||||||
"use_official_api": True,
|
"use_official_api": True,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": 'XML',
|
"results": "XML",
|
||||||
}
|
}
|
||||||
|
|
||||||
categories = ['science', 'scientific publications']
|
categories = ["science", "scientific publications"]
|
||||||
|
|
||||||
base_url = (
|
eutils_api = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
|
||||||
'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi' + '?db=pubmed&{query}&retstart={offset}&retmax={hits}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# engine dependent config
|
# engine dependent config
|
||||||
number_of_results = 10
|
number_of_results = 10
|
||||||
pubmed_url = 'https://www.ncbi.nlm.nih.gov/pubmed/'
|
pubmed_url = "https://www.ncbi.nlm.nih.gov/pubmed/"
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
# basic search
|
|
||||||
offset = (params['pageno'] - 1) * number_of_results
|
|
||||||
|
|
||||||
string_args = {
|
args = urlencode(
|
||||||
'query': urlencode({'term': query}),
|
{
|
||||||
'offset': offset,
|
"db": "pubmed",
|
||||||
'hits': number_of_results,
|
"term": query,
|
||||||
}
|
"retstart": (params["pageno"] - 1) * number_of_results,
|
||||||
|
"hits": number_of_results,
|
||||||
params['url'] = base_url.format(**string_args)
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp): # pylint: disable=too-many-locals
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# First retrieve notice of each result
|
|
||||||
pubmed_retrieve_api_url = (
|
|
||||||
'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?' + 'db=pubmed&retmode=xml&id={pmids_string}'
|
|
||||||
)
|
|
||||||
|
|
||||||
pmids_results = etree.XML(resp.content)
|
|
||||||
pmids = pmids_results.xpath('//eSearchResult/IdList/Id')
|
|
||||||
pmids_string = ''
|
|
||||||
|
|
||||||
for item in pmids:
|
|
||||||
pmids_string += item.text + ','
|
|
||||||
|
|
||||||
retrieve_notice_args = {'pmids_string': pmids_string}
|
|
||||||
|
|
||||||
retrieve_url_encoded = pubmed_retrieve_api_url.format(**retrieve_notice_args)
|
|
||||||
|
|
||||||
search_results_response = get(retrieve_url_encoded).content
|
|
||||||
search_results = etree.XML(search_results_response)
|
|
||||||
for entry in eval_xpath_list(search_results, '//PubmedArticle'):
|
|
||||||
medline = eval_xpath_getindex(entry, './MedlineCitation', 0)
|
|
||||||
|
|
||||||
title = eval_xpath_getindex(medline, './/Article/ArticleTitle', 0).text
|
|
||||||
pmid = eval_xpath_getindex(medline, './/PMID', 0).text
|
|
||||||
url = pubmed_url + pmid
|
|
||||||
content = extract_text(
|
|
||||||
eval_xpath_getindex(medline, './/Abstract/AbstractText//text()', 0, default=None), allow_none=True
|
|
||||||
)
|
|
||||||
doi = extract_text(
|
|
||||||
eval_xpath_getindex(medline, './/ELocationID[@EIdType="doi"]/text()', 0, default=None), allow_none=True
|
|
||||||
)
|
|
||||||
journal = extract_text(
|
|
||||||
eval_xpath_getindex(medline, './Article/Journal/Title/text()', 0, default=None), allow_none=True
|
|
||||||
)
|
|
||||||
issn = extract_text(
|
|
||||||
eval_xpath_getindex(medline, './Article/Journal/ISSN/text()', 0, default=None), allow_none=True
|
|
||||||
)
|
|
||||||
authors = []
|
|
||||||
for author in eval_xpath_list(medline, './Article/AuthorList/Author'):
|
|
||||||
f = eval_xpath_getindex(author, './ForeName', 0, default=None)
|
|
||||||
l = eval_xpath_getindex(author, './LastName', 0, default=None)
|
|
||||||
f = '' if f is None else f.text
|
|
||||||
l = '' if l is None else l.text
|
|
||||||
authors.append((f + ' ' + l).strip())
|
|
||||||
|
|
||||||
res_dict = {
|
|
||||||
'template': 'paper.html',
|
|
||||||
'url': url,
|
|
||||||
'title': title,
|
|
||||||
'content': content or "",
|
|
||||||
'journal': journal,
|
|
||||||
'issn': [issn],
|
|
||||||
'authors': authors,
|
|
||||||
'doi': doi,
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
esearch_url = f"{eutils_api}/esearch.fcgi?{args}"
|
||||||
|
# DTD: https://eutils.ncbi.nlm.nih.gov/eutils/dtd/20060628/esearch.dtd
|
||||||
|
esearch_resp: "SXNG_Response" = get(esearch_url)
|
||||||
|
pmids_results = etree.XML(esearch_resp.content)
|
||||||
|
pmids: list[str] = [i.text for i in pmids_results.xpath("//eSearchResult/IdList/Id")]
|
||||||
|
|
||||||
|
# send efetch request with the IDs from esearch response
|
||||||
|
args = urlencode(
|
||||||
|
{
|
||||||
|
"db": "pubmed",
|
||||||
|
"retmode": "xml",
|
||||||
|
"id": ",".join(pmids),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
efetch_url = f"{eutils_api}/efetch.fcgi?{args}"
|
||||||
|
params["url"] = efetch_url
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp: "SXNG_Response") -> EngineResults: # pylint: disable=too-many-locals
|
||||||
|
|
||||||
|
# DTD: https://dtd.nlm.nih.gov/ncbi/pubmed/out/pubmed_250101.dtd
|
||||||
|
|
||||||
|
# parse efetch response
|
||||||
|
efetch_xml = etree.XML(resp.content)
|
||||||
|
res = EngineResults()
|
||||||
|
|
||||||
|
def _field_txt(xml: ElementType, xpath_str: str) -> str:
|
||||||
|
elem = eval_xpath_getindex(xml, xpath_str, 0, default="")
|
||||||
|
return extract_text(elem, allow_none=True) or ""
|
||||||
|
|
||||||
|
for pubmed_article in eval_xpath_list(efetch_xml, "//PubmedArticle"):
|
||||||
|
|
||||||
|
medline_citation: ElementType = eval_xpath_getindex(pubmed_article, "./MedlineCitation", 0)
|
||||||
|
pubmed_data: ElementType = eval_xpath_getindex(pubmed_article, "./PubmedData", 0)
|
||||||
|
|
||||||
|
title: str = eval_xpath_getindex(medline_citation, ".//Article/ArticleTitle", 0).text
|
||||||
|
pmid: str = eval_xpath_getindex(medline_citation, ".//PMID", 0).text
|
||||||
|
url: str = pubmed_url + pmid
|
||||||
|
content = _field_txt(medline_citation, ".//Abstract/AbstractText//text()")
|
||||||
|
doi = _field_txt(medline_citation, ".//ELocationID[@EIdType='doi']/text()")
|
||||||
|
journal = _field_txt(medline_citation, "./Article/Journal/Title/text()")
|
||||||
|
issn = _field_txt(medline_citation, "./Article/Journal/ISSN/text()")
|
||||||
|
|
||||||
|
authors: list[str] = []
|
||||||
|
|
||||||
|
for author in eval_xpath_list(medline_citation, "./Article/AuthorList/Author"):
|
||||||
|
f = eval_xpath_getindex(author, "./ForeName", 0, default=None)
|
||||||
|
l = eval_xpath_getindex(author, "./LastName", 0, default=None)
|
||||||
|
author_name = f"{f.text if f is not None else ''} {l.text if l is not None else ''}".strip()
|
||||||
|
if author_name:
|
||||||
|
authors.append(author_name)
|
||||||
|
|
||||||
accepted_date = eval_xpath_getindex(
|
accepted_date = eval_xpath_getindex(
|
||||||
entry, './PubmedData/History//PubMedPubDate[@PubStatus="accepted"]', 0, default=None
|
pubmed_data, "./History//PubMedPubDate[@PubStatus='accepted']", 0, default=None
|
||||||
)
|
)
|
||||||
|
pub_date = None
|
||||||
if accepted_date is not None:
|
if accepted_date is not None:
|
||||||
year = eval_xpath_getindex(accepted_date, './Year', 0)
|
year = eval_xpath_getindex(accepted_date, "./Year", 0)
|
||||||
month = eval_xpath_getindex(accepted_date, './Month', 0)
|
month = eval_xpath_getindex(accepted_date, "./Month", 0)
|
||||||
day = eval_xpath_getindex(accepted_date, './Day', 0)
|
day = eval_xpath_getindex(accepted_date, "./Day", 0)
|
||||||
try:
|
try:
|
||||||
publishedDate = datetime.strptime(
|
pub_date = datetime(year=int(year.text), month=int(month.text), day=int(day.text))
|
||||||
year.text + '-' + month.text + '-' + day.text,
|
except ValueError:
|
||||||
'%Y-%m-%d',
|
pass
|
||||||
)
|
|
||||||
res_dict['publishedDate'] = publishedDate
|
|
||||||
except Exception as e: # pylint: disable=broad-exception-caught
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
results.append(res_dict)
|
res.add(
|
||||||
|
res.types.Paper(
|
||||||
return results
|
url=url,
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
journal=journal,
|
||||||
|
issn=[issn],
|
||||||
|
authors=authors,
|
||||||
|
doi=doi,
|
||||||
|
publishedDate=pub_date,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|||||||
@@ -1,125 +1,163 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Semantic Scholar (Science)"""
|
"""`Semantic Scholar`_ provides free, AI-driven search and discovery tools, and
|
||||||
|
open resources for the global research community. `Semantic Scholar`_ index
|
||||||
|
over 200 million academic papers sourced from publisher partnerships, data
|
||||||
|
providers, and web crawls.
|
||||||
|
|
||||||
|
.. _Semantic Scholar: https://www.semanticscholar.org/about
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
To get in use of this engine add the following entry to your engines list in
|
||||||
|
``settings.yml``:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: semantic scholar
|
||||||
|
engine: semantic_scholar
|
||||||
|
shortcut: se
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from json import dumps
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from lxml import html
|
from lxml import html
|
||||||
|
from flask_babel import gettext # pyright: ignore[reportUnknownVariableType]
|
||||||
|
|
||||||
from flask_babel import gettext
|
|
||||||
from searx.network import get
|
from searx.network import get
|
||||||
from searx.utils import eval_xpath_getindex, html_to_text
|
from searx.utils import eval_xpath_getindex, html_to_text
|
||||||
|
from searx.enginelib import EngineCache
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://www.semanticscholar.org/',
|
"website": "https://www.semanticscholar.org/",
|
||||||
"wikidata_id": 'Q22908627',
|
"wikidata_id": "Q22908627",
|
||||||
"official_api_documentation": 'https://api.semanticscholar.org/',
|
"official_api_documentation": "https://api.semanticscholar.org/",
|
||||||
"use_official_api": True,
|
"use_official_api": True,
|
||||||
"require_api_key": False,
|
"require_api_key": False,
|
||||||
"results": 'JSON',
|
"results": "JSON",
|
||||||
}
|
}
|
||||||
|
|
||||||
categories = ['science', 'scientific publications']
|
categories = ["science", "scientific publications"]
|
||||||
paging = True
|
paging = True
|
||||||
search_url = 'https://www.semanticscholar.org/api/1/search'
|
search_url = "https://www.semanticscholar.org/api/1/search"
|
||||||
base_url = 'https://www.semanticscholar.org'
|
base_url = "https://www.semanticscholar.org"
|
||||||
|
|
||||||
|
CACHE: EngineCache
|
||||||
|
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
|
||||||
|
seconds."""
|
||||||
|
|
||||||
|
|
||||||
def _get_ui_version():
|
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||||
resp = get(base_url)
|
global CACHE # pylint: disable=global-statement
|
||||||
if not resp.ok:
|
CACHE = EngineCache(engine_settings["name"])
|
||||||
raise RuntimeError("Can't determine Semantic Scholar UI version")
|
return True
|
||||||
|
|
||||||
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):
|
def get_ui_version() -> str:
|
||||||
params['url'] = search_url
|
ret_val: str = CACHE.get("X-S2-UI-Version")
|
||||||
params['method'] = 'POST'
|
if not ret_val:
|
||||||
params['headers'].update(
|
resp = get(base_url)
|
||||||
|
if not resp.ok:
|
||||||
|
raise RuntimeError("Can't determine Semantic Scholar UI version")
|
||||||
|
|
||||||
|
doc = html.fromstring(resp.text)
|
||||||
|
ret_val = eval_xpath_getindex(doc, "//meta[@name='s2-ui-version']/@content", 0)
|
||||||
|
if not ret_val:
|
||||||
|
raise RuntimeError("Can't determine Semantic Scholar UI version")
|
||||||
|
# hold the cached value for 5min
|
||||||
|
CACHE.set("X-S2-UI-Version", value=ret_val, expire=300)
|
||||||
|
logger.debug("X-S2-UI-Version: %s", ret_val)
|
||||||
|
return ret_val
|
||||||
|
|
||||||
|
|
||||||
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
|
params["url"] = search_url
|
||||||
|
params["method"] = "POST"
|
||||||
|
params["headers"].update(
|
||||||
{
|
{
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
'X-S2-UI-Version': _get_ui_version(),
|
"X-S2-UI-Version": get_ui_version(),
|
||||||
'X-S2-Client': "webapp-browser",
|
"X-S2-Client": "webapp-browser",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
params['data'] = dumps(
|
params["json"] = {
|
||||||
{
|
"queryString": query,
|
||||||
"queryString": query,
|
"page": params["pageno"],
|
||||||
"page": params['pageno'],
|
"pageSize": 10,
|
||||||
"pageSize": 10,
|
"sort": "relevance",
|
||||||
"sort": "relevance",
|
"getQuerySuggestions": False,
|
||||||
"getQuerySuggestions": False,
|
"authors": [],
|
||||||
"authors": [],
|
"coAuthors": [],
|
||||||
"coAuthors": [],
|
"venues": [],
|
||||||
"venues": [],
|
"performTitleMatch": True,
|
||||||
"performTitleMatch": True,
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp):
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
res = resp.json()
|
res = EngineResults()
|
||||||
|
json_data = resp.json()
|
||||||
|
|
||||||
results = []
|
for result in json_data["results"]:
|
||||||
for result in res['results']:
|
url: str = result.get("primaryPaperLink", {}).get("url")
|
||||||
url = result.get('primaryPaperLink', {}).get('url')
|
if not url and result.get("links"):
|
||||||
if not url and result.get('links'):
|
url = result.get("links")[0]
|
||||||
url = result.get('links')[0]
|
|
||||||
if not url:
|
if not url:
|
||||||
alternatePaperLinks = result.get('alternatePaperLinks')
|
alternatePaperLinks = result.get("alternatePaperLinks")
|
||||||
if alternatePaperLinks:
|
if alternatePaperLinks:
|
||||||
url = alternatePaperLinks[0].get('url')
|
url = alternatePaperLinks[0].get("url")
|
||||||
if not url:
|
if not url:
|
||||||
url = base_url + '/paper/%s' % result['id']
|
url = base_url + "/paper/%s" % result["id"]
|
||||||
|
|
||||||
# publishedDate
|
publishedDate: datetime | None
|
||||||
if 'pubDate' in result:
|
if "pubDate" in result:
|
||||||
publishedDate = datetime.strptime(result['pubDate'], "%Y-%m-%d")
|
publishedDate = datetime.strptime(result["pubDate"], "%Y-%m-%d")
|
||||||
else:
|
else:
|
||||||
publishedDate = None
|
publishedDate = None
|
||||||
|
|
||||||
# authors
|
# authors
|
||||||
authors = [author[0]['name'] for author in result.get('authors', [])]
|
authors: list[str] = [author[0]["name"] for author in result.get("authors", [])]
|
||||||
|
|
||||||
# pick for the first alternate link, but not from the crawler
|
# pick for the first alternate link, but not from the crawler
|
||||||
pdf_url = None
|
pdf_url: str = ""
|
||||||
for doc in result.get('alternatePaperLinks', []):
|
for doc in result.get("alternatePaperLinks", []):
|
||||||
if doc['linkType'] not in ('crawler', 'doi'):
|
if doc["linkType"] not in ("crawler", "doi"):
|
||||||
pdf_url = doc['url']
|
pdf_url = doc["url"]
|
||||||
break
|
break
|
||||||
|
|
||||||
# comments
|
# comments
|
||||||
comments = None
|
comments: str = ""
|
||||||
if 'citationStats' in result:
|
if "citationStats" in result:
|
||||||
comments = gettext(
|
comments = gettext(
|
||||||
'{numCitations} citations from the year {firstCitationVelocityYear} to {lastCitationVelocityYear}'
|
"{numCitations} citations from the year {firstCitationVelocityYear} to {lastCitationVelocityYear}"
|
||||||
).format(
|
).format(
|
||||||
numCitations=result['citationStats']['numCitations'],
|
numCitations=result["citationStats"]["numCitations"],
|
||||||
firstCitationVelocityYear=result['citationStats']['firstCitationVelocityYear'],
|
firstCitationVelocityYear=result["citationStats"]["firstCitationVelocityYear"],
|
||||||
lastCitationVelocityYear=result['citationStats']['lastCitationVelocityYear'],
|
lastCitationVelocityYear=result["citationStats"]["lastCitationVelocityYear"],
|
||||||
)
|
)
|
||||||
|
|
||||||
results.append(
|
res.add(
|
||||||
{
|
res.types.Paper(
|
||||||
'template': 'paper.html',
|
title=result["title"]["text"],
|
||||||
'url': url,
|
url=url,
|
||||||
'title': result['title']['text'],
|
content=html_to_text(result["paperAbstract"]["text"]),
|
||||||
'content': html_to_text(result['paperAbstract']['text']),
|
journal=result.get("venue", {}).get("text") or result.get("journal", {}).get("name"),
|
||||||
'journal': result.get('venue', {}).get('text') or result.get('journal', {}).get('name'),
|
doi=result.get("doiInfo", {}).get("doi"),
|
||||||
'doi': result.get('doiInfo', {}).get('doi'),
|
tags=result.get("fieldsOfStudy"),
|
||||||
'tags': result.get('fieldsOfStudy'),
|
authors=authors,
|
||||||
'authors': authors,
|
pdf_url=pdf_url,
|
||||||
'pdf_url': pdf_url,
|
publishedDate=publishedDate,
|
||||||
'publishedDate': publishedDate,
|
comments=comments,
|
||||||
'comments': comments,
|
)
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return res
|
||||||
|
|||||||
@@ -1,71 +1,175 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Springer Nature (science)
|
"""`Springer Nature`_ is a global publisher dedicated to providing service to
|
||||||
|
research community with official Springer-API_ (API-Playground_).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The Springer engine requires an API key, which can be obtained via the
|
||||||
|
`Springer subscription`_.
|
||||||
|
|
||||||
|
Since the search term is passed 1:1 to the API, SearXNG users can use the
|
||||||
|
`Supported Query Parameters`_.
|
||||||
|
|
||||||
|
- ``!springer (doi:10.1007/s10948-025-07019-1 OR doi:10.1007/s10948-025-07035-1)``
|
||||||
|
- ``!springer keyword:ybco``
|
||||||
|
|
||||||
|
However, please note that the available options depend on the subscription type.
|
||||||
|
|
||||||
|
For example, the ``year:`` filter requires a *Premium Plan* subscription.
|
||||||
|
|
||||||
|
- ``!springer keyword:ybco year:2024``
|
||||||
|
|
||||||
|
The engine uses the REST Meta-API_ `v2` endpoint, but there is also a `Python
|
||||||
|
API Wrapper`_.
|
||||||
|
|
||||||
|
.. _Python API Wrapper: https://pypi.org/project/springernature-api-client/
|
||||||
|
.. _Springer Nature: https://www.springernature.com/
|
||||||
|
.. _Springer subscription: https://dev.springernature.com/subscription/
|
||||||
|
.. _Springer-API: https://dev.springernature.com/docs/introduction/
|
||||||
|
.. _API-Playground: https://dev.springernature.com/docs/live-documentation/
|
||||||
|
.. _Meta-API: https://dev.springernature.com/docs/api-endpoints/meta-api/
|
||||||
|
.. _Supported Query Parameters: https://dev.springernature.com/docs/supported-query-params/
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
The engine has the following additional settings:
|
||||||
|
|
||||||
|
- :py:obj:`api_key`
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: springer nature
|
||||||
|
api_key: "..."
|
||||||
|
inactive: false
|
||||||
|
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import loads
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
from searx.network import raise_for_httperror
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
about = {
|
about = {
|
||||||
"website": 'https://www.springernature.com/',
|
"website": "https://www.springernature.com/",
|
||||||
"wikidata_id": 'Q21096327',
|
"wikidata_id": "Q21096327",
|
||||||
"official_api_documentation": 'https://dev.springernature.com/',
|
"official_api_documentation": "https://dev.springernature.com/docs/live-documentation/",
|
||||||
"use_official_api": True,
|
"use_official_api": True,
|
||||||
"require_api_key": True,
|
"require_api_key": True,
|
||||||
"results": 'JSON',
|
"results": "JSON",
|
||||||
}
|
}
|
||||||
|
|
||||||
categories = ['science', 'scientific publications']
|
categories = ["science", "scientific publications"]
|
||||||
|
|
||||||
paging = True
|
paging = True
|
||||||
nb_per_page = 10
|
nb_per_page = 10
|
||||||
api_key = 'unset'
|
"""Number of results to return in the request, see `Pagination and Limits`_ for
|
||||||
|
more details.
|
||||||
|
|
||||||
base_url = 'https://api.springernature.com/metadata/json?'
|
.. _Pagination and Limits:
|
||||||
|
https://dev.springernature.com/docs/advanced-querying/pagination-limits/
|
||||||
|
"""
|
||||||
|
|
||||||
|
api_key = ""
|
||||||
|
"""Key used for the Meta-API_. Get your API key from: `Springer subscription`_"""
|
||||||
|
|
||||||
|
base_url = "https://api.springernature.com/meta/v2/json"
|
||||||
|
"""An enhanced endpoint with additional metadata fields and optimized queries
|
||||||
|
for more efficient and comprehensive retrieval (Meta-API_ `v2`).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def request(query, params):
|
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||||
if api_key == 'unset':
|
"""Initialization of the Springer engine, checks whether the
|
||||||
raise SearxEngineAPIException('missing Springer-Nature API key')
|
:py:obj:`api_key` is set, otherwise the engine is inactive.
|
||||||
args = urlencode({'q': query, 's': nb_per_page * (params['pageno'] - 1), 'p': nb_per_page, 'api_key': api_key})
|
"""
|
||||||
params['url'] = base_url + args
|
key: str = engine_settings.get("api_key", "")
|
||||||
logger.debug("query_url --> %s", params['url'])
|
try:
|
||||||
return params
|
# Springer's API key is a hex value
|
||||||
|
int(key, 16)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
logger.error("Springer's API key is not set or invalid.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def response(resp):
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
results = []
|
args = {
|
||||||
json_data = loads(resp.text)
|
"api_key": api_key,
|
||||||
|
"q": query,
|
||||||
|
"s": nb_per_page * (params["pageno"] - 1),
|
||||||
|
"p": nb_per_page,
|
||||||
|
}
|
||||||
|
params["url"] = f"{base_url}?{urlencode(args)}"
|
||||||
|
# For example, the ``year:`` filter requires a *Premium Plan* subscription.
|
||||||
|
params["raise_for_httperror"] = False
|
||||||
|
|
||||||
for record in json_data['records']:
|
|
||||||
published = datetime.strptime(record['publicationDate'], '%Y-%m-%d')
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
authors = [" ".join(author['creator'].split(', ')[::-1]) for author in record['creators']]
|
|
||||||
tags = record.get('genre')
|
res = EngineResults()
|
||||||
if isinstance(tags, str):
|
json_data = resp.json()
|
||||||
tags = [tags]
|
|
||||||
results.append(
|
if (
|
||||||
{
|
resp.status_code == 403
|
||||||
'template': 'paper.html',
|
and json_data["status"].lower() == "fail"
|
||||||
'url': record['url'][0]['value'].replace('http://', 'https://', 1),
|
and "premium feature" in json_data["message"].lower()
|
||||||
'title': record['title'],
|
):
|
||||||
'content': record['abstract'],
|
return res
|
||||||
'comments': record['publicationName'],
|
raise_for_httperror(resp)
|
||||||
'tags': tags,
|
|
||||||
'publishedDate': published,
|
def field(k: str) -> str:
|
||||||
'type': record.get('contentType'),
|
return str(record.get(k, ""))
|
||||||
'authors': authors,
|
|
||||||
# 'editor': '',
|
for record in json_data["records"]:
|
||||||
'publisher': record.get('publisher'),
|
published = datetime.strptime(record["publicationDate"], "%Y-%m-%d")
|
||||||
'journal': record.get('publicationName'),
|
authors: list[str] = [" ".join(author["creator"].split(", ")[::-1]) for author in record["creators"]]
|
||||||
'volume': record.get('volume') or None,
|
|
||||||
'pages': '-'.join([x for x in [record.get('startingPage'), record.get('endingPage')] if x]),
|
pdf_url = ""
|
||||||
'number': record.get('number') or None,
|
html_url = ""
|
||||||
'doi': record.get('doi'),
|
url_list: list[dict[str, str]] = record["url"]
|
||||||
'issn': [x for x in [record.get('issn')] if x],
|
|
||||||
'isbn': [x for x in [record.get('isbn')] if x],
|
for item in url_list:
|
||||||
# 'pdf_url' : ''
|
if item["platform"] != "web":
|
||||||
}
|
continue
|
||||||
|
val = item["value"].replace("http://", "https://", 1)
|
||||||
|
if item["format"] == "html":
|
||||||
|
html_url = val
|
||||||
|
elif item["format"] == "pdf":
|
||||||
|
pdf_url = val
|
||||||
|
|
||||||
|
paper = res.types.Paper(
|
||||||
|
url=html_url,
|
||||||
|
# html_url=html_url,
|
||||||
|
pdf_url=pdf_url,
|
||||||
|
title=field("title"),
|
||||||
|
content=field("abstract"),
|
||||||
|
comments=field("publicationName"),
|
||||||
|
tags=record.get("keyword", []),
|
||||||
|
publishedDate=published,
|
||||||
|
type=field("contentType"),
|
||||||
|
authors=authors,
|
||||||
|
publisher=field("publisher"),
|
||||||
|
journal=field("publicationName"),
|
||||||
|
volume=field("volume"),
|
||||||
|
pages="-".join([x for x in [field("startingPage"), field("endingPage")] if x]),
|
||||||
|
number=field("number"),
|
||||||
|
doi=field("doi"),
|
||||||
|
issn=[x for x in [field("issn")] if x],
|
||||||
|
isbn=[x for x in [field("isbn")] if x],
|
||||||
)
|
)
|
||||||
return results
|
res.add(paper)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|||||||
@@ -39,15 +39,16 @@ from urllib.parse import quote
|
|||||||
from lxml import html
|
from lxml import html
|
||||||
from flask_babel import gettext # pyright: ignore[reportUnknownVariableType]
|
from flask_babel import gettext # pyright: ignore[reportUnknownVariableType]
|
||||||
|
|
||||||
from searx.utils import extract_text, eval_xpath, eval_xpath_list
|
from searx.utils import extract_text, eval_xpath, eval_xpath_list, ElementType
|
||||||
from searx.enginelib.traits import EngineTraits
|
from searx.enginelib.traits import EngineTraits
|
||||||
from searx.data import ENGINE_TRAITS
|
from searx.data import ENGINE_TRAITS
|
||||||
from searx.exceptions import SearxException
|
from searx.exceptions import SearxException
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from searx.extended_types import SXNG_Response
|
from searx.extended_types import SXNG_Response
|
||||||
|
from searx.search.processors import OnlineParams
|
||||||
|
|
||||||
# about
|
|
||||||
about: dict[str, t.Any] = {
|
about: dict[str, t.Any] = {
|
||||||
"website": "https://zlibrary-global.se",
|
"website": "https://zlibrary-global.se",
|
||||||
"wikidata_id": "Q104863992",
|
"wikidata_id": "Q104863992",
|
||||||
@@ -57,7 +58,7 @@ about: dict[str, t.Any] = {
|
|||||||
"results": "HTML",
|
"results": "HTML",
|
||||||
}
|
}
|
||||||
|
|
||||||
categories: list[str] = ["files"]
|
categories: list[str] = ["files", "books"]
|
||||||
paging: bool = True
|
paging: bool = True
|
||||||
base_url: str = "https://zlibrary-global.se"
|
base_url: str = "https://zlibrary-global.se"
|
||||||
|
|
||||||
@@ -74,8 +75,12 @@ zlib_ext: str = ""
|
|||||||
``PDF`` and ``EPUB``.
|
``PDF`` and ``EPUB``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
i18n_language = gettext("Language")
|
||||||
|
i18n_book_rating = gettext("Book rating")
|
||||||
|
i18n_file_quality = gettext("File quality")
|
||||||
|
|
||||||
def init(engine_settings: dict[str, t.Any] | None = None) -> None: # pylint: disable=unused-argument
|
|
||||||
|
def setup(engine_settings: dict[str, t.Any]) -> bool: # pylint: disable=unused-argument
|
||||||
"""Check of engine's settings."""
|
"""Check of engine's settings."""
|
||||||
traits: EngineTraits = EngineTraits(**ENGINE_TRAITS["z-library"])
|
traits: EngineTraits = EngineTraits(**ENGINE_TRAITS["z-library"])
|
||||||
|
|
||||||
@@ -85,10 +90,11 @@ def init(engine_settings: dict[str, t.Any] | None = None) -> None: # pylint: di
|
|||||||
raise ValueError(f"invalid setting year_from: {zlib_year_from}")
|
raise ValueError(f"invalid setting year_from: {zlib_year_from}")
|
||||||
if zlib_year_to and zlib_year_to not in traits.custom["year_to"]:
|
if zlib_year_to and zlib_year_to not in traits.custom["year_to"]:
|
||||||
raise ValueError(f"invalid setting year_to: {zlib_year_to}")
|
raise ValueError(f"invalid setting year_to: {zlib_year_to}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: dict[str, t.Any]) -> dict[str, t.Any]:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
lang: str = traits.get_language(params["language"], traits.all_locale) # type: ignore
|
lang: str | None = traits.get_language(params["searxng_locale"], traits.all_locale)
|
||||||
search_url: str = (
|
search_url: str = (
|
||||||
base_url
|
base_url
|
||||||
+ "/s/{search_query}/?page={pageno}"
|
+ "/s/{search_query}/?page={pageno}"
|
||||||
@@ -106,41 +112,35 @@ def request(query: str, params: dict[str, t.Any]) -> dict[str, t.Any]:
|
|||||||
zlib_ext=zlib_ext,
|
zlib_ext=zlib_ext,
|
||||||
)
|
)
|
||||||
params["verify"] = False
|
params["verify"] = False
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def domain_is_seized(dom):
|
def response(resp: "SXNG_Response") -> EngineResults:
|
||||||
return bool(dom.xpath('//title') and "seized" in dom.xpath('//title')[0].text.lower())
|
res = EngineResults()
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]:
|
|
||||||
results: list[dict[str, t.Any]] = []
|
|
||||||
dom = html.fromstring(resp.text)
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
if domain_is_seized(dom):
|
if domain_is_seized(dom):
|
||||||
raise SearxException(f"zlibrary domain is seized: {base_url}")
|
raise SearxException(f"zlibrary domain is seized: {base_url}")
|
||||||
|
|
||||||
for item in dom.xpath('//div[@id="searchResultBox"]//div[contains(@class, "resItemBox")]'):
|
for item in dom.xpath('//div[@id="searchResultBox"]//div[contains(@class, "resItemBox")]'):
|
||||||
results.append(_parse_result(item))
|
kwargs = _parse_result(item)
|
||||||
|
res.add(res.types.Paper(**kwargs))
|
||||||
|
|
||||||
return results
|
return res
|
||||||
|
|
||||||
|
|
||||||
def _text(item, selector: str) -> str | None:
|
def domain_is_seized(dom: ElementType):
|
||||||
|
return bool(dom.xpath('//title') and "seized" in dom.xpath('//title')[0].text.lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _text(item: ElementType, selector: str) -> str | None:
|
||||||
return extract_text(eval_xpath(item, selector))
|
return extract_text(eval_xpath(item, selector))
|
||||||
|
|
||||||
|
|
||||||
i18n_language = gettext("Language")
|
def _parse_result(item: ElementType) -> dict[str, t.Any]:
|
||||||
i18n_book_rating = gettext("Book rating")
|
|
||||||
i18n_file_quality = gettext("File quality")
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_result(item) -> dict[str, t.Any]:
|
|
||||||
|
|
||||||
author_elements = eval_xpath_list(item, './/div[@class="authors"]//a[@itemprop="author"]')
|
author_elements = eval_xpath_list(item, './/div[@class="authors"]//a[@itemprop="author"]')
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"template": "paper.html",
|
|
||||||
"url": base_url + item.xpath('(.//a[starts-with(@href, "/book/")])[1]/@href')[0],
|
"url": base_url + item.xpath('(.//a[starts-with(@href, "/book/")])[1]/@href')[0],
|
||||||
"title": _text(item, './/*[@itemprop="name"]'),
|
"title": _text(item, './/*[@itemprop="name"]'),
|
||||||
"authors": [extract_text(author) for author in author_elements],
|
"authors": [extract_text(author) for author in author_elements],
|
||||||
@@ -148,15 +148,15 @@ def _parse_result(item) -> dict[str, t.Any]:
|
|||||||
"type": _text(item, './/div[contains(@class, "property__file")]//div[contains(@class, "property_value")]'),
|
"type": _text(item, './/div[contains(@class, "property__file")]//div[contains(@class, "property_value")]'),
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail: str = _text(item, './/img[contains(@class, "cover")]/@data-src')
|
thumbnail = _text(item, './/img[contains(@class, "cover")]/@data-src')
|
||||||
if not thumbnail.startswith('/'):
|
if thumbnail and not thumbnail.startswith('/'):
|
||||||
result["thumbnail"] = thumbnail
|
result["thumbnail"] = thumbnail
|
||||||
|
|
||||||
year = _text(item, './/div[contains(@class, "property_year")]//div[contains(@class, "property_value")]')
|
year = _text(item, './/div[contains(@class, "property_year")]//div[contains(@class, "property_value")]')
|
||||||
if year:
|
if year:
|
||||||
result["publishedDate"] = datetime.strptime(year, '%Y')
|
result["publishedDate"] = datetime.strptime(year, '%Y')
|
||||||
|
|
||||||
content = []
|
content: list[str] = []
|
||||||
language = _text(item, './/div[contains(@class, "property_language")]//div[contains(@class, "property_value")]')
|
language = _text(item, './/div[contains(@class, "property_language")]//div[contains(@class, "property_value")]')
|
||||||
if language:
|
if language:
|
||||||
content.append(f"{i18n_language}: {language.capitalize()}")
|
content.append(f"{i18n_language}: {language.capitalize()}")
|
||||||
@@ -173,9 +173,10 @@ def _parse_result(item) -> dict[str, t.Any]:
|
|||||||
|
|
||||||
def fetch_traits(engine_traits: EngineTraits) -> None:
|
def fetch_traits(engine_traits: EngineTraits) -> None:
|
||||||
"""Fetch languages and other search arguments from zlibrary's search form."""
|
"""Fetch languages and other search arguments from zlibrary's search form."""
|
||||||
# pylint: disable=import-outside-toplevel, too-many-branches
|
# pylint: disable=import-outside-toplevel, too-many-branches, too-many-statements
|
||||||
|
|
||||||
import babel
|
import babel
|
||||||
|
import babel.core
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||||
@@ -197,7 +198,7 @@ def fetch_traits(engine_traits: EngineTraits) -> None:
|
|||||||
|
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
raise RuntimeError("Response from zlibrary's search page is not OK.")
|
raise RuntimeError("Response from zlibrary's search page is not OK.")
|
||||||
dom = html.fromstring(resp.text) # type: ignore
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
if domain_is_seized(dom):
|
if domain_is_seized(dom):
|
||||||
print(f"ERROR: zlibrary domain is seized: {base_url}")
|
print(f"ERROR: zlibrary domain is seized: {base_url}")
|
||||||
@@ -206,25 +207,30 @@ def fetch_traits(engine_traits: EngineTraits) -> None:
|
|||||||
|
|
||||||
engine_traits.all_locale = ""
|
engine_traits.all_locale = ""
|
||||||
engine_traits.custom["ext"] = []
|
engine_traits.custom["ext"] = []
|
||||||
engine_traits.custom["year_from"] = []
|
|
||||||
engine_traits.custom["year_to"] = []
|
|
||||||
|
|
||||||
|
l: list[str]
|
||||||
|
# years_from
|
||||||
|
l = []
|
||||||
for year in eval_xpath_list(dom, "//div[@id='advSearch-noJS']//select[@id='sf_yearFrom']/option"):
|
for year in eval_xpath_list(dom, "//div[@id='advSearch-noJS']//select[@id='sf_yearFrom']/option"):
|
||||||
engine_traits.custom["year_from"].append(year.get("value"))
|
l.append(year.get("value") or "")
|
||||||
|
engine_traits.custom["year_from"] = l
|
||||||
|
|
||||||
|
# years_to
|
||||||
|
l = []
|
||||||
for year in eval_xpath_list(dom, "//div[@id='advSearch-noJS']//select[@id='sf_yearTo']/option"):
|
for year in eval_xpath_list(dom, "//div[@id='advSearch-noJS']//select[@id='sf_yearTo']/option"):
|
||||||
engine_traits.custom["year_to"].append(year.get("value"))
|
l.append(year.get("value") or "")
|
||||||
|
engine_traits.custom["year_to"] = l
|
||||||
|
|
||||||
|
# ext (file extensions)
|
||||||
|
l = []
|
||||||
for ext in eval_xpath_list(dom, "//div[@id='advSearch-noJS']//select[@id='sf_extensions']/option"):
|
for ext in eval_xpath_list(dom, "//div[@id='advSearch-noJS']//select[@id='sf_extensions']/option"):
|
||||||
value: str | None = ext.get("value")
|
l.append(ext.get("value") or "")
|
||||||
if value is None:
|
engine_traits.custom["ext"] = l
|
||||||
value = ""
|
|
||||||
engine_traits.custom["ext"].append(value)
|
|
||||||
|
|
||||||
# Handle languages
|
# Handle languages
|
||||||
# Z-library uses English names for languages, so we need to map them to their respective locales
|
# Z-library uses English names for languages, so we need to map them to their respective locales
|
||||||
language_name_locale_map: dict[str, babel.Locale] = {}
|
language_name_locale_map: dict[str, babel.Locale] = {}
|
||||||
for locale in babel.core.localedata.locale_identifiers(): # type: ignore
|
for locale in babel.core.localedata.locale_identifiers():
|
||||||
# Create a Locale object for the current locale
|
# Create a Locale object for the current locale
|
||||||
loc = babel.Locale.parse(locale)
|
loc = babel.Locale.parse(locale)
|
||||||
if loc.english_name is None:
|
if loc.english_name is None:
|
||||||
|
|||||||
@@ -13,13 +13,9 @@ import threading
|
|||||||
import httpx
|
import httpx
|
||||||
from httpx_socks import AsyncProxyTransport
|
from httpx_socks import AsyncProxyTransport
|
||||||
from python_socks import parse_proxy_url, ProxyConnectionError, ProxyTimeoutError, ProxyError
|
from python_socks import parse_proxy_url, ProxyConnectionError, ProxyTimeoutError, ProxyError
|
||||||
import uvloop
|
|
||||||
|
|
||||||
from searx import logger
|
from searx import logger
|
||||||
|
|
||||||
|
|
||||||
uvloop.install()
|
|
||||||
|
|
||||||
CertTypes = str | tuple[str, str] | tuple[str, str, str]
|
CertTypes = str | tuple[str, str] | tuple[str, str, str]
|
||||||
SslContextKeyType = tuple[str | None, CertTypes | None, bool, bool]
|
SslContextKeyType = tuple[str | None, CertTypes | None, bool, bool]
|
||||||
|
|
||||||
|
|||||||
@@ -280,9 +280,9 @@ class Network:
|
|||||||
client.cookies = httpx.Cookies(cookies)
|
client.cookies = httpx.Cookies(cookies)
|
||||||
try:
|
try:
|
||||||
if stream:
|
if stream:
|
||||||
response = client.stream(method, url, **kwargs)
|
return client.stream(method, url, **kwargs)
|
||||||
else:
|
|
||||||
response = await client.request(method, url, **kwargs)
|
response = await client.request(method, url, **kwargs)
|
||||||
if self.is_valid_response(response) or retries <= 0:
|
if self.is_valid_response(response) or retries <= 0:
|
||||||
return self.patch_response(response, do_raise_for_httperror)
|
return self.patch_response(response, do_raise_for_httperror)
|
||||||
except httpx.RemoteProtocolError as e:
|
except httpx.RemoteProtocolError as e:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ __all__ = [
|
|||||||
"Translations",
|
"Translations",
|
||||||
"WeatherAnswer",
|
"WeatherAnswer",
|
||||||
"Code",
|
"Code",
|
||||||
|
"Paper",
|
||||||
]
|
]
|
||||||
|
|
||||||
import typing as t
|
import typing as t
|
||||||
@@ -31,6 +32,7 @@ from ._base import Result, MainResult, LegacyResult
|
|||||||
from .answer import AnswerSet, Answer, Translations, WeatherAnswer
|
from .answer import AnswerSet, Answer, Translations, WeatherAnswer
|
||||||
from .keyvalue import KeyValue
|
from .keyvalue import KeyValue
|
||||||
from .code import Code
|
from .code import Code
|
||||||
|
from .paper import Paper
|
||||||
|
|
||||||
|
|
||||||
class ResultList(list[Result | LegacyResult], abc.ABC):
|
class ResultList(list[Result | LegacyResult], abc.ABC):
|
||||||
@@ -44,6 +46,7 @@ class ResultList(list[Result | LegacyResult], abc.ABC):
|
|||||||
Answer = Answer
|
Answer = Answer
|
||||||
KeyValue = KeyValue
|
KeyValue = KeyValue
|
||||||
Code = Code
|
Code = Code
|
||||||
|
Paper = Paper
|
||||||
MainResult = MainResult
|
MainResult = MainResult
|
||||||
Result = Result
|
Result = Result
|
||||||
Translations = Translations
|
Translations = Translations
|
||||||
|
|||||||
@@ -362,7 +362,11 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
|||||||
"""The date on which the object was published."""
|
"""The date on which the object was published."""
|
||||||
|
|
||||||
pubdate: str = ""
|
pubdate: str = ""
|
||||||
"""String representation of :py:obj:`MainResult.publishedDate`"""
|
"""String representation of :py:obj:`MainResult.publishedDate`
|
||||||
|
|
||||||
|
Deprecated: it is still partially used in the templates, but will one day be
|
||||||
|
completely eliminated.
|
||||||
|
"""
|
||||||
|
|
||||||
length: time.struct_time | None = None
|
length: time.struct_time | None = None
|
||||||
"""Playing duration in seconds."""
|
"""Playing duration in seconds."""
|
||||||
|
|||||||
96
searx/result_types/paper.py
Normal file
96
searx/result_types/paper.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Typification of the *paper* results.
|
||||||
|
|
||||||
|
.. _BibTeX field types: https://en.wikipedia.org/wiki/BibTeX#Field_types
|
||||||
|
.. _BibTeX format: https://www.bibtex.com/g/bibtex-format/
|
||||||
|
|
||||||
|
Results of this type are rendered in the :origin:`paper.html
|
||||||
|
<searx/templates/simple/result_templates/paper.html>` template.
|
||||||
|
|
||||||
|
Related topics:
|
||||||
|
|
||||||
|
- `BibTeX field types`_
|
||||||
|
- `BibTeX format`_
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
.. autoclass:: Paper
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# pylint: disable=too-few-public-methods, disable=invalid-name
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__all__ = ["Paper"]
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from searx.weather import DateTime
|
||||||
|
from ._base import MainResult
|
||||||
|
|
||||||
|
|
||||||
|
@t.final
|
||||||
|
class Paper(MainResult, kw_only=True):
|
||||||
|
"""Result type suitable for displaying scientific papers and other
|
||||||
|
documents."""
|
||||||
|
|
||||||
|
template: str = "paper.html"
|
||||||
|
|
||||||
|
date_of_publication: DateTime | None = None
|
||||||
|
"""Date the document was published."""
|
||||||
|
|
||||||
|
content: str = ""
|
||||||
|
"""An abstract or excerpt from the document."""
|
||||||
|
|
||||||
|
comments: str = ""
|
||||||
|
"""Free text display in italic below the content."""
|
||||||
|
|
||||||
|
tags: list[str] = []
|
||||||
|
"""Free tag list."""
|
||||||
|
|
||||||
|
type: str = ""
|
||||||
|
"""Short description of medium type, e.g. *book*, *pdf* or *html* ..."""
|
||||||
|
|
||||||
|
authors: list[str] | set[str] = []
|
||||||
|
"""List of authors of the work (authors with a "s" suffix, the "author" is
|
||||||
|
in the :py:obj:`MainResult.author`)."""
|
||||||
|
|
||||||
|
editor: str = ""
|
||||||
|
"""Editor of the book/paper."""
|
||||||
|
|
||||||
|
publisher: str = ""
|
||||||
|
"""Name of the publisher."""
|
||||||
|
|
||||||
|
journal: str = ""
|
||||||
|
"""Name of the journal or magazine the article was published in."""
|
||||||
|
|
||||||
|
volume: str | int = ""
|
||||||
|
"""Volume number."""
|
||||||
|
|
||||||
|
pages: str = ""
|
||||||
|
"""Page range where the article is."""
|
||||||
|
|
||||||
|
number: str = ""
|
||||||
|
"""Number of the report or the issue number for a journal article."""
|
||||||
|
|
||||||
|
doi: str = ""
|
||||||
|
"""DOI number (like ``10.1038/d41586-018-07848-2``)."""
|
||||||
|
|
||||||
|
issn: list[str] = []
|
||||||
|
"""List of ISSN numbers like ``1476-4687``"""
|
||||||
|
|
||||||
|
isbn: list[str] = []
|
||||||
|
"""List of ISBN numbers like ``9780201896831``"""
|
||||||
|
|
||||||
|
pdf_url: str = ""
|
||||||
|
"""URL to the full article, the PDF version"""
|
||||||
|
|
||||||
|
html_url: str = ""
|
||||||
|
"""URL to full article, HTML version"""
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
if self.date_of_publication is None and self.publishedDate is not None:
|
||||||
|
self.date_of_publication = DateTime(self.publishedDate)
|
||||||
@@ -407,13 +407,12 @@ engines:
|
|||||||
require_api_key: false
|
require_api_key: false
|
||||||
results: JSON
|
results: JSON
|
||||||
|
|
||||||
# - name: astrophysics data system
|
- name: astrophysics data system
|
||||||
# engine: astrophysics_data_system
|
engine: astrophysics_data_system
|
||||||
# sort: asc
|
shortcut: ads
|
||||||
# weight: 5
|
# read https://docs.searxng.org/dev/engines/online/astrophysics_data_system.html
|
||||||
# categories: [science]
|
api_key: ""
|
||||||
# api_key: your-new-key
|
inactive: true
|
||||||
# shortcut: ads
|
|
||||||
|
|
||||||
- name: alpine linux packages
|
- name: alpine linux packages
|
||||||
engine: alpinelinux
|
engine: alpinelinux
|
||||||
@@ -424,6 +423,7 @@ engines:
|
|||||||
engine: annas_archive
|
engine: annas_archive
|
||||||
disabled: true
|
disabled: true
|
||||||
shortcut: aa
|
shortcut: aa
|
||||||
|
timeout: 5
|
||||||
|
|
||||||
- name: ansa
|
- name: ansa
|
||||||
engine: ansa
|
engine: ansa
|
||||||
@@ -490,7 +490,6 @@ engines:
|
|||||||
- name: arxiv
|
- name: arxiv
|
||||||
engine: arxiv
|
engine: arxiv
|
||||||
shortcut: arx
|
shortcut: arx
|
||||||
timeout: 4.0
|
|
||||||
|
|
||||||
- name: ask
|
- name: ask
|
||||||
engine: ask
|
engine: ask
|
||||||
@@ -659,12 +658,12 @@ engines:
|
|||||||
timeout: 30
|
timeout: 30
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
# - name: core.ac.uk
|
- name: core.ac.uk
|
||||||
# engine: core
|
engine: core
|
||||||
# categories: science
|
shortcut: cor
|
||||||
# shortcut: cor
|
# read https://docs.searxng.org/dev/engines/online/core.html
|
||||||
# # get your API key from: https://core.ac.uk/api-keys/register/
|
api_key: ""
|
||||||
# api_key: 'unset'
|
inactive: true
|
||||||
|
|
||||||
- name: cppreference
|
- name: cppreference
|
||||||
engine: cppreference
|
engine: cppreference
|
||||||
@@ -1254,9 +1253,10 @@ engines:
|
|||||||
- name: z-library
|
- name: z-library
|
||||||
engine: zlibrary
|
engine: zlibrary
|
||||||
shortcut: zlib
|
shortcut: zlib
|
||||||
categories: files
|
|
||||||
timeout: 7.0
|
timeout: 7.0
|
||||||
disabled: true
|
disabled: true
|
||||||
|
# https://github.com/searxng/searxng/issues/3610
|
||||||
|
inactive: true
|
||||||
|
|
||||||
- name: library of congress
|
- name: library of congress
|
||||||
engine: loc
|
engine: loc
|
||||||
@@ -1517,7 +1517,7 @@ engines:
|
|||||||
- name: openlibrary
|
- name: openlibrary
|
||||||
engine: openlibrary
|
engine: openlibrary
|
||||||
shortcut: ol
|
shortcut: ol
|
||||||
timeout: 5
|
timeout: 10
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- name: openmeteo
|
- name: openmeteo
|
||||||
@@ -1735,7 +1735,6 @@ engines:
|
|||||||
- name: pubmed
|
- name: pubmed
|
||||||
engine: pubmed
|
engine: pubmed
|
||||||
shortcut: pub
|
shortcut: pub
|
||||||
timeout: 3.0
|
|
||||||
|
|
||||||
- name: pypi
|
- name: pypi
|
||||||
shortcut: pypi
|
shortcut: pypi
|
||||||
@@ -1965,7 +1964,6 @@ engines:
|
|||||||
|
|
||||||
- name: semantic scholar
|
- name: semantic scholar
|
||||||
engine: semantic_scholar
|
engine: semantic_scholar
|
||||||
disabled: true
|
|
||||||
shortcut: se
|
shortcut: se
|
||||||
|
|
||||||
# Spotify needs API credentials
|
# Spotify needs API credentials
|
||||||
@@ -1986,13 +1984,13 @@ engines:
|
|||||||
# query_fields: '' # query fields
|
# query_fields: '' # query fields
|
||||||
# enable_http: true
|
# enable_http: true
|
||||||
|
|
||||||
# - name: springer nature
|
- name: springer nature
|
||||||
# engine: springer
|
engine: springer
|
||||||
# # get your API key from: https://dev.springernature.com/signup
|
shortcut: springer
|
||||||
# # working API key, for test & debug: "a69685087d07eca9f13db62f65b8f601"
|
timeout: 5
|
||||||
# api_key: 'unset'
|
# read https://docs.searxng.org/dev/engines/online/springer.html
|
||||||
# shortcut: springer
|
api_key: ""
|
||||||
# timeout: 15.0
|
inactive: true
|
||||||
|
|
||||||
- name: startpage
|
- name: startpage
|
||||||
engine: startpage
|
engine: startpage
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,34 +1,92 @@
|
|||||||
{% from 'simple/macros.html' import result_header, result_sub_header, result_sub_footer, result_footer, result_link with context %}
|
{% from 'simple/macros.html' import result_header, result_sub_header, result_sub_footer, result_footer, result_link with context %}
|
||||||
|
|
||||||
{{ result_header(result, favicons, image_proxify) -}}
|
{{ result_header(result, favicons, image_proxify) }}
|
||||||
|
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
{%- if result.publishedDate %}<div class="result_publishedDate"><span>{{ _("Published date") }}:</span><span><time class="published_date" datetime="{{ result.pubdate }}" >{{ result.publishedDate }}</time></span></div>{% endif -%}
|
{%- if result.date_of_publication %}
|
||||||
{%- if result.authors %}<div class="result_authors"><span>{{ _("Author") }}:</span><span>{{ result.authors | join(", ") }}</span></div>{% endif -%}
|
<div>
|
||||||
|
<span>{{ _("Published date") }}:</span>
|
||||||
|
<span>{{ result.date_of_publication.l10n_date("long", "UI") }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
|
{%- if result.authors %}
|
||||||
|
<div>
|
||||||
|
<span>{{ _("Author") }}:</span>
|
||||||
|
<span>{{ result.authors | join(", ") }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
{%- if result.journal -%}
|
{%- if result.journal -%}
|
||||||
<div class="result_journal">
|
<div>
|
||||||
<span>{{- _("Journal") }}:</span><span>{{ result.journal -}}
|
<span>{{- _("Journal") }}:</span>
|
||||||
{%- if result.volume -%}
|
<span>{{ result.journal -}}
|
||||||
{{- result.volume -}}
|
{%- if result.volume -%}
|
||||||
{%- if result.number -%}
|
{{- result.volume -}}
|
||||||
.{{- result.number -}}
|
{%- if result.number -%}.{{- result.number -}}{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- if result.pages -%} {{- result.pages -}}{%- endif -%}
|
||||||
{%- if result.pages -%}
|
|
||||||
{{- result.pages -}}
|
|
||||||
{%- endif -%}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if result.editor %}<div class="result_editor"><span>{{ _("Editor") }}:</span><span>{{ result.editor }}</span></div>{% endif -%}
|
{%- if result.editor %}
|
||||||
{%- if result.publisher %}<div class="result_publisher"><span>{{ _("Publisher") }}:</span><span>{{ result.publisher }}</span></div>{% endif -%}
|
<div>
|
||||||
{%- if result.type %}<div class="result_type"><span>{{ _("Type") }}:</span><span>{{ result.type }}</span></div>{% endif -%}
|
<span>{{ _("Editor") }}:</span>
|
||||||
{%- if result.tags %}<div class="result_tags"><span>{{ _("Tags") }}:</span><span>{{ result.tags | join(", ")}}</span></div>{%- endif -%}
|
<span>{{ result.editor }}</span>
|
||||||
{%- if result.doi %}<div class="result_doi"><span>{{ _("DOI") }}:</span><span>{{ result_link(doi_resolver + result.doi, result.doi) }}</span></div>{% endif -%}
|
</div>
|
||||||
{%- if result.issn %}<div class="result_issn"><span>{{ _("ISSN") }}:</span><span>{{ result.issn | join(", ") }}</span></div>{% endif -%}
|
{% endif -%}
|
||||||
{%- if result.isbn %}<div class="result_isbn"><span>{{ _("ISBN") }}:</span><span>{{ result.isbn | join(", ") }}</span></div>{% endif -%}
|
{%- if result.publisher %}
|
||||||
|
<div>
|
||||||
|
<span>{{ _("Publisher") }}:</span>
|
||||||
|
<span>{{ result.publisher }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
|
{%- if result.type %}
|
||||||
|
<div>
|
||||||
|
<span>{{ _("Type") }}:</span>
|
||||||
|
<span>{{ result.type }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
|
{%- if result.tags %}
|
||||||
|
<div>
|
||||||
|
<span>{{ _("Tags") }}:</span>
|
||||||
|
<span>{{ result.tags | join(", ")}}</span>
|
||||||
|
</div>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if result.doi %}
|
||||||
|
<div>
|
||||||
|
<span>{{ _("DOI") }}:</span>
|
||||||
|
<span>{{ result_link(doi_resolver + result.doi, result.doi) }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
|
{%- if result.issn %}
|
||||||
|
<div>
|
||||||
|
<span>{{ _("ISSN") }}:</span>
|
||||||
|
<span>{{ result.issn | join(", ") }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
|
{%- if result.isbn %}
|
||||||
|
<div class="result_isbn">
|
||||||
|
<span>{{ _("ISBN") }}:</span>
|
||||||
|
<span>{{ result.isbn | join(", ") }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
|
{%- if result.views %}
|
||||||
|
<div>
|
||||||
|
<span>{{ _('Views') }}:</span>
|
||||||
|
<span>{{ result.views }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif -%}
|
||||||
</div>
|
</div>
|
||||||
{%- if result.content -%}<p class="content">{{- result.content | safe -}}</p>{%- endif -%}
|
{%- if result.content -%}
|
||||||
{%- if result.comments -%}<p class="comments">{{- result.comments -}}</p>{%- endif -%}
|
<p class="content">{{- result.content | safe -}}</p>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if result.comments -%}
|
||||||
|
<p class="comments">{{- result.comments -}}</p>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if result.metadata %}
|
||||||
|
<div class="highlight">{{ result.metadata|safe }}</div>
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
<p class="altlink">
|
<p class="altlink">
|
||||||
{%- if result.pdf_url -%}{{ result_link(result.pdf_url, _('PDF')) }}{%- endif -%}
|
{%- if result.pdf_url -%}{{ result_link(result.pdf_url, _('PDF')) }}{%- endif -%}
|
||||||
{%- if result.html_url -%}{{ result_link(result.html_url, _('HTML')) }}{%- endif -%}
|
{%- if result.html_url -%}{{ result_link(result.html_url, _('HTML')) }}{%- endif -%}
|
||||||
|
|||||||
Binary file not shown.
@@ -23,8 +23,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: searx\n"
|
"Project-Id-Version: searx\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-09-01 12:54+0000\n"
|
"POT-Creation-Date: 2025-09-01 12:54+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-02 04:50+0000\n"
|
"PO-Revision-Date: 2025-09-14 00:25+0000\n"
|
||||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
"Last-Translator: artnay <artnay@noreply.codeberg.org>\n"
|
||||||
"Language-Team: Finnish <https://translate.codeberg.org/projects/searxng/"
|
"Language-Team: Finnish <https://translate.codeberg.org/projects/searxng/"
|
||||||
"searxng/fi/>\n"
|
"searxng/fi/>\n"
|
||||||
"Language: fi\n"
|
"Language: fi\n"
|
||||||
@@ -32,7 +32,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 5.12.2\n"
|
"X-Generator: Weblate 5.13.2\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||||
@@ -153,7 +153,7 @@ msgstr "web"
|
|||||||
#. CATEGORY_GROUPS['SCIENTIFIC PUBLICATIONS']
|
#. CATEGORY_GROUPS['SCIENTIFIC PUBLICATIONS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "scientific publications"
|
msgid "scientific publications"
|
||||||
msgstr "Tieteelliset Julkaisut"
|
msgstr "tieteelliset julkaisut"
|
||||||
|
|
||||||
#. STYLE_NAMES['AUTO']
|
#. STYLE_NAMES['AUTO']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -223,12 +223,12 @@ msgstr "Kosteus"
|
|||||||
#. WEATHER_TERMS['MAX TEMP.']
|
#. WEATHER_TERMS['MAX TEMP.']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Max temp."
|
msgid "Max temp."
|
||||||
msgstr "Maksimi lämpötila."
|
msgstr "Suurin lämpötila."
|
||||||
|
|
||||||
#. WEATHER_TERMS['MIN TEMP.']
|
#. WEATHER_TERMS['MIN TEMP.']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Min temp."
|
msgid "Min temp."
|
||||||
msgstr "Minimi lämpötila."
|
msgstr "Pienin lämpötila."
|
||||||
|
|
||||||
#. WEATHER_TERMS['MORNING']
|
#. WEATHER_TERMS['MORNING']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -268,7 +268,7 @@ msgstr "Lämpötila"
|
|||||||
#. WEATHER_TERMS['UV INDEX']
|
#. WEATHER_TERMS['UV INDEX']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "UV index"
|
msgid "UV index"
|
||||||
msgstr "UV indeksi"
|
msgstr "UV-indeksi"
|
||||||
|
|
||||||
#. WEATHER_TERMS['VISIBILITY']
|
#. WEATHER_TERMS['VISIBILITY']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -293,7 +293,7 @@ msgstr "Osittain pilvistä"
|
|||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Cloudy"
|
msgid "Cloudy"
|
||||||
msgstr "Pilvinen"
|
msgstr "Pilvistä"
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -303,7 +303,7 @@ msgstr "Selkeää"
|
|||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Fog"
|
msgid "Fog"
|
||||||
msgstr "Sumu"
|
msgstr "Sumuista"
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -463,7 +463,7 @@ msgstr ""
|
|||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow"
|
msgid "Snow"
|
||||||
msgstr ""
|
msgstr "Lumisadetta"
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -524,7 +524,7 @@ msgstr "pisteet"
|
|||||||
#. SOCIAL_MEDIA_TERMS['TITLE']
|
#. SOCIAL_MEDIA_TERMS['TITLE']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "Otsikko"
|
msgstr "otsikko"
|
||||||
|
|
||||||
#. SOCIAL_MEDIA_TERMS['AUTHOR']
|
#. SOCIAL_MEDIA_TERMS['AUTHOR']
|
||||||
#: searx/engines/hackernews.py:85 searx/searxng.msg
|
#: searx/engines/hackernews.py:85 searx/searxng.msg
|
||||||
@@ -534,7 +534,7 @@ msgstr "tekijä"
|
|||||||
#. SOCIAL_MEDIA_TERMS['THREAD OPEN']
|
#. SOCIAL_MEDIA_TERMS['THREAD OPEN']
|
||||||
#: searx/engines/discourse.py:149 searx/searxng.msg
|
#: searx/engines/discourse.py:149 searx/searxng.msg
|
||||||
msgid "open"
|
msgid "open"
|
||||||
msgstr "Avaa"
|
msgstr "avoin"
|
||||||
|
|
||||||
#. SOCIAL_MEDIA_TERMS['THREAD CLOSED']
|
#. SOCIAL_MEDIA_TERMS['THREAD CLOSED']
|
||||||
#: searx/engines/discourse.py:149 searx/searxng.msg
|
#: searx/engines/discourse.py:149 searx/searxng.msg
|
||||||
@@ -685,7 +685,7 @@ msgid ""
|
|||||||
"{numCitations} citations from the year {firstCitationVelocityYear} to "
|
"{numCitations} citations from the year {firstCitationVelocityYear} to "
|
||||||
"{lastCitationVelocityYear}"
|
"{lastCitationVelocityYear}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"{numCitations} Sitaatit vuodesta {firstCitationVelocityYear} vuoteen "
|
"{numCitations} sitaattia vuodesta {firstCitationVelocityYear} vuoteen "
|
||||||
"{lastCitationVelocityYear}"
|
"{lastCitationVelocityYear}"
|
||||||
|
|
||||||
#: searx/engines/tineye.py:48
|
#: searx/engines/tineye.py:48
|
||||||
@@ -792,11 +792,11 @@ msgstr "Selaimesi tunnistetiedot: "
|
|||||||
|
|
||||||
#: searx/plugins/time_zone.py:34
|
#: searx/plugins/time_zone.py:34
|
||||||
msgid "Timezones plugin"
|
msgid "Timezones plugin"
|
||||||
msgstr ""
|
msgstr "Aikavyöhykkeiden liitännäinen"
|
||||||
|
|
||||||
#: searx/plugins/time_zone.py:35
|
#: searx/plugins/time_zone.py:35
|
||||||
msgid "Display the current time on different time zones."
|
msgid "Display the current time on different time zones."
|
||||||
msgstr ""
|
msgstr "Näytä aika eri aikavyöhykkeillä."
|
||||||
|
|
||||||
#: searx/plugins/tor_check.py:42
|
#: searx/plugins/tor_check.py:42
|
||||||
msgid "Tor check plugin"
|
msgid "Tor check plugin"
|
||||||
@@ -924,7 +924,7 @@ msgstr "välimuistissa"
|
|||||||
|
|
||||||
#: searx/templates/simple/new_issue.html:64
|
#: searx/templates/simple/new_issue.html:64
|
||||||
msgid "Start submitting a new issue on GitHub"
|
msgid "Start submitting a new issue on GitHub"
|
||||||
msgstr "Avaa uusi issue GitHubissa"
|
msgstr "Avaa uusi ongelma GitHubissa"
|
||||||
|
|
||||||
#: searx/templates/simple/new_issue.html:66
|
#: searx/templates/simple/new_issue.html:66
|
||||||
msgid "Please check for existing bugs about this engine on GitHub"
|
msgid "Please check for existing bugs about this engine on GitHub"
|
||||||
@@ -932,13 +932,12 @@ msgstr "Tarkista tämän hakukoneen tämänhetkiset ongelmat GitHubista"
|
|||||||
|
|
||||||
#: searx/templates/simple/new_issue.html:69
|
#: searx/templates/simple/new_issue.html:69
|
||||||
msgid "I confirm there is no existing bug about the issue I encounter"
|
msgid "I confirm there is no existing bug about the issue I encounter"
|
||||||
msgstr "Vahvistan, että tästä bugista ei ole olemassaolevaa issue:ta"
|
msgstr "Vahvistan, että tästä bugista ei ole olemassa ongelmaa"
|
||||||
|
|
||||||
#: searx/templates/simple/new_issue.html:71
|
#: searx/templates/simple/new_issue.html:71
|
||||||
msgid "If this is a public instance, please specify the URL in the bug report"
|
msgid "If this is a public instance, please specify the URL in the bug report"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Jos kyseessä on julkinen instanssi, määritä sen URL-osoite "
|
"Jos kyseessä on julkinen palvelin, määritä sen URL-osoite vikailmoituksessa"
|
||||||
"vikailmoituksessa"
|
|
||||||
|
|
||||||
#: searx/templates/simple/new_issue.html:72
|
#: searx/templates/simple/new_issue.html:72
|
||||||
msgid "Submit a new issue on Github including the above information"
|
msgid "Submit a new issue on Github including the above information"
|
||||||
@@ -952,7 +951,7 @@ msgstr "Ei HTTPS-yhteyttä"
|
|||||||
#: searx/templates/simple/preferences.html:69
|
#: searx/templates/simple/preferences.html:69
|
||||||
#: searx/templates/simple/preferences.html:70
|
#: searx/templates/simple/preferences.html:70
|
||||||
msgid "View error logs and submit a bug report"
|
msgid "View error logs and submit a bug report"
|
||||||
msgstr "Katso virhelokit ja lähetä virhe raportti"
|
msgstr "Katso virhelokit ja lähetä virheraportti"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences.html:74
|
#: searx/templates/simple/preferences.html:74
|
||||||
msgid "!bang for this engine"
|
msgid "!bang for this engine"
|
||||||
@@ -960,7 +959,7 @@ msgstr "!bang tälle hakukoneelle"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences.html:80
|
#: searx/templates/simple/preferences.html:80
|
||||||
msgid "!bang for its categories"
|
msgid "!bang for its categories"
|
||||||
msgstr "!bang sen kategorioille"
|
msgstr "!bang sen luokille"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences.html:102
|
#: searx/templates/simple/preferences.html:102
|
||||||
#: searx/templates/simple/stats.html:64
|
#: searx/templates/simple/stats.html:64
|
||||||
@@ -1283,7 +1282,7 @@ msgstr "Tuloksia ei löytynyt. Voit:"
|
|||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:14
|
#: searx/templates/simple/messages/no_results.html:14
|
||||||
msgid "There are no more results. You can try to:"
|
msgid "There are no more results. You can try to:"
|
||||||
msgstr "Tuloksia ei ole enään lisää. Voit yrittää:"
|
msgstr "Ei enempää tuloksia. Voit yrittää:"
|
||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:19
|
#: searx/templates/simple/messages/no_results.html:19
|
||||||
msgid "Refresh the page."
|
msgid "Refresh the page."
|
||||||
@@ -1299,7 +1298,7 @@ msgstr "Vaihtaa käytettävää hakukonetta asetuksista:"
|
|||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:22
|
#: searx/templates/simple/messages/no_results.html:22
|
||||||
msgid "Switch to another instance:"
|
msgid "Switch to another instance:"
|
||||||
msgstr "Vaihtaa toiseen instanssiin:"
|
msgstr "Vaihtaa toiseen palvelimeen:"
|
||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:24
|
#: searx/templates/simple/messages/no_results.html:24
|
||||||
msgid "Search for another query or select another category."
|
msgid "Search for another query or select another category."
|
||||||
@@ -1307,7 +1306,7 @@ msgstr "Hae toista kyselyä tai valitse toinen luokka."
|
|||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:25
|
#: searx/templates/simple/messages/no_results.html:25
|
||||||
msgid "Go back to the previous page using the previous page button."
|
msgid "Go back to the previous page using the previous page button."
|
||||||
msgstr "Palaa edelliselle sivulle käyttäen edellinen sivu-nappia."
|
msgstr "Palaa edelliselle sivulle käyttäen edellinen sivu -painiketta."
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/answerers.html:4
|
#: searx/templates/simple/preferences/answerers.html:4
|
||||||
#: searx/templates/simple/preferences/engines.html:23
|
#: searx/templates/simple/preferences/engines.html:23
|
||||||
@@ -1361,7 +1360,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:3
|
#: searx/templates/simple/preferences/cookies.html:3
|
||||||
msgid "With this list, you can assess the transparency of SearXNG."
|
msgid "With this list, you can assess the transparency of SearXNG."
|
||||||
msgstr ""
|
msgstr "Tämän listan perusteella voit arvioida SearXNG:n läpinäkyvyyttä."
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:9
|
#: searx/templates/simple/preferences/cookies.html:9
|
||||||
msgid "Cookie name"
|
msgid "Cookie name"
|
||||||
@@ -1506,7 +1505,7 @@ msgstr "Kuvat välityspalvelimen kautta"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/image_proxy.html:14
|
#: searx/templates/simple/preferences/image_proxy.html:14
|
||||||
msgid "Proxy image results through SearXNG"
|
msgid "Proxy image results through SearXNG"
|
||||||
msgstr ""
|
msgstr "Välitä kuvatulokset SearXNG-välityspalvelimen kautta"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/infinite_scroll.html:2
|
#: searx/templates/simple/preferences/infinite_scroll.html:2
|
||||||
msgid "Infinite scroll"
|
msgid "Infinite scroll"
|
||||||
@@ -1569,6 +1568,8 @@ msgid ""
|
|||||||
"Perform a search immediately if a category is selected. Disable to select"
|
"Perform a search immediately if a category is selected. Disable to select"
|
||||||
" multiple categories"
|
" multiple categories"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Suorita haku välittömästi, jos luokka on valittu. Poista käytöstä "
|
||||||
|
"valitaksesi useita luokkia"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/theme.html:2
|
#: searx/templates/simple/preferences/theme.html:2
|
||||||
msgid "Theme"
|
msgid "Theme"
|
||||||
|
|||||||
Binary file not shown.
@@ -23,13 +23,15 @@
|
|||||||
# haderachhh <haderachhh@noreply.codeberg.org>, 2025.
|
# haderachhh <haderachhh@noreply.codeberg.org>, 2025.
|
||||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||||
# nhthinh <nhthinh@noreply.codeberg.org>, 2025.
|
# nhthinh <nhthinh@noreply.codeberg.org>, 2025.
|
||||||
|
# recreationalprogamer <recreationalprogamer@noreply.codeberg.org>, 2025.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: searx\n"
|
"Project-Id-Version: searx\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-09-01 12:54+0000\n"
|
"POT-Creation-Date: 2025-09-01 12:54+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-02 04:50+0000\n"
|
"PO-Revision-Date: 2025-09-17 13:09+0000\n"
|
||||||
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
"Last-Translator: recreationalprogamer "
|
||||||
|
"<recreationalprogamer@noreply.codeberg.org>\n"
|
||||||
"Language-Team: Vietnamese <https://translate.codeberg.org/projects/searxng/"
|
"Language-Team: Vietnamese <https://translate.codeberg.org/projects/searxng/"
|
||||||
"searxng/vi/>\n"
|
"searxng/vi/>\n"
|
||||||
"Language: vi\n"
|
"Language: vi\n"
|
||||||
@@ -37,7 +39,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
"X-Generator: Weblate 5.12.2\n"
|
"X-Generator: Weblate 5.13.2\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||||
@@ -53,17 +55,17 @@ msgstr "khác"
|
|||||||
#. CATEGORY_NAMES['FILES']
|
#. CATEGORY_NAMES['FILES']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "files"
|
msgid "files"
|
||||||
msgstr "các thư mục"
|
msgstr "tệp"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['GENERAL']
|
#. CATEGORY_NAMES['GENERAL']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "general"
|
msgid "general"
|
||||||
msgstr "tổng quát"
|
msgstr "chung"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['MUSIC']
|
#. CATEGORY_NAMES['MUSIC']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "music"
|
msgid "music"
|
||||||
msgstr "âm nhạc"
|
msgstr "nhạc"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['SOCIAL_MEDIA']
|
#. CATEGORY_NAMES['SOCIAL_MEDIA']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -73,22 +75,22 @@ msgstr "mạng xã hội"
|
|||||||
#. CATEGORY_NAMES['IMAGES']
|
#. CATEGORY_NAMES['IMAGES']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "images"
|
msgid "images"
|
||||||
msgstr "hình ảnh"
|
msgstr "ảnh"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['VIDEOS']
|
#. CATEGORY_NAMES['VIDEOS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "videos"
|
msgid "videos"
|
||||||
msgstr "băng hình"
|
msgstr "video"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['RADIO']
|
#. CATEGORY_NAMES['RADIO']
|
||||||
#: searx/engines/radio_browser.py:151 searx/searxng.msg
|
#: searx/engines/radio_browser.py:151 searx/searxng.msg
|
||||||
msgid "radio"
|
msgid "radio"
|
||||||
msgstr "máy radio"
|
msgstr "radio"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['TV']
|
#. CATEGORY_NAMES['TV']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "tv"
|
msgid "tv"
|
||||||
msgstr "tivi"
|
msgstr "TV"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['IT']
|
#. CATEGORY_NAMES['IT']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -108,7 +110,7 @@ msgstr "bản đồ"
|
|||||||
#. CATEGORY_NAMES['ONIONS']
|
#. CATEGORY_NAMES['ONIONS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "onions"
|
msgid "onions"
|
||||||
msgstr "định tuyến onion"
|
msgstr "trang onion"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['SCIENCE']
|
#. CATEGORY_NAMES['SCIENCE']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -118,22 +120,22 @@ msgstr "khoa học"
|
|||||||
#. CATEGORY_GROUPS['APPS']
|
#. CATEGORY_GROUPS['APPS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "apps"
|
msgid "apps"
|
||||||
msgstr "Ứng dụng"
|
msgstr "ứng dụng"
|
||||||
|
|
||||||
#. CATEGORY_GROUPS['DICTIONARIES']
|
#. CATEGORY_GROUPS['DICTIONARIES']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "dictionaries"
|
msgid "dictionaries"
|
||||||
msgstr "Từ điển"
|
msgstr "từ điển"
|
||||||
|
|
||||||
#. CATEGORY_GROUPS['LYRICS']
|
#. CATEGORY_GROUPS['LYRICS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "lyrics"
|
msgid "lyrics"
|
||||||
msgstr "Lời bài hát"
|
msgstr "lời bài hát"
|
||||||
|
|
||||||
#. CATEGORY_GROUPS['PACKAGES']
|
#. CATEGORY_GROUPS['PACKAGES']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "packages"
|
msgid "packages"
|
||||||
msgstr "gói kiện/gói hàng"
|
msgstr "gói"
|
||||||
|
|
||||||
#. CATEGORY_GROUPS['Q_A']
|
#. CATEGORY_GROUPS['Q_A']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -143,7 +145,7 @@ msgstr "hỏi đáp"
|
|||||||
#. CATEGORY_GROUPS['REPOS']
|
#. CATEGORY_GROUPS['REPOS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "repos"
|
msgid "repos"
|
||||||
msgstr "kho"
|
msgstr "kho mã"
|
||||||
|
|
||||||
#. CATEGORY_GROUPS['SOFTWARE_WIKIS']
|
#. CATEGORY_GROUPS['SOFTWARE_WIKIS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -153,27 +155,27 @@ msgstr "wiki về phần mềm"
|
|||||||
#. CATEGORY_GROUPS['WEB']
|
#. CATEGORY_GROUPS['WEB']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "web"
|
msgid "web"
|
||||||
msgstr "mạng lưới/mạng"
|
msgstr "mạng"
|
||||||
|
|
||||||
#. CATEGORY_GROUPS['SCIENTIFIC PUBLICATIONS']
|
#. CATEGORY_GROUPS['SCIENTIFIC PUBLICATIONS']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "scientific publications"
|
msgid "scientific publications"
|
||||||
msgstr "các công bố khoa học"
|
msgstr "công bố khoa học"
|
||||||
|
|
||||||
#. STYLE_NAMES['AUTO']
|
#. STYLE_NAMES['AUTO']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "auto"
|
msgid "auto"
|
||||||
msgstr "Tự động"
|
msgstr "tự động"
|
||||||
|
|
||||||
#. STYLE_NAMES['LIGHT']
|
#. STYLE_NAMES['LIGHT']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "light"
|
msgid "light"
|
||||||
msgstr "Sáng"
|
msgstr "sáng"
|
||||||
|
|
||||||
#. STYLE_NAMES['DARK']
|
#. STYLE_NAMES['DARK']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "dark"
|
msgid "dark"
|
||||||
msgstr "Tối"
|
msgstr "tối"
|
||||||
|
|
||||||
#. STYLE_NAMES['BLACK']
|
#. STYLE_NAMES['BLACK']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -498,18 +500,18 @@ msgstr "người đăng ký"
|
|||||||
#. SOCIAL_MEDIA_TERMS['POSTS']
|
#. SOCIAL_MEDIA_TERMS['POSTS']
|
||||||
#: searx/engines/lemmy.py:86 searx/searxng.msg
|
#: searx/engines/lemmy.py:86 searx/searxng.msg
|
||||||
msgid "posts"
|
msgid "posts"
|
||||||
msgstr "những bài đăng"
|
msgstr "bài đăng"
|
||||||
|
|
||||||
#. SOCIAL_MEDIA_TERMS['ACTIVE USERS']
|
#. SOCIAL_MEDIA_TERMS['ACTIVE USERS']
|
||||||
#: searx/engines/lemmy.py:87 searx/searxng.msg
|
#: searx/engines/lemmy.py:87 searx/searxng.msg
|
||||||
msgid "active users"
|
msgid "active users"
|
||||||
msgstr "Người dùng hoạt động"
|
msgstr "người dùng hoạt động"
|
||||||
|
|
||||||
#. SOCIAL_MEDIA_TERMS['COMMENTS']
|
#. SOCIAL_MEDIA_TERMS['COMMENTS']
|
||||||
#: searx/engines/discourse.py:157 searx/engines/hackernews.py:82
|
#: searx/engines/discourse.py:157 searx/engines/hackernews.py:82
|
||||||
#: searx/engines/lemmy.py:130 searx/searxng.msg
|
#: searx/engines/lemmy.py:130 searx/searxng.msg
|
||||||
msgid "comments"
|
msgid "comments"
|
||||||
msgstr "Bình luận"
|
msgstr "bình luận"
|
||||||
|
|
||||||
#. SOCIAL_MEDIA_TERMS['USER']
|
#. SOCIAL_MEDIA_TERMS['USER']
|
||||||
#: searx/engines/lemmy.py:129 searx/engines/lemmy.py:164 searx/searxng.msg
|
#: searx/engines/lemmy.py:129 searx/engines/lemmy.py:164 searx/searxng.msg
|
||||||
@@ -578,7 +580,7 @@ msgstr "lỗi tìm kiếm"
|
|||||||
|
|
||||||
#: searx/webutils.py:36
|
#: searx/webutils.py:36
|
||||||
msgid "timeout"
|
msgid "timeout"
|
||||||
msgstr "Hết thời gian"
|
msgstr "hết thời gian"
|
||||||
|
|
||||||
#: searx/webutils.py:37
|
#: searx/webutils.py:37
|
||||||
msgid "parsing error"
|
msgid "parsing error"
|
||||||
@@ -590,7 +592,7 @@ msgstr "Lỗi giao thức HTTP"
|
|||||||
|
|
||||||
#: searx/webutils.py:39
|
#: searx/webutils.py:39
|
||||||
msgid "network error"
|
msgid "network error"
|
||||||
msgstr "Lỗi mạng"
|
msgstr "lỗi mạng"
|
||||||
|
|
||||||
#: searx/webutils.py:40
|
#: searx/webutils.py:40
|
||||||
msgid "SSL error: certificate validation has failed"
|
msgid "SSL error: certificate validation has failed"
|
||||||
@@ -610,7 +612,7 @@ msgstr "Lỗi kết nối HTTP"
|
|||||||
|
|
||||||
#: searx/webutils.py:56
|
#: searx/webutils.py:56
|
||||||
msgid "proxy error"
|
msgid "proxy error"
|
||||||
msgstr "Lỗi proxy"
|
msgstr "lỗi proxy"
|
||||||
|
|
||||||
#: searx/webutils.py:57
|
#: searx/webutils.py:57
|
||||||
msgid "CAPTCHA"
|
msgid "CAPTCHA"
|
||||||
@@ -622,11 +624,11 @@ msgstr "quá nhiều yêu cầu"
|
|||||||
|
|
||||||
#: searx/webutils.py:59
|
#: searx/webutils.py:59
|
||||||
msgid "access denied"
|
msgid "access denied"
|
||||||
msgstr "Truy cập bị từ chối"
|
msgstr "truy cập bị từ chối"
|
||||||
|
|
||||||
#: searx/webutils.py:60
|
#: searx/webutils.py:60
|
||||||
msgid "server API error"
|
msgid "server API error"
|
||||||
msgstr "Lỗi máy chủ API"
|
msgstr "lỗi máy chủ API"
|
||||||
|
|
||||||
#: searx/webutils.py:79
|
#: searx/webutils.py:79
|
||||||
msgid "Suspended"
|
msgid "Suspended"
|
||||||
@@ -635,7 +637,7 @@ msgstr "Treo/gián đoạn/chặn"
|
|||||||
#: searx/webutils.py:306
|
#: searx/webutils.py:306
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{minutes} minute(s) ago"
|
msgid "{minutes} minute(s) ago"
|
||||||
msgstr "{minutes} phút() trước"
|
msgstr "{minutes} phút trước"
|
||||||
|
|
||||||
#: searx/webutils.py:307
|
#: searx/webutils.py:307
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@@ -649,7 +651,7 @@ msgstr "Tạo các giá trị ngẫu nhiên khác nhau"
|
|||||||
#: searx/answerers/statistics.py:37
|
#: searx/answerers/statistics.py:37
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Compute {func} of the arguments"
|
msgid "Compute {func} of the arguments"
|
||||||
msgstr "Tính toán {func} của các tham số"
|
msgstr "Tính toán {func} của các đối số"
|
||||||
|
|
||||||
#: searx/engines/openstreetmap.py:158
|
#: searx/engines/openstreetmap.py:158
|
||||||
msgid "Show route in map .."
|
msgid "Show route in map .."
|
||||||
@@ -662,7 +664,7 @@ msgstr "{title} (HẾT HẠN)"
|
|||||||
|
|
||||||
#: searx/engines/pdbe.py:103
|
#: searx/engines/pdbe.py:103
|
||||||
msgid "This entry has been superseded by"
|
msgid "This entry has been superseded by"
|
||||||
msgstr "Mục này đã được thay thế bởi"
|
msgstr "Mục này đã được thay thế bằng"
|
||||||
|
|
||||||
#: searx/engines/qwant.py:293
|
#: searx/engines/qwant.py:293
|
||||||
msgid "Channel"
|
msgid "Channel"
|
||||||
@@ -713,7 +715,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: searx/engines/tineye.py:59
|
#: searx/engines/tineye.py:59
|
||||||
msgid "The image could not be downloaded."
|
msgid "The image could not be downloaded."
|
||||||
msgstr "Hình ảnh không thể được hiển thị."
|
msgstr "Không thể tải hình ảnh."
|
||||||
|
|
||||||
#: searx/engines/zlibrary.py:138
|
#: searx/engines/zlibrary.py:138
|
||||||
msgid "Book rating"
|
msgid "Book rating"
|
||||||
@@ -725,7 +727,7 @@ msgstr "Chất lượng tệp"
|
|||||||
|
|
||||||
#: searx/plugins/ahmia_filter.py:32
|
#: searx/plugins/ahmia_filter.py:32
|
||||||
msgid "Ahmia blacklist"
|
msgid "Ahmia blacklist"
|
||||||
msgstr "Ahmia blacklist"
|
msgstr "Danh sách đen của Ahmia"
|
||||||
|
|
||||||
#: searx/plugins/ahmia_filter.py:33
|
#: searx/plugins/ahmia_filter.py:33
|
||||||
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
msgid "Filter out onion results that appear in Ahmia's blacklist."
|
||||||
@@ -748,8 +750,8 @@ msgid ""
|
|||||||
"Converts strings to different hash digests. Available functions: md5, "
|
"Converts strings to different hash digests. Available functions: md5, "
|
||||||
"sha1, sha224, sha256, sha384, sha512."
|
"sha1, sha224, sha256, sha384, sha512."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Chuyển đổi chuỗi thành các giá trị băm với các hàm hỗ trợ: md5, sha1, "
|
"Chuyển đổi xâu thành các giá trị băm với các hàm hỗ trợ: md5, sha1, sha224, "
|
||||||
"sha224, sha256, sha384, sha512."
|
"sha256, sha384, sha512."
|
||||||
|
|
||||||
#: searx/plugins/hash_plugin.py:64
|
#: searx/plugins/hash_plugin.py:64
|
||||||
msgid "hash digest"
|
msgid "hash digest"
|
||||||
@@ -797,15 +799,15 @@ msgstr "Tác nhân người dùng của bạn là: "
|
|||||||
|
|
||||||
#: searx/plugins/time_zone.py:34
|
#: searx/plugins/time_zone.py:34
|
||||||
msgid "Timezones plugin"
|
msgid "Timezones plugin"
|
||||||
msgstr ""
|
msgstr "Tiện ích múi giờ"
|
||||||
|
|
||||||
#: searx/plugins/time_zone.py:35
|
#: searx/plugins/time_zone.py:35
|
||||||
msgid "Display the current time on different time zones."
|
msgid "Display the current time on different time zones."
|
||||||
msgstr ""
|
msgstr "HIện thời gian hiện tại trong nhiều múi giờ khác nhau."
|
||||||
|
|
||||||
#: searx/plugins/tor_check.py:42
|
#: searx/plugins/tor_check.py:42
|
||||||
msgid "Tor check plugin"
|
msgid "Tor check plugin"
|
||||||
msgstr "Kiểm tra Tor plugin"
|
msgstr "Plugin kiểm tra Tor"
|
||||||
|
|
||||||
#: searx/plugins/tor_check.py:44
|
#: searx/plugins/tor_check.py:44
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -818,7 +820,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: searx/plugins/tor_check.py:65
|
#: searx/plugins/tor_check.py:65
|
||||||
msgid "Could not download the list of Tor exit-nodes from"
|
msgid "Could not download the list of Tor exit-nodes from"
|
||||||
msgstr "Không thể tải xuống danh sách các đoạn trích xuất từ TOR từ"
|
msgstr "Không thể tải xuống danh sách các nút thoát Tor từ"
|
||||||
|
|
||||||
#: searx/plugins/tor_check.py:72
|
#: searx/plugins/tor_check.py:72
|
||||||
msgid "You are using Tor and it looks like you have the external IP address"
|
msgid "You are using Tor and it looks like you have the external IP address"
|
||||||
@@ -877,7 +879,7 @@ msgstr "Được cung cấp bởi"
|
|||||||
|
|
||||||
#: searx/templates/simple/base.html:64
|
#: searx/templates/simple/base.html:64
|
||||||
msgid "a privacy-respecting, open metasearch engine"
|
msgid "a privacy-respecting, open metasearch engine"
|
||||||
msgstr "một siêu công cụ tìm kiếm mã nguồn mỡ và tôn trọng quyền riêng tư"
|
msgstr "một siêu công cụ tìm kiếm mã nguồn mở và tôn trọng quyền riêng tư"
|
||||||
|
|
||||||
#: searx/templates/simple/base.html:65
|
#: searx/templates/simple/base.html:65
|
||||||
#: searx/templates/simple/result_templates/packages.html:59
|
#: searx/templates/simple/result_templates/packages.html:59
|
||||||
@@ -886,11 +888,11 @@ msgstr "Mã nguồn"
|
|||||||
|
|
||||||
#: searx/templates/simple/base.html:66
|
#: searx/templates/simple/base.html:66
|
||||||
msgid "Issue tracker"
|
msgid "Issue tracker"
|
||||||
msgstr "công cụ theo dõi các trục trặc"
|
msgstr "Trang theo dõi vấn đề"
|
||||||
|
|
||||||
#: searx/templates/simple/base.html:67 searx/templates/simple/stats.html:18
|
#: searx/templates/simple/base.html:67 searx/templates/simple/stats.html:18
|
||||||
msgid "Engine stats"
|
msgid "Engine stats"
|
||||||
msgstr "Các thông số về trình tìm kiếm"
|
msgstr "Thông số về trình tìm kiếm"
|
||||||
|
|
||||||
#: searx/templates/simple/base.html:69
|
#: searx/templates/simple/base.html:69
|
||||||
msgid "Public instances"
|
msgid "Public instances"
|
||||||
@@ -902,7 +904,7 @@ msgstr "Chính sách bảo mật"
|
|||||||
|
|
||||||
#: searx/templates/simple/base.html:75
|
#: searx/templates/simple/base.html:75
|
||||||
msgid "Contact instance maintainer"
|
msgid "Contact instance maintainer"
|
||||||
msgstr "Liên hệ người bảo toàn thực thể"
|
msgstr "Liên hệ người bảo trì thực thể"
|
||||||
|
|
||||||
#: searx/templates/simple/categories.html:30
|
#: searx/templates/simple/categories.html:30
|
||||||
msgid "Click on the magnifier to perform search"
|
msgid "Click on the magnifier to perform search"
|
||||||
@@ -986,7 +988,7 @@ msgstr "P95"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences.html:136
|
#: searx/templates/simple/preferences.html:136
|
||||||
msgid "Failed checker test(s): "
|
msgid "Failed checker test(s): "
|
||||||
msgstr "Số bài kiểm định đã thất bại của công cụ kiểm tra: "
|
msgstr "Số bài kiểm định thất bại: "
|
||||||
|
|
||||||
#: searx/templates/simple/preferences.html:138
|
#: searx/templates/simple/preferences.html:138
|
||||||
msgid "Errors:"
|
msgid "Errors:"
|
||||||
@@ -1131,19 +1133,19 @@ msgstr "Tham số"
|
|||||||
#: searx/templates/simple/result_templates/files.html:36
|
#: searx/templates/simple/result_templates/files.html:36
|
||||||
#: searx/templates/simple/stats.html:119
|
#: searx/templates/simple/stats.html:119
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
msgstr "Tên file"
|
msgstr "Tên tệp"
|
||||||
|
|
||||||
#: searx/templates/simple/stats.html:120
|
#: searx/templates/simple/stats.html:120
|
||||||
msgid "Function"
|
msgid "Function"
|
||||||
msgstr "Chức năng"
|
msgstr "Hàm"
|
||||||
|
|
||||||
#: searx/templates/simple/stats.html:121
|
#: searx/templates/simple/stats.html:121
|
||||||
msgid "Code"
|
msgid "Code"
|
||||||
msgstr "Code"
|
msgstr "Mã nguồn"
|
||||||
|
|
||||||
#: searx/templates/simple/stats.html:128
|
#: searx/templates/simple/stats.html:128
|
||||||
msgid "Checker"
|
msgid "Checker"
|
||||||
msgstr "Người kiểm duyệt"
|
msgstr "Trình kiểm tra"
|
||||||
|
|
||||||
#: searx/templates/simple/stats.html:131
|
#: searx/templates/simple/stats.html:131
|
||||||
msgid "Failed test"
|
msgid "Failed test"
|
||||||
@@ -1156,7 +1158,7 @@ msgstr "Bình luận"
|
|||||||
#: searx/templates/simple/answer/translations.html:12
|
#: searx/templates/simple/answer/translations.html:12
|
||||||
#: searx/templates/simple/preferences/answerers.html:8
|
#: searx/templates/simple/preferences/answerers.html:8
|
||||||
msgid "Examples"
|
msgid "Examples"
|
||||||
msgstr "Các ví dụ"
|
msgstr "Ví dụ"
|
||||||
|
|
||||||
#: searx/templates/simple/answer/translations.html:21
|
#: searx/templates/simple/answer/translations.html:21
|
||||||
msgid "Definitions"
|
msgid "Definitions"
|
||||||
@@ -1206,7 +1208,7 @@ msgstr "Sao chép"
|
|||||||
|
|
||||||
#: searx/templates/simple/elements/suggestions.html:3
|
#: searx/templates/simple/elements/suggestions.html:3
|
||||||
msgid "Suggestions"
|
msgid "Suggestions"
|
||||||
msgstr "Các gợi ý"
|
msgstr "Gợi ý"
|
||||||
|
|
||||||
#: searx/templates/simple/filters/languages.html:1
|
#: searx/templates/simple/filters/languages.html:1
|
||||||
#: searx/templates/simple/preferences/language.html:2
|
#: searx/templates/simple/preferences/language.html:2
|
||||||
@@ -1270,7 +1272,7 @@ msgstr "Tháng trước"
|
|||||||
|
|
||||||
#: searx/templates/simple/filters/time_range.html:15
|
#: searx/templates/simple/filters/time_range.html:15
|
||||||
msgid "Last year"
|
msgid "Last year"
|
||||||
msgstr "Năm ngoái"
|
msgstr "Năm trước"
|
||||||
|
|
||||||
#: searx/templates/simple/messages/no_cookies.html:3
|
#: searx/templates/simple/messages/no_cookies.html:3
|
||||||
msgid "Information!"
|
msgid "Information!"
|
||||||
@@ -1302,7 +1304,7 @@ msgstr "Tìm kiếm truy vấn khác hoặc chọn danh mục khác (ở phía t
|
|||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:21
|
#: searx/templates/simple/messages/no_results.html:21
|
||||||
msgid "Change the search engine used in the preferences:"
|
msgid "Change the search engine used in the preferences:"
|
||||||
msgstr "Thay đổi công cụ tìm kiếm sẽ được dùng trong phần tùy chọn:"
|
msgstr "Thay đổi công cụ tìm kiếm được dùng trong phần tùy chọn:"
|
||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:22
|
#: searx/templates/simple/messages/no_results.html:22
|
||||||
msgid "Switch to another instance:"
|
msgid "Switch to another instance:"
|
||||||
@@ -1314,7 +1316,7 @@ msgstr "Tìm kiếm bằng truy vấn khác hoặc chọn mục khác."
|
|||||||
|
|
||||||
#: searx/templates/simple/messages/no_results.html:25
|
#: searx/templates/simple/messages/no_results.html:25
|
||||||
msgid "Go back to the previous page using the previous page button."
|
msgid "Go back to the previous page using the previous page button."
|
||||||
msgstr "Quay lại trang t rước bằng nút bấm trang trước"
|
msgstr "Quay lại trang trước bằng nút bấm trang trước"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/answerers.html:4
|
#: searx/templates/simple/preferences/answerers.html:4
|
||||||
#: searx/templates/simple/preferences/engines.html:23
|
#: searx/templates/simple/preferences/engines.html:23
|
||||||
@@ -1340,7 +1342,7 @@ msgstr "Danh sách các mô-đun trả lời nhanh của SearXNG."
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/answerers.html:29
|
#: searx/templates/simple/preferences/answerers.html:29
|
||||||
msgid "This is the list of plugins."
|
msgid "This is the list of plugins."
|
||||||
msgstr "Danh sách các plugins."
|
msgstr "Danh sách các plugin."
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/autocomplete.html:2
|
#: searx/templates/simple/preferences/autocomplete.html:2
|
||||||
msgid "Autocomplete"
|
msgid "Autocomplete"
|
||||||
@@ -1356,15 +1358,15 @@ msgstr "Căn giữa"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/center_alignment.html:14
|
#: searx/templates/simple/preferences/center_alignment.html:14
|
||||||
msgid "Display results in the center of the page (Oscar layout)."
|
msgid "Display results in the center of the page (Oscar layout)."
|
||||||
msgstr "Hiển thị kết quả ở giữa trang (Oscar layout)."
|
msgstr "Hiển thị kết quả ở giữa trang (bố cục Oscar)."
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:2
|
#: searx/templates/simple/preferences/cookies.html:2
|
||||||
msgid ""
|
msgid ""
|
||||||
"This is the list of cookies and their values SearXNG is storing on your "
|
"This is the list of cookies and their values SearXNG is storing on your "
|
||||||
"computer."
|
"computer."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Danh sách tên và giá trị của những cookies mà SearXNG lưu trữ trên máy "
|
"Danh sách tên và giá trị của những cookie mà SearXNG lưu trữ trên máy tính "
|
||||||
"tính của bạn."
|
"của bạn."
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:3
|
#: searx/templates/simple/preferences/cookies.html:3
|
||||||
msgid "With this list, you can assess the transparency of SearXNG."
|
msgid "With this list, you can assess the transparency of SearXNG."
|
||||||
@@ -1387,15 +1389,12 @@ msgid ""
|
|||||||
"Note: specifying custom settings in the search URL can reduce privacy by "
|
"Note: specifying custom settings in the search URL can reduce privacy by "
|
||||||
"leaking data to the clicked result sites."
|
"leaking data to the clicked result sites."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ghi chú: việc định rõ cài đặt cá nhân trong URL tìm kiếm có thể làm suy "
|
"Lưu ý: việc định rõ cài đặt cá nhân trong URL tìm kiếm có thể làm suy giảm "
|
||||||
"giảm mức độ riêng tư vì nó chuyển dữ liệu đến các trang kết quả được nhấp"
|
"mức độ riêng tư vì nó chuyển dữ liệu đến các trang kết quả được nhấp chọn."
|
||||||
" chọn."
|
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:35
|
#: searx/templates/simple/preferences/cookies.html:35
|
||||||
msgid "URL to restore your preferences in another browser"
|
msgid "URL to restore your preferences in another browser"
|
||||||
msgstr ""
|
msgstr "URL dùng để khôi phục những tuỳ chọn của bạn trong một trình duyệt khác"
|
||||||
"URL dùng để khôi phục những lựa chọn ưu tiên của bạn trong một trình "
|
|
||||||
"duyệt khác"
|
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:43
|
#: searx/templates/simple/preferences/cookies.html:43
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -1407,7 +1406,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:46
|
#: searx/templates/simple/preferences/cookies.html:46
|
||||||
msgid "Copy preferences hash"
|
msgid "Copy preferences hash"
|
||||||
msgstr "Sao chép những mã băm được ưu tiên"
|
msgstr "Sao chép mã băm tuỳ chọn"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:57
|
#: searx/templates/simple/preferences/cookies.html:57
|
||||||
msgid "Insert copied preferences hash (without URL) to restore"
|
msgid "Insert copied preferences hash (without URL) to restore"
|
||||||
@@ -1415,7 +1414,7 @@ msgstr "Nhập mã băm của tùy chọn đã sao chép (không kèm URL) để
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:59
|
#: searx/templates/simple/preferences/cookies.html:59
|
||||||
msgid "Preferences hash"
|
msgid "Preferences hash"
|
||||||
msgstr "Mã băm được ưu tiên"
|
msgstr "Mã băm tuỳ chọn"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/doi_resolver.html:1
|
#: searx/templates/simple/preferences/doi_resolver.html:1
|
||||||
msgid "Digital Object Identifier (DOI)"
|
msgid "Digital Object Identifier (DOI)"
|
||||||
@@ -1502,7 +1501,7 @@ msgstr "Phím tắt"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/hotkeys.html:13
|
#: searx/templates/simple/preferences/hotkeys.html:13
|
||||||
msgid "Vim-like"
|
msgid "Vim-like"
|
||||||
msgstr "Vim-like"
|
msgstr "Giống Vim"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/hotkeys.html:18
|
#: searx/templates/simple/preferences/hotkeys.html:18
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -1568,7 +1567,7 @@ msgstr "Mở các liên kết của kết quả trong tab trình duyệt mới"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/safesearch.html:20
|
#: searx/templates/simple/preferences/safesearch.html:20
|
||||||
msgid "Filter content"
|
msgid "Filter content"
|
||||||
msgstr "Lọc các nội dung"
|
msgstr "Lọc nội dung"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/search_on_category_select.html:2
|
#: searx/templates/simple/preferences/search_on_category_select.html:2
|
||||||
msgid "Search on category select"
|
msgid "Search on category select"
|
||||||
@@ -1600,11 +1599,11 @@ msgstr "Chọn tự động để tuân thủ cài đặt của trình duyệt"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/tokens.html:2
|
#: searx/templates/simple/preferences/tokens.html:2
|
||||||
msgid "Engine tokens"
|
msgid "Engine tokens"
|
||||||
msgstr "Các vé của công cụ tìm kiếm"
|
msgstr "Mã truy cập của công cụ tìm kiếm"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/tokens.html:9
|
#: searx/templates/simple/preferences/tokens.html:9
|
||||||
msgid "Access tokens for private engines"
|
msgid "Access tokens for private engines"
|
||||||
msgstr "Truy cập các vé cho các công cụ tìm kiếm riêng tư"
|
msgstr "Mã truy cập cho các công cụ tìm kiếm riêng tư"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/ui_locale.html:2
|
#: searx/templates/simple/preferences/ui_locale.html:2
|
||||||
msgid "Interface language"
|
msgid "Interface language"
|
||||||
@@ -1632,7 +1631,7 @@ msgstr "Máy chủ"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/urlformatting.html:23
|
#: searx/templates/simple/preferences/urlformatting.html:23
|
||||||
msgid "Change result URL formatting"
|
msgid "Change result URL formatting"
|
||||||
msgstr "Thay đổi kết quả định dạng URL"
|
msgstr "Thay đổi định dạng URL kết quả"
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/code.html:13
|
#: searx/templates/simple/result_templates/code.html:13
|
||||||
msgid "Repository"
|
msgid "Repository"
|
||||||
@@ -1652,13 +1651,13 @@ msgstr "ẩn nội dung"
|
|||||||
#: searx/templates/simple/result_templates/default.html:14
|
#: searx/templates/simple/result_templates/default.html:14
|
||||||
#: searx/templates/simple/result_templates/videos.html:14
|
#: searx/templates/simple/result_templates/videos.html:14
|
||||||
msgid "This site did not provide any description."
|
msgid "This site did not provide any description."
|
||||||
msgstr "Trang web này không cung cấp bất kỳ mô tả."
|
msgstr "Trang web này không cung cấp bất kỳ mô tả nào."
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/files.html:38
|
#: searx/templates/simple/result_templates/files.html:38
|
||||||
#: searx/templates/simple/result_templates/images.html:22
|
#: searx/templates/simple/result_templates/images.html:22
|
||||||
#: searx/templates/simple/result_templates/torrent.html:18
|
#: searx/templates/simple/result_templates/torrent.html:18
|
||||||
msgid "Filesize"
|
msgid "Filesize"
|
||||||
msgstr "Kích thước tập tin"
|
msgstr "Kích cỡ tệp"
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/files.html:40
|
#: searx/templates/simple/result_templates/files.html:40
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
@@ -1687,7 +1686,7 @@ msgstr "Xem nguồn"
|
|||||||
|
|
||||||
#: searx/templates/simple/result_templates/map.html:12
|
#: searx/templates/simple/result_templates/map.html:12
|
||||||
msgid "address"
|
msgid "address"
|
||||||
msgstr "Địa chỉ"
|
msgstr "địa chỉ"
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/map.html:43
|
#: searx/templates/simple/result_templates/map.html:43
|
||||||
msgid "show map"
|
msgid "show map"
|
||||||
@@ -1773,7 +1772,7 @@ msgstr "liên kết magnet"
|
|||||||
|
|
||||||
#: searx/templates/simple/result_templates/torrent.html:8
|
#: searx/templates/simple/result_templates/torrent.html:8
|
||||||
msgid "torrent file"
|
msgid "torrent file"
|
||||||
msgstr "tập tin torrent"
|
msgstr "tệp torrent"
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/torrent.html:13
|
#: searx/templates/simple/result_templates/torrent.html:13
|
||||||
msgid "Seeder"
|
msgid "Seeder"
|
||||||
@@ -1785,15 +1784,15 @@ msgstr "Leecher"
|
|||||||
|
|
||||||
#: searx/templates/simple/result_templates/torrent.html:19
|
#: searx/templates/simple/result_templates/torrent.html:19
|
||||||
msgid "Number of Files"
|
msgid "Number of Files"
|
||||||
msgstr "Số lượng tập tin"
|
msgstr "Số lượng tệp"
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/videos.html:6
|
#: searx/templates/simple/result_templates/videos.html:6
|
||||||
msgid "show video"
|
msgid "show video"
|
||||||
msgstr "hiện"
|
msgstr "hiện video"
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/videos.html:6
|
#: searx/templates/simple/result_templates/videos.html:6
|
||||||
msgid "hide video"
|
msgid "hide video"
|
||||||
msgstr "ẩn phim"
|
msgstr "ẩn video"
|
||||||
|
|
||||||
#~ msgid "Engine time (sec)"
|
#~ msgid "Engine time (sec)"
|
||||||
#~ msgstr "Thời gian trình tìm kiếm (giây)"
|
#~ msgstr "Thời gian trình tìm kiếm (giây)"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ from datetime import timedelta
|
|||||||
from markdown_it import MarkdownIt
|
from markdown_it import MarkdownIt
|
||||||
|
|
||||||
from lxml import html
|
from lxml import html
|
||||||
from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError
|
from lxml.etree import XPath, XPathError, XPathSyntaxError
|
||||||
|
from lxml.etree import ElementBase, _Element # pyright: ignore[reportPrivateUsage]
|
||||||
|
|
||||||
from searx import settings
|
from searx import settings
|
||||||
from searx.data import USER_AGENTS, data_dir
|
from searx.data import USER_AGENTS, data_dir
|
||||||
@@ -40,6 +41,9 @@ XPathSpecType: t.TypeAlias = str | XPath
|
|||||||
"""Type alias used by :py:obj:`searx.utils.get_xpath`,
|
"""Type alias used by :py:obj:`searx.utils.get_xpath`,
|
||||||
:py:obj:`searx.utils.eval_xpath` and other XPath selectors."""
|
:py:obj:`searx.utils.eval_xpath` and other XPath selectors."""
|
||||||
|
|
||||||
|
ElementType: t.TypeAlias = ElementBase | _Element
|
||||||
|
|
||||||
|
|
||||||
_BLOCKED_TAGS = ('script', 'style')
|
_BLOCKED_TAGS = ('script', 'style')
|
||||||
|
|
||||||
_ECMA_UNESCAPE4_RE = re.compile(r'%u([0-9a-fA-F]{4})', re.UNICODE)
|
_ECMA_UNESCAPE4_RE = re.compile(r'%u([0-9a-fA-F]{4})', re.UNICODE)
|
||||||
@@ -204,15 +208,23 @@ def markdown_to_text(markdown_str: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def extract_text(
|
def extract_text(
|
||||||
xpath_results: list[ElementBase] | ElementBase | str | Number | bool | None,
|
xpath_results: list[ElementType] | ElementType | str | Number | bool | None,
|
||||||
allow_none: bool = False,
|
allow_none: bool = False,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Extract text from a lxml result
|
"""Extract text from a lxml result
|
||||||
|
|
||||||
* if xpath_results is list, extract the text from each result and concat the list
|
- If ``xpath_results`` is a list of :py:obj:`ElementType` objects, extract
|
||||||
* if xpath_results is a xml element, extract all the text node from it
|
the text from each result and concatenate the list in a string.
|
||||||
( text_content() method from lxml )
|
|
||||||
* if xpath_results is a string element, then it's already done
|
- If ``xpath_results`` is a :py:obj:`ElementType` object, extract all the
|
||||||
|
text node from it ( :py:obj:`lxml.html.tostring`, ``method="text"`` )
|
||||||
|
|
||||||
|
- If ``xpath_results`` is of type :py:obj:`str` or :py:obj:`Number`,
|
||||||
|
:py:obj:`bool` the string value is returned.
|
||||||
|
|
||||||
|
- If ``xpath_results`` is of type ``None`` a :py:obj:`ValueError` is raised,
|
||||||
|
except ``allow_none`` is ``True`` where ``None`` is returned.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(xpath_results, list):
|
if isinstance(xpath_results, list):
|
||||||
# it's list of result : concat everything using recursive call
|
# it's list of result : concat everything using recursive call
|
||||||
@@ -220,7 +232,7 @@ def extract_text(
|
|||||||
for e in xpath_results:
|
for e in xpath_results:
|
||||||
result = result + (extract_text(e) or '')
|
result = result + (extract_text(e) or '')
|
||||||
return result.strip()
|
return result.strip()
|
||||||
if isinstance(xpath_results, ElementBase):
|
if isinstance(xpath_results, ElementType):
|
||||||
# it's a element
|
# it's a element
|
||||||
text: str = html.tostring( # type: ignore
|
text: str = html.tostring( # type: ignore
|
||||||
xpath_results, # pyright: ignore[reportArgumentType]
|
xpath_results, # pyright: ignore[reportArgumentType]
|
||||||
@@ -289,7 +301,7 @@ def normalize_url(url: str, base_url: str) -> str:
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def extract_url(xpath_results: list[ElementBase] | ElementBase | str | Number | bool | None, base_url: str) -> str:
|
def extract_url(xpath_results: list[ElementType] | ElementType | str | Number | bool | None, base_url: str) -> str:
|
||||||
"""Extract and normalize URL from lxml Element
|
"""Extract and normalize URL from lxml Element
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@@ -520,7 +532,7 @@ def get_xpath(xpath_spec: XPathSpecType) -> XPath:
|
|||||||
raise TypeError('xpath_spec must be either a str or a lxml.etree.XPath') # pyright: ignore[reportUnreachable]
|
raise TypeError('xpath_spec must be either a str or a lxml.etree.XPath') # pyright: ignore[reportUnreachable]
|
||||||
|
|
||||||
|
|
||||||
def eval_xpath(element: ElementBase, xpath_spec: XPathSpecType) -> t.Any:
|
def eval_xpath(element: ElementType, xpath_spec: XPathSpecType) -> t.Any:
|
||||||
"""Equivalent of ``element.xpath(xpath_str)`` but compile ``xpath_str`` into
|
"""Equivalent of ``element.xpath(xpath_str)`` but compile ``xpath_str`` into
|
||||||
a :py:obj:`lxml.etree.XPath` object once for all. The return value of
|
a :py:obj:`lxml.etree.XPath` object once for all. The return value of
|
||||||
``xpath(..)`` is complex, read `XPath return values`_ for more details.
|
``xpath(..)`` is complex, read `XPath return values`_ for more details.
|
||||||
@@ -548,12 +560,12 @@ def eval_xpath(element: ElementBase, xpath_spec: XPathSpecType) -> t.Any:
|
|||||||
raise SearxEngineXPathException(xpath_spec, arg) from e
|
raise SearxEngineXPathException(xpath_spec, arg) from e
|
||||||
|
|
||||||
|
|
||||||
def eval_xpath_list(element: ElementBase, xpath_spec: XPathSpecType, min_len: int | None = None) -> list[t.Any]:
|
def eval_xpath_list(element: ElementType, xpath_spec: XPathSpecType, min_len: int | None = None) -> list[t.Any]:
|
||||||
"""Same as :py:obj:`searx.utils.eval_xpath`, but additionally ensures the
|
"""Same as :py:obj:`searx.utils.eval_xpath`, but additionally ensures the
|
||||||
return value is a :py:obj:`list`. The minimum length of the list is also
|
return value is a :py:obj:`list`. The minimum length of the list is also
|
||||||
checked (if ``min_len`` is set)."""
|
checked (if ``min_len`` is set)."""
|
||||||
|
|
||||||
result = eval_xpath(element, xpath_spec)
|
result: list[t.Any] = eval_xpath(element, xpath_spec)
|
||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
raise SearxEngineXPathException(xpath_spec, 'the result is not a list')
|
raise SearxEngineXPathException(xpath_spec, 'the result is not a list')
|
||||||
if min_len is not None and min_len > len(result):
|
if min_len is not None and min_len > len(result):
|
||||||
@@ -562,7 +574,7 @@ def eval_xpath_list(element: ElementBase, xpath_spec: XPathSpecType, min_len: in
|
|||||||
|
|
||||||
|
|
||||||
def eval_xpath_getindex(
|
def eval_xpath_getindex(
|
||||||
element: ElementBase,
|
element: ElementType,
|
||||||
xpath_spec: XPathSpecType,
|
xpath_spec: XPathSpecType,
|
||||||
index: int,
|
index: int,
|
||||||
default: t.Any = _NOTSET,
|
default: t.Any = _NOTSET,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import babel
|
|||||||
import babel.numbers
|
import babel.numbers
|
||||||
import babel.dates
|
import babel.dates
|
||||||
import babel.languages
|
import babel.languages
|
||||||
|
import flask_babel
|
||||||
|
|
||||||
from searx import network
|
from searx import network
|
||||||
from searx.cache import ExpireCache, ExpireCacheCfg
|
from searx.cache import ExpireCache, ExpireCacheCfg
|
||||||
@@ -197,6 +198,7 @@ class GeoLocation:
|
|||||||
|
|
||||||
|
|
||||||
DateTimeFormats = typing.Literal["full", "long", "medium", "short"]
|
DateTimeFormats = typing.Literal["full", "long", "medium", "short"]
|
||||||
|
DateTimeLocaleTypes = typing.Literal["UI"]
|
||||||
|
|
||||||
|
|
||||||
@typing.final
|
@typing.final
|
||||||
@@ -205,6 +207,13 @@ class DateTime:
|
|||||||
conveniently combines :py:obj:`datetime.datetime` and
|
conveniently combines :py:obj:`datetime.datetime` and
|
||||||
:py:obj:`babel.dates.format_datetime`. A conversion of time zones is not
|
:py:obj:`babel.dates.format_datetime`. A conversion of time zones is not
|
||||||
provided (in the current version).
|
provided (in the current version).
|
||||||
|
|
||||||
|
The localized string representation can be obtained via the
|
||||||
|
:py:obj:`DateTime.l10n` and :py:obj:`DateTime.l10n_date` methods, where the
|
||||||
|
``locale`` parameter defaults to the search language. Alternatively, a
|
||||||
|
:py:obj:`GeoLocation` or a :py:obj:`babel.Locale` instance can be passed
|
||||||
|
directly. If the UI language is to be used, the string ``UI`` can be passed
|
||||||
|
as the value for the ``locale``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, time: datetime.datetime):
|
def __init__(self, time: datetime.datetime):
|
||||||
@@ -216,15 +225,32 @@ class DateTime:
|
|||||||
def l10n(
|
def l10n(
|
||||||
self,
|
self,
|
||||||
fmt: DateTimeFormats | str = "medium",
|
fmt: DateTimeFormats | str = "medium",
|
||||||
locale: babel.Locale | GeoLocation | None = None,
|
locale: DateTimeLocaleTypes | babel.Locale | GeoLocation | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Localized representation of date & time."""
|
"""Localized representation of date & time."""
|
||||||
if isinstance(locale, GeoLocation):
|
if isinstance(locale, str) and locale == "UI":
|
||||||
|
locale = flask_babel.get_locale()
|
||||||
|
elif isinstance(locale, GeoLocation):
|
||||||
locale = locale.locale()
|
locale = locale.locale()
|
||||||
elif locale is None:
|
elif locale is None:
|
||||||
locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
|
locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
|
||||||
return babel.dates.format_datetime(self.datetime, format=fmt, locale=locale)
|
return babel.dates.format_datetime(self.datetime, format=fmt, locale=locale)
|
||||||
|
|
||||||
|
def l10n_date(
|
||||||
|
self,
|
||||||
|
fmt: DateTimeFormats | str = "medium",
|
||||||
|
locale: DateTimeLocaleTypes | babel.Locale | GeoLocation | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Localized representation of date."""
|
||||||
|
|
||||||
|
if isinstance(locale, str) and locale == "UI":
|
||||||
|
locale = flask_babel.get_locale()
|
||||||
|
elif isinstance(locale, GeoLocation):
|
||||||
|
locale = locale.locale()
|
||||||
|
elif locale is None:
|
||||||
|
locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
|
||||||
|
return babel.dates.format_date(self.datetime, format=fmt, locale=locale)
|
||||||
|
|
||||||
|
|
||||||
@typing.final
|
@typing.final
|
||||||
class Temperature:
|
class Temperature:
|
||||||
|
|||||||
@@ -85,9 +85,20 @@ container.build() {
|
|||||||
info_msg "Set \$DOCKER_TAG: $DOCKER_TAG"
|
info_msg "Set \$DOCKER_TAG: $DOCKER_TAG"
|
||||||
info_msg "Set \$GIT_URL: $GIT_URL"
|
info_msg "Set \$GIT_URL: $GIT_URL"
|
||||||
|
|
||||||
|
# change cmp to lockfile when available
|
||||||
|
timestamp_requirements_main=$(git log -1 --format='%ct' ./requirements.txt)
|
||||||
|
timestamp_requirements_server=$(git log -1 --format='%ct' ./requirements-server.txt)
|
||||||
|
if [[ "$timestamp_requirements_main" -ge "$timestamp_requirements_server" ]]; then
|
||||||
|
timestamp_venv="$timestamp_requirements_main"
|
||||||
|
else
|
||||||
|
timestamp_venv="$timestamp_requirements_server"
|
||||||
|
fi
|
||||||
|
|
||||||
|
timestamp_searx_settings=$(git log -1 --format='%ct' ./searx/settings.yml)
|
||||||
|
|
||||||
if [ "$container_engine" = "podman" ]; then
|
if [ "$container_engine" = "podman" ]; then
|
||||||
params_build_builder="build --format=oci --platform=$platform --layers --identity-label=false"
|
params_build_builder="build --format=oci --platform=$platform --layers --identity-label=false --timestamp=$timestamp_venv"
|
||||||
params_build=$params_build_builder
|
params_build="build --format=oci --platform=$platform --layers --identity-label=false"
|
||||||
else
|
else
|
||||||
params_build_builder="build --platform=$platform"
|
params_build_builder="build --platform=$platform"
|
||||||
params_build=$params_build_builder
|
params_build=$params_build_builder
|
||||||
@@ -102,8 +113,8 @@ container.build() {
|
|||||||
|
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
"$container_engine" $params_build_builder \
|
"$container_engine" $params_build_builder \
|
||||||
--build-arg="TIMESTAMP=$(git log -1 --date=format:'%Y%m%d%H%M.%S' --format='%ad')" \
|
--build-arg="TIMESTAMP_VENV=$timestamp_venv" \
|
||||||
--build-arg="TIMESTAMP_SETTINGS=$(git log -1 --date=format:'%Y%m%d%H%M.%S' --format='%ad' ./searx/settings.yml)" \
|
--build-arg="TIMESTAMP_SETTINGS=$timestamp_searx_settings" \
|
||||||
--tag="localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder" \
|
--tag="localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder" \
|
||||||
--file="./container/builder.dockerfile" \
|
--file="./container/builder.dockerfile" \
|
||||||
.
|
.
|
||||||
|
|||||||
Reference in New Issue
Block a user