mirror of
https://github.com/searxng/searxng.git
synced 2026-07-31 12:21:24 +00:00
Compare commits
6 Commits
237267ffbe
...
7b4612e862
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b4612e862 | ||
|
|
9ffa9fb730 | ||
|
|
5daa4f0460 | ||
|
|
33661cc5c3 | ||
|
|
b231cb4b59 | ||
|
|
c8b419fcbb |
@@ -176,3 +176,4 @@ features or generally made searx better:
|
|||||||
- Patrick Evans `https://github.com/holysoles`
|
- Patrick Evans `https://github.com/holysoles`
|
||||||
- Daniel Mowitz `<https://daniel.mowitz.rocks>`
|
- Daniel Mowitz `<https://daniel.mowitz.rocks>`
|
||||||
- `Bearz314 <https://github.com/bearz314>`_
|
- `Bearz314 <https://github.com/bearz314>`_
|
||||||
|
- Tommaso Colella `<https://github.com/gioleppe>`
|
||||||
|
|||||||
8
docs/dev/engines/online/reuters.rst
Normal file
8
docs/dev/engines/online/reuters.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.. _reuters engine:
|
||||||
|
|
||||||
|
=======
|
||||||
|
Reuters
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.reuters
|
||||||
|
:members:
|
||||||
56
searx/engines/bitchute.py
Normal file
56
searx/engines/bitchute.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""bitchute (Videos)"""
|
||||||
|
|
||||||
|
from json import dumps
|
||||||
|
from datetime import datetime
|
||||||
|
from searx.utils import html_to_text
|
||||||
|
|
||||||
|
about = {
|
||||||
|
"website": 'https://bitchute.com',
|
||||||
|
"wikidata_id": "Q45287179",
|
||||||
|
"official_api_documentation": None,
|
||||||
|
"use_official_api": False,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "JSON",
|
||||||
|
}
|
||||||
|
|
||||||
|
base_url = "https://api.bitchute.com/api/beta/search/videos"
|
||||||
|
categories = ['videos']
|
||||||
|
paging = True
|
||||||
|
results_per_page = 20
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
|
||||||
|
start_index = (params["pageno"] - 1) * results_per_page
|
||||||
|
data = {"offset": start_index, "limit": results_per_page, "query": query, "sensitivity_id": "normal", "sort": "new"}
|
||||||
|
params["url"] = base_url
|
||||||
|
params["method"] = 'POST'
|
||||||
|
params['headers']['content-type'] = "application/json"
|
||||||
|
params['data'] = dumps(data)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp):
|
||||||
|
search_res = resp.json()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for item in search_res.get('videos', []):
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"title": item['video_name'],
|
||||||
|
"url": 'https://www.bitchute.com/video/' + item['video_id'],
|
||||||
|
"content": html_to_text(item['description']),
|
||||||
|
"author": item['channel']['channel_name'],
|
||||||
|
"publishedDate": datetime.strptime(item["date_published"], "%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||||
|
"length": item['duration'],
|
||||||
|
"views": item['view_count'],
|
||||||
|
"thumbnail": item['thumbnail_url'],
|
||||||
|
"iframe_src": 'https://www.bitchute.com/embed/' + item['video_id'],
|
||||||
|
"template": "videos.html",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
71
searx/engines/il_post.py
Normal file
71
searx/engines/il_post.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Engine for Il Post, a largely independent online Italian newspaper.
|
||||||
|
|
||||||
|
To use this engine add the following entry to your engines
|
||||||
|
list in ``settings.yml``:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: il post
|
||||||
|
engine: il_post
|
||||||
|
shortcut: pst
|
||||||
|
disabled: false
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
engine_type = "online"
|
||||||
|
language_support = False
|
||||||
|
categories = ["news"]
|
||||||
|
paging = True
|
||||||
|
page_size = 10
|
||||||
|
|
||||||
|
time_range_support = True
|
||||||
|
time_range_args = {"month": "pub_date:ultimi_30_giorni", "year": "pub_date:ultimo_anno"}
|
||||||
|
|
||||||
|
search_api = "https://api.ilpost.org/search/api/site_search/?"
|
||||||
|
|
||||||
|
about = {
|
||||||
|
"website": "https://www.ilpost.it",
|
||||||
|
"wikidata_id": "Q3792882",
|
||||||
|
"official_api_documentation": None,
|
||||||
|
"use_official_api": True,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "JSON",
|
||||||
|
"language": "it",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
query_params = {
|
||||||
|
"qs": query,
|
||||||
|
"pg": params["pageno"],
|
||||||
|
"sort": "date_d",
|
||||||
|
"filters": "ctype:articoli",
|
||||||
|
}
|
||||||
|
|
||||||
|
if params["time_range"]:
|
||||||
|
if params["time_range"] not in time_range_args:
|
||||||
|
return None
|
||||||
|
query_params["filters"] += f";{time_range_args.get(params['time_range'], 'pub_date:da_sempre')}"
|
||||||
|
params["url"] = search_api + urlencode(query_params)
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp) -> EngineResults:
|
||||||
|
res = EngineResults()
|
||||||
|
json_data = resp.json()
|
||||||
|
|
||||||
|
for result in json_data["docs"]:
|
||||||
|
res.add(
|
||||||
|
res.types.MainResult(
|
||||||
|
url=result["link"],
|
||||||
|
title=result["title"],
|
||||||
|
content=result.get("summary", ""),
|
||||||
|
thumbnail=result.get("image"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
90
searx/engines/niconico.py
Normal file
90
searx/engines/niconico.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Niconico search engine for searxng"""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
|
from searx.utils import eval_xpath_getindex, eval_xpath_list, eval_xpath, extract_text
|
||||||
|
|
||||||
|
about = {
|
||||||
|
"website": "https://www.nicovideo.jp/",
|
||||||
|
"wikidata_id": "Q697233",
|
||||||
|
"use_official_api": False,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "HTML",
|
||||||
|
"language": "ja",
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = ["videos"]
|
||||||
|
paging = True
|
||||||
|
|
||||||
|
time_range_support = True
|
||||||
|
time_range_dict = {"day": 1, "week": 7, "month": 30, "year": 365}
|
||||||
|
|
||||||
|
base_url = "https://www.nicovideo.jp"
|
||||||
|
embed_url = "https://embed.nicovideo.jp"
|
||||||
|
|
||||||
|
results_xpath = '//li[@data-video-item]'
|
||||||
|
url_xpath = './/a[@class="itemThumbWrap"]/@href'
|
||||||
|
video_length_xpath = './/span[@class="videoLength"]'
|
||||||
|
upload_time_xpath = './/p[@class="itemTime"]//span[@class="time"]/text()'
|
||||||
|
title_xpath = './/p[@class="itemTitle"]/a'
|
||||||
|
content_xpath = './/p[@class="itemDescription"]/@title'
|
||||||
|
thumbnail_xpath = './/img[@class="thumb"]/@src'
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
query_params = {"page": params['pageno']}
|
||||||
|
|
||||||
|
if time_range_dict.get(params['time_range']):
|
||||||
|
time_diff_days = time_range_dict[params['time_range']]
|
||||||
|
start_date = datetime.now() - timedelta(days=time_diff_days)
|
||||||
|
query_params['start'] = start_date.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
params['url'] = f"{base_url}/search/{query}?{urlencode(query_params)}"
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp):
|
||||||
|
results = []
|
||||||
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
|
for item in eval_xpath_list(dom, results_xpath):
|
||||||
|
relative_url = eval_xpath_getindex(item, url_xpath, 0)
|
||||||
|
video_id = relative_url.rsplit('?', maxsplit=1)[0].split('/')[-1]
|
||||||
|
|
||||||
|
url = f"{base_url}/watch/{video_id}"
|
||||||
|
iframe_src = f"{embed_url}/watch/{video_id}"
|
||||||
|
|
||||||
|
length = None
|
||||||
|
video_length = eval_xpath_getindex(item, video_length_xpath, 0)
|
||||||
|
if len(video_length) > 0:
|
||||||
|
try:
|
||||||
|
timediff = datetime.strptime(video_length, "%M:%S")
|
||||||
|
length = timedelta(minutes=timediff.minute, seconds=timediff.second)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
published_date = None
|
||||||
|
upload_time = eval_xpath_getindex(item, upload_time_xpath, 0)
|
||||||
|
if len(upload_time) > 0:
|
||||||
|
try:
|
||||||
|
published_date = datetime.strptime(upload_time, "%Y/%m/%d %H:%M")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
'template': 'videos.html',
|
||||||
|
'title': extract_text(eval_xpath(item, title_xpath)),
|
||||||
|
'content': eval_xpath_getindex(item, content_xpath, 0),
|
||||||
|
'url': url,
|
||||||
|
"iframe_src": iframe_src,
|
||||||
|
'thumbnail': eval_xpath_getindex(item, thumbnail_xpath, 0),
|
||||||
|
'length': length,
|
||||||
|
"publishedDate": published_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
60
searx/engines/ollama.py
Normal file
60
searx/engines/ollama.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Ollama model search engine for searxng"""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from datetime import datetime
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
|
from searx.utils import eval_xpath_list, eval_xpath_getindex, eval_xpath, extract_text
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
about = {
|
||||||
|
"website": "https://ollama.com",
|
||||||
|
"wikidata_id": "Q124636097",
|
||||||
|
"use_official_api": False,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "HTML",
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = ["it", "repos"]
|
||||||
|
|
||||||
|
base_url = "https://ollama.com"
|
||||||
|
|
||||||
|
results_xpath = '//li[@x-test-model]'
|
||||||
|
title_xpath = './/span[@x-test-search-response-title]/text()'
|
||||||
|
content_xpath = './/p[@class="max-w-lg break-words text-neutral-800 text-md"]/text()'
|
||||||
|
url_xpath = './a/@href'
|
||||||
|
publish_date_xpath = './/span[contains(@class, "flex items-center")]/@title'
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
query_params = {"q": query}
|
||||||
|
|
||||||
|
params['url'] = f"{base_url}/search?{urlencode(query_params)}"
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp) -> EngineResults:
|
||||||
|
res = EngineResults()
|
||||||
|
|
||||||
|
dom = html.fromstring(resp.text)
|
||||||
|
|
||||||
|
for item in eval_xpath_list(dom, results_xpath):
|
||||||
|
published_date = None
|
||||||
|
try:
|
||||||
|
published_date = datetime.strptime(
|
||||||
|
extract_text(eval_xpath(item, publish_date_xpath)), "%b %d, %Y %I:%M %p %Z"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
res.add(
|
||||||
|
res.types.MainResult(
|
||||||
|
title=extract_text(eval_xpath(item, title_xpath)),
|
||||||
|
content=extract_text(eval_xpath(item, content_xpath)),
|
||||||
|
url=f"{base_url}{eval_xpath_getindex(item, url_xpath, 0)}",
|
||||||
|
publishedDate=published_date,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
383
searx/engines/quark.py
Normal file
383
searx/engines/quark.py
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Quark (Shenma) search engine for searxng"""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
|
from searx.utils import html_to_text, gen_useragent
|
||||||
|
from searx.exceptions import SearxEngineAPIException, SearxEngineCaptchaException
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
about = {
|
||||||
|
"website": "https://m.quark.cn/",
|
||||||
|
"wikidata_id": "Q48816502",
|
||||||
|
"use_official_api": False,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "HTML",
|
||||||
|
"language": "zh",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Engine Configuration
|
||||||
|
categories = []
|
||||||
|
paging = True
|
||||||
|
results_per_page = 10
|
||||||
|
|
||||||
|
quark_category = 'general'
|
||||||
|
|
||||||
|
time_range_support = True
|
||||||
|
time_range_dict = {'day': '4', 'week': '3', 'month': '2', 'year': '1'}
|
||||||
|
|
||||||
|
CAPTCHA_PATTERN = r'\{[^{]*?"action"\s*:\s*"captcha"\s*,\s*"url"\s*:\s*"([^"]+)"[^{]*?\}'
|
||||||
|
|
||||||
|
|
||||||
|
def is_alibaba_captcha(html):
|
||||||
|
"""
|
||||||
|
Detects if the response contains an Alibaba X5SEC CAPTCHA page.
|
||||||
|
|
||||||
|
Quark may return a CAPTCHA challenge after 9 requests in a short period.
|
||||||
|
|
||||||
|
Typically, the ban duration is around 15 minutes.
|
||||||
|
"""
|
||||||
|
return bool(re.search(CAPTCHA_PATTERN, html))
|
||||||
|
|
||||||
|
|
||||||
|
def init(_):
|
||||||
|
if quark_category not in ('general', 'images'):
|
||||||
|
raise SearxEngineAPIException(f"Unsupported category: {quark_category}")
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
page_num = params["pageno"]
|
||||||
|
|
||||||
|
category_config = {
|
||||||
|
'general': {
|
||||||
|
'endpoint': 'https://m.quark.cn/s',
|
||||||
|
'params': {
|
||||||
|
"q": query,
|
||||||
|
"layout": "html",
|
||||||
|
"page": page_num,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'images': {
|
||||||
|
'endpoint': 'https://vt.sm.cn/api/pic/list',
|
||||||
|
'params': {
|
||||||
|
"query": query,
|
||||||
|
"limit": results_per_page,
|
||||||
|
"start": (page_num - 1) * results_per_page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
query_params = category_config[quark_category]['params']
|
||||||
|
query_url = category_config[quark_category]['endpoint']
|
||||||
|
|
||||||
|
if time_range_dict.get(params['time_range']) and quark_category == 'general':
|
||||||
|
query_params["tl_request"] = time_range_dict.get(params['time_range'])
|
||||||
|
|
||||||
|
params["url"] = f"{query_url}?{urlencode(query_params)}"
|
||||||
|
params["headers"] = {
|
||||||
|
"User-Agent": gen_useragent(),
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp):
|
||||||
|
results = []
|
||||||
|
text = resp.text
|
||||||
|
|
||||||
|
if is_alibaba_captcha(text):
|
||||||
|
raise SearxEngineCaptchaException(
|
||||||
|
suspended_time=900, message="Alibaba CAPTCHA detected. Please try again later."
|
||||||
|
)
|
||||||
|
|
||||||
|
if quark_category == 'images':
|
||||||
|
data = json.loads(text)
|
||||||
|
for item in data.get('data', {}).get('hit', {}).get('imgInfo', {}).get('item', []):
|
||||||
|
try:
|
||||||
|
published_date = datetime.fromtimestamp(int(item.get("publish_time")))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
published_date = None
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"template": "images.html",
|
||||||
|
"url": item.get("imgUrl"),
|
||||||
|
"thumbnail_src": item.get("img"),
|
||||||
|
"img_src": item.get("bigPicUrl"),
|
||||||
|
"title": item.get("title"),
|
||||||
|
"source": item.get("site"),
|
||||||
|
"resolution": f"{item['width']} x {item['height']}",
|
||||||
|
"publishedDate": published_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if quark_category == 'general':
|
||||||
|
# Quark returns a variety of different sc values on a single page, depending on the query type.
|
||||||
|
source_category_parsers = {
|
||||||
|
'addition': parse_addition,
|
||||||
|
'ai_page': parse_ai_page,
|
||||||
|
'baike_sc': parse_baike_sc,
|
||||||
|
'finance_shuidi': parse_finance_shuidi,
|
||||||
|
'kk_yidian_all': parse_kk_yidian_all,
|
||||||
|
'life_show_general_image': parse_life_show_general_image,
|
||||||
|
'med_struct': parse_med_struct,
|
||||||
|
'music_new_song': parse_music_new_song,
|
||||||
|
'nature_result': parse_nature_result,
|
||||||
|
'news_uchq': parse_news_uchq,
|
||||||
|
'ss_note': parse_ss_note,
|
||||||
|
# ss_kv, ss_pic, ss_text, ss_video, baike, structure_web_novel use the same struct as ss_doc
|
||||||
|
'ss_doc': parse_ss_doc,
|
||||||
|
'ss_kv': parse_ss_doc,
|
||||||
|
'ss_pic': parse_ss_doc,
|
||||||
|
'ss_text': parse_ss_doc,
|
||||||
|
'ss_video': parse_ss_doc,
|
||||||
|
'baike': parse_ss_doc,
|
||||||
|
'structure_web_novel': parse_ss_doc,
|
||||||
|
'travel_dest_overview': parse_travel_dest_overview,
|
||||||
|
'travel_ranking_list': parse_travel_ranking_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = r'<script\s+type="application/json"\s+id="s-data-[^"]+"\s+data-used-by="hydrate">(.*?)</script>'
|
||||||
|
matches = re.findall(pattern, text, re.DOTALL)
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
data = json.loads(match)
|
||||||
|
initial_data = data.get('data', {}).get('initialData', {})
|
||||||
|
extra_data = data.get('extraData', {})
|
||||||
|
|
||||||
|
source_category = extra_data.get('sc')
|
||||||
|
|
||||||
|
parsers = source_category_parsers.get(source_category)
|
||||||
|
if parsers:
|
||||||
|
parsed_results = parsers(initial_data)
|
||||||
|
if isinstance(parsed_results, list):
|
||||||
|
# Extend if the result is a list
|
||||||
|
results.extend(parsed_results)
|
||||||
|
else:
|
||||||
|
# Append if it's a single result
|
||||||
|
results.append(parsed_results)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_addition(data):
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('title', {}).get('content')),
|
||||||
|
"url": data.get('source', {}).get('url'),
|
||||||
|
"content": html_to_text(data.get('summary', {}).get('content')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ai_page(data):
|
||||||
|
results = []
|
||||||
|
for item in data.get('list', []):
|
||||||
|
content = (
|
||||||
|
" | ".join(map(str, item.get('content', [])))
|
||||||
|
if isinstance(item.get('content'), list)
|
||||||
|
else str(item.get('content'))
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
published_date = datetime.fromtimestamp(int(item.get('source', {}).get('time')))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
published_date = None
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"title": html_to_text(item.get('title')),
|
||||||
|
"url": item.get('url'),
|
||||||
|
"content": html_to_text(content),
|
||||||
|
"publishedDate": published_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_baike_sc(data):
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('data', {}).get('title')),
|
||||||
|
"url": data.get('data', {}).get('url'),
|
||||||
|
"content": html_to_text(data.get('data', {}).get('abstract')),
|
||||||
|
"thumbnail": data.get('data', {}).get('img').replace("http://", "https://"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_finance_shuidi(data):
|
||||||
|
content = " | ".join(
|
||||||
|
(
|
||||||
|
info
|
||||||
|
for info in [
|
||||||
|
data.get('establish_time'),
|
||||||
|
data.get('company_status'),
|
||||||
|
data.get('controled_type'),
|
||||||
|
data.get('company_type'),
|
||||||
|
data.get('capital'),
|
||||||
|
data.get('address'),
|
||||||
|
data.get('business_scope'),
|
||||||
|
]
|
||||||
|
if info
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('company_name')),
|
||||||
|
"url": data.get('title_url'),
|
||||||
|
"content": html_to_text(content),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_kk_yidian_all(data):
|
||||||
|
content_list = []
|
||||||
|
for section in data.get('list_container', []):
|
||||||
|
for item in section.get('list_container', []):
|
||||||
|
if 'dot_text' in item:
|
||||||
|
content_list.append(item['dot_text'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('title')),
|
||||||
|
"url": data.get('title_url'),
|
||||||
|
"content": html_to_text(' '.join(content_list)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_life_show_general_image(data):
|
||||||
|
results = []
|
||||||
|
for item in data.get('image', []):
|
||||||
|
try:
|
||||||
|
published_date = datetime.fromtimestamp(int(item.get("publish_time")))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
published_date = None
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"template": "images.html",
|
||||||
|
"url": item.get("imgUrl"),
|
||||||
|
"thumbnail_src": item.get("img"),
|
||||||
|
"img_src": item.get("bigPicUrl"),
|
||||||
|
"title": item.get("title"),
|
||||||
|
"source": item.get("site"),
|
||||||
|
"resolution": f"{item['width']} x {item['height']}",
|
||||||
|
"publishedDate": published_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_med_struct(data):
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('title')),
|
||||||
|
"url": data.get('message', {}).get('statistics', {}).get('nu'),
|
||||||
|
"content": html_to_text(data.get('message', {}).get('content_text')),
|
||||||
|
"thumbnail": data.get('message', {}).get('video_img').replace("http://", "https://"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_music_new_song(data):
|
||||||
|
results = []
|
||||||
|
for item in data.get('hit3', []):
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"title": f"{item['song_name']} | {item['song_singer']}",
|
||||||
|
"url": item.get("play_url"),
|
||||||
|
"content": html_to_text(item.get("lyrics")),
|
||||||
|
"thumbnail": item.get("image_url").replace("http://", "https://"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_nature_result(data):
|
||||||
|
return {"title": html_to_text(data.get('title')), "url": data.get('url'), "content": html_to_text(data.get('desc'))}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_news_uchq(data):
|
||||||
|
results = []
|
||||||
|
for item in data.get('feed', []):
|
||||||
|
try:
|
||||||
|
published_date = datetime.strptime(item.get('time'), "%Y-%m-%d")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# Sometime Quark will return non-standard format like "1天前", set published_date as None
|
||||||
|
published_date = None
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"title": html_to_text(item.get('title')),
|
||||||
|
"url": item.get('url'),
|
||||||
|
"content": html_to_text(item.get('summary')),
|
||||||
|
"thumbnail": item.get('image').replace("http://", "https://"),
|
||||||
|
"publishedDate": published_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ss_doc(data):
|
||||||
|
published_date = None
|
||||||
|
try:
|
||||||
|
timestamp = int(data.get('sourceProps', {}).get('time'))
|
||||||
|
|
||||||
|
# Sometime Quark will return 0, set published_date as None
|
||||||
|
if timestamp != 0:
|
||||||
|
published_date = datetime.fromtimestamp(timestamp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
thumbnail = data.get('picListProps', [])[0].get('src').replace("http://", "https://")
|
||||||
|
except (ValueError, TypeError, IndexError):
|
||||||
|
thumbnail = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": html_to_text(
|
||||||
|
data.get('titleProps', {}).get('content')
|
||||||
|
# ss_kv variant 1 & 2
|
||||||
|
or data.get('title')
|
||||||
|
),
|
||||||
|
"url": data.get('sourceProps', {}).get('dest_url')
|
||||||
|
# ss_kv variant 1
|
||||||
|
or data.get('normal_url')
|
||||||
|
# ss_kv variant 2
|
||||||
|
or data.get('url'),
|
||||||
|
"content": html_to_text(
|
||||||
|
data.get('summaryProps', {}).get('content')
|
||||||
|
# ss_doc variant 1
|
||||||
|
or data.get('message', {}).get('replyContent')
|
||||||
|
# ss_kv variant 1
|
||||||
|
or data.get('show_body')
|
||||||
|
# ss_kv variant 2
|
||||||
|
or data.get('desc')
|
||||||
|
),
|
||||||
|
"publishedDate": published_date,
|
||||||
|
"thumbnail": thumbnail,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ss_note(data):
|
||||||
|
try:
|
||||||
|
published_date = datetime.fromtimestamp(int(data.get('source', {}).get('time')))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
published_date = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('title', {}).get('content')),
|
||||||
|
"url": data.get('source', {}).get('dest_url'),
|
||||||
|
"content": html_to_text(data.get('summary', {}).get('content')),
|
||||||
|
"publishedDate": published_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_travel_dest_overview(data):
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('strong', {}).get('title')),
|
||||||
|
"url": data.get('strong', {}).get('baike_url'),
|
||||||
|
"content": html_to_text(data.get('strong', {}).get('baike_text')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_travel_ranking_list(data):
|
||||||
|
return {
|
||||||
|
"title": html_to_text(data.get('title', {}).get('text')),
|
||||||
|
"url": data.get('title', {}).get('url'),
|
||||||
|
"content": html_to_text(data.get('title', {}).get('title_tag')),
|
||||||
|
}
|
||||||
90
searx/engines/reuters.py
Normal file
90
searx/engines/reuters.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Reuters_ (news) is an international news agency.
|
||||||
|
|
||||||
|
.. _Reuters: https://www.reuters.com
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
The engine has the following additional settings:
|
||||||
|
|
||||||
|
- :py:obj:`sort_order`
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: reuters
|
||||||
|
engine: reuters
|
||||||
|
shortcut: reu
|
||||||
|
sort_order: "relevance"
|
||||||
|
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from json import dumps
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
|
||||||
|
about = {
|
||||||
|
"website": "https://www.reuters.com",
|
||||||
|
"wikidata_id": "Q130879",
|
||||||
|
"official_api_documentation": None,
|
||||||
|
"use_official_api": False,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "JSON",
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = ["news"]
|
||||||
|
time_range_support = True
|
||||||
|
paging = True
|
||||||
|
|
||||||
|
base_url = "https://www.reuters.com"
|
||||||
|
|
||||||
|
results_per_page = 20
|
||||||
|
sort_order = "relevance"
|
||||||
|
"""Sort order, one of ``relevance``, ``display_date:desc`` or ``display_data:asc``."""
|
||||||
|
|
||||||
|
time_range_duration_map = {
|
||||||
|
"day": 1,
|
||||||
|
"week": 7,
|
||||||
|
"month": 30,
|
||||||
|
"year": 365,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
args = {
|
||||||
|
"keyword": query,
|
||||||
|
"offset": (params["pageno"] - 1) * results_per_page,
|
||||||
|
"orderby": sort_order,
|
||||||
|
"size": results_per_page,
|
||||||
|
"website": "reuters",
|
||||||
|
}
|
||||||
|
if params["time_range"]:
|
||||||
|
time_diff_days = time_range_duration_map[params["time_range"]]
|
||||||
|
start_date = datetime.now() - timedelta(days=time_diff_days)
|
||||||
|
args["start_date"] = start_date.isoformat()
|
||||||
|
|
||||||
|
params["url"] = f"{base_url}/pf/api/v3/content/fetch/articles-by-search-v2?query={quote_plus(dumps(args))}"
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp) -> EngineResults:
|
||||||
|
res = EngineResults()
|
||||||
|
|
||||||
|
for result in resp.json().get("result", {}).get("articles", []):
|
||||||
|
res.add(
|
||||||
|
res.types.MainResult(
|
||||||
|
url=base_url + result["canonical_url"],
|
||||||
|
title=result["web"],
|
||||||
|
content=result["description"],
|
||||||
|
thumbnail=result.get("thumbnail", {}).get("url", ""),
|
||||||
|
metadata=result.get("kicker", {}).get("name"),
|
||||||
|
publishedDate=datetime.strptime(result["display_time"], "%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return res
|
||||||
@@ -25,6 +25,8 @@ import re
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import warnings
|
import warnings
|
||||||
import typing
|
import typing
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
@@ -212,6 +214,15 @@ def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result |
|
|||||||
result.normalize_result_fields()
|
result.normalize_result_fields()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_date_fields(result: MainResult | LegacyResult):
|
||||||
|
|
||||||
|
if result.publishedDate: # do not try to get a date from an empty string or a None type
|
||||||
|
try: # test if publishedDate >= 1900 (datetime module bug)
|
||||||
|
result.pubdate = result.publishedDate.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||||
|
except ValueError:
|
||||||
|
result.publishedDate = None
|
||||||
|
|
||||||
|
|
||||||
class Result(msgspec.Struct, kw_only=True):
|
class Result(msgspec.Struct, kw_only=True):
|
||||||
"""Base class of all result types :ref:`result types`."""
|
"""Base class of all result types :ref:`result types`."""
|
||||||
|
|
||||||
@@ -347,6 +358,24 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
|||||||
thumbnail: str = ""
|
thumbnail: str = ""
|
||||||
"""URL of a thumbnail that is displayed in the result item."""
|
"""URL of a thumbnail that is displayed in the result item."""
|
||||||
|
|
||||||
|
publishedDate: datetime.datetime | None = None
|
||||||
|
"""The date on which the object was published."""
|
||||||
|
|
||||||
|
pubdate: str = ""
|
||||||
|
"""String representation of :py:obj:`MainResult.publishedDate`"""
|
||||||
|
|
||||||
|
length: time.struct_time | None = None
|
||||||
|
"""Playing duration in seconds."""
|
||||||
|
|
||||||
|
views: str = ""
|
||||||
|
"""View count in humanized number format."""
|
||||||
|
|
||||||
|
author: str = ""
|
||||||
|
"""Author of the title."""
|
||||||
|
|
||||||
|
metadata: str = ""
|
||||||
|
"""Miscellaneous metadata."""
|
||||||
|
|
||||||
priority: typing.Literal["", "high", "low"] = ""
|
priority: typing.Literal["", "high", "low"] = ""
|
||||||
"""The priority can be set via :ref:`hostnames plugin`, for example."""
|
"""The priority can be set via :ref:`hostnames plugin`, for example."""
|
||||||
|
|
||||||
@@ -379,8 +408,8 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
|||||||
|
|
||||||
def normalize_result_fields(self):
|
def normalize_result_fields(self):
|
||||||
super().normalize_result_fields()
|
super().normalize_result_fields()
|
||||||
|
|
||||||
_normalize_text_fields(self)
|
_normalize_text_fields(self)
|
||||||
|
_normalize_date_fields(self)
|
||||||
if self.engine:
|
if self.engine:
|
||||||
self.engines.add(self.engine)
|
self.engines.add(self.engine)
|
||||||
|
|
||||||
@@ -419,6 +448,8 @@ class LegacyResult(dict):
|
|||||||
positions: list[int]
|
positions: list[int]
|
||||||
score: float
|
score: float
|
||||||
category: str
|
category: str
|
||||||
|
publishedDate: datetime.datetime | None = None
|
||||||
|
pubdate: str = ""
|
||||||
|
|
||||||
# infobox result
|
# infobox result
|
||||||
urls: list[dict[str, str]]
|
urls: list[dict[str, str]]
|
||||||
@@ -514,6 +545,7 @@ class LegacyResult(dict):
|
|||||||
return f"LegacyResult: {super().__repr__()}"
|
return f"LegacyResult: {super().__repr__()}"
|
||||||
|
|
||||||
def normalize_result_fields(self):
|
def normalize_result_fields(self):
|
||||||
|
_normalize_date_fields(self)
|
||||||
_normalize_url_fields(self)
|
_normalize_url_fields(self)
|
||||||
_normalize_text_fields(self)
|
_normalize_text_fields(self)
|
||||||
if self.engine:
|
if self.engine:
|
||||||
|
|||||||
@@ -560,6 +560,11 @@ engines:
|
|||||||
engine: bing_videos
|
engine: bing_videos
|
||||||
shortcut: biv
|
shortcut: biv
|
||||||
|
|
||||||
|
- name: bitchute
|
||||||
|
engine: bitchute
|
||||||
|
shortcut: bit
|
||||||
|
disabled: true
|
||||||
|
|
||||||
- name: bitbucket
|
- name: bitbucket
|
||||||
engine: xpath
|
engine: xpath
|
||||||
paging: true
|
paging: true
|
||||||
@@ -1121,6 +1126,11 @@ engines:
|
|||||||
require_api_key: false
|
require_api_key: false
|
||||||
results: JSON
|
results: JSON
|
||||||
|
|
||||||
|
- name: il post
|
||||||
|
engine: il_post
|
||||||
|
shortcut: pst
|
||||||
|
disabled: true
|
||||||
|
|
||||||
- name: imdb
|
- name: imdb
|
||||||
engine: imdb
|
engine: imdb
|
||||||
shortcut: imdb
|
shortcut: imdb
|
||||||
@@ -1349,6 +1359,11 @@ engines:
|
|||||||
shortcut: mwm
|
shortcut: mwm
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
|
- name: niconico
|
||||||
|
engine: niconico
|
||||||
|
shortcut: nico
|
||||||
|
disabled: true
|
||||||
|
|
||||||
- name: npm
|
- name: npm
|
||||||
engine: npm
|
engine: npm
|
||||||
shortcut: npm
|
shortcut: npm
|
||||||
@@ -1390,6 +1405,11 @@ engines:
|
|||||||
shortcut: od
|
shortcut: od
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
|
- name: ollama
|
||||||
|
engine: ollama
|
||||||
|
shortcut: ollama
|
||||||
|
disabled: true
|
||||||
|
|
||||||
- name: openairedatasets
|
- name: openairedatasets
|
||||||
engine: json_engine
|
engine: json_engine
|
||||||
paging: true
|
paging: true
|
||||||
@@ -1651,6 +1671,20 @@ engines:
|
|||||||
shortcut: pypi
|
shortcut: pypi
|
||||||
engine: pypi
|
engine: pypi
|
||||||
|
|
||||||
|
- name: quark
|
||||||
|
quark_category: general
|
||||||
|
categories: [general]
|
||||||
|
engine: quark
|
||||||
|
shortcut: qk
|
||||||
|
disabled: true
|
||||||
|
|
||||||
|
- name: quark images
|
||||||
|
quark_category: images
|
||||||
|
categories: [images]
|
||||||
|
engine: quark
|
||||||
|
shortcut: qki
|
||||||
|
disabled: true
|
||||||
|
|
||||||
- name: qwant
|
- name: qwant
|
||||||
qwant_categ: web
|
qwant_categ: web
|
||||||
engine: qwant
|
engine: qwant
|
||||||
@@ -1712,6 +1746,12 @@ engines:
|
|||||||
page_size: 25
|
page_size: 25
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
|
- name: reuters
|
||||||
|
engine: reuters
|
||||||
|
shortcut: reu
|
||||||
|
# https://docs.searxng.org/dev/engines/online/reuters.html
|
||||||
|
# sort_order = "relevance"
|
||||||
|
|
||||||
- name: right dao
|
- name: right dao
|
||||||
engine: xpath
|
engine: xpath
|
||||||
paging: true
|
paging: true
|
||||||
|
|||||||
@@ -694,14 +694,6 @@ def search():
|
|||||||
if 'title' in result and result['title']:
|
if 'title' in result and result['title']:
|
||||||
result['title'] = highlight_content(escape(result['title'] or ''), search_query.query)
|
result['title'] = highlight_content(escape(result['title'] or ''), search_query.query)
|
||||||
|
|
||||||
if getattr(result, 'publishedDate', None): # do not try to get a date from an empty string or a None type
|
|
||||||
try: # test if publishedDate >= 1900 (datetime module bug)
|
|
||||||
result['pubdate'] = result['publishedDate'].strftime('%Y-%m-%d %H:%M:%S%z')
|
|
||||||
except ValueError:
|
|
||||||
result['publishedDate'] = None
|
|
||||||
else:
|
|
||||||
result['publishedDate'] = webutils.searxng_l10n_timespan(result['publishedDate'])
|
|
||||||
|
|
||||||
# set result['open_group'] = True when the template changes from the previous result
|
# set result['open_group'] = True when the template changes from the previous result
|
||||||
# set result['close_group'] = True when the template changes on the next result
|
# set result['close_group'] = True when the template changes on the next result
|
||||||
if current_template != result.template:
|
if current_template != result.template:
|
||||||
|
|||||||
Reference in New Issue
Block a user