123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- # SPDX-License-Identifier: AGPL-3.0-or-later
- """Torznab_ is an API specification that provides a standardized way to query
- torrent site for content. It is used by a number of torrent applications,
- including Prowlarr_ and Jackett_.
- Using this engine together with Prowlarr_ or Jackett_ allows you to search
- a huge number of torrent sites which are not directly supported.
- Configuration
- =============
- The engine has the following settings:
- ``base_url``:
- Torznab endpoint URL.
- ``api_key``:
- The API key to use for authentication.
- ``torznab_categories``:
- The categories to use for searching. This is a list of category IDs. See
- Prowlarr-categories_ or Jackett-categories_ for more information.
- ``show_torrent_files``:
- Whether to show the torrent file in the search results. Be careful as using
- this with Prowlarr_ or Jackett_ leaks the API key. This should be used only
- if you are querying a Torznab endpoint without authentication or if the
- instance is private. Be aware that private trackers may ban you if you share
- the torrent file. Defaults to ``false``.
- ``show_magnet_links``:
- Whether to show the magnet link in the search results. Be aware that private
- trackers may ban you if you share the magnet link. Defaults to ``true``.
- .. _Torznab:
- https://torznab.github.io/spec-1.3-draft/index.html
- .. _Prowlarr:
- https://github.com/Prowlarr/Prowlarr
- .. _Jackett:
- https://github.com/Jackett/Jackett
- .. _Prowlarr-categories:
- https://wiki.servarr.com/en/prowlarr/cardigann-yml-definition#categories
- .. _Jackett-categories:
- https://github.com/Jackett/Jackett/wiki/Jackett-Categories
- Implementations
- ===============
- """
- from __future__ import annotations
- from typing import TYPE_CHECKING
- from typing import List, Dict, Any
- from datetime import datetime
- from urllib.parse import quote
- from lxml import etree # type: ignore
- from searx.exceptions import SearxEngineAPIException
- from searx.utils import humanize_bytes
- if TYPE_CHECKING:
- import httpx
- import logging
- logger: logging.Logger
- # engine settings
- about: Dict[str, Any] = {
- "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: List[str] = ['files']
- paging: bool = False
- time_range_support: bool = False
- # defined in settings.yml
- # example (Jackett): "http://localhost:9117/api/v2.0/indexers/all/results/torznab"
- base_url: str = ''
- api_key: str = ''
- # https://newznab.readthedocs.io/en/latest/misc/api/#predefined-categories
- torznab_categories: List[str] = []
- show_torrent_files: bool = False
- show_magnet_links: bool = True
- def init(engine_settings=None): # pylint: disable=unused-argument
- """Initialize the engine."""
- if len(base_url) < 1:
- raise ValueError('missing torznab base_url')
- def request(query: str, params: Dict[str, Any]) -> Dict[str, Any]:
- """Build the request params."""
- search_url: str = 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: httpx.Response) -> List[Dict[str, Any]]:
- """Parse the XML response and return a list of results."""
- 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"))
- channel: etree.Element = search_results[0]
- item: etree.Element
- for item in channel.iterfind('item'):
- result: Dict[str, Any] = build_result(item)
- results.append(result)
- return results
- def build_result(item: etree.Element) -> Dict[str, Any]:
- """Build a result from a XML item."""
- # extract attributes from XML
- # see https://torznab.github.io/spec-1.3-draft/torznab/Specification-v1.3.html#predefined-attributes
- enclosure: etree.Element | None = item.find('enclosure')
- enclosure_url: str | None = None
- if enclosure is not None:
- enclosure_url = enclosure.get('url')
- filesize = get_attribute(item, 'size')
- if not filesize and enclosure:
- filesize = enclosure.get('length')
- guid = get_attribute(item, 'guid')
- comments = get_attribute(item, 'comments')
- pubDate = get_attribute(item, 'pubDate')
- seeders = get_torznab_attribute(item, 'seeders')
- leechers = get_torznab_attribute(item, 'leechers')
- peers = get_torznab_attribute(item, 'peers')
- # map attributes to searx result
- result: Dict[str, Any] = {
- 'template': 'torrent.html',
- 'title': get_attribute(item, 'title'),
- 'filesize': humanize_bytes(int(filesize)) if filesize else None,
- 'files': get_attribute(item, 'files'),
- 'seed': seeders,
- 'leech': _map_leechers(leechers, seeders, peers),
- 'url': _map_result_url(guid, comments),
- 'publishedDate': _map_published_date(pubDate),
- 'torrentfile': None,
- 'magnetlink': None,
- }
- link = get_attribute(item, 'link')
- if show_torrent_files:
- result['torrentfile'] = _map_torrent_file(link, enclosure_url)
- if show_magnet_links:
- magneturl = get_torznab_attribute(item, 'magneturl')
- result['magnetlink'] = _map_magnet_link(magneturl, guid, enclosure_url, link)
- return result
- def _map_result_url(guid: str | None, comments: str | None) -> str | None:
- if guid and guid.startswith('http'):
- return guid
- if comments and comments.startswith('http'):
- return comments
- return None
- def _map_leechers(leechers: str | None, seeders: str | None, peers: str | None) -> str | None:
- if leechers:
- return leechers
- if seeders and peers:
- return str(int(peers) - int(seeders))
- return None
- def _map_published_date(pubDate: str | None) -> datetime | None:
- if pubDate is not None:
- try:
- return datetime.strptime(pubDate, '%a, %d %b %Y %H:%M:%S %z')
- except (ValueError, TypeError) as e:
- logger.debug("ignore exception (publishedDate): %s", e)
- return None
- def _map_torrent_file(link: str | None, enclosure_url: str | None) -> str | None:
- if link and link.startswith('http'):
- return link
- if enclosure_url and enclosure_url.startswith('http'):
- return enclosure_url
- return None
- def _map_magnet_link(
- magneturl: str | None,
- guid: str | None,
- enclosure_url: str | None,
- link: str | None,
- ) -> str | None:
- if magneturl and magneturl.startswith('magnet'):
- return magneturl
- if guid and guid.startswith('magnet'):
- return guid
- if enclosure_url and enclosure_url.startswith('magnet'):
- return enclosure_url
- if link and link.startswith('magnet'):
- return link
- return None
- def get_attribute(item: etree.Element, property_name: str) -> str | None:
- """Get attribute from item."""
- property_element: etree.Element | None = item.find(property_name)
- if property_element is not None:
- return property_element.text
- return None
- def get_torznab_attribute(item: etree.Element, attribute_name: str) -> str | None:
- """Get torznab special attribute from item."""
- element: etree.Element | None = item.find(
- './/torznab:attr[@name="{attribute_name}"]'.format(attribute_name=attribute_name),
- {'torznab': 'http://torznab.com/schemas/2015/feed'},
- )
- if element is not None:
- return element.get("value")
- return None
|