Repository: searxng/searxng

Issue:
Jackett engine request
**Working URL to the engine**
Jackett is a self hosted application.

**Why do you want to add this engine?**
Jackett is the standard the facto to aggregate torrent search. Searx already sypports some torrent websites but using jackett it would open the possibility to search a lot of indexers, expecially private ones.

**Features of this engine**
<!-- Features of this engine: Doesn't track its users, fast, easy to integrate, ... -->
Jackett is open source and self hostable.

**Applicable category of this engine**
<!-- Where should this new engine fit in SearXNG? Current categories in SearXNG:
general, files, images, it, map, music, news, science, social media and videos.
You can add multiple categories at the same time. -->
Files




Discussion:

Resolving patch:
diff --git a/searx/engines/torznab.py b/searx/engines/torznab.py
new file mode 100644
--- /dev/null
+++ b/searx/engines/torznab.py
@@ -0,0 +1,145 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# lint: pylint
+"""Torznab WebAPI
+
+A engine that implements the `torznab WebAPI`_.
+
+.. _torznab WebAPI: https://torznab.github.io/spec-1.3-draft/torznab
+
+"""
+
+from datetime import datetime
+from urllib.parse import quote
+from lxml import etree
+
+from searx.exceptions import SearxEngineAPIException
+
+# about
+about = {
+    "website": None,
+    "wikidata_id": None,
+    "official_api_documentation": "https://torznab.github.io/spec-1.3-draft",
+    "use_official_api": True,
+    "require_api_key": False,
+    "results": 'XML',
+}
+
+categories = ['files']
+paging = False
+time_range_support = False
+
+# defined in settings.yml
+# example (Jackett): "http://localhost:9117/api/v2.0/indexers/all/results/torznab"
+base_url = ''
+api_key = ''
+# https://newznab.readthedocs.io/en/latest/misc/api/#predefined-categories
+torznab_categories = []
+
+def init(engine_settings=None): # pylint: disable=unused-argument
+    if len(base_url) < 1:
+        raise ValueError('missing torznab base_url')
+
+def request(query, params):
+
+    search_url = base_url + '?t=search&q={search_query}'
+    if len(api_key) > 0:
+        search_url += '&apikey={api_key}'
+    if len(torznab_categories) > 0:
+        search_url += '&cat={torznab_categories}'
+
+    params['url'] = search_url.format(
+        search_query = quote(query),
+        api_key = api_key,
+        torznab_categories = ",".join([str(x) for x in torznab_categories])
+    )
+
+    return params
+
+def response(resp):
+    results = []
+
+    search_results = etree.XML(resp.content)
+
+    # handle errors
+    # https://newznab.readthedocs.io/en/latest/misc/api/#newznab-error-codes
+    if search_results.tag == "error":
+        raise SearxEngineAPIException(search_results.get("description"))
+
+    for item in search_results[0].iterfind('item'):
+        result = {'template': 'torrent.html'}
+
+        enclosure = item.find('enclosure')
+
+        result["filesize"] = int(enclosure.get('length'))
+
+        link = get_property(item, 'link')
+        guid = get_property(item, 'guid')
+        comments = get_property(item, 'comments')
+
+        # define url
+        result["url"] = enclosure.get('url')
+        if comments is not None and comments.startswith('http'):
+            result["url"] = comments
+        elif guid is not None and guid.startswith('http'):
+            result["url"] = guid
+
+        # define torrent file url
+        result["torrentfile"] = None
+        if enclosure.get('url').startswith("http"):
+            result["torrentfile"] = enclosure.get('url')
+        elif link is not None and link.startswith('http'):
+            result["torrentfile"] = link
+
+        # define magnet link
+        result["magnetlink"] = get_torznab_attr(item, 'magneturl')
+        if result["magnetlink"] is None:
+            if enclosure.get('url').startswith("magnet"):
+                result["magnetlink"] = enclosure.get('url')
+            elif link is not None and link.startswith('magnet'):
+                result["magnetlink"] = link
+
+        result["title"] = get_property(item, 'title')
+        result["files"] = get_property(item, 'files')
+
+        result["publishedDate"] = None
+        try:
+            result["publishedDate"] = datetime.strptime(
+                get_property(item, 'pubDate'), '%a, %d %b %Y %H:%M:%S %z')
+        except (ValueError, TypeError) as e:
+            logger.debug("ignore exception (publishedDate): %s", e)
+
+        result["seed"] = get_torznab_attr(item, 'seeders')
+
+        # define leech
+        result["leech"] = get_torznab_attr(item, 'leechers')
+        if result["leech"] is None and result["seed"] is not None:
+            peers = get_torznab_attr(item, 'peers')
+            if peers is not None:
+                result["leech"] = int(peers) - int(result["seed"])
+
+        results.append(result)
+
+    return results
+
+
+def get_property(item, property_name):
+    property_element = item.find(property_name)
+
+    if property_element is not None:
+        return property_element.text
+
+    return None
+
+
+def get_torznab_attr(item, attr_name):
+    element = item.find(
+        './/torznab:attr[@name="{attr_name}"]'.format(attr_name=attr_name),
+        {
+            'torznab': 'http://torznab.com/schemas/2015/feed'
+        }
+    )
+
+    if element is not None:
+        return element.get("value")
+
+    return None


Test patch: