19 Commits

Author SHA1 Message Date
Ivan Gabaldon
b7ecc1c240 [enh] container: reproducible layers (#5222)
* [enh] container: reproducible layers

We are not aiming for reproducibility compliance, but we look to make most
builder layers reproducible without caching at least for a short period of time
(until the builder's base image changes or the child dependencies of a
requirements.txt package are updated).

This feature is only available on Podman.

This targets https://github.com/searxng/searxng/pull/5086 main goal.

* [fix] misc: apply suggestions

Suggested: https://github.com/searxng/searxng/pull/5222#discussion_r2364630496
Suggested: https://github.com/searxng/searxng/pull/5222#discussion_r2364630511

* [enh] container: prevent useless layer
2025-09-20 11:33:23 +02:00
Ivan Gabaldon
164167dea0 [mod] py: remove uvloop (#5220)
We get some good stuff without uvloop, 13MB~ less of dependencies, 3
minutes of build time for armv7 saved, and we are one step closer to NT
compatibility. Although it's true that theoretically the raw performance
have worsened on network side (we only used uvloop for that), the latest
cpython versions have been improving on asyncio performance.
2025-09-20 11:12:34 +02:00
github-actions[bot]
b3eb7657b9 [l10n] update translations from Weblate (#5216)
a0783e1a1 - 2025-09-16 - recreationalprogamer <recreationalprogamer@noreply.codeberg.org>
130f66498 - 2025-09-13 - artnay <artnay@noreply.codeberg.org>

Co-authored-by: searxng-bot <searxng-bot@users.noreply.github.com>
2025-09-20 10:57:35 +02:00
Markus Heiser
d2b4bff856 [mod] demo engines: smaller improvement
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
1520a8d545 [mod] ADS engine: revision of the engine (Paper result)
Revision of the Astrophysics Data System (ADS) engine / use of the result type
Paper as well as other typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
f8f7adce6b [mod] Z-Library engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

The engine has been placed on inactive because no service is currently
available, or at least not known in the SearXNG community [1]

[1] https://github.com/searxng/searxng/issues/3610

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
4c42704c80 [mod] Springer Nature engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
4b4bf0ecaf [mod] Semantic Scholar engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
bb22bb1831 [mod] PubMed engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
96e63df8ca [mod] Open Library engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
0691e50e13 [mod] OpenAlex engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
599d9488c5 [mod] Google Scholar engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
078c9fcb68 [mod] Crossref engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
3ec6d65f9b [mod] CORE engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
22e73727c0 [mod] Anna's Archive engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
6c3fb9e42b [mod] arXiv engine: revision of the engine (Paper result)
Revision of the engine / use of the result type Paper as well as other
typifications.

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Markus Heiser
f9b4869426 [build] /static 2025-09-20 10:56:46 +02:00
Markus Heiser
7eedd44f5f [mod] typification of SearXNG: add new result type Paper
This patch adds a new result type: Paper

- Python class:   searx/result_types/paper.py
- Jinja template: searx/templates/simple/result_templates/paper.html
- CSS (less)      client/simple/src/less/result_types/paper.less

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-09-20 10:56:46 +02:00
Léon Tiekötter
57ef342ad1 [fix] image proxy: object has no attribute 'status_code' (#5212)
Commit 8f8343d [1] introduced a bug in the network logic of SearXNG where stream
requests (such as the one from the image proxy) would fail because a wrapper was
returned instead of a response object with the correct attribute.

This is just a quick in place fix I implemented to get it working again. It
would be better to implement corresponding logic to give stream requests the
correct object.

[1] https://github.com/searxng/searxng/pull/5204
2025-09-20 07:54:58 +02:00
47 changed files with 1729 additions and 1030 deletions

View File

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

View 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);
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
.. _arxiv engine:
=====
arXiv
=====
.. automodule:: searx.engines.arxiv
:members:

View File

@@ -0,0 +1,8 @@
.. _astrophysics_data_system engine:
==============================
Astrophysics Data System (ADS)
==============================
.. automodule:: searx.engines.astrophysics_data_system
:members:

View File

@@ -0,0 +1,8 @@
.. _crossref engine:
========
Crossref
========
.. automodule:: searx.engines.crossref
:members:

View File

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

View File

@@ -0,0 +1,8 @@
.. _openlibrary engine:
============
Open Library
============
.. automodule:: searx.engines.openlibrary
:members:

View File

@@ -0,0 +1,8 @@
.. _pubmed engine:
======
PubMed
======
.. automodule:: searx.engines.pubmed
:members:

View File

@@ -0,0 +1,8 @@
.. _semantic_scholar engine:
================
Semantic Scholar
================
.. automodule:: searx.engines.semantic_scholar
:members:

View File

@@ -0,0 +1,8 @@
.. _springer engine:
===============
Springer Nature
===============
.. automodule:: searx.engines.springer
:members:

View File

@@ -0,0 +1,7 @@
.. _result_types.paper:
=============
Paper Results
=============
.. automodule:: searx.result_types.paper

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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,
params['headers']['Authorization'] = f'Bearer {api_key}' "fl": ",".join(ads_field_list),
params['url'] = f"{base_url}/query?{urlencode(query_params)}" "rows": ads_rows,
"start": ads_rows * (params["pageno"] - 1),
return params
def response(resp):
try:
resp_json = loads(resp.text)
except Exception as e:
raise SearxEngineAPIException("failed to parse response") from e
if 'error' in resp_json:
raise SearxEngineAPIException(resp_json['error']['msg'])
resp_json = resp_json["response"]
result_len = resp_json["numFound"]
results = []
for res in resp_json["docs"]:
author = res.get("author")
if author:
author = author[0] + ' et al.'
results.append(
{
'url': result_base_url + res.get("bibcode") + "/",
'title': res.get("title")[0],
'author': author,
'content': res.get("abstract"),
'doi': res.get("doi"),
'publishedDate': datetime.fromisoformat(res.get("date")),
} }
if ads_sort:
args["sort"] = ads_sort
params["headers"]["Authorization"] = f"Bearer {api_key}"
params["url"] = f"{base_url}?{urlencode(args)}"
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_data: dict[str, dict[str, t.Any]] = resp.json()
if "error" in json_data:
raise SearxEngineAPIException(json_data["error"]["msg"])
def _str(k: str) -> str:
return str(doc.get(k, ""))
def _list(k: str) -> list[str]:
return doc.get(k, [])
for doc in json_data["response"]["docs"]:
authors: list[str] = doc["author"]
if len(authors) > 15:
# There are articles with hundreds of authors
authors = authors[:15] + ["et al."]
paper = res.types.Paper(
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

View File

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

View File

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

View File

@@ -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`."""

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}'
) )
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")]
pmids_results = etree.XML(resp.content) # send efetch request with the IDs from esearch response
pmids = pmids_results.xpath('//eSearchResult/IdList/Id') args = urlencode(
pmids_string = '' {
"db": "pubmed",
for item in pmids: "retmode": "xml",
pmids_string += item.text + ',' "id": ",".join(pmids),
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,
} }
)
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.add(
res.types.Paper(
url=url,
title=title,
content=content,
journal=journal,
issn=[issn],
authors=authors,
doi=doi,
publishedDate=pub_date,
) )
res_dict['publishedDate'] = publishedDate )
except Exception as e: # pylint: disable=broad-exception-caught return res
print(e)
results.append(res_dict)
return results

View File

@@ -1,57 +1,98 @@
# 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:
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return True
def get_ui_version() -> str:
ret_val: str = CACHE.get("X-S2-UI-Version")
if not ret_val:
resp = get(base_url) resp = get(base_url)
if not resp.ok: if not resp.ok:
raise RuntimeError("Can't determine Semantic Scholar UI version") raise RuntimeError("Can't determine Semantic Scholar UI version")
doc = html.fromstring(resp.text) doc = html.fromstring(resp.text)
ui_version = eval_xpath_getindex(doc, "//meta[@name='s2-ui-version']/@content", 0) ret_val = eval_xpath_getindex(doc, "//meta[@name='s2-ui-version']/@content", 0)
if not ui_version: if not ret_val:
raise RuntimeError("Can't determine Semantic Scholar UI version") raise RuntimeError("Can't determine Semantic Scholar UI version")
# hold the cached value for 5min
return ui_version 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, params): def request(query: str, params: "OnlineParams") -> None:
params['url'] = search_url params["url"] = search_url
params['method'] = 'POST' params["method"] = "POST"
params['headers'].update( 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,
@@ -60,66 +101,63 @@ def request(query, params):
"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

View File

@@ -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,
for record in json_data['records']: "s": nb_per_page * (params["pageno"] - 1),
published = datetime.strptime(record['publicationDate'], '%Y-%m-%d') "p": nb_per_page,
authors = [" ".join(author['creator'].split(', ')[::-1]) for author in record['creators']]
tags = record.get('genre')
if isinstance(tags, str):
tags = [tags]
results.append(
{
'template': 'paper.html',
'url': record['url'][0]['value'].replace('http://', 'https://', 1),
'title': record['title'],
'content': record['abstract'],
'comments': record['publicationName'],
'tags': tags,
'publishedDate': published,
'type': record.get('contentType'),
'authors': authors,
# 'editor': '',
'publisher': record.get('publisher'),
'journal': record.get('publicationName'),
'volume': record.get('volume') or None,
'pages': '-'.join([x for x in [record.get('startingPage'), record.get('endingPage')] if x]),
'number': record.get('number') or None,
'doi': record.get('doi'),
'issn': [x for x in [record.get('issn')] if x],
'isbn': [x for x in [record.get('isbn')] if x],
# 'pdf_url' : ''
} }
params["url"] = f"{base_url}?{urlencode(args)}"
# For example, the ``year:`` filter requires a *Premium Plan* subscription.
params["raise_for_httperror"] = False
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_data = resp.json()
if (
resp.status_code == 403
and json_data["status"].lower() == "fail"
and "premium feature" in json_data["message"].lower()
):
return res
raise_for_httperror(resp)
def field(k: str) -> str:
return str(record.get(k, ""))
for record in json_data["records"]:
published = datetime.strptime(record["publicationDate"], "%Y-%m-%d")
authors: list[str] = [" ".join(author["creator"].split(", ")[::-1]) for author in record["creators"]]
pdf_url = ""
html_url = ""
url_list: list[dict[str, str]] = record["url"]
for item in url_list:
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

View File

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

View File

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

View File

@@ -280,8 +280,8 @@ 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)

View File

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

View File

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

View 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)

View File

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

View File

@@ -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>
<span>{{ result.journal -}}
{%- if result.volume -%} {%- if result.volume -%}
&nbsp;{{- result.volume -}} &nbsp;{{- result.volume -}}
{%- if result.number -%} {%- if result.number -%}.{{- result.number -}}{%- endif -%}
.{{- result.number -}}
{%- endif -%}
{%- endif -%}
{%- if result.pages -%}
&nbsp;{{- result.pages -}}
{%- endif -%} {%- endif -%}
{%- if result.pages -%}&nbsp;{{- 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 -%}

View File

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

View File

@@ -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"
#. 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 t 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)"

View File

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

View File

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

View File

@@ -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" \
. .