[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:
Markus Heiser
2025-10-13 09:28:42 +02:00
committed by Markus Heiser
parent ee6d4f322f
commit 9371658531
14 changed files with 493 additions and 254 deletions

View 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;
}
}

View File

@@ -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";

View File

@@ -0,0 +1,7 @@
.. _result_types.file:
============
File Results
============
.. automodule:: searx.result_types.file

View 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`

View File

@@ -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``

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = ""

View 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

View File

@@ -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

View File

@@ -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">

View 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) }}

View File

@@ -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> &bull; <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) }}