[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-news,
article[data-vim-selected].category-map, article[data-vim-selected].category-map,
article[data-vim-selected].category-music, article[data-vim-selected].category-music,
article[data-vim-selected].category-files,
article[data-vim-selected].category-social { article[data-vim-selected].category-social {
border: 1px solid var(--color-result-vim-arrow); border: 1px solid var(--color-result-vim-arrow);
.rounded-corners; .rounded-corners;
@@ -387,7 +386,6 @@ article[data-vim-selected].category-social {
.category-news, .category-news,
.category-map, .category-map,
.category-music, .category-music,
.category-files,
.category-social { .category-social {
border: 1px solid var(--color-result-border); border: 1px solid var(--color-result-border);
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important; margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
@@ -1168,3 +1166,4 @@ pre code {
@import "result_types/keyvalue.less"; @import "result_types/keyvalue.less";
@import "result_types/code.less"; @import "result_types/code.less";
@import "result_types/paper.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/keyvalue
main/code main/code
main/paper main/paper
main/file
The :ref:`LegacyResult <LegacyResult>` is used internally for the results that 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 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 torrent`
- :ref:`template map` - :ref:`template map`
- :ref:`template packages` - :ref:`template packages`
- :ref:`template files`
- :ref:`template products` - :ref:`template products`

View File

@@ -60,7 +60,7 @@ Fields used in the template :origin:`macro result_sub_header
publishedDate : :py:obj:`datetime.datetime` publishedDate : :py:obj:`datetime.datetime`
The date on which the object was published. The date on which the object was published.
length: :py:obj:`time.struct_time` length: :py:obj:`datetime.timedelta`
Playing duration in seconds. Playing duration in seconds.
views: :py:class:`str` views: :py:class:`str`
@@ -469,38 +469,6 @@ links : :py:class:`dict`
Additional links in the form of ``{'link_name': 'http://example.com'}`` 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: .. _template products:
``products.html`` ``products.html``

View File

@@ -13,23 +13,12 @@ Configuration
You must configure the following settings: You must configure the following settings:
``base_url``: - :py:obj:`base_url`
Location where recoll-webui can be reached. - :py:obj:`mount_prefix`
- :py:obj:`dl_prefix`
- :py:obj:`search_dir`
``mount_prefix``: Example scenario:
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:
#. Recoll indexes a local filesystem mounted in ``/export/documents/reference``, #. Recoll indexes a local filesystem mounted in ``/export/documents/reference``,
#. the Recoll search interface can be reached at https://recoll.example.org/ and #. the Recoll search interface can be reached at https://recoll.example.org/ and
@@ -37,107 +26,128 @@ Scenario:
.. code:: yaml .. code:: yaml
base_url: https://recoll.example.org/ base_url: https://recoll.example.org
mount_prefix: /export/documents mount_prefix: /export/documents
dl_prefix: https://download.example.org dl_prefix: https://download.example.org
search_dir: '' search_dir: ""
Implementations Implementations
=============== ===============
""" """
import typing as t
from datetime import date, timedelta from datetime import date, timedelta
from json import loads
from urllib.parse import urlencode, quote 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 = { about = {
"website": None, "website": None,
"wikidata_id": 'Q15735774', "wikidata_id": "Q15735774",
"official_api_documentation": 'https://www.lesbonscomptes.com/recoll/', "official_api_documentation": "https://www.lesbonscomptes.com/recoll/",
"use_official_api": True, "use_official_api": True,
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": "JSON",
} }
# engine dependent config
paging = True paging = True
time_range_support = True time_range_support = True
# parameters from settings.yml base_url: str = ""
base_url = None """Location where recoll-webui can be reached."""
search_dir = ''
mount_prefix = None
dl_prefix = None
# embedded mount_prefix: str = ""
embedded_url = '<{ttype} controls height="166px" ' + 'src="{url}" type="{mtype}"></{ttype}>' """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 setup(engine_settings: dict[str, t.Any]) -> bool:
def get_time_range(time_range): """Initialization of the Recoll engine, checks if the mandatory values are
sw = {'day': 1, 'week': 7, 'month': 30, 'year': 365} # pylint: disable=invalid-name 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: if not offset:
return '' return ""
return (date.today() - timedelta(days=offset)).isoformat() return (date.today() - timedelta(days=offset)).isoformat()
# do search-request def request(query: str, params: "OnlineParams") -> None:
def request(query, params): args = {
search_after = get_time_range(params['time_range']) "query": query,
search_url = base_url + 'json?{query}&highlight=0' "page": params["pageno"],
params['url'] = search_url.format( "after": search_after(params["time_range"]),
query=urlencode({'query': query, 'page': params['pageno'], 'after': search_after, 'dir': search_dir}) "dir": search_dir,
) "highlight": 0,
}
return params params["url"] = f"{base_url}/json?{urlencode(args)}"
# get response from search-request def response(resp: "SXNG_Response") -> EngineResults:
def response(resp):
results = []
response_json = loads(resp.text) res = EngineResults()
json_data = resp.json()
if not response_json: if not json_data:
return [] return res
for result in response_json.get('results', []): for result in json_data.get("results", []):
title = result['label']
url = result['url'].replace('file://' + mount_prefix, dl_prefix)
content = '{}'.format(result['snippet'])
# append result url = result.get("url", "").replace("file://" + mount_prefix, dl_prefix)
item = {'url': url, 'title': title, 'content': content, 'template': 'files.html'}
if result['size']: mtype = subtype = result.get("mime", "")
item['size'] = int(result['size']) if mtype:
mtype, subtype = (mtype.split("/", 1) + [""])[:2]
for parameter in ['filename', 'abstract', 'author', 'mtype', 'time']:
if result[parameter]:
item[parameter] = result[parameter]
# facilitate preview support for known mime types # facilitate preview support for known mime types
if 'mtype' in result and '/' in result['mtype']: thumbnail = embedded = ""
(mtype, subtype) = result['mtype'].split('/') if mtype in ["audio", "video"]:
item['mtype'] = mtype embedded_url = '<{ttype} controls height="166px" ' + 'src="{url}" type="{mtype}"></{ttype}>'
item['subtype'] = subtype 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']: res.add(
item['embedded'] = embedded_url.format( res.types.File(
ttype=mtype, url=quote(url.encode('utf8'), '/:'), mtype=result['mtype'] title=result.get("label", ""),
) url=url,
content=result.get("snippet", ""),
if mtype in ['image'] and subtype in ['bmp', 'gif', 'jpeg', 'png']: size=result.get("size", ""),
item['thumbnail'] = url filename=result.get("filename", ""),
abstract=result.get("abstract", ""),
results.append(item) author=result.get("author", ""),
mtype=mtype,
if 'nres' in response_json: subtype=subtype,
results.append({'number_of_results': response_json['nres']}) time=result.get("time", ""),
embedded=embedded,
return results thumbnail=thumbnail,
)
)
return res

View File

@@ -1,102 +1,208 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # 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 import datetime
import pathlib
from urllib.parse import urlencode from urllib.parse import urlencode, unquote
from searx.utils import html_to_text, humanize_bytes 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 = { about = {
"website": 'https://commons.wikimedia.org/', "website": "https://commons.wikimedia.org/",
"wikidata_id": 'Q565', "wikidata_id": "Q565",
"official_api_documentation": 'https://commons.wikimedia.org/w/api.php', "official_api_documentation": "https://commons.wikimedia.org/w/api.php",
"use_official_api": True, "use_official_api": True,
"require_api_key": False, "require_api_key": False,
"results": 'JSON', "results": "JSON",
} }
categories = ['images']
search_type = 'images'
base_url = "https://commons.wikimedia.org" categories: list[str] = []
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
)
paging = True paging = True
number_of_results = 10 number_of_results = 10
search_types = { wc_api_url = "https://commons.wikimedia.org/w/api.php"
'images': 'bitmap|drawing', wc_search_type: str = ""
'videos': 'video',
'audio': 'audio', SEARCH_TYPES: dict[str, str] = {
'files': 'multimedia|office|archive|3d', "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): def setup(engine_settings: dict[str, t.Any]) -> bool:
language = 'en' """Initialization of the Wikimedia engine, checks if the value configured in
if params['language'] != 'all': :py:obj:`wc_search_type` is valid."""
language = params['language'].split('-')[0]
if search_type not in search_types: if engine_settings.get("wc_search_type") not in SEARCH_TYPES:
raise ValueError(f"Unsupported search type: {search_type}") 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 = { args = {
'uselang': language, # https://commons.wikimedia.org/w/api.php
'gsrlimit': number_of_results, "format": "json",
'gsroffset': number_of_results * (params["pageno"] - 1), "uselang": uselang,
'gsrsearch': f"filetype:{filetype} {query}", "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"{wc_api_url}?{urlencode(args, safe=':|')}"
params["url"] = f"{base_url}/w/api.php{search_prefix}&{urlencode(args, safe=':|')}"
return params
def response(resp): def response(resp: "SXNG_Response") -> EngineResults:
results = []
json = resp.json()
if not json.get("query", {}).get("pages"): res = EngineResults()
return results json_data = resp.json()
for item in json["query"]["pages"].values(): pages = json_data.get("query", {}).get("pages", {}).values()
for item in pages:
if not item.get("imageinfo", []):
continue
imageinfo = item["imageinfo"][0] 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": title: str = item["title"].replace("File:", "").rsplit(".", 1)[0]
result['template'] = 'images.html' content = html_to_text(item["snippet"])
result['img_src'] = imageinfo["url"]
result['thumbnail_src'] = imageinfo["thumburl"]
result['resolution'] = f'{imageinfo["width"]} x {imageinfo["height"]}'
else:
result['thumbnail'] = imageinfo["thumburl"]
if search_type == "videos": url: str = imageinfo["descriptionurl"]
result['template'] = 'videos.html' media_url: str = imageinfo["url"]
if imageinfo.get('duration'): mimetype: str = imageinfo["mime"]
result['length'] = datetime.timedelta(seconds=int(imageinfo['duration'])) thumbnail: str = imageinfo["thumburl"]
result['iframe_src'] = imageinfo['url'] size = imageinfo.get("size")
elif search_type == "files": if size:
result['template'] = 'files.html' size = humanize_bytes(size)
result['metadata'] = imageinfo['mime']
result['size'] = humanize_bytes(imageinfo['size'])
elif search_type == "audio":
result['iframe_src'] = imageinfo['url']
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", "WeatherAnswer",
"Code", "Code",
"Paper", "Paper",
"File",
] ]
import typing as t import typing as t
@@ -33,6 +34,7 @@ from .answer import AnswerSet, Answer, Translations, WeatherAnswer
from .keyvalue import KeyValue from .keyvalue import KeyValue
from .code import Code from .code import Code
from .paper import Paper from .paper import Paper
from .file import File
class ResultList(list[Result | LegacyResult], abc.ABC): class ResultList(list[Result | LegacyResult], abc.ABC):
@@ -47,6 +49,7 @@ class ResultList(list[Result | LegacyResult], abc.ABC):
KeyValue = KeyValue KeyValue = KeyValue
Code = Code Code = Code
Paper = Paper Paper = Paper
File = File
MainResult = MainResult MainResult = MainResult
Result = Result Result = Result
Translations = Translations Translations = Translations

View File

@@ -27,7 +27,6 @@ import typing as t
import re import re
import urllib.parse import urllib.parse
import warnings import warnings
import time
import datetime import datetime
from collections.abc import Callable from collections.abc import Callable
@@ -236,13 +235,6 @@ class Result(msgspec.Struct, kw_only=True):
url: str | None = None url: str | None = None
"""A link related to this *result*""" """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 = "" engine: str | None = ""
"""Name of the engine *this* result comes from. In case of *plugins* a """Name of the engine *this* result comes from. In case of *plugins* a
prefix ``plugin:`` is set, in case of *answerer* prefix ``answerer:`` is 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 class MainResult(Result): # pylint: disable=missing-class-docstring
"""Base class of all result types displayed in :ref:`area main results`.""" """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 = "" title: str = ""
"""Link title of the result item.""" """Link title of the result item."""
@@ -359,6 +358,12 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
img_src: str = "" img_src: str = ""
"""URL of a image that is displayed in the result item.""" """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 = "" thumbnail: str = ""
"""URL of a thumbnail that is displayed in the result item.""" """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. completely eliminated.
""" """
length: time.struct_time | None = None length: datetime.timedelta | None = None
"""Playing duration in seconds.""" """Playing duration in seconds."""
views: str = "" 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 - name: wikicommons.images
engine: wikicommons engine: wikicommons
shortcut: wc shortcut: wci
categories: images categories: images
search_type: images wc_search_type: image
number_of_results: 10
- name: wikicommons.videos - name: wikicommons.videos
engine: wikicommons engine: wikicommons
shortcut: wcv shortcut: wcv
categories: videos categories: videos
search_type: videos wc_search_type: video
number_of_results: 10
- name: wikicommons.audio - name: wikicommons.audio
engine: wikicommons engine: wikicommons
shortcut: wca shortcut: wca
categories: music categories: music
search_type: audio wc_search_type: audio
number_of_results: 10
- name: wikicommons.files - name: wikicommons.files
engine: wikicommons engine: wikicommons
shortcut: wcf shortcut: wcf
categories: files categories: files
search_type: files wc_search_type: file
number_of_results: 10
- name: wolframalpha - name: wolframalpha
shortcut: wa shortcut: wa

View File

@@ -3,7 +3,7 @@
{{ result_header(result, favicons, image_proxify) -}} {{ result_header(result, favicons, image_proxify) -}}
{{- result_sub_header(result) -}} {{- result_sub_header(result) -}}
{% if result.iframe_src -%} {% 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 %} {%- endif %}
{%- if result.content %} {%- if result.content %}
<p class="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) }}