2"""Torznab_ is an API specification that provides a standardized way to query 
    3torrent site for content. It is used by a number of torrent applications, 
    4including Prowlarr_ and Jackett_. 
    6Using this engine together with Prowlarr_ or Jackett_ allows you to search 
    7a huge number of torrent sites which are not directly supported. 
   12The engine has the following settings: 
   18  The API key to use for authentication. 
   20``torznab_categories``: 
   21  The categories to use for searching. This is a list of category IDs.  See 
   22  Prowlarr-categories_ or Jackett-categories_ for more information. 
   24``show_torrent_files``: 
   25  Whether to show the torrent file in the search results.  Be careful as using 
   26  this with Prowlarr_ or Jackett_ leaks the API key.  This should be used only 
   27  if you are querying a Torznab endpoint without authentication or if the 
   28  instance is private.  Be aware that private trackers may ban you if you share 
   29  the torrent file.  Defaults to ``false``. 
   32  Whether to show the magnet link in the search results.  Be aware that private 
   33  trackers may ban you if you share the magnet link.  Defaults to ``true``. 
   36   https://torznab.github.io/spec-1.3-draft/index.html 
   38   https://github.com/Prowlarr/Prowlarr 
   40   https://github.com/Jackett/Jackett 
   41.. _Prowlarr-categories: 
   42   https://wiki.servarr.com/en/prowlarr/cardigann-yml-definition#categories 
   43.. _Jackett-categories: 
   44   https://github.com/Jackett/Jackett/wiki/Jackett-Categories 
   52from datetime 
import datetime
 
   53from urllib.parse 
import quote
 
   64about: dict[str, t.Any] = {
 
   67    "official_api_documentation": 
"https://torznab.github.io/spec-1.3-draft",
 
   68    "use_official_api": 
True,
 
   69    "require_api_key": 
False,
 
   72categories: list[str] = [
'files']
 
   74time_range_support: bool = 
False 
   81torznab_categories: list[str] = []
 
   82show_torrent_files: bool = 
False 
   83show_magnet_links: bool = 
True 
   86def init(engine_settings=None):  
 
   87    """Initialize the engine.""" 
   89        raise ValueError(
'missing torznab base_url')
 
 
   92def request(query: str, params: dict[str, t.Any]) -> dict[str, t.Any]:
 
   93    """Build the request params.""" 
   94    search_url: str = base_url + 
'?t=search&q={search_query}' 
   97        search_url += 
'&apikey={api_key}' 
   98    if len(torznab_categories) > 0:
 
   99        search_url += 
'&cat={torznab_categories}' 
  101    params[
'url'] = search_url.format(
 
  102        search_query=quote(query), api_key=api_key, torznab_categories=
",".join([str(x) 
for x 
in torznab_categories])
 
 
  108def response(resp: 
"SXNG_Response") -> list[dict[str, t.Any]]:
 
  109    """Parse the XML response and return a list of results.""" 
  111    search_results = etree.XML(resp.content)
 
  114    if search_results.tag == 
"error":
 
  117    channel: etree.Element = search_results[0]
 
  120    for item 
in channel.iterfind(
'item'):
 
  122        results.append(result)
 
 
  128    """Build a result from a XML item.""" 
  132    enclosure: etree.Element | 
None = item.find(
'enclosure')
 
  133    enclosure_url: str | 
None = 
None 
  134    if enclosure 
is not None:
 
  135        enclosure_url = enclosure.get(
'url')
 
  138    if not filesize 
and enclosure:
 
  139        filesize = enclosure.get(
'length')
 
  149    result: dict[str, t.Any] = {
 
  150        'template': 
'torrent.html',
 
  152        'filesize': humanize_bytes(int(filesize)) 
if filesize 
else None,
 
  163    if show_torrent_files:
 
  165    if show_magnet_links:
 
  167        result[
'magnetlink'] = 
_map_magnet_link(magneturl, guid, enclosure_url, link)
 
 
  172    if guid 
and guid.startswith(
'http'):
 
  174    if comments 
and comments.startswith(
'http'):
 
 
  179def _map_leechers(leechers: str | 
None, seeders: str | 
None, peers: str | 
None) -> str | 
None:
 
  182    if seeders 
and peers:
 
  183        return str(int(peers) - int(seeders))
 
 
  188    if pubDate 
is not None:
 
  190            return datetime.strptime(pubDate, 
'%a, %d %b %Y %H:%M:%S %z')
 
  191        except (ValueError, TypeError) 
as e:
 
  192            logger.debug(
"ignore exception (publishedDate): %s", e)
 
 
  197    if link 
and link.startswith(
'http'):
 
  199    if enclosure_url 
and enclosure_url.startswith(
'http'):
 
 
  205    magneturl: str | 
None,
 
  207    enclosure_url: str | 
None,
 
  210    if magneturl 
and magneturl.startswith(
'magnet'):
 
  212    if guid 
and guid.startswith(
'magnet'):
 
  214    if enclosure_url 
and enclosure_url.startswith(
'magnet'):
 
  216    if link 
and link.startswith(
'magnet'):
 
 
  222    """Get attribute from item.""" 
  223    property_element: etree.Element | 
None = item.find(property_name)
 
  224    if property_element 
is not None:
 
  225        return property_element.text
 
 
  230    """Get torznab special attribute from item.""" 
  231    element: etree.Element | 
None = item.find(
 
  232        './/torznab:attr[@name="{attribute_name}"]'.format(attribute_name=attribute_name),
 
  233        {
'torznab': 
'http://torznab.com/schemas/2015/feed'},
 
  235    if element 
is not None:
 
  236        return element.get(
"value")
 
 
datetime|None _map_published_date(str|None pubDate)
 
str|None _map_result_url(str|None guid, str|None comments)
 
str|None _map_torrent_file(str|None link, str|None enclosure_url)
 
str|None get_torznab_attribute(etree.Element item, str attribute_name)
 
str|None _map_leechers(str|None leechers, str|None seeders, str|None peers)
 
init(engine_settings=None)
 
list[dict[str, t.Any]] response("SXNG_Response" resp)
 
dict[str, t.Any] build_result(etree.Element item)
 
str|None _map_magnet_link(str|None magneturl, str|None guid, str|None enclosure_url, str|None link)
 
str|None get_attribute(etree.Element item, str property_name)
 
dict[str, t.Any] request(str query, dict[str, t.Any] params)