mirror of
https://github.com/searxng/searxng.git
synced 2025-12-22 19:50:00 +00:00
[mod] typification of SearXNG: add new result type File
This PR adds a new result type: File
Python class: searx/result_types/file.py
Jinja template: searx/templates/simple/result_templates/file.html
CSS (less) client/simple/src/less/result_types/file.less
Class 'File' (singular) replaces template 'files.html' (plural). The renaming
was carried out because there is only one file (singular) in a result. Not to be
confused with the category 'files' where in multiple results can exist.
As mentioned in issue [1], the class '.category-files' was removed from the CSS
and the stylesheet was adopted in result_types/file.less (there based on the
templates and no longer based on the category).
[1] https://github.com/searxng/searxng/issues/5198
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
committed by
Markus Heiser
parent
ee6d4f322f
commit
9371658531
22
client/simple/src/less/result_types/file.less
Normal file
22
client/simple/src/less/result_types/file.less
Normal file
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Files result class
|
||||
*/
|
||||
|
||||
#main_results .result-file {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
.rounded-corners;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,6 @@ article[data-vim-selected].category-videos,
|
||||
article[data-vim-selected].category-news,
|
||||
article[data-vim-selected].category-map,
|
||||
article[data-vim-selected].category-music,
|
||||
article[data-vim-selected].category-files,
|
||||
article[data-vim-selected].category-social {
|
||||
border: 1px solid var(--color-result-vim-arrow);
|
||||
.rounded-corners;
|
||||
@@ -387,7 +386,6 @@ article[data-vim-selected].category-social {
|
||||
.category-news,
|
||||
.category-map,
|
||||
.category-music,
|
||||
.category-files,
|
||||
.category-social {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
@@ -1168,3 +1166,4 @@ pre code {
|
||||
@import "result_types/keyvalue.less";
|
||||
@import "result_types/code.less";
|
||||
@import "result_types/paper.less";
|
||||
@import "result_types/file.less";
|
||||
|
||||
7
docs/dev/result_types/main/file.rst
Normal file
7
docs/dev/result_types/main/file.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
.. _result_types.file:
|
||||
|
||||
============
|
||||
File Results
|
||||
============
|
||||
|
||||
.. automodule:: searx.result_types.file
|
||||
@@ -17,6 +17,7 @@ following types have been implemented so far ..
|
||||
main/keyvalue
|
||||
main/code
|
||||
main/paper
|
||||
main/file
|
||||
|
||||
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
|
||||
@@ -28,5 +29,4 @@ final typing is complete.
|
||||
- :ref:`template torrent`
|
||||
- :ref:`template map`
|
||||
- :ref:`template packages`
|
||||
- :ref:`template files`
|
||||
- :ref:`template products`
|
||||
|
||||
@@ -60,7 +60,7 @@ Fields used in the template :origin:`macro result_sub_header
|
||||
publishedDate : :py:obj:`datetime.datetime`
|
||||
The date on which the object was published.
|
||||
|
||||
length: :py:obj:`time.struct_time`
|
||||
length: :py:obj:`datetime.timedelta`
|
||||
Playing duration in seconds.
|
||||
|
||||
views: :py:class:`str`
|
||||
@@ -469,38 +469,6 @@ links : :py:class:`dict`
|
||||
Additional links in the form of ``{'link_name': 'http://example.com'}``
|
||||
|
||||
|
||||
.. _template files:
|
||||
|
||||
``files.html``
|
||||
--------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`code.html
|
||||
<searx/templates/simple/result_templates/files.html>`:
|
||||
|
||||
filename, size, time: :py:class:`str`
|
||||
Filename, Filesize and Date of the file.
|
||||
|
||||
mtype : ``audio`` | ``video`` | :py:class:`str`
|
||||
Mimetype type of the file.
|
||||
|
||||
subtype : :py:class:`str`
|
||||
Mimetype / subtype of the file.
|
||||
|
||||
abstract : :py:class:`str`
|
||||
Abstract of the file.
|
||||
|
||||
author : :py:class:`str`
|
||||
Name of the author of the file
|
||||
|
||||
embedded : :py:class:`str`
|
||||
URL of an embedded media type (``audio`` or ``video``) / is collapsible.
|
||||
|
||||
|
||||
.. _template products:
|
||||
|
||||
``products.html``
|
||||
|
||||
@@ -13,23 +13,12 @@ Configuration
|
||||
|
||||
You must configure the following settings:
|
||||
|
||||
``base_url``:
|
||||
Location where recoll-webui can be reached.
|
||||
- :py:obj:`base_url`
|
||||
- :py:obj:`mount_prefix`
|
||||
- :py:obj:`dl_prefix`
|
||||
- :py:obj:`search_dir`
|
||||
|
||||
``mount_prefix``:
|
||||
Location where the file hierarchy is mounted on your *local* filesystem.
|
||||
|
||||
``dl_prefix``:
|
||||
Location where the file hierarchy as indexed by recoll can be reached.
|
||||
|
||||
``search_dir``:
|
||||
Part of the indexed file hierarchy to be search, if empty the full domain is
|
||||
searched.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
Scenario:
|
||||
Example scenario:
|
||||
|
||||
#. Recoll indexes a local filesystem mounted in ``/export/documents/reference``,
|
||||
#. the Recoll search interface can be reached at https://recoll.example.org/ and
|
||||
@@ -37,107 +26,128 @@ Scenario:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
base_url: https://recoll.example.org/
|
||||
base_url: https://recoll.example.org
|
||||
mount_prefix: /export/documents
|
||||
dl_prefix: https://download.example.org
|
||||
search_dir: ''
|
||||
search_dir: ""
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
|
||||
from datetime import date, timedelta
|
||||
from json import loads
|
||||
from urllib.parse import urlencode, quote
|
||||
|
||||
# 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": None,
|
||||
"wikidata_id": 'Q15735774',
|
||||
"official_api_documentation": 'https://www.lesbonscomptes.com/recoll/',
|
||||
"wikidata_id": "Q15735774",
|
||||
"official_api_documentation": "https://www.lesbonscomptes.com/recoll/",
|
||||
"use_official_api": True,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
"results": "JSON",
|
||||
}
|
||||
|
||||
# engine dependent config
|
||||
paging = True
|
||||
time_range_support = True
|
||||
|
||||
# parameters from settings.yml
|
||||
base_url = None
|
||||
search_dir = ''
|
||||
mount_prefix = None
|
||||
dl_prefix = None
|
||||
base_url: str = ""
|
||||
"""Location where recoll-webui can be reached."""
|
||||
|
||||
# embedded
|
||||
embedded_url = '<{ttype} controls height="166px" ' + 'src="{url}" type="{mtype}"></{ttype}>'
|
||||
mount_prefix: str = ""
|
||||
"""Location where the file hierarchy is mounted on your *local* filesystem."""
|
||||
|
||||
dl_prefix: str = ""
|
||||
"""Location where the file hierarchy as indexed by recoll can be reached."""
|
||||
|
||||
search_dir: str = ""
|
||||
"""Part of the indexed file hierarchy to be search, if empty the full domain is
|
||||
searched."""
|
||||
|
||||
_s2i: dict[str | None, int] = {"day": 1, "week": 7, "month": 30, "year": 365}
|
||||
|
||||
|
||||
# helper functions
|
||||
def get_time_range(time_range):
|
||||
sw = {'day': 1, 'week': 7, 'month': 30, 'year': 365} # pylint: disable=invalid-name
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
"""Initialization of the Recoll engine, checks if the mandatory values are
|
||||
configured.
|
||||
"""
|
||||
missing: list[str] = []
|
||||
for cfg_name in ["base_url", "mount_prefix", "dl_prefix"]:
|
||||
if not engine_settings.get(cfg_name):
|
||||
missing.append(cfg_name)
|
||||
if missing:
|
||||
logger.error("missing recoll configuration: %s", missing)
|
||||
return False
|
||||
|
||||
offset = sw.get(time_range, 0)
|
||||
if engine_settings["base_url"].endswith("/"):
|
||||
engine_settings["base_url"] = engine_settings["base_url"][:-1]
|
||||
return True
|
||||
|
||||
|
||||
def search_after(time_range: str | None) -> str:
|
||||
offset = _s2i.get(time_range, 0)
|
||||
if not offset:
|
||||
return ''
|
||||
|
||||
return ""
|
||||
return (date.today() - timedelta(days=offset)).isoformat()
|
||||
|
||||
|
||||
# do search-request
|
||||
def request(query, params):
|
||||
search_after = get_time_range(params['time_range'])
|
||||
search_url = base_url + 'json?{query}&highlight=0'
|
||||
params['url'] = search_url.format(
|
||||
query=urlencode({'query': query, 'page': params['pageno'], 'after': search_after, 'dir': search_dir})
|
||||
)
|
||||
|
||||
return params
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
args = {
|
||||
"query": query,
|
||||
"page": params["pageno"],
|
||||
"after": search_after(params["time_range"]),
|
||||
"dir": search_dir,
|
||||
"highlight": 0,
|
||||
}
|
||||
params["url"] = f"{base_url}/json?{urlencode(args)}"
|
||||
|
||||
|
||||
# get response from search-request
|
||||
def response(resp):
|
||||
results = []
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
|
||||
response_json = loads(resp.text)
|
||||
res = EngineResults()
|
||||
json_data = resp.json()
|
||||
|
||||
if not response_json:
|
||||
return []
|
||||
if not json_data:
|
||||
return res
|
||||
|
||||
for result in response_json.get('results', []):
|
||||
title = result['label']
|
||||
url = result['url'].replace('file://' + mount_prefix, dl_prefix)
|
||||
content = '{}'.format(result['snippet'])
|
||||
for result in json_data.get("results", []):
|
||||
|
||||
# append result
|
||||
item = {'url': url, 'title': title, 'content': content, 'template': 'files.html'}
|
||||
url = result.get("url", "").replace("file://" + mount_prefix, dl_prefix)
|
||||
|
||||
if result['size']:
|
||||
item['size'] = int(result['size'])
|
||||
|
||||
for parameter in ['filename', 'abstract', 'author', 'mtype', 'time']:
|
||||
if result[parameter]:
|
||||
item[parameter] = result[parameter]
|
||||
mtype = subtype = result.get("mime", "")
|
||||
if mtype:
|
||||
mtype, subtype = (mtype.split("/", 1) + [""])[:2]
|
||||
|
||||
# facilitate preview support for known mime types
|
||||
if 'mtype' in result and '/' in result['mtype']:
|
||||
(mtype, subtype) = result['mtype'].split('/')
|
||||
item['mtype'] = mtype
|
||||
item['subtype'] = subtype
|
||||
thumbnail = embedded = ""
|
||||
if mtype in ["audio", "video"]:
|
||||
embedded_url = '<{ttype} controls height="166px" ' + 'src="{url}" type="{mtype}"></{ttype}>'
|
||||
embedded = embedded_url.format(ttype=mtype, url=quote(url.encode("utf8"), "/:"), mtype=result["mtype"])
|
||||
if mtype in ["image"] and subtype in ["bmp", "gif", "jpeg", "png"]:
|
||||
thumbnail = url
|
||||
|
||||
if mtype in ['audio', 'video']:
|
||||
item['embedded'] = embedded_url.format(
|
||||
ttype=mtype, url=quote(url.encode('utf8'), '/:'), mtype=result['mtype']
|
||||
)
|
||||
|
||||
if mtype in ['image'] and subtype in ['bmp', 'gif', 'jpeg', 'png']:
|
||||
item['thumbnail'] = url
|
||||
|
||||
results.append(item)
|
||||
|
||||
if 'nres' in response_json:
|
||||
results.append({'number_of_results': response_json['nres']})
|
||||
|
||||
return results
|
||||
res.add(
|
||||
res.types.File(
|
||||
title=result.get("label", ""),
|
||||
url=url,
|
||||
content=result.get("snippet", ""),
|
||||
size=result.get("size", ""),
|
||||
filename=result.get("filename", ""),
|
||||
abstract=result.get("abstract", ""),
|
||||
author=result.get("author", ""),
|
||||
mtype=mtype,
|
||||
subtype=subtype,
|
||||
time=result.get("time", ""),
|
||||
embedded=embedded,
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -1,102 +1,208 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Wikimedia Commons (images)"""
|
||||
"""`Wikimedia Commons`_ is a collection of more than 120 millions freely usable
|
||||
media files to which anyone can contribute.
|
||||
|
||||
This engine uses the `MediaWiki query API`_, with which engines can be configured
|
||||
for searching images, videos, audio, and other files in the Wikimedia.
|
||||
|
||||
.. _MediaWiki query API: https://commons.wikimedia.org/w/api.php?action=help&modules=query
|
||||
.. _Wikimedia Commons: https://commons.wikimedia.org/
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The engine has the following additional settings:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- name: wikicommons.images
|
||||
engine: wikicommons
|
||||
wc_search_type: image
|
||||
|
||||
- name: wikicommons.videos
|
||||
engine: wikicommons
|
||||
wc_search_type: video
|
||||
|
||||
- name: wikicommons.audio
|
||||
engine: wikicommons
|
||||
wc_search_type: audio
|
||||
|
||||
- name: wikicommons.files
|
||||
engine: wikicommons
|
||||
wc_search_type: file
|
||||
|
||||
|
||||
Implementations
|
||||
===============
|
||||
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
import datetime
|
||||
|
||||
from urllib.parse import urlencode
|
||||
import pathlib
|
||||
from urllib.parse import urlencode, unquote
|
||||
|
||||
from searx.utils import html_to_text, humanize_bytes
|
||||
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://commons.wikimedia.org/',
|
||||
"wikidata_id": 'Q565',
|
||||
"official_api_documentation": 'https://commons.wikimedia.org/w/api.php',
|
||||
"website": "https://commons.wikimedia.org/",
|
||||
"wikidata_id": "Q565",
|
||||
"official_api_documentation": "https://commons.wikimedia.org/w/api.php",
|
||||
"use_official_api": True,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
"results": "JSON",
|
||||
}
|
||||
categories = ['images']
|
||||
search_type = 'images'
|
||||
|
||||
base_url = "https://commons.wikimedia.org"
|
||||
search_prefix = (
|
||||
'?action=query'
|
||||
'&format=json'
|
||||
'&generator=search'
|
||||
'&gsrnamespace=6'
|
||||
'&gsrprop=snippet'
|
||||
'&prop=info|imageinfo'
|
||||
'&iiprop=url|size|mime'
|
||||
'&iiurlheight=180' # needed for the thumb url
|
||||
)
|
||||
categories: list[str] = []
|
||||
paging = True
|
||||
number_of_results = 10
|
||||
|
||||
search_types = {
|
||||
'images': 'bitmap|drawing',
|
||||
'videos': 'video',
|
||||
'audio': 'audio',
|
||||
'files': 'multimedia|office|archive|3d',
|
||||
wc_api_url = "https://commons.wikimedia.org/w/api.php"
|
||||
wc_search_type: str = ""
|
||||
|
||||
SEARCH_TYPES: dict[str, str] = {
|
||||
"image": "bitmap|drawing",
|
||||
"video": "video",
|
||||
"audio": "audio",
|
||||
"file": "multimedia|office|archive|3d",
|
||||
}
|
||||
# FileType = t.Literal["bitmap", "drawing", "video", "audio", "multimedia", "office", "archive", "3d"]
|
||||
# FILE_TYPES = list(t.get_args(FileType))
|
||||
|
||||
|
||||
def request(query, params):
|
||||
language = 'en'
|
||||
if params['language'] != 'all':
|
||||
language = params['language'].split('-')[0]
|
||||
def setup(engine_settings: dict[str, t.Any]) -> bool:
|
||||
"""Initialization of the Wikimedia engine, checks if the value configured in
|
||||
:py:obj:`wc_search_type` is valid."""
|
||||
|
||||
if search_type not in search_types:
|
||||
raise ValueError(f"Unsupported search type: {search_type}")
|
||||
if engine_settings.get("wc_search_type") not in SEARCH_TYPES:
|
||||
logger.error(
|
||||
"wc_search_type: %s isn't a valid file type (%s)",
|
||||
engine_settings.get("wc_search_type"),
|
||||
",".join(SEARCH_TYPES.keys()),
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
filetype = search_types[search_type]
|
||||
|
||||
def request(query: str, params: "OnlineParams") -> None:
|
||||
uselang: str = "en"
|
||||
if params["searxng_locale"] != "all":
|
||||
uselang = params["searxng_locale"].split("-")[0]
|
||||
filetype = SEARCH_TYPES[wc_search_type]
|
||||
args = {
|
||||
'uselang': language,
|
||||
'gsrlimit': number_of_results,
|
||||
'gsroffset': number_of_results * (params["pageno"] - 1),
|
||||
'gsrsearch': f"filetype:{filetype} {query}",
|
||||
# https://commons.wikimedia.org/w/api.php
|
||||
"format": "json",
|
||||
"uselang": uselang,
|
||||
"action": "query",
|
||||
# https://commons.wikimedia.org/w/api.php?action=help&modules=query
|
||||
"prop": "info|imageinfo",
|
||||
# generator (gsr optins) https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bsearch
|
||||
"generator": "search",
|
||||
"gsrnamespace": "6", # https://www.mediawiki.org/wiki/Help:Namespaces#Renaming_namespaces
|
||||
"gsrprop": "snippet",
|
||||
"gsrlimit": number_of_results,
|
||||
"gsroffset": number_of_results * (params["pageno"] - 1),
|
||||
"gsrsearch": f"filetype:{filetype} {query}",
|
||||
# imageinfo: https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bimageinfo
|
||||
"iiprop": "url|size|mime",
|
||||
"iiurlheight": "180", # needed for the thumb url
|
||||
}
|
||||
|
||||
params["url"] = f"{base_url}/w/api.php{search_prefix}&{urlencode(args, safe=':|')}"
|
||||
return params
|
||||
params["url"] = f"{wc_api_url}?{urlencode(args, safe=':|')}"
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
json = resp.json()
|
||||
def response(resp: "SXNG_Response") -> EngineResults:
|
||||
|
||||
if not json.get("query", {}).get("pages"):
|
||||
return results
|
||||
for item in json["query"]["pages"].values():
|
||||
res = EngineResults()
|
||||
json_data = resp.json()
|
||||
pages = json_data.get("query", {}).get("pages", {}).values()
|
||||
|
||||
for item in pages:
|
||||
|
||||
if not item.get("imageinfo", []):
|
||||
continue
|
||||
imageinfo = item["imageinfo"][0]
|
||||
title = item["title"].replace("File:", "").rsplit('.', 1)[0]
|
||||
result = {
|
||||
'url': imageinfo["descriptionurl"],
|
||||
'title': title,
|
||||
'content': html_to_text(item["snippet"]),
|
||||
}
|
||||
|
||||
if search_type == "images":
|
||||
result['template'] = 'images.html'
|
||||
result['img_src'] = imageinfo["url"]
|
||||
result['thumbnail_src'] = imageinfo["thumburl"]
|
||||
result['resolution'] = f'{imageinfo["width"]} x {imageinfo["height"]}'
|
||||
else:
|
||||
result['thumbnail'] = imageinfo["thumburl"]
|
||||
title: str = item["title"].replace("File:", "").rsplit(".", 1)[0]
|
||||
content = html_to_text(item["snippet"])
|
||||
|
||||
if search_type == "videos":
|
||||
result['template'] = 'videos.html'
|
||||
if imageinfo.get('duration'):
|
||||
result['length'] = datetime.timedelta(seconds=int(imageinfo['duration']))
|
||||
result['iframe_src'] = imageinfo['url']
|
||||
elif search_type == "files":
|
||||
result['template'] = 'files.html'
|
||||
result['metadata'] = imageinfo['mime']
|
||||
result['size'] = humanize_bytes(imageinfo['size'])
|
||||
elif search_type == "audio":
|
||||
result['iframe_src'] = imageinfo['url']
|
||||
url: str = imageinfo["descriptionurl"]
|
||||
media_url: str = imageinfo["url"]
|
||||
mimetype: str = imageinfo["mime"]
|
||||
thumbnail: str = imageinfo["thumburl"]
|
||||
size = imageinfo.get("size")
|
||||
if size:
|
||||
size = humanize_bytes(size)
|
||||
|
||||
results.append(result)
|
||||
duration = None
|
||||
seconds: str = imageinfo.get("duration")
|
||||
if seconds:
|
||||
try:
|
||||
duration = datetime.timedelta(seconds=int(seconds))
|
||||
except OverflowError:
|
||||
pass
|
||||
|
||||
return results
|
||||
if wc_search_type == "file":
|
||||
res.add(
|
||||
res.types.File(
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
size=size,
|
||||
mimetype=mimetype,
|
||||
filename=unquote(pathlib.Path(media_url).name),
|
||||
embedded=media_url,
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if wc_search_type == "image":
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
template="images.html",
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
img_src=imageinfo["url"],
|
||||
thumbnail_src=thumbnail,
|
||||
resolution=f"{imageinfo['width']} x {imageinfo['height']}",
|
||||
img_format=imageinfo["mime"],
|
||||
filesize=size,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if wc_search_type == "video":
|
||||
res.add(
|
||||
res.types.LegacyResult(
|
||||
template="videos.html",
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
iframe_src=media_url,
|
||||
length=duration,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if wc_search_type == "audio":
|
||||
res.add(
|
||||
res.types.MainResult(
|
||||
template="default.html",
|
||||
title=title,
|
||||
url=url,
|
||||
content=content,
|
||||
audio_src=media_url,
|
||||
length=duration,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
return res
|
||||
|
||||
@@ -23,6 +23,7 @@ __all__ = [
|
||||
"WeatherAnswer",
|
||||
"Code",
|
||||
"Paper",
|
||||
"File",
|
||||
]
|
||||
|
||||
import typing as t
|
||||
@@ -33,6 +34,7 @@ from .answer import AnswerSet, Answer, Translations, WeatherAnswer
|
||||
from .keyvalue import KeyValue
|
||||
from .code import Code
|
||||
from .paper import Paper
|
||||
from .file import File
|
||||
|
||||
|
||||
class ResultList(list[Result | LegacyResult], abc.ABC):
|
||||
@@ -47,6 +49,7 @@ class ResultList(list[Result | LegacyResult], abc.ABC):
|
||||
KeyValue = KeyValue
|
||||
Code = Code
|
||||
Paper = Paper
|
||||
File = File
|
||||
MainResult = MainResult
|
||||
Result = Result
|
||||
Translations = Translations
|
||||
|
||||
@@ -27,7 +27,6 @@ import typing as t
|
||||
import re
|
||||
import urllib.parse
|
||||
import warnings
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from collections.abc import Callable
|
||||
@@ -236,13 +235,6 @@ class Result(msgspec.Struct, kw_only=True):
|
||||
url: str | None = None
|
||||
"""A link related to this *result*"""
|
||||
|
||||
template: str = "default.html"
|
||||
"""Name of the template used to render the result.
|
||||
|
||||
By default :origin:`result_templates/default.html
|
||||
<searx/templates/simple/result_templates/default.html>` is used.
|
||||
"""
|
||||
|
||||
engine: str | None = ""
|
||||
"""Name of the engine *this* result comes from. In case of *plugins* a
|
||||
prefix ``plugin:`` is set, in case of *answerer* prefix ``answerer:`` is
|
||||
@@ -350,6 +342,13 @@ class Result(msgspec.Struct, kw_only=True):
|
||||
class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
"""Base class of all result types displayed in :ref:`area main results`."""
|
||||
|
||||
template: str = "default.html"
|
||||
"""Name of the template used to render the result.
|
||||
|
||||
By default :origin:`result_templates/default.html
|
||||
<searx/templates/simple/result_templates/default.html>` is used.
|
||||
"""
|
||||
|
||||
title: str = ""
|
||||
"""Link title of the result item."""
|
||||
|
||||
@@ -359,6 +358,12 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
img_src: str = ""
|
||||
"""URL of a image that is displayed in the result item."""
|
||||
|
||||
iframe_src: str = ""
|
||||
"""URL of an embedded ``<iframe>`` / the frame is collapsible."""
|
||||
|
||||
audio_src: str = ""
|
||||
"""URL of an embedded ``<audio controls>``."""
|
||||
|
||||
thumbnail: str = ""
|
||||
"""URL of a thumbnail that is displayed in the result item."""
|
||||
|
||||
@@ -372,7 +377,7 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
completely eliminated.
|
||||
"""
|
||||
|
||||
length: time.struct_time | None = None
|
||||
length: datetime.timedelta | None = None
|
||||
"""Playing duration in seconds."""
|
||||
|
||||
views: str = ""
|
||||
|
||||
94
searx/result_types/file.py
Normal file
94
searx/result_types/file.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Typification of the *file* results. Results of this type are rendered in
|
||||
the :origin:`file.html <searx/templates/simple/result_templates/file.html>`
|
||||
template.
|
||||
|
||||
----
|
||||
|
||||
.. autoclass:: File
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
"""
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
__all__ = ["File"]
|
||||
|
||||
import typing as t
|
||||
import mimetypes
|
||||
|
||||
from ._base import MainResult
|
||||
|
||||
|
||||
@t.final
|
||||
class File(MainResult, kw_only=True):
|
||||
"""Class for results of type *file*"""
|
||||
|
||||
template: str = "file.html"
|
||||
|
||||
filename: str = ""
|
||||
"""Name of the file."""
|
||||
|
||||
size: str = ""
|
||||
"""Size of bytes in human readable notation (``MB`` for 1024 * 1024 Bytes
|
||||
file size.)"""
|
||||
|
||||
time: str = ""
|
||||
"""Indication of a time, such as the date of the last modification or the
|
||||
date of creation. This is a simple string, the *date* of which can be freely
|
||||
chosen according to the context."""
|
||||
|
||||
mimetype: str = ""
|
||||
"""Mimetype/Subtype of the file. For ``audio`` and ``video``, a URL can be
|
||||
passed in the :py:obj:`File.embedded` field to embed the referenced media in
|
||||
the result. If no value is specified, the MIME type is determined from
|
||||
``self.filename`` or, alternatively, from ``self.embedded`` (if either of
|
||||
the two values is set)."""
|
||||
|
||||
abstract: str = ""
|
||||
"""Abstract of the file."""
|
||||
|
||||
author: str = ""
|
||||
"""Author of the file."""
|
||||
|
||||
embedded: str = ""
|
||||
"""URL of an embedded media type (audio or video) / is collapsible."""
|
||||
|
||||
mtype: str = ""
|
||||
"""Used for displaying :py:obj:`File.embedded`. Its value is automatically
|
||||
populated from the base type of :py:obj:`File.mimetype`, and can be
|
||||
explicitly set to enforce e.g. ``audio`` or ``video`` when mimetype is
|
||||
something like "application/ogg" but its know the content is for example a
|
||||
video."""
|
||||
|
||||
subtype: str = ""
|
||||
"""Used for displaying :py:obj:`File.embedded`. Its value is automatically
|
||||
populated from the subtype type of :py:obj:`File.mimetype`, and can be
|
||||
explicitly set to enforce a subtype for the :py:obj:`File.embedded`
|
||||
element."""
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
if not self.mtype or not self.subtype:
|
||||
|
||||
fn = self.filename or self.embedded
|
||||
if not self.mimetype and fn:
|
||||
self.mimetype = mimetypes.guess_type(fn, strict=False)[0] or ""
|
||||
|
||||
mtype, subtype = (self.mimetype.split("/", 1) + [""])[:2]
|
||||
|
||||
if not self.mtype:
|
||||
# I don't know why, but the ogg video stream is not displayed,
|
||||
# may https://github.com/videojs/video.js can help?
|
||||
if self.embedded.endswith(".ogv"):
|
||||
self.mtype = "video"
|
||||
elif self.embedded.endswith(".oga"):
|
||||
self.mtype = "audio"
|
||||
else:
|
||||
self.mtype = mtype
|
||||
|
||||
if not self.subtype:
|
||||
self.subtype = subtype
|
||||
@@ -2298,31 +2298,27 @@ engines:
|
||||
|
||||
- name: wikicommons.images
|
||||
engine: wikicommons
|
||||
shortcut: wc
|
||||
shortcut: wci
|
||||
categories: images
|
||||
search_type: images
|
||||
number_of_results: 10
|
||||
wc_search_type: image
|
||||
|
||||
- name: wikicommons.videos
|
||||
engine: wikicommons
|
||||
shortcut: wcv
|
||||
categories: videos
|
||||
search_type: videos
|
||||
number_of_results: 10
|
||||
wc_search_type: video
|
||||
|
||||
- name: wikicommons.audio
|
||||
engine: wikicommons
|
||||
shortcut: wca
|
||||
categories: music
|
||||
search_type: audio
|
||||
number_of_results: 10
|
||||
wc_search_type: audio
|
||||
|
||||
- name: wikicommons.files
|
||||
engine: wikicommons
|
||||
shortcut: wcf
|
||||
categories: files
|
||||
search_type: files
|
||||
number_of_results: 10
|
||||
wc_search_type: file
|
||||
|
||||
- name: wolframalpha
|
||||
shortcut: wa
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ result_header(result, favicons, image_proxify) -}}
|
||||
{{- result_sub_header(result) -}}
|
||||
{% if result.iframe_src -%}
|
||||
<p class="altlink"><a class="btn-collapse collapsed media-loader disabled_if_nojs" data-target="#result-media-{{ index }}" data-btn-text-collapsed="{{ _('show media') }}" data-btn-text-not-collapsed="{{ _('hide media') }}">{{ icon('music-note') }} {{ _('show media') }}</a></p>
|
||||
<p class="altlink"><a class="btn-collapse collapsed media-loader disabled_if_nojs" data-target="#result-media-{{ index }}" data-btn-text-collapsed="{{ _('show media') }}" data-btn-text-not-collapsed="{{ _('hide media') }}">{{ icon('play') }} {{ _('show media') }}</a></p>
|
||||
{%- endif %}
|
||||
{%- if result.content %}
|
||||
<p class="content">
|
||||
|
||||
74
searx/templates/simple/result_templates/file.html
Normal file
74
searx/templates/simple/result_templates/file.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% from "simple/macros.html" import result_header, result_sub_header, result_sub_footer, result_footer, result_link with context %}
|
||||
{% from "simple/icons.html" import icon_small %}
|
||||
|
||||
{{ result_header(result, favicons, image_proxify) }}
|
||||
{{ result_sub_header(result) }}
|
||||
|
||||
{% if result.abstract %}
|
||||
<p class="abstract">{{ result.abstract|safe }}</p>
|
||||
{% endif -%}
|
||||
|
||||
{%- if result.content %}
|
||||
<p class="content">{{ result.content|safe }}</p>
|
||||
{% endif -%}
|
||||
|
||||
<div class="attributes">
|
||||
{% if result.author %}
|
||||
<div>
|
||||
<span>{{ _("Author") }}:</span>
|
||||
<span>{{ result.author }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if result.filename %}
|
||||
<div>
|
||||
<span>{{ _("Filename") }}:</span>
|
||||
<span>{{ result.filename }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if result.size %}
|
||||
<div>
|
||||
<span>{{ _("Filesize") }}:</span>
|
||||
<span>{{ result.size }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if result.time %}
|
||||
<div>
|
||||
<span>{{ _("Date") }}:</span>
|
||||
<span>{{ result.time }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if result.mimetype %}
|
||||
<div>
|
||||
<span>{{ _("Type") }}:</span>
|
||||
<span>{{ result.mimetype }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if result.embedded %}
|
||||
{% if result.mtype in ("audio", "video") %}
|
||||
<p class="altlink">
|
||||
<a class="btn-collapse collapsed media-loader disabled_if_nojs"
|
||||
data-target="#result-media-{{ index }}"
|
||||
data-btn-text-collapsed="{{ _("show media") }}"
|
||||
data-btn-text-not-collapsed="{{ _("hide media") }}"
|
||||
>
|
||||
{{ _("show media") }}
|
||||
</a>
|
||||
</p>
|
||||
<div id="result-media-{{ index }}" class="embedded-{{ result.mtype }} invisible">
|
||||
<{{ result.mtype }} controls preload="metadata" {% if result.thumbnail %}poster="{{ result.thumbnail }}" {% endif %}>
|
||||
<source src="{{result.embedded}}" type="{{ result.mtype }}/{{ result.subtype }}">
|
||||
</{{ result.mtype }}>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="altlink">
|
||||
<a href="{{result.embedded }}" target="_blank" rel="noopener noreferrer" download>
|
||||
{{ _("Download") }}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{{ result_sub_footer(result) }}
|
||||
{{ result_footer(result) }}
|
||||
@@ -1,45 +0,0 @@
|
||||
{% from 'simple/macros.html' import result_header, result_sub_header, result_sub_footer, result_footer, result_link with context %}
|
||||
{% from 'simple/icons.html' import icon_small %}
|
||||
|
||||
{{- result_header(result, favicons, image_proxify) -}}
|
||||
{{- result_sub_header(result) -}}
|
||||
|
||||
{%- if result.embedded -%}
|
||||
<small> • <a class="text-info btn-collapse collapsed cursor-pointer media-loader disabled_if_nojs" data-toggle="collapse" data-target="#result-media-{{ index }}" data-btn-text-collapsed="{{ _('show media') }}" data-btn-text-not-collapsed="{{ _('hide media') }}">
|
||||
{%- if result.mtype == 'audio' %}{{ icon_small('musical-notes') -}}
|
||||
{%- elif result.mtype == 'video' %} {{ icon_small('play') -}}
|
||||
{%- endif %} {{ _('show media') }}</a></small>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if result.embedded -%}
|
||||
<div id="result-media-{{ index }}" class="collapse invisible">
|
||||
{{- result.embedded|safe -}}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if result.abstract %}<p class="result-content result-abstract">{{ result.abstract|safe }}</p>{% endif -%}
|
||||
|
||||
{%- if result.img_src -%}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<img src="{{ image_proxify(result.img_src) }}" alt="{{ result.title|striptags }}" title="{{ result.title|striptags }}" style="width: auto; max-height: 60px; min-height: 60px;" class="col-xs-2 col-sm-4 col-md-4 result-content">
|
||||
{%- if result.content %}<p class="result-content col-xs-8 col-sm-8 col-md-8">{{ result.content|safe }}</p>{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- else -%}
|
||||
{%- if result.content %}<p class="result-content">{{ result.content|safe }}</p>{% endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
<table class="result-metadata result-content">
|
||||
{%- if result.author %}<tr><td>{{ _('Author') }}</td><td>{{ result.author|safe }}</td></tr>{% endif -%}
|
||||
|
||||
{%- if result.filename %}<tr><td>{{ _('Filename') }}</td><td>{{ result.filename|safe }}</td></tr>{% endif -%}
|
||||
|
||||
{%- if result.size %}<tr><td>{{ _('Filesize') }}</td><td>{{ result.size|safe }}</td></tr>{%- endif -%}
|
||||
|
||||
{%- if result.time %}<tr><td>{{ _('Date') }}</td><td>{{ result.time|safe }}</td></tr>{% endif -%}
|
||||
|
||||
{%- if result.mtype %}<tr><td>{{ _('Type') }}</td><td>{{ result.mtype|safe }}/{{ result.subtype|safe }}</td></tr>{% endif -%}
|
||||
</table>
|
||||
|
||||
{{ result_footer(result) }}
|
||||
Reference in New Issue
Block a user