mirror of
https://github.com/searxng/searxng.git
synced 2026-09-14 10:16:03 +00:00
Compare commits
19 Commits
0ce0d957b1
...
b7ecc1c240
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ecc1c240 | ||
|
|
164167dea0 | ||
|
|
b3eb7657b9 | ||
|
|
d2b4bff856 | ||
|
|
1520a8d545 | ||
|
|
f8f7adce6b | ||
|
|
4c42704c80 | ||
|
|
4b4bf0ecaf | ||
|
|
bb22bb1831 | ||
|
|
96e63df8ca | ||
|
|
0691e50e13 | ||
|
|
599d9488c5 | ||
|
|
078c9fcb68 | ||
|
|
3ec6d65f9b | ||
|
|
22e73727c0 | ||
|
|
6c3fb9e42b | ||
|
|
f9b4869426 | ||
|
|
7eedd44f5f | ||
|
|
57ef342ad1 |
1
.github/workflows/container.yml
vendored
1
.github/workflows/container.yml
vendored
@@ -134,6 +134,7 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
|
||||
72
client/simple/src/less/result_types/paper.less
Normal file
72
client/simple/src/less/result_types/paper.less
Normal file
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Paper result class
|
||||
*/
|
||||
|
||||
.result-paper {
|
||||
.attributes {
|
||||
display: table;
|
||||
border-spacing: 0.125rem;
|
||||
|
||||
div {
|
||||
display: table-row;
|
||||
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
display: table-cell;
|
||||
|
||||
time {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
color: var(--color-base-font);
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: var(--color-result-publishdate-font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.comments {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0 0 0;
|
||||
padding: 0;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.24;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @phone) {
|
||||
.result-paper {
|
||||
.attributes {
|
||||
display: block;
|
||||
|
||||
div {
|
||||
display: block;
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
.ltr-margin-left(0.5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,11 +309,11 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
}
|
||||
|
||||
.result-paper,
|
||||
.result-packages {
|
||||
.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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
8
docs/dev/engines/online/arxiv.rst
Normal file
8
docs/dev/engines/online/arxiv.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _arxiv engine:
|
||||
|
||||
=====
|
||||
arXiv
|
||||
=====
|
||||
|
||||
.. automodule:: searx.engines.arxiv
|
||||
:members:
|
||||
8
docs/dev/engines/online/astrophysics_data_system.rst
Normal file
8
docs/dev/engines/online/astrophysics_data_system.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _astrophysics_data_system engine:
|
||||
|
||||
==============================
|
||||
Astrophysics Data System (ADS)
|
||||
==============================
|
||||
|
||||
.. automodule:: searx.engines.astrophysics_data_system
|
||||
:members:
|
||||
8
docs/dev/engines/online/crossref.rst
Normal file
8
docs/dev/engines/online/crossref.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _crossref engine:
|
||||
|
||||
========
|
||||
Crossref
|
||||
========
|
||||
|
||||
.. automodule:: searx.engines.crossref
|
||||
:members:
|
||||
@@ -1,100 +1,8 @@
|
||||
.. _openalex engine:
|
||||
|
||||
=========
|
||||
OpenAlex
|
||||
=========
|
||||
|
||||
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:
|
||||
|
||||
8
docs/dev/engines/online/openlibrary.rst
Normal file
8
docs/dev/engines/online/openlibrary.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _openlibrary engine:
|
||||
|
||||
============
|
||||
Open Library
|
||||
============
|
||||
|
||||
.. automodule:: searx.engines.openlibrary
|
||||
:members:
|
||||
8
docs/dev/engines/online/pubmed.rst
Normal file
8
docs/dev/engines/online/pubmed.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _pubmed engine:
|
||||
|
||||
======
|
||||
PubMed
|
||||
======
|
||||
|
||||
.. automodule:: searx.engines.pubmed
|
||||
:members:
|
||||
8
docs/dev/engines/online/semantic_scholar.rst
Normal file
8
docs/dev/engines/online/semantic_scholar.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _semantic_scholar engine:
|
||||
|
||||
================
|
||||
Semantic Scholar
|
||||
================
|
||||
|
||||
.. automodule:: searx.engines.semantic_scholar
|
||||
:members:
|
||||
8
docs/dev/engines/online/springer.rst
Normal file
8
docs/dev/engines/online/springer.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. _springer engine:
|
||||
|
||||
===============
|
||||
Springer Nature
|
||||
===============
|
||||
|
||||
.. automodule:: searx.engines.springer
|
||||
:members:
|
||||
7
docs/dev/result_types/main/paper.rst
Normal file
7
docs/dev/result_types/main/paper.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
.. _result_types.paper:
|
||||
|
||||
=============
|
||||
Paper Results
|
||||
=============
|
||||
|
||||
.. automodule:: searx.result_types.paper
|
||||
@@ -16,6 +16,7 @@ following types have been implemented so far ..
|
||||
main/mainresult
|
||||
main/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
1
manage
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
96
searx/result_types/paper.py
Normal file
96
searx/result_types/paper.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Typification of the *paper* results.
|
||||
|
||||
.. _BibTeX field types: https://en.wikipedia.org/wiki/BibTeX#Field_types
|
||||
.. _BibTeX format: https://www.bibtex.com/g/bibtex-format/
|
||||
|
||||
Results of this type are rendered in the :origin:`paper.html
|
||||
<searx/templates/simple/result_templates/paper.html>` template.
|
||||
|
||||
Related topics:
|
||||
|
||||
- `BibTeX field types`_
|
||||
- `BibTeX format`_
|
||||
|
||||
----
|
||||
|
||||
.. autoclass:: Paper
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
"""
|
||||
# pylint: disable=too-few-public-methods, disable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["Paper"]
|
||||
|
||||
import typing as t
|
||||
|
||||
from searx.weather import DateTime
|
||||
from ._base import MainResult
|
||||
|
||||
|
||||
@t.final
|
||||
class Paper(MainResult, kw_only=True):
|
||||
"""Result type suitable for displaying scientific papers and other
|
||||
documents."""
|
||||
|
||||
template: str = "paper.html"
|
||||
|
||||
date_of_publication: DateTime | None = None
|
||||
"""Date the document was published."""
|
||||
|
||||
content: str = ""
|
||||
"""An abstract or excerpt from the document."""
|
||||
|
||||
comments: str = ""
|
||||
"""Free text display in italic below the content."""
|
||||
|
||||
tags: list[str] = []
|
||||
"""Free tag list."""
|
||||
|
||||
type: str = ""
|
||||
"""Short description of medium type, e.g. *book*, *pdf* or *html* ..."""
|
||||
|
||||
authors: list[str] | set[str] = []
|
||||
"""List of authors of the work (authors with a "s" suffix, the "author" is
|
||||
in the :py:obj:`MainResult.author`)."""
|
||||
|
||||
editor: str = ""
|
||||
"""Editor of the book/paper."""
|
||||
|
||||
publisher: str = ""
|
||||
"""Name of the publisher."""
|
||||
|
||||
journal: str = ""
|
||||
"""Name of the journal or magazine the article was published in."""
|
||||
|
||||
volume: str | int = ""
|
||||
"""Volume number."""
|
||||
|
||||
pages: str = ""
|
||||
"""Page range where the article is."""
|
||||
|
||||
number: str = ""
|
||||
"""Number of the report or the issue number for a journal article."""
|
||||
|
||||
doi: str = ""
|
||||
"""DOI number (like ``10.1038/d41586-018-07848-2``)."""
|
||||
|
||||
issn: list[str] = []
|
||||
"""List of ISSN numbers like ``1476-4687``"""
|
||||
|
||||
isbn: list[str] = []
|
||||
"""List of ISBN numbers like ``9780201896831``"""
|
||||
|
||||
pdf_url: str = ""
|
||||
"""URL to the full article, the PDF version"""
|
||||
|
||||
html_url: str = ""
|
||||
"""URL to full article, HTML version"""
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.date_of_publication is None and self.publishedDate is not None:
|
||||
self.date_of_publication = DateTime(self.publishedDate)
|
||||
@@ -407,13 +407,12 @@ engines:
|
||||
require_api_key: false
|
||||
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
@@ -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 -%}
|
||||
{{- result.volume -}}
|
||||
{%- if result.number -%}
|
||||
.{{- result.number -}}
|
||||
<div>
|
||||
<span>{{- _("Journal") }}:</span>
|
||||
<span>{{ result.journal -}}
|
||||
{%- if result.volume -%}
|
||||
{{- result.volume -}}
|
||||
{%- if result.number -%}.{{- result.number -}}{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if result.pages -%}
|
||||
{{- result.pages -}}
|
||||
{%- endif -%}
|
||||
{%- if result.pages -%} {{- 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 -%}
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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 mã"
|
||||
|
||||
#. 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 trì 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)"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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" \
|
||||
.
|
||||
|
||||
Reference in New Issue
Block a user