mirror of
https://github.com/searxng/searxng.git
synced 2026-07-25 09:21:25 +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`
|
||||
- Daniel Mowitz `<https://daniel.mowitz.rocks>`
|
||||
- `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 warnings
|
||||
import typing
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -212,6 +214,15 @@ def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result |
|
||||
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):
|
||||
"""Base class of all result types :ref:`result types`."""
|
||||
|
||||
@@ -347,6 +358,24 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
thumbnail: str = ""
|
||||
"""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"] = ""
|
||||
"""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):
|
||||
super().normalize_result_fields()
|
||||
|
||||
_normalize_text_fields(self)
|
||||
_normalize_date_fields(self)
|
||||
if self.engine:
|
||||
self.engines.add(self.engine)
|
||||
|
||||
@@ -419,6 +448,8 @@ class LegacyResult(dict):
|
||||
positions: list[int]
|
||||
score: float
|
||||
category: str
|
||||
publishedDate: datetime.datetime | None = None
|
||||
pubdate: str = ""
|
||||
|
||||
# infobox result
|
||||
urls: list[dict[str, str]]
|
||||
@@ -514,6 +545,7 @@ class LegacyResult(dict):
|
||||
return f"LegacyResult: {super().__repr__()}"
|
||||
|
||||
def normalize_result_fields(self):
|
||||
_normalize_date_fields(self)
|
||||
_normalize_url_fields(self)
|
||||
_normalize_text_fields(self)
|
||||
if self.engine:
|
||||
|
||||
@@ -560,6 +560,11 @@ engines:
|
||||
engine: bing_videos
|
||||
shortcut: biv
|
||||
|
||||
- name: bitchute
|
||||
engine: bitchute
|
||||
shortcut: bit
|
||||
disabled: true
|
||||
|
||||
- name: bitbucket
|
||||
engine: xpath
|
||||
paging: true
|
||||
@@ -1121,6 +1126,11 @@ engines:
|
||||
require_api_key: false
|
||||
results: JSON
|
||||
|
||||
- name: il post
|
||||
engine: il_post
|
||||
shortcut: pst
|
||||
disabled: true
|
||||
|
||||
- name: imdb
|
||||
engine: imdb
|
||||
shortcut: imdb
|
||||
@@ -1349,6 +1359,11 @@ engines:
|
||||
shortcut: mwm
|
||||
disabled: true
|
||||
|
||||
- name: niconico
|
||||
engine: niconico
|
||||
shortcut: nico
|
||||
disabled: true
|
||||
|
||||
- name: npm
|
||||
engine: npm
|
||||
shortcut: npm
|
||||
@@ -1390,6 +1405,11 @@ engines:
|
||||
shortcut: od
|
||||
disabled: true
|
||||
|
||||
- name: ollama
|
||||
engine: ollama
|
||||
shortcut: ollama
|
||||
disabled: true
|
||||
|
||||
- name: openairedatasets
|
||||
engine: json_engine
|
||||
paging: true
|
||||
@@ -1651,6 +1671,20 @@ engines:
|
||||
shortcut: 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
|
||||
qwant_categ: web
|
||||
engine: qwant
|
||||
@@ -1712,6 +1746,12 @@ engines:
|
||||
page_size: 25
|
||||
disabled: true
|
||||
|
||||
- name: reuters
|
||||
engine: reuters
|
||||
shortcut: reu
|
||||
# https://docs.searxng.org/dev/engines/online/reuters.html
|
||||
# sort_order = "relevance"
|
||||
|
||||
- name: right dao
|
||||
engine: xpath
|
||||
paging: true
|
||||
|
||||
@@ -694,14 +694,6 @@ def search():
|
||||
if 'title' in result and result['title']:
|
||||
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['close_group'] = True when the template changes on the next result
|
||||
if current_template != result.template:
|
||||
|
||||
Reference in New Issue
Block a user