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
50from __future__
import annotations
51from typing
import TYPE_CHECKING
53from typing
import List, Dict, Any
54from datetime
import datetime
55from urllib.parse
import quote
65 logger: logging.Logger
68about: Dict[str, Any] = {
71 "official_api_documentation":
"https://torznab.github.io/spec-1.3-draft",
72 "use_official_api":
True,
73 "require_api_key":
False,
76categories: List[str] = [
'files']
78time_range_support: bool =
False
85torznab_categories: List[str] = []
86show_torrent_files: bool =
False
87show_magnet_links: bool =
True
90def init(engine_settings=None):
91 """Initialize the engine."""
93 raise ValueError(
'missing torznab base_url')
96def request(query: str, params: Dict[str, Any]) -> Dict[str, Any]:
97 """Build the request params."""
98 search_url: str = base_url +
'?t=search&q={search_query}'
101 search_url +=
'&apikey={api_key}'
102 if len(torznab_categories) > 0:
103 search_url +=
'&cat={torznab_categories}'
105 params[
'url'] = search_url.format(
106 search_query=quote(query), api_key=api_key, torznab_categories=
",".join([str(x)
for x
in torznab_categories])
112def response(resp: httpx.Response) -> List[Dict[str, Any]]:
113 """Parse the XML response and return a list of results."""
115 search_results = etree.XML(resp.content)
118 if search_results.tag ==
"error":
121 channel: etree.Element = search_results[0]
124 for item
in channel.iterfind(
'item'):
126 results.append(result)
132 """Build a result from a XML item."""
136 enclosure: etree.Element |
None = item.find(
'enclosure')
137 enclosure_url: str |
None =
None
138 if enclosure
is not None:
139 enclosure_url = enclosure.get(
'url')
142 if not filesize
and enclosure:
143 filesize = enclosure.get(
'length')
153 result: Dict[str, Any] = {
154 'template':
'torrent.html',
156 'filesize': humanize_bytes(int(filesize))
if filesize
else None,
167 if show_torrent_files:
169 if show_magnet_links:
171 result[
'magnetlink'] =
_map_magnet_link(magneturl, guid, enclosure_url, link)
176 if guid
and guid.startswith(
'http'):
178 if comments
and comments.startswith(
'http'):
183def _map_leechers(leechers: str |
None, seeders: str |
None, peers: str |
None) -> str |
None:
186 if seeders
and peers:
187 return str(int(peers) - int(seeders))
192 if pubDate
is not None:
194 return datetime.strptime(pubDate,
'%a, %d %b %Y %H:%M:%S %z')
195 except (ValueError, TypeError)
as e:
196 logger.debug(
"ignore exception (publishedDate): %s", e)
201 if link
and link.startswith(
'http'):
203 if enclosure_url
and enclosure_url.startswith(
'http'):
209 magneturl: str |
None,
211 enclosure_url: str |
None,
214 if magneturl
and magneturl.startswith(
'magnet'):
216 if guid
and guid.startswith(
'magnet'):
218 if enclosure_url
and enclosure_url.startswith(
'magnet'):
220 if link
and link.startswith(
'magnet'):
226 """Get attribute from item."""
227 property_element: etree.Element |
None = item.find(property_name)
228 if property_element
is not None:
229 return property_element.text
234 """Get torznab special attribute from item."""
235 element: etree.Element |
None = item.find(
236 './/torznab:attr[@name="{attribute_name}"]'.
format(attribute_name=attribute_name),
237 {
'torznab':
'http://torznab.com/schemas/2015/feed'},
239 if element
is not None:
240 return element.get(
"value")
datetime|None _map_published_date(str|None pubDate)
str|None _map_result_url(str|None guid, str|None comments)
List[Dict[str, Any]] response(httpx.Response resp)
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)
str|None _map_magnet_link(str|None magneturl, str|None guid, str|None enclosure_url, str|None link)
Dict[str, Any] request(str query, Dict[str, Any] params)
str|None get_attribute(etree.Element item, str property_name)
Dict[str, Any] build_result(etree.Element item)