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
with:
persist-credentials: "false"
fetch-depth: "0"
- name: Setup cache Python
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 {
.attributes {
display: table;
border-spacing: 0.125rem;
margin-top: 0.3rem;
div {
display: table-row;
@@ -353,12 +353,6 @@ article[data-vim-selected].category-social {
}
}
.result-packages {
.attributes {
margin-top: 0.3rem;
}
}
.template_group_images {
display: flex;
flex-wrap: wrap;
@@ -1118,7 +1112,6 @@ summary.title {
display: none;
}
.result-paper,
.result-packages {
.attributes {
display: block;
@@ -1164,3 +1157,4 @@ pre code {
// import layouts of the Result types
@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
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 pip install --no-managed-python --compile-bytecode --requirements ./requirements.txt --requirements ./requirements-server.txt; \
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/
ARG TIMESTAMP_SETTINGS="0"
RUN set -eux; \
RUN set -eux -o pipefail; \
python -m compileall -q ./searx/; \
touch -c -t $TIMESTAMP_SETTINGS ./searx/settings.yml; \
find ./searx/static/ -type f \
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
-exec gzip -9 -k {} + \
-exec brotli -9 -k {} + \
-exec gzip --test {}.gz + \
-exec brotli --test {}.br +; \
# Move always changing files to /usr/local/searxng/
mv ./searx/version_frozen.py ./
touch -c --date="@$TIMESTAMP_SETTINGS" ./searx/settings.yml

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/searx/ ./searx/
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 VERSION="unknown"
@@ -30,7 +30,6 @@ ENV SEARXNG_VERSION="$VERSION" \
GRANIAN_HOST="::" \
GRANIAN_PORT="8080" \
GRANIAN_WEBSOCKETS="false" \
GRANIAN_LOOP="uvloop" \
GRANIAN_BLOCKING_THREADS="4" \
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
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
=========
Overview
========
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
===============
OpenAlex
========
.. automodule:: searx.engines.openalex
: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/keyvalue
main/code
main/paper
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
@@ -26,7 +27,6 @@ final typing is complete.
- :ref:`template videos`
- :ref:`template torrent`
- :ref:`template map`
- :ref:`template paper`
- :ref:`template packages`
- :ref:`template files`
- :ref:`template products`

1
manage
View File

@@ -159,7 +159,6 @@ webapp.run() {
GRANIAN_HOST="::" \
GRANIAN_PORT="8888" \
GRANIAN_WEBSOCKETS="false" \
GRANIAN_LOOP="uvloop" \
GRANIAN_BLOCKING_THREADS="4" \
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m" \

View File

@@ -10,7 +10,6 @@ pyyaml==6.0.2
httpx[http2]==0.28.1
httpx-socks[asyncio]==0.10.0
Brotli==1.1.0
uvloop==0.21.0
setproctitle==1.3.7
valkey==6.1.1
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
engine: annas_archive
categories = ["general", "articles"]
shortcut: aaa
aa_content: 'magazine'
aa_ext: 'pdf'
aa_sort: 'newest'
aa_content: "magazine"
aa_ext: "pdf"
aa_sort: "newest"
Implementations
===============
@@ -47,6 +49,7 @@ 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: dict[str, t.Any] = {
@@ -59,7 +62,7 @@ about: dict[str, t.Any] = {
}
# engine dependent config
categories: list[str] = ["files"]
categories = ["files", "books"]
paging: bool = True
# search-url
@@ -72,14 +75,14 @@ aa_content: str = ""
To not filter use an empty string (default).
"""
aa_sort: str = ''
aa_sort: str = ""
"""Sort Anna's results, possible values::
newest, oldest, largest, smallest
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
``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."""
traits = EngineTraits(**ENGINE_TRAITS['annas archive'])
traits = EngineTraits(**ENGINE_TRAITS["annas archive"])
if aa_content and aa_content not in traits.custom['content']:
raise ValueError(f'invalid setting content: {aa_content}')
if aa_content and aa_content not in traits.custom["content"]:
raise ValueError(f"invalid setting content: {aa_content}")
if aa_sort and aa_sort not in traits.custom['sort']:
raise ValueError(f'invalid setting sort: {aa_sort}')
if aa_sort and aa_sort not in traits.custom["sort"]:
raise ValueError(f"invalid setting sort: {aa_sort}")
if aa_ext and aa_ext not in traits.custom['ext']:
raise ValueError(f'invalid setting ext: {aa_ext}')
if aa_ext and aa_ext not in traits.custom["ext"]:
raise ValueError(f"invalid setting ext: {aa_ext}")
return True
def request(query: str, params: dict[str, t.Any]) -> None:
lang = traits.get_language(params["language"], traits.all_locale)
def request(query: str, params: "OnlineParams") -> None:
lang = traits.get_language(params["searxng_locale"], traits.all_locale)
args = {
'lang': lang,
'content': aa_content,
'ext': aa_ext,
'sort': aa_sort,
'q': query,
'page': params['pageno'],
"lang": lang,
"content": aa_content,
"ext": aa_ext,
"sort": aa_sort,
"q": query,
"page": params["pageno"],
}
# filter out None and empty values
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
# 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
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:
kwargs: dict[str, t.Any] = _get_result(item)
except SearxEngineXPathException:
continue
res.add(res.types.LegacyResult(**kwargs))
res.add(res.types.Paper(**kwargs))
return res
def _get_result(item: ElementBase) -> dict[str, t.Any]:
return {
'template': 'paper.html',
'url': base_url + eval_xpath_getindex(item, './a/@href', 0),
'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))],
'publisher': extract_text(
eval_xpath_getindex(item, './/a[starts-with(@href, "/search")]', 1, default=None), allow_none=True
"url": base_url + eval_xpath_getindex(item, "./a/@href", 0),
"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))],
"publisher": extract_text(
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")]')),
'thumbnail': extract_text(eval_xpath_getindex(item, './/img/@src', 0, default=None), allow_none=True),
"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),
}
@@ -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.locales import language_tag
engine_traits.all_locale = ''
engine_traits.custom['content'] = []
engine_traits.custom['ext'] = []
engine_traits.custom['sort'] = []
engine_traits.all_locale = ""
engine_traits.custom["content"] = []
engine_traits.custom["ext"] = []
engine_traits.custom["sort"] = []
resp = get(base_url + '/search')
resp = get(base_url + "/search")
if not resp.ok:
raise RuntimeError("Response from Anna's search page is not OK.")
dom = html.fromstring(resp.text)
# supported language codes
lang_map = {}
lang_map: dict[str, str] = {}
for x in eval_xpath_list(dom, "//form//input[@name='lang']"):
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
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:
# silently ignore unknown languages
# 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']"):
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']"):
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"):
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
engine_traits.custom['content'].sort()
engine_traits.custom['ext'].sort()
engine_traits.custom['sort'].sort()
engine_traits.custom["content"].sort()
engine_traits.custom["ext"].sort()
engine_traits.custom["sort"].sort()

View File

@@ -1,110 +1,129 @@
# 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 urllib.parse import urlencode
from lxml import etree
from lxml.etree import XPath
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 = {
"website": 'https://arxiv.org',
"wikidata_id": 'Q118398',
"official_api_documentation": 'https://arxiv.org/help/api',
"website": "https://arxiv.org",
"wikidata_id": "Q118398",
"official_api_documentation": "https://info.arxiv.org/help/api/user-manual.html",
"use_official_api": True,
"require_api_key": False,
"results": 'XML-RSS',
"results": "XML-RSS",
}
categories = ['science', 'scientific publications']
categories = ["science", "scientific publications"]
paging = True
arxiv_max_results = 10
arxiv_search_prefix = "all"
"""Search fields, for more details see, `Details of Query Construction`_.
base_url = (
'https://export.arxiv.org/api/query?search_query=all:' + '{query}&start={offset}&max_results={number_of_results}'
)
.. _Details of Query Construction:
https://info.arxiv.org/help/api/user-manual.html#51-details-of-query-construction
"""
# engine dependent config
number_of_results = 10
base_url = "https://export.arxiv.org/api/query"
"""`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 = {
"atom": "http://www.w3.org/2005/Atom",
"arxiv": "http://arxiv.org/schemas/atom",
}
xpath_entry = XPath('//atom:entry', namespaces=arxiv_namespaces)
xpath_title = XPath('.//atom:title', namespaces=arxiv_namespaces)
xpath_id = XPath('.//atom:id', namespaces=arxiv_namespaces)
xpath_summary = XPath('.//atom:summary', namespaces=arxiv_namespaces)
xpath_author_name = XPath('.//atom:author/atom:name', namespaces=arxiv_namespaces)
xpath_doi = XPath('.//arxiv:doi', namespaces=arxiv_namespaces)
xpath_pdf = XPath('.//atom:link[@title="pdf"]', namespaces=arxiv_namespaces)
xpath_published = XPath('.//atom:published', namespaces=arxiv_namespaces)
xpath_journal = XPath('.//arxiv:journal_ref', namespaces=arxiv_namespaces)
xpath_category = XPath('.//atom:category/@term', namespaces=arxiv_namespaces)
xpath_comment = XPath('./arxiv:comment', namespaces=arxiv_namespaces)
xpath_entry = XPath("//atom:entry", namespaces=arxiv_namespaces)
xpath_title = XPath(".//atom:title", namespaces=arxiv_namespaces)
xpath_id = XPath(".//atom:id", namespaces=arxiv_namespaces)
xpath_summary = XPath(".//atom:summary", namespaces=arxiv_namespaces)
xpath_author_name = XPath(".//atom:author/atom:name", namespaces=arxiv_namespaces)
xpath_doi = XPath(".//arxiv:doi", namespaces=arxiv_namespaces)
xpath_pdf = XPath(".//atom:link[@title='pdf']", namespaces=arxiv_namespaces)
xpath_published = XPath(".//atom:published", namespaces=arxiv_namespaces)
xpath_journal = XPath(".//arxiv:journal_ref", namespaces=arxiv_namespaces)
xpath_category = XPath(".//atom:category/@term", namespaces=arxiv_namespaces)
xpath_comment = XPath("./arxiv:comment", namespaces=arxiv_namespaces)
def request(query, params):
# basic search
offset = (params['pageno'] - 1) * number_of_results
def request(query: str, params: "OnlineParams") -> None:
string_args = {'query': query, 'offset': offset, 'number_of_results': number_of_results}
params['url'] = base_url.format(**string_args)
return params
args = {
"search_query": f"{arxiv_search_prefix}:{query}",
"start": (params["pageno"] - 1) * arxiv_max_results,
"max_results": arxiv_max_results,
}
params["url"] = f"{base_url}?{urlencode(args)}"
def response(resp):
results = []
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
dom = etree.fromstring(resp.content)
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
abstract = eval_xpath_getindex(entry, xpath_summary, 0).text
title: str = eval_xpath_getindex(entry, xpath_title, 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_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_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_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
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_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 = {
'template': 'paper.html',
'url': url,
'title': title,
'publishedDate': publishedDate,
'content': abstract,
'doi': doi,
'authors': authors,
'journal': journal,
'tags': tags,
'comments': comments,
'pdf_url': pdf_url,
}
res.add(
res.types.Paper(
url=url,
title=title,
publishedDate=publishedDate,
content=abstract,
doi=doi,
authors=authors,
journal=journal,
tags=tags,
comments=comments,
pdf_url=pdf_url,
)
)
results.append(res_dict)
return results
return res

View File

@@ -1,93 +1,163 @@
# 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,
operated by the Smithsonian Astrophysical Observatory (SAO) under a NASA grant.
The engine is adapted from the solr engine.
.. note::
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 json import loads
from urllib.parse import urlencode
from searx.utils import html_to_text
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 = {
"website": 'https://ui.adsabs.harvard.edu/',
"wikidata_id": 'Q752099',
"official_api_documentation": 'https://ui.adsabs.harvard.edu/help/api/api-docs.html',
"website": "https://ui.adsabs.harvard.edu/",
"wikidata_id": "Q752099",
"official_api_documentation": "https://ui.adsabs.harvard.edu/help/api/api-docs.html",
"use_official_api": True,
"require_api_key": True,
"results": 'JSON',
"results": "JSON",
}
base_url = 'https://api.adsabs.harvard.edu/v1/search'
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
categories = ["science", "scientific publications"]
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(_):
if api_key == 'unset':
raise SearxEngineAPIException('missing ADS API key')
def setup(engine_settings: dict[str, t.Any]) -> bool:
"""Initialization of the ADS_ engine, checks whether the :py:obj:`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):
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
def request(query: str, params: "OnlineParams") -> None:
query_params['start'] = rows * (params['pageno'] - 1)
args: dict[str, str | int] = {
"q": query,
"fl": ",".join(ads_field_list),
"rows": ads_rows,
"start": ads_rows * (params["pageno"] - 1),
}
if ads_sort:
args["sort"] = ads_sort
params['headers']['Authorization'] = f'Bearer {api_key}'
params['url'] = f"{base_url}/query?{urlencode(query_params)}"
return params
params["headers"]["Authorization"] = f"Bearer {api_key}"
params["url"] = f"{base_url}?{urlencode(args)}"
def response(resp):
try:
resp_json = loads(resp.text)
except Exception as e:
raise SearxEngineAPIException("failed to parse response") from e
def response(resp: "SXNG_Response") -> EngineResults:
if 'error' in resp_json:
raise SearxEngineAPIException(resp_json['error']['msg'])
res = EngineResults()
json_data: dict[str, dict[str, t.Any]] = resp.json()
resp_json = resp_json["response"]
result_len = resp_json["numFound"]
results = []
if "error" in json_data:
raise SearxEngineAPIException(json_data["error"]["msg"])
for res in resp_json["docs"]:
author = res.get("author")
def _str(k: str) -> str:
return str(doc.get(k, ""))
if author:
author = author[0] + ' et al.'
def _list(k: str) -> list[str]:
return doc.get(k, [])
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")),
}
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 results
return res

View File

@@ -5,6 +5,10 @@ research from repositories and journals.
.. _CORE: https://core.ac.uk/about
.. note::
The CORE engine requires an :py:obj:`API key <api_key>`.
.. _core engine config:
Configuration
@@ -17,135 +21,141 @@ The engine has the following additional settings:
.. code:: yaml
- name: core.ac.uk
engine: core
categories: science
shortcut: cor
api_key: "..."
timeout: 5
inactive: false
Implementations
===============
"""
# pylint: disable=too-many-branches
import typing as t
from datetime import datetime
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 = {
"website": 'https://core.ac.uk',
"wikidata_id": 'Q22661180',
"official_api_documentation": 'https://api.core.ac.uk/docs/v3',
"website": "https://core.ac.uk",
"wikidata_id": "Q22661180",
"official_api_documentation": "https://api.core.ac.uk/docs/v3",
"use_official_api": 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
the API key in the engine :ref:`core engine config`."""
categories = ['science', 'scientific publications']
categories = ["science", "scientific publications"]
paging = True
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):
if api_key == 'unset':
raise SearxEngineAPIException('missing CORE API key')
def setup(engine_settings: dict[str, t.Any]) -> bool:
"""Initialization of the CORE_ engine, checks whether the :py:obj:`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
search_params = {
'q': query,
'offset': (params['pageno'] - 1) * nb_per_page,
'limit': nb_per_page,
'sort': 'relevance',
"q": query,
"offset": (params["pageno"] - 1) * nb_per_page,
"limit": nb_per_page,
"sort": "relevance",
}
params['url'] = base_url + '?' + urlencode(search_params)
params['headers'] = {'Authorization': f'Bearer {api_key}'}
return params
params["url"] = base_url + "?" + urlencode(search_params)
params["headers"] = {"Authorization": f"Bearer {api_key}"}
def response(resp):
results = []
def response(resp: "SXNG_Response") -> EngineResults:
# pylint: disable=too-many-branches
res = EngineResults()
json_data = resp.json()
for result in json_data.get('results', []):
for result in json_data.get("results", []):
# Get title
if not result.get('title'):
if not result.get("title"):
continue
# Get URL - try different options
url = None
url: str | None = None
# Try DOI first
doi = result.get('doi')
doi: str = result.get("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
url = 'https://doi.org/' + str(result['doi'])
elif result.get('id'):
url = 'https://core.ac.uk/works/' + str(result['id'])
elif result.get('downloadUrl'):
url = result['downloadUrl']
elif result.get('sourceFulltextUrls'):
url = result['sourceFulltextUrls']
url = "https://doi.org/" + str(result["doi"])
elif result.get("id"):
url = "https://core.ac.uk/works/" + str(result["id"])
elif result.get("downloadUrl"):
url = result["downloadUrl"]
elif result.get("sourceFulltextUrls"):
url = result["sourceFulltextUrls"]
else:
continue
# Published date
published_date = None
raw_date = result.get('publishedDate') or result.get('depositedDate')
raw_date = result.get("publishedDate") or result.get("depositedDate")
if raw_date:
try:
published_date = datetime.fromisoformat(result['publishedDate'].replace('Z', '+00:00'))
published_date = datetime.fromisoformat(result["publishedDate"].replace("Z", "+00:00"))
except (ValueError, AttributeError):
pass
# Handle journals
journals = []
if result.get('journals'):
journals = [j.get('title') for j in result['journals'] if j.get('title')]
if result.get("journals"):
journals = [j.get("title") for j in result["journals"] if j.get("title")]
# Handle publisher
publisher = result.get('publisher', '').strip("'")
if publisher:
publisher = publisher.strip("'")
publisher = result.get("publisher", "").strip("'")
# Handle authors
authors = set()
for i in result.get('authors', []):
name = i.get("name")
authors: set[str] = set()
for i in result.get("authors", []):
name: str | None = i.get("name")
if name:
authors.add(name)
results.append(
{
'template': 'paper.html',
'title': result.get('title'),
'url': url,
'content': result.get('fullText', '') or '',
# 'comments': '',
'tags': result.get('fieldOfStudy', []),
'publishedDate': published_date,
'type': result.get('documentType', '') or '',
'authors': authors,
'editor': ', '.join(result.get('contributors', [])),
'publisher': publisher,
'journal': ', '.join(journals),
'doi': result.get('doi'),
# 'issn' : ''
# 'isbn' : ''
'pdf_url': result.get('downloadUrl', {}) or result.get("sourceFulltextUrls", {}),
}
res.add(
res.types.Paper(
title=result.get("title"),
url=url,
content=result.get("fullText", "") or "",
tags=result.get("fieldOfStudy", []),
publishedDate=published_date,
type=result.get("documentType", "") or "",
authors=authors,
editor=", ".join(result.get("contributors", [])),
publisher=publisher,
journal=", ".join(journals),
doi=result.get("doi"),
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
"""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 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 = {
"website": "https://www.crossref.org/",
"wikidata_id": "Q5188229",
"official_api_documentation": "https://api.crossref.org",
"use_official_api": False,
"official_api_documentation": "https://api.crossref.org/swagger-ui/",
"use_official_api": True,
"require_api_key": False,
"results": "JSON",
}
@@ -16,48 +29,69 @@ about = {
categories = ["science", "scientific publications"]
paging = True
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):
params["url"] = search_url + "?" + urlencode({"query": query, "offset": 20 * (params["pageno"] - 1)})
return params
def request(query: str, params: "OnlineParams") -> None:
args = {
"query": query,
"offset": 20 * (params["pageno"] - 1),
}
params["url"] = f"{search_url}?{urlencode(args)}"
def response(resp):
results = []
for record in resp.json()["message"]["items"]:
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
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":
# 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
result = {
"template": "paper.html",
"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"),
}
title: str = ""
journal: str = ""
if record["type"] == "book-chapter":
result["title"] = record["container-title"][0]
if record["title"][0].lower().strip() != result["title"].lower().strip():
result["title"] += f" ({record['title'][0]})"
title = record["container-title"][0]
if record["title"][0].lower().strip() != title.lower().strip():
title += f" ({record['title'][0]})"
else:
result["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
title = record["title"][0] if "title" in record else record.get("container-title", [None])[0]
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"]:
result["url"] = record["resource"]["primary"]["URL"]
item.url = record["resource"]["primary"]["URL"]
if "published" in record and "date-parts" in record["published"]:
result["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.publishedDate = datetime(*(record["published"]["date-parts"][0] + [1, 1][:3]))
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"
# result["pdf_url"] = record.get("link", [{"URL": None}])[0]["URL"]
# item.pdf_url = record.get("link", [{"URL": None}])[0]["URL"]
results.append(result)
return results
return res

View File

@@ -57,7 +57,7 @@ about = {
_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.
For more details see :py:obj:`searx.enginelib.Engine.setup`."""

View File

@@ -11,6 +11,8 @@ engines:
"""
import typing as t
import re
import random
import string
@@ -28,8 +30,10 @@ from searx.exceptions import SearxEngineCaptchaException
from searx.enginelib.traits import EngineTraits
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.google.com',
"wikidata_id": 'Q9366',
@@ -89,7 +93,7 @@ def ui_async(start: int) -> str:
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
API`).
@@ -144,7 +148,7 @@ def get_google_info(params, eng_traits):
"""
ret_val = {
ret_val: dict[str, t.Any] = {
'language': None,
'country': None,
'subdomain': None,
@@ -273,7 +277,7 @@ def detect_google_sorry(resp):
raise SearxEngineCaptchaException()
def request(query, params):
def request(query: str, params: "OnlineParams") -> None:
"""Google search request"""
# pylint: disable=line-too-long
start = (params['pageno'] - 1) * 10
@@ -317,7 +321,6 @@ def request(query, params):
params['cookies'] = google_info['cookies']
params['headers'].update(google_info['headers'])
return params
# =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
def response(resp) -> EngineResults:
def response(resp: "SXNG_Response"):
"""Get response from google's search request"""
# pylint: disable=too-many-branches, too-many-statements
detect_google_sorry(resp)

View File

@@ -1,12 +1,29 @@
# 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
and there does not exists `async` API. Even though the API slightly vintage we
can make use of the :ref:`google API` to assemble the arguments of the GET
and there does not exists ``async`` API. Even though the API slightly vintage
we can make use of the :ref:`google API` to assemble the arguments of the GET
request.
Configuration
=============
.. code:: yaml
- name: google scholar
engine: google_scholar
shortcut: gos
Implementations
===============
"""
import typing as t
from urllib.parse import urlencode
from datetime import datetime
from lxml import html
@@ -16,6 +33,7 @@ from searx.utils import (
eval_xpath_getindex,
eval_xpath_list,
extract_text,
ElementType,
)
from searx.exceptions import SearxEngineCaptchaException
@@ -26,18 +44,23 @@ from searx.engines.google import (
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 = {
"website": 'https://scholar.google.com',
"wikidata_id": 'Q494817',
"official_api_documentation": 'https://developers.google.com/custom-search',
"website": "https://scholar.google.com",
"wikidata_id": "Q494817",
"official_api_documentation": "https://developers.google.com/custom-search",
"use_official_api": False,
"require_api_key": False,
"results": 'HTML',
"results": "HTML",
}
# engine dependent config
categories = ['science', 'scientific publications']
categories = ["science", "scientific publications"]
paging = True
max_page = 50
"""`Google max 50 pages`_
@@ -50,9 +73,97 @@ safesearch = False
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
``params['time_range']``.
``params["time_range"]``.
Google Scholar supports a detailed search by year. Searching by *last
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
ranges (*day*, *week*, *month*, *year*) are mapped to *year*. If no range
is set an empty dictionary of arguments is returned. Example; when
user selects a time range (current year minus one in 2022):
is set an empty dictionary of arguments is returned.
Example; when user selects a time range and we find ourselves in the year
2025 (current year minus one):
.. code:: python
{ 'as_ylo' : 2021 }
{ "as_ylo" : 2024 }
"""
ret_val = {}
if params['time_range'] in time_range_dict:
ret_val['as_ylo'] = datetime.now().year - 1
ret_val: dict[str, int] = {}
if params["time_range"] in time_range_dict:
ret_val["as_ylo"] = datetime.now().year - 1
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
not redirected to ``sorry.google.com``.
"""
@@ -82,29 +195,7 @@ def detect_google_captcha(dom):
raise SearxEngineCaptchaException()
def request(query, params):
"""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):
def parse_gs_a(text: str | None) -> tuple[list[str], str, str, datetime | None]:
"""Parse the text written in green.
Possible formats:
@@ -113,98 +204,28 @@ def parse_gs_a(text: str | None):
* "{authors} - {publisher}"
"""
if text is None or text == "":
return None, None, None, None
return [], "", "", None
s_text = text.split(' - ')
authors = s_text[0].split(', ')
publisher = s_text[-1]
s_text = text.split(" - ")
authors: list[str] = s_text[0].split(", ")
publisher: str = s_text[-1]
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}"
# get journal and year
journal_year = s_text[1].split(', ')
journal_year = s_text[1].split(", ")
# journal is optional and may contains some coma
if len(journal_year) > 1:
journal = ', '.join(journal_year[0:-1])
if journal == '':
journal = None
journal: str = ", ".join(journal_year[0:-1])
if journal == "":
journal = ""
else:
journal = None
journal = ""
# year
year = journal_year[-1]
try:
publishedDate = datetime.strptime(year.strip(), '%Y')
publishedDate = datetime.strptime(year.strip(), "%Y")
except ValueError:
publishedDate = None
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
# pylint: disable=missing-module-docstring
#
# Engine is documented in: docs/dev/engines/online/openalex.rst
"""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 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
from datetime import datetime
from urllib.parse import urlencode
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 = {
@@ -31,7 +120,7 @@ search_url = "https://api.openalex.org/works"
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
args = {
"search": query,
@@ -60,7 +149,7 @@ def request(query: str, params: dict[str, t.Any]) -> None:
params["url"] = f"{search_url}?{urlencode(args)}"
def response(resp: SXNG_Response) -> EngineResults:
def response(resp: "SXNG_Response") -> EngineResults:
data = resp.json()
res = EngineResults()
@@ -71,12 +160,11 @@ def response(resp: SXNG_Response) -> EngineResults:
authors = _extract_authors(item)
journal, publisher, pages, volume, number, published_date = _extract_biblio(item)
doi = _doi_to_plain(item.get("doi"))
tags = _extract_tags(item) or None
tags = _extract_tags(item)
comments = _extract_comments(item)
res.add(
res.types.LegacyResult(
template="paper.html",
res.types.Paper(
url=url,
title=title,
content=content,
@@ -99,7 +187,7 @@ def response(resp: SXNG_Response) -> EngineResults:
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")
last_page = biblio.get("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)
if last_page:
return str(last_page)
return None
return ""
def _parse_date(value: str | None) -> datetime | None:
@@ -123,9 +211,9 @@ def _parse_date(value: str | None) -> datetime | 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:
return None
return ""
# OpenAlex `doi` field is commonly a full URL like https://doi.org/10.1234/abcd
return doi_value.removeprefix("https://doi.org/")
@@ -151,14 +239,17 @@ def _reconstruct_abstract(
return text if text != "" else None
def _extract_links(item: dict[str, t.Any]) -> tuple[str, str | None, str | None]:
primary_location = item.get("primary_location", {})
landing_page_url: str | None = primary_location.get("landing_page_url")
def _extract_links(item: dict[str, t.Any]) -> tuple[str, str, str]:
primary_location: dict[str, str] = item.get("primary_location", {})
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", "")
url: str = landing_page_url or work_url
open_access = item.get("open_access", {})
pdf_url: str | None = primary_location.get("pdf_url") or open_access.get("oa_url")
html_url: str | None = landing_page_url
html_url: str = landing_page_url
pdf_url: str = primary_location.get("pdf_url") or open_access.get("oa_url") or ""
return url, html_url, pdf_url
@@ -185,20 +276,21 @@ def _extract_tags(item: dict[str, t.Any]) -> list[str]:
def _extract_biblio(
item: dict[str, t.Any],
) -> tuple[str | None, str | None, str | None, str | None, str | None, datetime | None]:
host_venue = item.get("host_venue", {})
biblio = item.get("biblio", {})
journal: str | None = host_venue.get("display_name")
publisher: str | None = host_venue.get("publisher")
pages = _stringify_pages(biblio)
volume = biblio.get("volume")
number = biblio.get("issue")
) -> tuple[str, str, str, str, str, datetime | None]:
host_venue: dict[str, str] = item.get("host_venue", {})
biblio: dict[str, str] = item.get("biblio", {})
journal: str = host_venue.get("display_name", "")
publisher: str = host_venue.get("publisher", "")
pages: str = _stringify_pages(biblio)
volume = biblio.get("volume", "")
number = biblio.get("issue", "")
published_date = _parse_date(item.get("publication_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")
if isinstance(cited_by_count, int):
return f"{cited_by_count} citations"
return None
return ""

View File

@@ -1,71 +1,109 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Open library (books)
"""
from urllib.parse import urlencode
import re
"""`Open Library`_ is an open, editable library catalog, building towards a web
page for every book ever published.
.. _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 searx.result_types import EngineResults
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
'website': 'https://openlibrary.org',
'wikidata_id': 'Q1201876',
'require_api_key': False,
'use_official_api': False,
'official_api_documentation': 'https://openlibrary.org/developers/api',
"website": "https://openlibrary.org",
"wikidata_id": "Q1201876",
"require_api_key": False,
"use_official_api": False,
"official_api_documentation": "https://openlibrary.org/developers/api",
}
paging = True
categories = []
categories = ["general", "books"]
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
def request(query, params):
def request(query: str, params: "OnlineParams") -> None:
args = {
'q': query,
'page': params['pageno'],
'limit': results_per_page,
"q": query,
"page": params["pageno"],
"limit": results_per_page,
"fields": "*",
}
params['url'] = f"{base_url}/search.json?{urlencode(args)}"
return params
params["url"] = f"{search_api}?{urlencode(args)}"
logger.debug("REST API: %s", params["url"])
def _parse_date(date):
try:
return parser.parse(date)
except parser.ParserError:
return None
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_data = resp.json()
def response(resp):
results = []
for item in resp.json().get("docs", []):
cover = None
if 'lending_identifier_s' in item:
for item in json_data.get("docs", []):
cover = ""
if "lending_identifier_s" in item:
cover = f"https://archive.org/services/img/{item['lending_identifier_s']}"
published = item.get('publish_date')
published = item.get("publish_date")
if published:
published_dates = [date for date in map(_parse_date, published) if date]
if published_dates:
published = min(published_dates)
if not published:
published = parser.parse(str(item.get('first_published_year')))
published = _parse_date(str(item.get("first_publish_year")))
result = {
'template': 'paper.html',
'url': f"{base_url}{item['key']}",
'title': item['title'],
'content': re.sub(r"\{|\}", "", item['first_sentence'][0]) if item.get('first_sentence') else '',
'isbn': item.get('isbn', [])[:5],
'authors': item.get('author_name', []),
'thumbnail': cover,
'publishedDate': published,
'tags': item.get('subject', [])[:10] + item.get('place', [])[:10],
}
results.append(result)
content = " / ".join(item.get("first_sentence", []))
res.add(
res.types.Paper(
url=f"{base_url}/{item['key']}",
title=item["title"],
content=content,
isbn=item.get("isbn", [])[:5],
authors=item.get("author_name", []),
thumbnail=cover,
publishedDate=published,
tags=item.get("subject", [])[:10] + item.get("place", [])[:10],
)
)
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
"""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 urllib.parse import urlencode
from lxml import etree
from searx.result_types import EngineResults
from searx.network import get
from searx.utils import (
eval_xpath_getindex,
eval_xpath_list,
extract_text,
ElementType,
)
# about
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about = {
"website": 'https://www.ncbi.nlm.nih.gov/pubmed/',
"wikidata_id": 'Q1540899',
"website": "https://www.ncbi.nlm.nih.gov/pubmed/",
"wikidata_id": "Q1540899",
"official_api_documentation": {
'url': 'https://www.ncbi.nlm.nih.gov/home/develop/api/',
'comment': 'More info on api: https://www.ncbi.nlm.nih.gov/books/NBK25501/',
"url": "https://www.ncbi.nlm.nih.gov/home/develop/api/",
"comment": "More info on api: https://www.ncbi.nlm.nih.gov/books/NBK25501/",
},
"use_official_api": True,
"require_api_key": False,
"results": 'XML',
"results": "XML",
}
categories = ['science', 'scientific publications']
categories = ["science", "scientific publications"]
base_url = (
'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi' + '?db=pubmed&{query}&retstart={offset}&retmax={hits}'
)
eutils_api = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
# engine dependent config
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):
# basic search
offset = (params['pageno'] - 1) * number_of_results
def request(query: str, params: "OnlineParams") -> None:
string_args = {
'query': urlencode({'term': query}),
'offset': offset,
'hits': number_of_results,
}
params['url'] = base_url.format(**string_args)
return params
def response(resp): # pylint: disable=too-many-locals
results = []
# First retrieve notice of each result
pubmed_retrieve_api_url = (
'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?' + 'db=pubmed&retmode=xml&id={pmids_string}'
)
pmids_results = etree.XML(resp.content)
pmids = pmids_results.xpath('//eSearchResult/IdList/Id')
pmids_string = ''
for item in pmids:
pmids_string += item.text + ','
retrieve_notice_args = {'pmids_string': pmids_string}
retrieve_url_encoded = pubmed_retrieve_api_url.format(**retrieve_notice_args)
search_results_response = get(retrieve_url_encoded).content
search_results = etree.XML(search_results_response)
for entry in eval_xpath_list(search_results, '//PubmedArticle'):
medline = eval_xpath_getindex(entry, './MedlineCitation', 0)
title = eval_xpath_getindex(medline, './/Article/ArticleTitle', 0).text
pmid = eval_xpath_getindex(medline, './/PMID', 0).text
url = pubmed_url + pmid
content = extract_text(
eval_xpath_getindex(medline, './/Abstract/AbstractText//text()', 0, default=None), allow_none=True
)
doi = extract_text(
eval_xpath_getindex(medline, './/ELocationID[@EIdType="doi"]/text()', 0, default=None), allow_none=True
)
journal = extract_text(
eval_xpath_getindex(medline, './Article/Journal/Title/text()', 0, default=None), allow_none=True
)
issn = extract_text(
eval_xpath_getindex(medline, './Article/Journal/ISSN/text()', 0, default=None), allow_none=True
)
authors = []
for author in eval_xpath_list(medline, './Article/AuthorList/Author'):
f = eval_xpath_getindex(author, './ForeName', 0, default=None)
l = eval_xpath_getindex(author, './LastName', 0, default=None)
f = '' if f is None else f.text
l = '' if l is None else l.text
authors.append((f + ' ' + l).strip())
res_dict = {
'template': 'paper.html',
'url': url,
'title': title,
'content': content or "",
'journal': journal,
'issn': [issn],
'authors': authors,
'doi': doi,
args = urlencode(
{
"db": "pubmed",
"term": query,
"retstart": (params["pageno"] - 1) * number_of_results,
"hits": number_of_results,
}
)
esearch_url = f"{eutils_api}/esearch.fcgi?{args}"
# DTD: https://eutils.ncbi.nlm.nih.gov/eutils/dtd/20060628/esearch.dtd
esearch_resp: "SXNG_Response" = get(esearch_url)
pmids_results = etree.XML(esearch_resp.content)
pmids: list[str] = [i.text for i in pmids_results.xpath("//eSearchResult/IdList/Id")]
# send efetch request with the IDs from esearch response
args = urlencode(
{
"db": "pubmed",
"retmode": "xml",
"id": ",".join(pmids),
}
)
efetch_url = f"{eutils_api}/efetch.fcgi?{args}"
params["url"] = efetch_url
def response(resp: "SXNG_Response") -> EngineResults: # pylint: disable=too-many-locals
# DTD: https://dtd.nlm.nih.gov/ncbi/pubmed/out/pubmed_250101.dtd
# parse efetch response
efetch_xml = etree.XML(resp.content)
res = EngineResults()
def _field_txt(xml: ElementType, xpath_str: str) -> str:
elem = eval_xpath_getindex(xml, xpath_str, 0, default="")
return extract_text(elem, allow_none=True) or ""
for pubmed_article in eval_xpath_list(efetch_xml, "//PubmedArticle"):
medline_citation: ElementType = eval_xpath_getindex(pubmed_article, "./MedlineCitation", 0)
pubmed_data: ElementType = eval_xpath_getindex(pubmed_article, "./PubmedData", 0)
title: str = eval_xpath_getindex(medline_citation, ".//Article/ArticleTitle", 0).text
pmid: str = eval_xpath_getindex(medline_citation, ".//PMID", 0).text
url: str = pubmed_url + pmid
content = _field_txt(medline_citation, ".//Abstract/AbstractText//text()")
doi = _field_txt(medline_citation, ".//ELocationID[@EIdType='doi']/text()")
journal = _field_txt(medline_citation, "./Article/Journal/Title/text()")
issn = _field_txt(medline_citation, "./Article/Journal/ISSN/text()")
authors: list[str] = []
for author in eval_xpath_list(medline_citation, "./Article/AuthorList/Author"):
f = eval_xpath_getindex(author, "./ForeName", 0, default=None)
l = eval_xpath_getindex(author, "./LastName", 0, default=None)
author_name = f"{f.text if f is not None else ''} {l.text if l is not None else ''}".strip()
if author_name:
authors.append(author_name)
accepted_date = eval_xpath_getindex(
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:
year = eval_xpath_getindex(accepted_date, './Year', 0)
month = eval_xpath_getindex(accepted_date, './Month', 0)
day = eval_xpath_getindex(accepted_date, './Day', 0)
year = eval_xpath_getindex(accepted_date, "./Year", 0)
month = eval_xpath_getindex(accepted_date, "./Month", 0)
day = eval_xpath_getindex(accepted_date, "./Day", 0)
try:
publishedDate = datetime.strptime(
year.text + '-' + month.text + '-' + day.text,
'%Y-%m-%d',
)
res_dict['publishedDate'] = publishedDate
except Exception as e: # pylint: disable=broad-exception-caught
print(e)
pub_date = datetime(year=int(year.text), month=int(month.text), day=int(day.text))
except ValueError:
pass
results.append(res_dict)
return results
res.add(
res.types.Paper(
url=url,
title=title,
content=content,
journal=journal,
issn=[issn],
authors=authors,
doi=doi,
publishedDate=pub_date,
)
)
return res

View File

@@ -1,125 +1,163 @@
# 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 lxml import html
from flask_babel import gettext # pyright: ignore[reportUnknownVariableType]
from flask_babel import gettext
from searx.network import get
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 = {
"website": 'https://www.semanticscholar.org/',
"wikidata_id": 'Q22908627',
"official_api_documentation": 'https://api.semanticscholar.org/',
"website": "https://www.semanticscholar.org/",
"wikidata_id": "Q22908627",
"official_api_documentation": "https://api.semanticscholar.org/",
"use_official_api": True,
"require_api_key": False,
"results": 'JSON',
"results": "JSON",
}
categories = ['science', 'scientific publications']
categories = ["science", "scientific publications"]
paging = True
search_url = 'https://www.semanticscholar.org/api/1/search'
base_url = 'https://www.semanticscholar.org'
search_url = "https://www.semanticscholar.org/api/1/search"
base_url = "https://www.semanticscholar.org"
CACHE: EngineCache
"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
seconds."""
def _get_ui_version():
resp = get(base_url)
if not resp.ok:
raise RuntimeError("Can't determine Semantic Scholar UI version")
doc = html.fromstring(resp.text)
ui_version = eval_xpath_getindex(doc, "//meta[@name='s2-ui-version']/@content", 0)
if not ui_version:
raise RuntimeError("Can't determine Semantic Scholar UI version")
return ui_version
def setup(engine_settings: dict[str, t.Any]) -> bool:
global CACHE # pylint: disable=global-statement
CACHE = EngineCache(engine_settings["name"])
return True
def request(query, params):
params['url'] = search_url
params['method'] = 'POST'
params['headers'].update(
def get_ui_version() -> str:
ret_val: str = CACHE.get("X-S2-UI-Version")
if not ret_val:
resp = get(base_url)
if not resp.ok:
raise RuntimeError("Can't determine Semantic Scholar UI version")
doc = html.fromstring(resp.text)
ret_val = eval_xpath_getindex(doc, "//meta[@name='s2-ui-version']/@content", 0)
if not ret_val:
raise RuntimeError("Can't determine Semantic Scholar UI version")
# hold the cached value for 5min
CACHE.set("X-S2-UI-Version", value=ret_val, expire=300)
logger.debug("X-S2-UI-Version: %s", ret_val)
return ret_val
def request(query: str, params: "OnlineParams") -> None:
params["url"] = search_url
params["method"] = "POST"
params["headers"].update(
{
'Content-Type': 'application/json',
'X-S2-UI-Version': _get_ui_version(),
'X-S2-Client': "webapp-browser",
"Content-Type": "application/json",
"X-S2-UI-Version": get_ui_version(),
"X-S2-Client": "webapp-browser",
}
)
params['data'] = dumps(
{
"queryString": query,
"page": params['pageno'],
"pageSize": 10,
"sort": "relevance",
"getQuerySuggestions": False,
"authors": [],
"coAuthors": [],
"venues": [],
"performTitleMatch": True,
}
)
return params
params["json"] = {
"queryString": query,
"page": params["pageno"],
"pageSize": 10,
"sort": "relevance",
"getQuerySuggestions": False,
"authors": [],
"coAuthors": [],
"venues": [],
"performTitleMatch": True,
}
def response(resp):
res = resp.json()
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
json_data = resp.json()
results = []
for result in res['results']:
url = result.get('primaryPaperLink', {}).get('url')
if not url and result.get('links'):
url = result.get('links')[0]
for result in json_data["results"]:
url: str = result.get("primaryPaperLink", {}).get("url")
if not url and result.get("links"):
url = result.get("links")[0]
if not url:
alternatePaperLinks = result.get('alternatePaperLinks')
alternatePaperLinks = result.get("alternatePaperLinks")
if alternatePaperLinks:
url = alternatePaperLinks[0].get('url')
url = alternatePaperLinks[0].get("url")
if not url:
url = base_url + '/paper/%s' % result['id']
url = base_url + "/paper/%s" % result["id"]
# publishedDate
if 'pubDate' in result:
publishedDate = datetime.strptime(result['pubDate'], "%Y-%m-%d")
publishedDate: datetime | None
if "pubDate" in result:
publishedDate = datetime.strptime(result["pubDate"], "%Y-%m-%d")
else:
publishedDate = None
# 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
pdf_url = None
for doc in result.get('alternatePaperLinks', []):
if doc['linkType'] not in ('crawler', 'doi'):
pdf_url = doc['url']
pdf_url: str = ""
for doc in result.get("alternatePaperLinks", []):
if doc["linkType"] not in ("crawler", "doi"):
pdf_url = doc["url"]
break
# comments
comments = None
if 'citationStats' in result:
comments: str = ""
if "citationStats" in result:
comments = gettext(
'{numCitations} citations from the year {firstCitationVelocityYear} to {lastCitationVelocityYear}'
"{numCitations} citations from the year {firstCitationVelocityYear} to {lastCitationVelocityYear}"
).format(
numCitations=result['citationStats']['numCitations'],
firstCitationVelocityYear=result['citationStats']['firstCitationVelocityYear'],
lastCitationVelocityYear=result['citationStats']['lastCitationVelocityYear'],
numCitations=result["citationStats"]["numCitations"],
firstCitationVelocityYear=result["citationStats"]["firstCitationVelocityYear"],
lastCitationVelocityYear=result["citationStats"]["lastCitationVelocityYear"],
)
results.append(
{
'template': 'paper.html',
'url': url,
'title': result['title']['text'],
'content': html_to_text(result['paperAbstract']['text']),
'journal': result.get('venue', {}).get('text') or result.get('journal', {}).get('name'),
'doi': result.get('doiInfo', {}).get('doi'),
'tags': result.get('fieldsOfStudy'),
'authors': authors,
'pdf_url': pdf_url,
'publishedDate': publishedDate,
'comments': comments,
}
res.add(
res.types.Paper(
title=result["title"]["text"],
url=url,
content=html_to_text(result["paperAbstract"]["text"]),
journal=result.get("venue", {}).get("text") or result.get("journal", {}).get("name"),
doi=result.get("doiInfo", {}).get("doi"),
tags=result.get("fieldsOfStudy"),
authors=authors,
pdf_url=pdf_url,
publishedDate=publishedDate,
comments=comments,
)
)
return results
return res

View File

@@ -1,71 +1,175 @@
# 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 json import loads
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 = {
"website": 'https://www.springernature.com/',
"wikidata_id": 'Q21096327',
"official_api_documentation": 'https://dev.springernature.com/',
"website": "https://www.springernature.com/",
"wikidata_id": "Q21096327",
"official_api_documentation": "https://dev.springernature.com/docs/live-documentation/",
"use_official_api": True,
"require_api_key": True,
"results": 'JSON',
"results": "JSON",
}
categories = ['science', 'scientific publications']
categories = ["science", "scientific publications"]
paging = True
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):
if api_key == 'unset':
raise SearxEngineAPIException('missing Springer-Nature API key')
args = urlencode({'q': query, 's': nb_per_page * (params['pageno'] - 1), 'p': nb_per_page, 'api_key': api_key})
params['url'] = base_url + args
logger.debug("query_url --> %s", params['url'])
return params
def setup(engine_settings: dict[str, t.Any]) -> bool:
"""Initialization of the Springer engine, checks whether the
:py:obj:`api_key` is set, otherwise the engine is inactive.
"""
key: str = engine_settings.get("api_key", "")
try:
# 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):
results = []
json_data = loads(resp.text)
def request(query: str, params: "OnlineParams") -> None:
args = {
"api_key": api_key,
"q": query,
"s": nb_per_page * (params["pageno"] - 1),
"p": nb_per_page,
}
params["url"] = f"{base_url}?{urlencode(args)}"
# For example, the ``year:`` filter requires a *Premium Plan* subscription.
params["raise_for_httperror"] = False
for record in json_data['records']:
published = datetime.strptime(record['publicationDate'], '%Y-%m-%d')
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' : ''
}
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 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.data import ENGINE_TRAITS
from searx.exceptions import SearxException
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: dict[str, t.Any] = {
"website": "https://zlibrary-global.se",
"wikidata_id": "Q104863992",
@@ -57,7 +58,7 @@ about: dict[str, t.Any] = {
"results": "HTML",
}
categories: list[str] = ["files"]
categories: list[str] = ["files", "books"]
paging: bool = True
base_url: str = "https://zlibrary-global.se"
@@ -74,8 +75,12 @@ zlib_ext: str = ""
``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."""
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}")
if zlib_year_to and zlib_year_to not in traits.custom["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]:
lang: str = traits.get_language(params["language"], traits.all_locale) # type: ignore
def request(query: str, params: "OnlineParams") -> None:
lang: str | None = traits.get_language(params["searxng_locale"], traits.all_locale)
search_url: str = (
base_url
+ "/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,
)
params["verify"] = False
return params
def domain_is_seized(dom):
return bool(dom.xpath('//title') and "seized" in dom.xpath('//title')[0].text.lower())
def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]:
results: list[dict[str, t.Any]] = []
def response(resp: "SXNG_Response") -> EngineResults:
res = EngineResults()
dom = html.fromstring(resp.text)
if domain_is_seized(dom):
raise SearxException(f"zlibrary domain is seized: {base_url}")
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))
i18n_language = gettext("Language")
i18n_book_rating = gettext("Book rating")
i18n_file_quality = gettext("File quality")
def _parse_result(item) -> dict[str, t.Any]:
def _parse_result(item: ElementType) -> dict[str, t.Any]:
author_elements = eval_xpath_list(item, './/div[@class="authors"]//a[@itemprop="author"]')
result = {
"template": "paper.html",
"url": base_url + item.xpath('(.//a[starts-with(@href, "/book/")])[1]/@href')[0],
"title": _text(item, './/*[@itemprop="name"]'),
"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")]'),
}
thumbnail: str = _text(item, './/img[contains(@class, "cover")]/@data-src')
if not thumbnail.startswith('/'):
thumbnail = _text(item, './/img[contains(@class, "cover")]/@data-src')
if thumbnail and not thumbnail.startswith('/'):
result["thumbnail"] = thumbnail
year = _text(item, './/div[contains(@class, "property_year")]//div[contains(@class, "property_value")]')
if year:
result["publishedDate"] = datetime.strptime(year, '%Y')
content = []
content: list[str] = []
language = _text(item, './/div[contains(@class, "property_language")]//div[contains(@class, "property_value")]')
if language:
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:
"""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.core
import httpx
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:
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):
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.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"):
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"):
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"):
value: str | None = ext.get("value")
if value is None:
value = ""
engine_traits.custom["ext"].append(value)
l.append(ext.get("value") or "")
engine_traits.custom["ext"] = l
# Handle languages
# 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] = {}
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
loc = babel.Locale.parse(locale)
if loc.english_name is None:

View File

@@ -13,13 +13,9 @@ import threading
import httpx
from httpx_socks import AsyncProxyTransport
from python_socks import parse_proxy_url, ProxyConnectionError, ProxyTimeoutError, ProxyError
import uvloop
from searx import logger
uvloop.install()
CertTypes = str | tuple[str, str] | tuple[str, str, str]
SslContextKeyType = tuple[str | None, CertTypes | None, bool, bool]

View File

@@ -280,9 +280,9 @@ class Network:
client.cookies = httpx.Cookies(cookies)
try:
if stream:
response = client.stream(method, url, **kwargs)
else:
response = await client.request(method, url, **kwargs)
return client.stream(method, url, **kwargs)
response = await client.request(method, url, **kwargs)
if self.is_valid_response(response) or retries <= 0:
return self.patch_response(response, do_raise_for_httperror)
except httpx.RemoteProtocolError as e:

View File

@@ -22,6 +22,7 @@ __all__ = [
"Translations",
"WeatherAnswer",
"Code",
"Paper",
]
import typing as t
@@ -31,6 +32,7 @@ from ._base import Result, MainResult, LegacyResult
from .answer import AnswerSet, Answer, Translations, WeatherAnswer
from .keyvalue import KeyValue
from .code import Code
from .paper import Paper
class ResultList(list[Result | LegacyResult], abc.ABC):
@@ -44,6 +46,7 @@ class ResultList(list[Result | LegacyResult], abc.ABC):
Answer = Answer
KeyValue = KeyValue
Code = Code
Paper = Paper
MainResult = MainResult
Result = Result
Translations = Translations

View File

@@ -362,7 +362,11 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
"""The date on which the object was published."""
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
"""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
results: JSON
# - name: astrophysics data system
# engine: astrophysics_data_system
# sort: asc
# weight: 5
# categories: [science]
# api_key: your-new-key
# shortcut: ads
- name: astrophysics data system
engine: astrophysics_data_system
shortcut: ads
# read https://docs.searxng.org/dev/engines/online/astrophysics_data_system.html
api_key: ""
inactive: true
- name: alpine linux packages
engine: alpinelinux
@@ -424,6 +423,7 @@ engines:
engine: annas_archive
disabled: true
shortcut: aa
timeout: 5
- name: ansa
engine: ansa
@@ -490,7 +490,6 @@ engines:
- name: arxiv
engine: arxiv
shortcut: arx
timeout: 4.0
- name: ask
engine: ask
@@ -659,12 +658,12 @@ engines:
timeout: 30
disabled: true
# - name: core.ac.uk
# engine: core
# categories: science
# shortcut: cor
# # get your API key from: https://core.ac.uk/api-keys/register/
# api_key: 'unset'
- name: core.ac.uk
engine: core
shortcut: cor
# read https://docs.searxng.org/dev/engines/online/core.html
api_key: ""
inactive: true
- name: cppreference
engine: cppreference
@@ -1254,9 +1253,10 @@ engines:
- name: z-library
engine: zlibrary
shortcut: zlib
categories: files
timeout: 7.0
disabled: true
# https://github.com/searxng/searxng/issues/3610
inactive: true
- name: library of congress
engine: loc
@@ -1517,7 +1517,7 @@ engines:
- name: openlibrary
engine: openlibrary
shortcut: ol
timeout: 5
timeout: 10
disabled: true
- name: openmeteo
@@ -1735,7 +1735,6 @@ engines:
- name: pubmed
engine: pubmed
shortcut: pub
timeout: 3.0
- name: pypi
shortcut: pypi
@@ -1965,7 +1964,6 @@ engines:
- name: semantic scholar
engine: semantic_scholar
disabled: true
shortcut: se
# Spotify needs API credentials
@@ -1986,13 +1984,13 @@ engines:
# query_fields: '' # query fields
# enable_http: true
# - name: springer nature
# engine: springer
# # get your API key from: https://dev.springernature.com/signup
# # working API key, for test & debug: "a69685087d07eca9f13db62f65b8f601"
# api_key: 'unset'
# shortcut: springer
# timeout: 15.0
- name: springer nature
engine: springer
shortcut: springer
timeout: 5
# read https://docs.searxng.org/dev/engines/online/springer.html
api_key: ""
inactive: true
- name: 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 %}
{{ result_header(result, favicons, image_proxify) -}}
{{ result_header(result, favicons, image_proxify) }}
<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.authors %}<div class="result_authors"><span>{{ _("Author") }}:</span><span>{{ result.authors | join(", ") }}</span></div>{% endif -%}
{%- if result.date_of_publication %}
<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 -%}
<div class="result_journal">
<span>{{- _("Journal") }}:</span><span>{{ result.journal -}}
{%- if result.volume -%}
&nbsp;{{- result.volume -}}
{%- if result.number -%}
.{{- result.number -}}
<div>
<span>{{- _("Journal") }}:</span>
<span>{{ result.journal -}}
{%- if result.volume -%}
&nbsp;{{- result.volume -}}
{%- if result.number -%}.{{- result.number -}}{%- endif -%}
{%- endif -%}
{%- endif -%}
{%- if result.pages -%}
&nbsp;{{- result.pages -}}
{%- endif -%}
{%- if result.pages -%}&nbsp;{{- result.pages -}}{%- endif -%}
</span>
</div>
{%- endif %}
{%- if result.editor %}<div class="result_editor"><span>{{ _("Editor") }}:</span><span>{{ result.editor }}</span></div>{% endif -%}
{%- if result.publisher %}<div class="result_publisher"><span>{{ _("Publisher") }}:</span><span>{{ result.publisher }}</span></div>{% endif -%}
{%- if result.type %}<div class="result_type"><span>{{ _("Type") }}:</span><span>{{ result.type }}</span></div>{% endif -%}
{%- if result.tags %}<div class="result_tags"><span>{{ _("Tags") }}:</span><span>{{ result.tags | join(", ")}}</span></div>{%- endif -%}
{%- if result.doi %}<div class="result_doi"><span>{{ _("DOI") }}:</span><span>{{ result_link(doi_resolver + result.doi, result.doi) }}</span></div>{% endif -%}
{%- if result.issn %}<div class="result_issn"><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.editor %}
<div>
<span>{{ _("Editor") }}:</span>
<span>{{ result.editor }}</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>
{%- if result.content -%}<p class="content">{{- result.content | safe -}}</p>{%- endif -%}
{%- if result.comments -%}<p class="comments">{{- result.comments -}}</p>{%- endif -%}
{%- if result.content -%}
<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">
{%- if result.pdf_url -%}{{ result_link(result.pdf_url, _('PDF')) }}{%- endif -%}
{%- if result.html_url -%}{{ result_link(result.html_url, _('HTML')) }}{%- endif -%}

View File

@@ -23,8 +23,8 @@ msgstr ""
"Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-09-01 12:54+0000\n"
"PO-Revision-Date: 2025-09-02 04:50+0000\n"
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
"PO-Revision-Date: 2025-09-14 00:25+0000\n"
"Last-Translator: artnay <artnay@noreply.codeberg.org>\n"
"Language-Team: Finnish <https://translate.codeberg.org/projects/searxng/"
"searxng/fi/>\n"
"Language: fi\n"
@@ -32,7 +32,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.12.2\n"
"X-Generator: Weblate 5.13.2\n"
"Generated-By: Babel 2.17.0\n"
#. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -153,7 +153,7 @@ msgstr "web"
#. CATEGORY_GROUPS['SCIENTIFIC PUBLICATIONS']
#: searx/searxng.msg
msgid "scientific publications"
msgstr "Tieteelliset Julkaisut"
msgstr "tieteelliset julkaisut"
#. STYLE_NAMES['AUTO']
#: searx/searxng.msg
@@ -223,12 +223,12 @@ msgstr "Kosteus"
#. WEATHER_TERMS['MAX TEMP.']
#: searx/searxng.msg
msgid "Max temp."
msgstr "Maksimi lämpötila."
msgstr "Suurin lämpötila."
#. WEATHER_TERMS['MIN TEMP.']
#: searx/searxng.msg
msgid "Min temp."
msgstr "Minimi lämpötila."
msgstr "Pienin lämpötila."
#. WEATHER_TERMS['MORNING']
#: searx/searxng.msg
@@ -268,7 +268,7 @@ msgstr "Lämpötila"
#. WEATHER_TERMS['UV INDEX']
#: searx/searxng.msg
msgid "UV index"
msgstr "UV indeksi"
msgstr "UV-indeksi"
#. WEATHER_TERMS['VISIBILITY']
#: searx/searxng.msg
@@ -293,7 +293,7 @@ msgstr "Osittain pilvistä"
#. WEATHER_CONDITIONS
#: searx/searxng.msg
msgid "Cloudy"
msgstr "Pilvinen"
msgstr "Pilvistä"
#. WEATHER_CONDITIONS
#: searx/searxng.msg
@@ -303,7 +303,7 @@ msgstr "Selkeää"
#. WEATHER_CONDITIONS
#: searx/searxng.msg
msgid "Fog"
msgstr "Sumu"
msgstr "Sumuista"
#. WEATHER_CONDITIONS
#: searx/searxng.msg
@@ -463,7 +463,7 @@ msgstr ""
#. WEATHER_CONDITIONS
#: searx/searxng.msg
msgid "Snow"
msgstr ""
msgstr "Lumisadetta"
#. WEATHER_CONDITIONS
#: searx/searxng.msg
@@ -524,7 +524,7 @@ msgstr "pisteet"
#. SOCIAL_MEDIA_TERMS['TITLE']
#: searx/searxng.msg
msgid "title"
msgstr "Otsikko"
msgstr "otsikko"
#. SOCIAL_MEDIA_TERMS['AUTHOR']
#: searx/engines/hackernews.py:85 searx/searxng.msg
@@ -534,7 +534,7 @@ msgstr "tekijä"
#. SOCIAL_MEDIA_TERMS['THREAD OPEN']
#: searx/engines/discourse.py:149 searx/searxng.msg
msgid "open"
msgstr "Avaa"
msgstr "avoin"
#. SOCIAL_MEDIA_TERMS['THREAD CLOSED']
#: searx/engines/discourse.py:149 searx/searxng.msg
@@ -685,7 +685,7 @@ msgid ""
"{numCitations} citations from the year {firstCitationVelocityYear} to "
"{lastCitationVelocityYear}"
msgstr ""
"{numCitations} Sitaatit vuodesta {firstCitationVelocityYear} vuoteen "
"{numCitations} sitaattia vuodesta {firstCitationVelocityYear} vuoteen "
"{lastCitationVelocityYear}"
#: searx/engines/tineye.py:48
@@ -792,11 +792,11 @@ msgstr "Selaimesi tunnistetiedot: "
#: searx/plugins/time_zone.py:34
msgid "Timezones plugin"
msgstr ""
msgstr "Aikavyöhykkeiden liitännäinen"
#: searx/plugins/time_zone.py:35
msgid "Display the current time on different time zones."
msgstr ""
msgstr "Näytä aika eri aikavyöhykkeillä."
#: searx/plugins/tor_check.py:42
msgid "Tor check plugin"
@@ -924,7 +924,7 @@ msgstr "välimuistissa"
#: searx/templates/simple/new_issue.html:64
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
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
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
msgid "If this is a public instance, please specify the URL in the bug report"
msgstr ""
"Jos kyseessä on julkinen instanssi, määritä sen URL-osoite "
"vikailmoituksessa"
"Jos kyseessä on julkinen palvelin, määritä sen URL-osoite vikailmoituksessa"
#: searx/templates/simple/new_issue.html:72
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:70
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
msgid "!bang for this engine"
@@ -960,7 +959,7 @@ msgstr "!bang tälle hakukoneelle"
#: searx/templates/simple/preferences.html:80
msgid "!bang for its categories"
msgstr "!bang sen kategorioille"
msgstr "!bang sen luokille"
#: searx/templates/simple/preferences.html:102
#: searx/templates/simple/stats.html:64
@@ -1283,7 +1282,7 @@ msgstr "Tuloksia ei löytynyt. Voit:"
#: searx/templates/simple/messages/no_results.html:14
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
msgid "Refresh the page."
@@ -1299,7 +1298,7 @@ msgstr "Vaihtaa käytettävää hakukonetta asetuksista:"
#: searx/templates/simple/messages/no_results.html:22
msgid "Switch to another instance:"
msgstr "Vaihtaa toiseen instanssiin:"
msgstr "Vaihtaa toiseen palvelimeen:"
#: searx/templates/simple/messages/no_results.html:24
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
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/engines.html:23
@@ -1361,7 +1360,7 @@ msgstr ""
#: searx/templates/simple/preferences/cookies.html:3
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
msgid "Cookie name"
@@ -1506,7 +1505,7 @@ msgstr "Kuvat välityspalvelimen kautta"
#: searx/templates/simple/preferences/image_proxy.html:14
msgid "Proxy image results through SearXNG"
msgstr ""
msgstr "Välitä kuvatulokset SearXNG-välityspalvelimen kautta"
#: searx/templates/simple/preferences/infinite_scroll.html:2
msgid "Infinite scroll"
@@ -1569,6 +1568,8 @@ msgid ""
"Perform a search immediately if a category is selected. Disable to select"
" multiple categories"
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
msgid "Theme"

View File

@@ -23,13 +23,15 @@
# haderachhh <haderachhh@noreply.codeberg.org>, 2025.
# return42 <return42@noreply.codeberg.org>, 2025.
# nhthinh <nhthinh@noreply.codeberg.org>, 2025.
# recreationalprogamer <recreationalprogamer@noreply.codeberg.org>, 2025.
msgid ""
msgstr ""
"Project-Id-Version: searx\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-09-01 12:54+0000\n"
"PO-Revision-Date: 2025-09-02 04:50+0000\n"
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
"PO-Revision-Date: 2025-09-17 13:09+0000\n"
"Last-Translator: recreationalprogamer "
"<recreationalprogamer@noreply.codeberg.org>\n"
"Language-Team: Vietnamese <https://translate.codeberg.org/projects/searxng/"
"searxng/vi/>\n"
"Language: vi\n"
@@ -37,7 +39,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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"
#. CONSTANT_NAMES['NO_SUBGROUPING']
@@ -53,17 +55,17 @@ msgstr "khác"
#. CATEGORY_NAMES['FILES']
#: searx/searxng.msg
msgid "files"
msgstr "các thư mục"
msgstr "tệp"
#. CATEGORY_NAMES['GENERAL']
#: searx/searxng.msg
msgid "general"
msgstr "tổng quát"
msgstr "chung"
#. CATEGORY_NAMES['MUSIC']
#: searx/searxng.msg
msgid "music"
msgstr "âm nhạc"
msgstr "nhạc"
#. CATEGORY_NAMES['SOCIAL_MEDIA']
#: searx/searxng.msg
@@ -73,22 +75,22 @@ msgstr "mạng xã hội"
#. CATEGORY_NAMES['IMAGES']
#: searx/searxng.msg
msgid "images"
msgstr "hình ảnh"
msgstr "ảnh"
#. CATEGORY_NAMES['VIDEOS']
#: searx/searxng.msg
msgid "videos"
msgstr "băng hình"
msgstr "video"
#. CATEGORY_NAMES['RADIO']
#: searx/engines/radio_browser.py:151 searx/searxng.msg
msgid "radio"
msgstr "máy radio"
msgstr "radio"
#. CATEGORY_NAMES['TV']
#: searx/searxng.msg
msgid "tv"
msgstr "tivi"
msgstr "TV"
#. CATEGORY_NAMES['IT']
#: searx/searxng.msg
@@ -108,7 +110,7 @@ msgstr "bản đồ"
#. CATEGORY_NAMES['ONIONS']
#: searx/searxng.msg
msgid "onions"
msgstr "định tuyến onion"
msgstr "trang onion"
#. CATEGORY_NAMES['SCIENCE']
#: searx/searxng.msg
@@ -118,22 +120,22 @@ msgstr "khoa học"
#. CATEGORY_GROUPS['APPS']
#: searx/searxng.msg
msgid "apps"
msgstr "ng dụng"
msgstr "ng dụng"
#. CATEGORY_GROUPS['DICTIONARIES']
#: searx/searxng.msg
msgid "dictionaries"
msgstr "Từ điển"
msgstr "từ điển"
#. CATEGORY_GROUPS['LYRICS']
#: searx/searxng.msg
msgid "lyrics"
msgstr "Lời bài hát"
msgstr "lời bài hát"
#. CATEGORY_GROUPS['PACKAGES']
#: searx/searxng.msg
msgid "packages"
msgstr "gói kiện/gói hàng"
msgstr "gói"
#. CATEGORY_GROUPS['Q_A']
#: searx/searxng.msg
@@ -143,7 +145,7 @@ msgstr "hỏi đáp"
#. CATEGORY_GROUPS['REPOS']
#: searx/searxng.msg
msgid "repos"
msgstr "kho"
msgstr "kho"
#. CATEGORY_GROUPS['SOFTWARE_WIKIS']
#: searx/searxng.msg
@@ -153,27 +155,27 @@ msgstr "wiki về phần mềm"
#. CATEGORY_GROUPS['WEB']
#: searx/searxng.msg
msgid "web"
msgstr "mạng lưới/mạng"
msgstr "mạng"
#. CATEGORY_GROUPS['SCIENTIFIC PUBLICATIONS']
#: searx/searxng.msg
msgid "scientific publications"
msgstr "các công bố khoa học"
msgstr "công bố khoa học"
#. STYLE_NAMES['AUTO']
#: searx/searxng.msg
msgid "auto"
msgstr "Tự động"
msgstr "tự động"
#. STYLE_NAMES['LIGHT']
#: searx/searxng.msg
msgid "light"
msgstr "Sáng"
msgstr "sáng"
#. STYLE_NAMES['DARK']
#: searx/searxng.msg
msgid "dark"
msgstr "Tối"
msgstr "tối"
#. STYLE_NAMES['BLACK']
#: searx/searxng.msg
@@ -498,18 +500,18 @@ msgstr "người đăng ký"
#. SOCIAL_MEDIA_TERMS['POSTS']
#: searx/engines/lemmy.py:86 searx/searxng.msg
msgid "posts"
msgstr "những bài đăng"
msgstr "bài đăng"
#. SOCIAL_MEDIA_TERMS['ACTIVE USERS']
#: searx/engines/lemmy.py:87 searx/searxng.msg
msgid "active users"
msgstr "Người dùng hoạt động"
msgstr "người dùng hoạt động"
#. SOCIAL_MEDIA_TERMS['COMMENTS']
#: searx/engines/discourse.py:157 searx/engines/hackernews.py:82
#: searx/engines/lemmy.py:130 searx/searxng.msg
msgid "comments"
msgstr "Bình luận"
msgstr "bình luận"
#. SOCIAL_MEDIA_TERMS['USER']
#: 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
msgid "timeout"
msgstr "Hết thời gian"
msgstr "hết thời gian"
#: searx/webutils.py:37
msgid "parsing error"
@@ -590,7 +592,7 @@ msgstr "Lỗi giao thức HTTP"
#: searx/webutils.py:39
msgid "network error"
msgstr "Lỗi mạng"
msgstr "lỗi mạng"
#: searx/webutils.py:40
msgid "SSL error: certificate validation has failed"
@@ -610,7 +612,7 @@ msgstr "Lỗi kết nối HTTP"
#: searx/webutils.py:56
msgid "proxy error"
msgstr "Lỗi proxy"
msgstr "lỗi proxy"
#: searx/webutils.py:57
msgid "CAPTCHA"
@@ -622,11 +624,11 @@ msgstr "quá nhiều yêu cầu"
#: searx/webutils.py:59
msgid "access denied"
msgstr "Truy cập bị từ chối"
msgstr "truy cập bị từ chối"
#: searx/webutils.py:60
msgid "server API error"
msgstr "Lỗi máy chủ API"
msgstr "lỗi máy chủ API"
#: searx/webutils.py:79
msgid "Suspended"
@@ -635,7 +637,7 @@ msgstr "Treo/gián đoạn/chặn"
#: searx/webutils.py:306
#, python-brace-format
msgid "{minutes} minute(s) ago"
msgstr "{minutes} phút() trước"
msgstr "{minutes} phút trước"
#: searx/webutils.py:307
#, 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
#, python-brace-format
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
msgid "Show route in map .."
@@ -662,7 +664,7 @@ msgstr "{title} (HẾT HẠN)"
#: searx/engines/pdbe.py:103
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
msgid "Channel"
@@ -713,7 +715,7 @@ msgstr ""
#: searx/engines/tineye.py:59
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
msgid "Book rating"
@@ -725,7 +727,7 @@ msgstr "Chất lượng tệp"
#: searx/plugins/ahmia_filter.py:32
msgid "Ahmia blacklist"
msgstr "Ahmia blacklist"
msgstr "Danh sách đen của Ahmia"
#: searx/plugins/ahmia_filter.py:33
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, "
"sha1, sha224, sha256, sha384, sha512."
msgstr ""
"Chuyển đổi chuỗi thành các giá trị băm với các hàm hỗ trợ: md5, sha1, "
"sha224, sha256, sha384, sha512."
"Chuyển đổi xâu thành các giá trị băm với các hàm hỗ trợ: md5, sha1, sha224, "
"sha256, sha384, sha512."
#: searx/plugins/hash_plugin.py:64
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
msgid "Timezones plugin"
msgstr ""
msgstr "Tiện ích múi giờ"
#: searx/plugins/time_zone.py:35
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
msgid "Tor check plugin"
msgstr "Kiểm tra Tor plugin"
msgstr "Plugin kiểm tra Tor"
#: searx/plugins/tor_check.py:44
msgid ""
@@ -818,7 +820,7 @@ msgstr ""
#: searx/plugins/tor_check.py:65
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
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
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/result_templates/packages.html:59
@@ -886,11 +888,11 @@ msgstr "Mã nguồn"
#: searx/templates/simple/base.html:66
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
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
msgid "Public instances"
@@ -902,7 +904,7 @@ msgstr "Chính sách bảo mật"
#: searx/templates/simple/base.html:75
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
msgid "Click on the magnifier to perform search"
@@ -986,7 +988,7 @@ msgstr "P95"
#: searx/templates/simple/preferences.html:136
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
msgid "Errors:"
@@ -1131,19 +1133,19 @@ msgstr "Tham số"
#: searx/templates/simple/result_templates/files.html:36
#: searx/templates/simple/stats.html:119
msgid "Filename"
msgstr "Tên file"
msgstr "Tên tệp"
#: searx/templates/simple/stats.html:120
msgid "Function"
msgstr "Chức năng"
msgstr "Hàm"
#: searx/templates/simple/stats.html:121
msgid "Code"
msgstr "Code"
msgstr "Mã nguồn"
#: searx/templates/simple/stats.html:128
msgid "Checker"
msgstr "Người kiểm duyệt"
msgstr "Trình kiểm tra"
#: searx/templates/simple/stats.html:131
msgid "Failed test"
@@ -1156,7 +1158,7 @@ msgstr "Bình luận"
#: searx/templates/simple/answer/translations.html:12
#: searx/templates/simple/preferences/answerers.html:8
msgid "Examples"
msgstr "Các ví dụ"
msgstr "Ví dụ"
#: searx/templates/simple/answer/translations.html:21
msgid "Definitions"
@@ -1206,7 +1208,7 @@ msgstr "Sao chép"
#: searx/templates/simple/elements/suggestions.html:3
msgid "Suggestions"
msgstr "Các gợi ý"
msgstr "Gợi ý"
#: searx/templates/simple/filters/languages.html:1
#: searx/templates/simple/preferences/language.html:2
@@ -1270,7 +1272,7 @@ msgstr "Tháng trước"
#: searx/templates/simple/filters/time_range.html:15
msgid "Last year"
msgstr "Năm ngoái"
msgstr "Năm trước"
#: searx/templates/simple/messages/no_cookies.html:3
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
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
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
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/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
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
msgid "Autocomplete"
@@ -1356,15 +1358,15 @@ msgstr "Căn giữa"
#: searx/templates/simple/preferences/center_alignment.html:14
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
msgid ""
"This is the list of cookies and their values SearXNG is storing on your "
"computer."
msgstr ""
"Danh sách tên và giá trị của những cookies mà SearXNG lưu trữ trên máy "
"tính của bạn."
"Danh sách tên và giá trị của những cookie mà SearXNG lưu trữ trên máy tính "
"của bạn."
#: searx/templates/simple/preferences/cookies.html:3
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 "
"leaking data to the clicked result sites."
msgstr ""
"Ghi chú: việc định rõ cài đặt cá nhân trong URL tìm kiếm có thể làm suy "
"giảm mức độ riêng tư vì nó chuyển dữ liệu đến các trang kết quả được nhấp"
" chọn."
"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 "
"mức độ riêng tư vì nó chuyển dữ liệu đến các trang kết quả được nhấp chọn."
#: searx/templates/simple/preferences/cookies.html:35
msgid "URL to restore your preferences in another browser"
msgstr ""
"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"
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"
#: searx/templates/simple/preferences/cookies.html:43
msgid ""
@@ -1407,7 +1406,7 @@ msgstr ""
#: searx/templates/simple/preferences/cookies.html:46
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
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
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
msgid "Digital Object Identifier (DOI)"
@@ -1502,7 +1501,7 @@ msgstr "Phím tắt"
#: searx/templates/simple/preferences/hotkeys.html:13
msgid "Vim-like"
msgstr "Vim-like"
msgstr "Giống Vim"
#: searx/templates/simple/preferences/hotkeys.html:18
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
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
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
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
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
msgid "Interface language"
@@ -1632,7 +1631,7 @@ msgstr "Máy chủ"
#: searx/templates/simple/preferences/urlformatting.html:23
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
msgid "Repository"
@@ -1652,13 +1651,13 @@ msgstr "ẩn nội dung"
#: searx/templates/simple/result_templates/default.html:14
#: searx/templates/simple/result_templates/videos.html:14
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/images.html:22
#: searx/templates/simple/result_templates/torrent.html:18
msgid "Filesize"
msgstr "Kích thước tập tin"
msgstr "Kích cỡ tệp"
#: searx/templates/simple/result_templates/files.html:40
msgid "Date"
@@ -1687,7 +1686,7 @@ msgstr "Xem nguồn"
#: searx/templates/simple/result_templates/map.html:12
msgid "address"
msgstr "Địa chỉ"
msgstr "địa chỉ"
#: searx/templates/simple/result_templates/map.html:43
msgid "show map"
@@ -1773,7 +1772,7 @@ msgstr "liên kết magnet"
#: searx/templates/simple/result_templates/torrent.html:8
msgid "torrent file"
msgstr "tập tin torrent"
msgstr "tệp torrent"
#: searx/templates/simple/result_templates/torrent.html:13
msgid "Seeder"
@@ -1785,15 +1784,15 @@ msgstr "Leecher"
#: searx/templates/simple/result_templates/torrent.html:19
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
msgid "show video"
msgstr "hiện"
msgstr "hiện video"
#: searx/templates/simple/result_templates/videos.html:6
msgid "hide video"
msgstr "ẩn phim"
msgstr "ẩn video"
#~ msgid "Engine time (sec)"
#~ 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 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.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`,
:py:obj:`searx.utils.eval_xpath` and other XPath selectors."""
ElementType: t.TypeAlias = ElementBase | _Element
_BLOCKED_TAGS = ('script', 'style')
_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(
xpath_results: list[ElementBase] | ElementBase | str | Number | bool | None,
xpath_results: list[ElementType] | ElementType | str | Number | bool | None,
allow_none: bool = False,
) -> str | None:
"""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 xml element, extract all the text node from it
( text_content() method from lxml )
* if xpath_results is a string element, then it's already done
- If ``xpath_results`` is a list of :py:obj:`ElementType` objects, extract
the text from each result and concatenate the list in a string.
- 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):
# it's list of result : concat everything using recursive call
@@ -220,7 +232,7 @@ def extract_text(
for e in xpath_results:
result = result + (extract_text(e) or '')
return result.strip()
if isinstance(xpath_results, ElementBase):
if isinstance(xpath_results, ElementType):
# it's a element
text: str = html.tostring( # type: ignore
xpath_results, # pyright: ignore[reportArgumentType]
@@ -289,7 +301,7 @@ def normalize_url(url: str, base_url: str) -> str:
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
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]
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
a :py:obj:`lxml.etree.XPath` object once for all. The return value of
``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
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
return value is a :py:obj:`list`. The minimum length of the list is also
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):
raise SearxEngineXPathException(xpath_spec, 'the result is not a list')
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(
element: ElementBase,
element: ElementType,
xpath_spec: XPathSpecType,
index: int,
default: t.Any = _NOTSET,

View File

@@ -27,6 +27,7 @@ import babel
import babel.numbers
import babel.dates
import babel.languages
import flask_babel
from searx import network
from searx.cache import ExpireCache, ExpireCacheCfg
@@ -197,6 +198,7 @@ class GeoLocation:
DateTimeFormats = typing.Literal["full", "long", "medium", "short"]
DateTimeLocaleTypes = typing.Literal["UI"]
@typing.final
@@ -205,6 +207,13 @@ class DateTime:
conveniently combines :py:obj:`datetime.datetime` and
:py:obj:`babel.dates.format_datetime`. A conversion of time zones is not
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):
@@ -216,15 +225,32 @@ class DateTime:
def l10n(
self,
fmt: DateTimeFormats | str = "medium",
locale: babel.Locale | GeoLocation | None = None,
locale: DateTimeLocaleTypes | babel.Locale | GeoLocation | None = None,
) -> str:
"""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()
elif locale is None:
locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
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
class Temperature:

View File

@@ -85,9 +85,20 @@ container.build() {
info_msg "Set \$DOCKER_TAG: $DOCKER_TAG"
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
params_build_builder="build --format=oci --platform=$platform --layers --identity-label=false"
params_build=$params_build_builder
params_build_builder="build --format=oci --platform=$platform --layers --identity-label=false --timestamp=$timestamp_venv"
params_build="build --format=oci --platform=$platform --layers --identity-label=false"
else
params_build_builder="build --platform=$platform"
params_build=$params_build_builder
@@ -102,8 +113,8 @@ container.build() {
# shellcheck disable=SC2086
"$container_engine" $params_build_builder \
--build-arg="TIMESTAMP=$(git log -1 --date=format:'%Y%m%d%H%M.%S' --format='%ad')" \
--build-arg="TIMESTAMP_SETTINGS=$(git log -1 --date=format:'%Y%m%d%H%M.%S' --format='%ad' ./searx/settings.yml)" \
--build-arg="TIMESTAMP_VENV=$timestamp_venv" \
--build-arg="TIMESTAMP_SETTINGS=$timestamp_searx_settings" \
--tag="localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder" \
--file="./container/builder.dockerfile" \
.