from __future__ import annotations
import json
import time
import urllib
import urllib.parse
import xmlrpc.client
from collections.abc import Iterable
from typing import (
Any,
Iterator,
Literal,
Protocol,
TypeVar,
cast,
)
from urllib.parse import quote
from xmlrpc.client import dumps as xml_dumps
from xmlrpc.client import loads as xml_loads
import bencode2
# TypeAlias need 3.10, and deprecated since 3.12
# TypedDict need 3.11.
from typing_extensions import NotRequired, TypeAlias, TypedDict
from rtorrent_rpc._jsonrpc import JSONRpc, JSONRpcError
from rtorrent_rpc._transport import (
BadStatusError,
SCGIXmlTransport,
Transport,
_HTTPTransport,
_SCGITcpTransport,
_SCGIUnixTransport,
)
__all__ = [
"BadStatusError",
"JSONRpcError",
"MultiCall",
"RTorrent",
"RutorrentCompatibilityDisabledError",
]
T = TypeVar("T")
class RutorrentCompatibilityDisabledError(Exception): ...
Unknown: TypeAlias = Any
class MultiCall(TypedDict):
methodName: str
params: NotRequired[Any]
class _DirectoryRpc(Protocol):
def __call__(self, info_hash: str, /) -> str: ...
def set(self, info_hash: str, value: str, /) -> None: ...
class _CustomSet(Protocol):
def __call__(self, info_hash: str, key: str, /) -> str:
"""get download's custom value"""
def set(self, info_hash: str, key: str, value: str, /) -> int:
"""set download's custom value"""
[docs]
class _DownloadRpc(Protocol):
"""this is not a real class, it's a typing protocol for rpc typing"""
[docs]
def save_resume(self, info_hash: str) -> None:
"""save resume data"""
[docs]
def multicall2(self, _: Literal[""], view: str, *commands: str) -> Iterable[Any]:
"""run multiple rpc calls"""
@property
def directory_base(self) -> _DirectoryRpc:
"""base directory"""
@property
def directory(self) -> _DirectoryRpc:
"""directory"""
@property
def custom(self) -> _CustomSet: ...
[docs]
class _SystemRpc(Protocol):
"""this is not a real class, it's a typing protocol for rpc typing"""
[docs]
def multicall(self, commands: list[MultiCall]) -> Any:
"""run multiple rpc calls"""
[docs]
class _TrackerRpc(Protocol):
"""this is not a real class, it's a typing protocol for rpc typing"""
[docs]
def multicall(
self, info_hash: str, _: Literal[""], *commands: str
) -> Iterable[Any]:
"""run multiple rpc calls"""
[docs]
def is_enabled(self, tracker_id: str) -> int:
"""tracker_id is in format ``{info_hash}:t{index}``"""
class _FileRpc(Protocol):
"""this is not a real class, it's a typing protocol for rpc typing"""
def multicall(
self, info_hash: str, _: Literal[""], *commands: str
) -> Iterable[Any]:
"""run multiple rpc calls"""
[docs]
class RTorrent:
"""
RTorrent rpc client
.. code-block:: python
rt = RTorrent('scgi://127.0.0.1:5000')
rt = RTorrent('scgi:///home/ubuntu/.local/rtorrent.sock')
If you are using ruTorrent or nginx scgi proxy, http(s) protocol are also supported
.. code-block:: python
rt = RTorrent('http://127.0.0.1')
by default, ``/RPC2`` path is used.
"""
rpc: xmlrpc.client.ServerProxy
"""
Underling ``xmlrpc.client.ServerProxy`` are exposed as instance property ``.rpc``,
you can use ``rt.rpc`` for direct rpc call.
"""
jsonrpc: JSONRpc
"""
a :class:`JSONRpc` instance to send rpc request with json-rpc.
"""
_transport: Transport
"""
a low level internal :class:`Transport` can be used to send request directly.
"""
[docs]
def __init__(
self,
address: str,
rutorrent_compatibility: bool = True,
timeout: float | None = 5.0,
):
"""
Args:
address: rtorrent rpc address
rutorrent_compatibility: compatibility for ruTorrent or flood.
timeout: socket timeout. RTorrent may hang infinitely, without timeout.
"""
u = urllib.parse.urlparse(address)
if u.scheme == "scgi":
if u.hostname:
if not u.port:
raise ValueError("port is required for scgi protocol")
self._transport = _SCGITcpTransport(u.hostname, u.port, timeout=timeout)
else:
self._transport = _SCGIUnixTransport(u.path, timeout=timeout)
elif u.scheme in ("http", "https"):
self._transport = _HTTPTransport(address, timeout=timeout)
else:
raise ValueError(f"unsupported protocol {u.scheme}")
xml_transport = SCGIXmlTransport(transport=self._transport)
if u.scheme == "scgi":
# xmlrpc.client.ServerProxy doesn't like scgi protocol
self.rpc = _SCGIServerProxy(address, xml_transport)
else:
self.rpc = xmlrpc.client.ServerProxy(address, xml_transport)
self.jsonrpc = JSONRpc(self._transport)
self.rutorrent_compatibility: bool = rutorrent_compatibility
def __xml_call(self, method_name: str, params: Any = ()) -> Any:
req = xml_dumps(params=tuple(params), methodname=method_name)
res = self._transport.request(req.encode(), content_type="text/xml")
return xml_loads(res.decode())[0][0]
[docs]
def get_session_path(self) -> str:
"""get current rtorrent session path"""
return self.rpc.session.path() # type: ignore
def session_save(self) -> int:
return self.__xml_call("session.save")
[docs]
def add_torrent_by_file(
self,
content: bytes,
directory_base: str,
tags: Iterable[str] | None = None,
extras: Iterable[str] = (),
custom: dict[str, str] | None = None,
) -> None:
"""
Add a torrent to the client by providing the torrent file content as bytes.
Note:
rTorrent need some time to handle your torrent,
there is a chance info_hash of the torrent you just added is not inoperable.
In that case, you need wait some time after you just add a torrent.
Args:
content: The content of the torrent file as bytes.
directory_base: The base directory where the downloaded files will be saved.
tags: A sequence of tags associated with the torrent. Defaults to None.
This argument is compatible with ruTorrent and flood.
extras: extra commands to run for this download.
for example ``extras=["d.connection_seed.set=initial_seed"]``
custom: extra custom values
"""
params: list[str | bytes] = [
"",
content,
'd.tied_to_file.set=""',
f'd.directory_base.set="{directory_base}"',
*_real_iterator_of_str(extras),
]
extra_custom: dict[str, Any] = custom or {}
if self.rutorrent_compatibility:
# download add time
extra_custom["addtime"] = int(time.time())
if tags:
params.append(f'd.custom1.set="{_encode_tags(tags)}"')
t = bencode2.bdecode(content)
if b"comment" in t:
params.append(
f'd.custom2.set="VRS24mrker{quote(t[b"comment"].decode().strip())}"'
)
elif tags:
raise RutorrentCompatibilityDisabledError(
"need enable `rutorrent_compatibility` for tags support"
)
for key, value in extra_custom.items():
params.append(f"d.custom.set={key},{json.dumps(value)}")
self.rpc.load.raw_start_verbose(*params) # type: ignore
[docs]
def stop_torrent(self, info_hash: str) -> None:
"""
Stop and close a torrent.
Args:
info_hash (str): The info hash of the torrent to be stopped.
Returns:
None: This function does not return anything.
"""
self.system.multicall(
[
MultiCall(methodName="d.stop", params=[info_hash]),
MultiCall(methodName="d.close", params=[info_hash]),
]
)
def start_torrent(self, info_hash: str) -> None:
self.system.multicall(
[
MultiCall(methodName="d.open", params=[info_hash]),
MultiCall(methodName="d.start", params=[info_hash]),
]
)
[docs]
def enable_super_seeding(self, info_hash: str) -> Any:
"""enable bep 16 super seeding mode for a download"""
return self.system.multicall(
[
MultiCall(methodName="d.stop", params=[info_hash]),
MultiCall(methodName="d.close", params=[info_hash]),
MultiCall(
methodName="d.connection_seed.set",
params=[info_hash, "initial_seed"],
),
MultiCall(methodName="d.open", params=[info_hash]),
MultiCall(methodName="d.start", params=[info_hash]),
]
)
[docs]
def disable_super_seeding(self, info_hash: str) -> Any:
"""disable bep 16 super seeding mode for a download"""
return self.system.multicall(
[
MultiCall(methodName="d.stop", params=[info_hash]),
MultiCall(methodName="d.close", params=[info_hash]),
MultiCall(
methodName="d.connection_seed.set", params=[info_hash, "seed"]
),
MultiCall(methodName="d.open", params=[info_hash]),
MultiCall(methodName="d.start", params=[info_hash]),
]
)
[docs]
def download_list(self) -> list[str]:
"""get list of info hash for current downloads"""
return self.rpc.download_list() # type: ignore
@property
def system(self) -> _SystemRpc:
"""method call with ``system`` prefix
.. code-block:: python
rt.system.listMethods(...)
"""
return self.rpc.system # type: ignore
[docs]
def system_list_methods(self) -> list[str]:
"""get supported methods"""
return self.rpc.system.listMethods() # type: ignore
@property
def d(self) -> _DownloadRpc:
"""method call with ``d`` prefix
.. code-block:: python
rt.d.save_resume(...)
rt.d.open(...)
"""
return self.rpc.d # type: ignore
[docs]
def d_set_torrent_base_directory(self, info_hash: str, directory: str) -> None:
"""change base directory of a download.
you may need to stop/close torrent first.
"""
self.rpc.d.directory_base.set(info_hash, directory)
[docs]
def d_save_resume(self, info_hash: str) -> None:
"""alias of ``d.save_resume``"""
self.rpc.d.save_resume(info_hash)
[docs]
def d_set_custom(self, info_hash: str, key: str, value: str) -> int:
"""set custom key value pair on download"""
return self.d.custom.set(info_hash, key, value)
[docs]
def d_get_custom(self, info_hash: str, key: str) -> str:
"""get custom value by key, return empty str if key not set"""
return self.d.custom(info_hash, key)
[docs]
def d_tracker_send_scrape(self, info_hash: str, delay: Unknown) -> None:
"""force announce"""
self.rpc.d.tracker.send_scrape(info_hash, delay)
[docs]
def d_add_tracker(self, info_hash: str, url: str, *, group: int = 0) -> None:
"""add a tracker to download"""
self.rpc.d.tracker.insert(info_hash, group, url)
[docs]
def d_get_choke_group_index(self, info_hash: str) -> None:
"""get choke group for this download"""
return self.__xml_call("d.group", [info_hash])
[docs]
def d_get_choke_group_name(self, info_hash: str) -> None:
"""get choke group name for this download"""
return self.__xml_call("d.group.name", [info_hash])
[docs]
def d_set_choke_group(self, info_hash: str, group: str | int) -> None:
"""set choke group for this download"""
return self.__xml_call("d.group.set", [info_hash, str(group)])
@property
def t(self) -> _TrackerRpc:
"""method call with ``t`` prefix
.. code-block:: python
rt.t.is_enabled(...)
"""
return self.rpc.t # type: ignore
[docs]
def t_enable_tracker(self, info_hash: str, tracker_index: int) -> None:
"""enable a tracker of download"""
self.rpc.t.is_enabled.set(f"{info_hash}:t{tracker_index}", 1)
[docs]
def t_disable_tracker(self, info_hash: str, tracker_index: int) -> None:
"""disable a tracker of download"""
self.rpc.t.is_enabled.set(f"{info_hash}:t{tracker_index}", 0)
@property
def f(self) -> _FileRpc:
"""method call with ``d`` prefix
.. code-block:: python
rt.f.multicall("<info_hash>", "", "f.path=")
"""
return self.rpc.f # type: ignore
def list_choke_groups(self) -> list[str]:
return self.__xml_call("choke_group.list", [""])
def get_choke_group(self, name_or_index: str | int, /) -> int:
return self.__xml_call("choke_group.index_of", ["", str(name_or_index)])
def get_choke_group_size(self, name: str, /) -> int:
return self.__xml_call("choke_group.general.size", ["", name])
def get_choke_group_tracker_mode(self, name_or_index: str | int, /) -> str:
return self.__xml_call("choke_group.tracker.mode", ["", str(name_or_index)])
def set_choke_group_tracker_mode(
self, name_or_index: str | int, mode: str, /
) -> int:
return self.__xml_call(
"choke_group.tracker.mode.set", ["", str(name_or_index), mode]
)
[docs]
def set_choke_group_max_upload(
self, name_or_index: str | int, slots: int, /
) -> Unknown:
"""set max upload slots"""
return self.__xml_call(
"choke_group.up.max.set", ["", str(name_or_index), str(slots)]
)
[docs]
def set_choke_group_max_download(
self, name_or_index: str | int, slots: int, /
) -> Unknown:
"""set max upload slots."""
return self.__xml_call(
"choke_group.down.max.set", ["", str(name_or_index), str(slots)]
)
[docs]
def set_choke_group_update_heuristics(
self, name_or_index: str | int, mode: str, /
) -> Unknown:
"""you can get available modes from `call("strings.choke_heuristics.upload", [""])`"""
return self.__xml_call(
"choke_group.up.heuristics.set", ["", str(name_or_index), mode]
)
[docs]
def set_choke_group_download_heuristics(
self, name_or_index: str | int, mode: str, /
) -> Unknown:
"""you can get available modes from `call("strings.choke_heuristics.download", [""])`"""
return self.__xml_call(
"choke_group.download.heuristics.set", ["", str(name_or_index), mode]
)
def set_upload_speed_limit(self, name: str, speed: int, /) -> Unknown:
# Don't know why they define rpc method like this
return self.__xml_call("throttle.up", ["", [name, int(speed)]])
_methods = [
"system.methodExist",
"system.methodHelp",
"system.methodSignature",
"system.multicall",
"system.shutdown",
"system.capabilities",
"system.getCapabilities",
"add_peer",
"and",
"bind",
"build_branch",
"cat",
"catch",
"check_hash",
"choke_group.down.heuristics",
"choke_group.down.heuristics.set",
"choke_group.down.max",
"choke_group.down.max.set",
"choke_group.down.max.unlimited",
"choke_group.down.queued",
"choke_group.down.rate",
"choke_group.down.total",
"choke_group.down.unchoked",
"choke_group.general.size",
"choke_group.index_of",
"choke_group.insert",
"choke_group.list",
"choke_group.size",
"choke_group.tracker.mode",
"choke_group.tracker.mode.set",
"choke_group.up.heuristics",
"choke_group.up.heuristics.set",
"choke_group.up.max",
"choke_group.up.max.set",
"choke_group.up.max.unlimited",
"choke_group.up.queued",
"choke_group.up.rate",
"choke_group.up.total",
"choke_group.up.unchoked",
"close_low_diskspace",
"close_untied",
"compare",
"connection_leech",
"connection_seed",
"convert.date",
"convert.elapsed_time",
"convert.gm_date",
"convert.gm_time",
"convert.kb",
"convert.mb",
"convert.throttle",
"convert.time",
"convert.xb",
"d.accepting_seeders",
"d.accepting_seeders.disable",
"d.accepting_seeders.enable",
"d.base_filename",
"d.base_path",
"d.bitfield",
"d.bytes_done",
"d.check_hash",
"d.chunk_size",
"d.chunks_hashed",
"d.chunks_seen",
"d.close",
"d.close.directly",
"d.complete",
"d.completed_bytes",
"d.completed_chunks",
"d.connection_current",
"d.connection_current.set",
"d.connection_leech",
"d.connection_leech.set",
"d.connection_seed",
"d.connection_seed.set",
"d.create_link",
"d.creation_date",
"d.custom",
"d.custom.if_z",
"d.custom.items",
"d.custom.keys",
"d.custom.set",
"d.custom1",
"d.custom1.set",
"d.custom2",
"d.custom2.set",
"d.custom3",
"d.custom3.set",
"d.custom4",
"d.custom4.set",
"d.custom5",
"d.custom5.set",
"d.custom_throw",
"d.delete_link",
"d.delete_tied",
"d.directory",
"d.directory.set",
"d.directory_base",
"d.directory_base.set",
"d.disconnect.seeders",
"d.down.choke_heuristics",
"d.down.choke_heuristics.leech",
"d.down.choke_heuristics.seed",
"d.down.choke_heuristics.set",
"d.down.rate",
"d.down.sequential",
"d.down.sequential.set",
"d.down.total",
"d.downloads_max",
"d.downloads_max.set",
"d.downloads_min",
"d.downloads_min.set",
"d.erase",
"d.free_diskspace",
"d.group",
"d.group.name",
"d.group.set",
"d.hash",
"d.hashing",
"d.hashing_failed",
"d.hashing_failed.set",
"d.ignore_commands",
"d.ignore_commands.set",
"d.incomplete",
"d.is_active",
"d.is_hash_checked",
"d.is_hash_checking",
"d.is_meta",
"d.is_multi_file",
"d.is_not_partially_done",
"d.is_open",
"d.is_partially_done",
"d.is_pex_active",
"d.is_private",
"d.left_bytes",
"d.load_date",
"d.loaded_file",
"d.local_id",
"d.local_id_html",
"d.max_file_size",
"d.max_file_size.set",
"d.max_size_pex",
"d.message",
"d.message.set",
"d.mode",
"d.multicall.filtered",
"d.multicall2",
"d.name",
"d.open",
"d.pause",
"d.peer_exchange",
"d.peer_exchange.set",
"d.peers_accounted",
"d.peers_complete",
"d.peers_connected",
"d.peers_max",
"d.peers_max.set",
"d.peers_min",
"d.peers_min.set",
"d.peers_not_connected",
"d.priority",
"d.priority.set",
"d.priority_str",
"d.ratio",
"d.resume",
"d.save_full_session",
"d.save_resume",
"d.size_bytes",
"d.size_chunks",
"d.size_files",
"d.size_pex",
"d.skip.rate",
"d.skip.total",
"d.start",
"d.state",
"d.state_changed",
"d.state_counter",
"d.stop",
"d.throttle_name",
"d.throttle_name.set",
"d.tied_to_file",
"d.tied_to_file.set",
"d.timestamp.finished",
"d.timestamp.last_active",
"d.timestamp.started",
"d.tracker.insert",
"d.tracker.send_scrape",
"d.tracker_announce",
"d.tracker_announce.force",
"d.tracker_focus",
"d.tracker_numwant",
"d.tracker_numwant.set",
"d.tracker_size",
"d.try_close",
"d.try_start",
"d.try_stop",
"d.up.choke_heuristics",
"d.up.choke_heuristics.leech",
"d.up.choke_heuristics.seed",
"d.up.choke_heuristics.set",
"d.up.rate",
"d.up.total",
"d.update_priorities",
"d.uploads_max",
"d.uploads_max.set",
"d.uploads_min",
"d.uploads_min.set",
"d.views",
"d.views.has",
"d.views.push_back",
"d.views.push_back_unique",
"d.views.remove",
"d.wanted_chunks",
"dht",
"dht.add_bootstrap",
"dht.add_node",
"dht.mode.set",
"dht.port",
"dht.port.set",
"dht.statistics",
"dht.throttle.name",
"dht.throttle.name.set",
"dht_port",
"directory",
"directory.default",
"directory.default.set",
"directory.watch.added",
"download_list",
"download_rate",
"elapsed.greater",
"elapsed.less",
"encoding.add",
"encoding_list",
"encryption",
"equal",
"event.download.active",
"event.download.closed",
"event.download.erased",
"event.download.finished",
"event.download.hash_done",
"event.download.hash_failed",
"event.download.hash_final_failed",
"event.download.hash_queued",
"event.download.hash_removed",
"event.download.inactive",
"event.download.inserted",
"event.download.inserted_new",
"event.download.inserted_session",
"event.download.opened",
"event.download.paused",
"event.download.resumed",
"event.system.shutdown",
"event.system.startup_done",
"event.view.hide",
"event.view.show",
"execute",
"execute.capture",
"execute.capture_nothrow",
"execute.nothrow",
"execute.nothrow.bg",
"execute.raw",
"execute.raw.bg",
"execute.raw_nothrow",
"execute.raw_nothrow.bg",
"execute.throw",
"execute.throw.bg",
"execute2",
"d.completed_chunks",
"d.frozen_path",
"d.is_create_queued",
"d.is_created",
"d.is_open",
"d.is_resize_queued",
"d.last_touched",
"d.match_depth_next",
"d.match_depth_prev",
"d.multicall",
"d.offset",
"d.path",
"d.path_components",
"d.path_depth",
"d.prioritize_first",
"d.prioritize_first.disable",
"d.prioritize_first.enable",
"d.prioritize_last",
"d.prioritize_last.disable",
"d.prioritize_last.enable",
"d.priority",
"d.priority.set",
"d.range_first",
"d.range_second",
"d.set_create_queued",
"d.set_resize_queued",
"d.size_bytes",
"d.size_chunks",
"d.unset_create_queued",
"d.unset_resize_queued",
"false",
"fi.filename_last",
"fi.is_file",
"file.append",
"file.prioritize_toc",
"file.prioritize_toc.first",
"file.prioritize_toc.first.push_back",
"file.prioritize_toc.first.set",
"file.prioritize_toc.last",
"file.prioritize_toc.last.push_back",
"file.prioritize_toc.last.set",
"file.prioritize_toc.set",
"fs.homedir",
"fs.homedir.nothrow",
"fs.mkdir",
"fs.mkdir.nothrow",
"fs.mkdir.recursive",
"fs.mkdir.recursive.nothrow",
"greater",
"group.insert",
"group.insert_persistent_view",
"group.seeding.ratio.command",
"group.seeding.ratio.disable",
"group.seeding.ratio.enable",
"group2.seeding.ratio.max",
"group2.seeding.ratio.max.set",
"group2.seeding.ratio.min",
"group2.seeding.ratio.min.set",
"group2.seeding.ratio.upload",
"group2.seeding.ratio.upload.set",
"group2.seeding.view",
"group2.seeding.view.set",
"if",
"import",
"ip",
"key_layout",
"keys.layout",
"keys.layout.set",
"less",
"load.normal",
"load.raw",
"load.raw_start",
"load.raw_start_verbose",
"load.raw_verbose",
"load.start",
"load.start_throw",
"load.start_verbose",
"load.throw",
"load.verbose",
"log.add_output",
"log.append_file",
"log.append_gz_file",
"log.close",
"log.execute",
"log.open_file",
"log.open_file_pid",
"log.open_gz_file",
"log.open_gz_file_pid",
"log.rpc",
"log.vmmap.dump",
"match",
"math.add",
"math.avg",
"math.cnt",
"math.div",
"math.max",
"math.med",
"math.min",
"math.mod",
"math.mul",
"math.sub",
"max_downloads",
"max_downloads_div",
"max_downloads_global",
"max_memory_usage",
"max_peers",
"max_peers_seed",
"max_uploads",
"max_uploads_div",
"max_uploads_global",
"method.const",
"method.const.enable",
"method.erase",
"method.get",
"method.has_key",
"method.insert",
"method.insert.bool",
"method.insert.c_simple",
"method.insert.list",
"method.insert.s_c_simple",
"method.insert.simple",
"method.insert.string",
"method.insert.value",
"method.list_keys",
"method.redirect",
"method.rlookup",
"method.rlookup.clear",
"method.set",
"method.set_key",
"method.use_deprecated",
"method.use_deprecated.set",
"method.use_intermediate",
"method.use_intermediate.set",
"min_downloads",
"min_peers",
"min_peers_seed",
"min_uploads",
"network.bind_address",
"network.bind_address.set",
"network.http.cacert",
"network.http.cacert.set",
"network.http.capath",
"network.http.capath.set",
"network.http.current_open",
"network.http.dns_cache_timeout",
"network.http.dns_cache_timeout.set",
"network.http.max_open",
"network.http.max_open.set",
"network.http.proxy_address",
"network.http.proxy_address.set",
"network.http.ssl_verify_host",
"network.http.ssl_verify_host.set",
"network.http.ssl_verify_peer",
"network.http.ssl_verify_peer.set",
"network.listen.backlog",
"network.listen.backlog.set",
"network.listen.port",
"network.local_address",
"network.local_address.set",
"network.max_open_files",
"network.max_open_files.set",
"network.max_open_sockets",
"network.max_open_sockets.set",
"network.open_files",
"network.open_sockets",
"network.port_open",
"network.port_open.set",
"network.port_random",
"network.port_random.set",
"network.port_range",
"network.port_range.set",
"network.proxy_address",
"network.proxy_address.set",
"network.receive_buffer.size",
"network.receive_buffer.size.set",
"network.scgi.dont_route",
"network.scgi.dont_route.set",
"network.scgi.open_local",
"network.scgi.open_port",
"network.send_buffer.size",
"network.send_buffer.size.set",
"network.tos.set",
"network.total_handshakes",
"network.xmlrpc.dialect.set",
"network.xmlrpc.size_limit",
"network.xmlrpc.size_limit.set",
"not",
"on_ratio",
"or",
"p.address",
"p.banned",
"p.banned.set",
"p.call_target",
"p.client_version",
"p.completed_percent",
"p.disconnect",
"p.disconnect_delayed",
"p.down_rate",
"p.down_total",
"p.id",
"p.id_html",
"p.is_encrypted",
"p.is_incoming",
"p.is_obfuscated",
"p.is_preferred",
"p.is_snubbed",
"p.is_unwanted",
"p.multicall",
"p.options_str",
"p.peer_rate",
"p.peer_total",
"p.port",
"p.snubbed",
"p.snubbed.set",
"p.up_rate",
"p.up_total",
"pieces.hash.on_completion",
"pieces.hash.on_completion.set",
"pieces.hash.queue_size",
"pieces.memory.block_count",
"pieces.memory.current",
"pieces.memory.max",
"pieces.memory.max.set",
"pieces.memory.sync_queue",
"pieces.preload.min_rate",
"pieces.preload.min_rate.set",
"pieces.preload.min_size",
"pieces.preload.min_size.set",
"pieces.preload.type",
"pieces.preload.type.set",
"pieces.stats.total_size",
"pieces.stats_not_preloaded",
"pieces.stats_preloaded",
"pieces.sync.always_safe",
"pieces.sync.always_safe.set",
"pieces.sync.queue_size",
"pieces.sync.safe_free_diskspace",
"pieces.sync.timeout",
"pieces.sync.timeout.set",
"pieces.sync.timeout_safe",
"pieces.sync.timeout_safe.set",
"port_random",
"port_range",
"print",
"protocol.choke_heuristics.down.leech",
"protocol.choke_heuristics.down.leech.set",
"protocol.choke_heuristics.down.seed",
"protocol.choke_heuristics.down.seed.set",
"protocol.choke_heuristics.up.leech",
"protocol.choke_heuristics.up.leech.set",
"protocol.choke_heuristics.up.seed",
"protocol.choke_heuristics.up.seed.set",
"protocol.connection.leech",
"protocol.connection.leech.set",
"protocol.connection.seed",
"protocol.connection.seed.set",
"protocol.encryption.set",
"protocol.pex",
"protocol.pex.set",
"proxy_address",
"ratio.disable",
"ratio.enable",
"ratio.max",
"ratio.max.set",
"ratio.min",
"ratio.min.set",
"ratio.upload",
"ratio.upload.set",
"remove_untied",
"scgi_local",
"scgi_port",
"schedule",
"schedule2",
"schedule_remove",
"schedule_remove2",
"scheduler.max_active",
"scheduler.max_active.set",
"scheduler.simple.added",
"scheduler.simple.removed",
"scheduler.simple.update",
"session",
"session.name",
"session.name.set",
"session.on_completion",
"session.on_completion.set",
"session.path",
"session.path.set",
"session.save",
"session.use_lock",
"session.use_lock.set",
"start_tied",
"stop_untied",
"strings.choke_heuristics",
"strings.choke_heuristics.download",
"strings.choke_heuristics.upload",
"strings.connection_type",
"strings.encryption",
"strings.ip_filter",
"strings.ip_tos",
"strings.log_group",
"strings.tracker_event",
"strings.tracker_mode",
"system.api_version",
"system.client_version",
"system.cwd",
"system.cwd.set",
"system.daemon",
"system.daemon.set",
"system.env",
"system.file.allocate",
"system.file.allocate.set",
"system.file.max_size",
"system.file.max_size.set",
"system.file.split_size",
"system.file.split_size.set",
"system.file.split_suffix",
"system.file.split_suffix.set",
"system.file_status_cache.prune",
"system.file_status_cache.size",
"system.files.closed_counter",
"system.files.failed_counter",
"system.files.opened_counter",
"system.hostname",
"system.library_version",
"system.pid",
"system.shutdown.normal",
"system.shutdown.quick",
"system.time",
"system.time_seconds",
"system.time_usec",
"system.umask.set",
"t.activity_time_last",
"t.activity_time_next",
"t.can_scrape",
"t.disable",
"t.enable",
"t.failed_counter",
"t.failed_time_last",
"t.failed_time_next",
"t.group",
"t.id",
"t.is_busy",
"t.is_enabled",
"t.is_enabled.set",
"t.is_extra_tracker",
"t.is_open",
"t.is_usable",
"t.latest_event",
"t.latest_new_peers",
"t.latest_sum_peers",
"t.min_interval",
"t.multicall",
"t.normal_interval",
"t.scrape_complete",
"t.scrape_counter",
"t.scrape_downloaded",
"t.scrape_incomplete",
"t.scrape_time_last",
"t.success_counter",
"t.success_time_last",
"t.success_time_next",
"t.type",
"t.url",
"throttle.down",
"throttle.down.max",
"throttle.down.rate",
"throttle.global_down.max_rate",
"throttle.global_down.max_rate.set",
"throttle.global_down.max_rate.set_kb",
"throttle.global_down.rate",
"throttle.global_down.total",
"throttle.global_up.max_rate",
"throttle.global_up.max_rate.set",
"throttle.global_up.max_rate.set_kb",
"throttle.global_up.rate",
"throttle.global_up.total",
"throttle.ip",
"throttle.max_downloads",
"throttle.max_downloads.div",
"throttle.max_downloads.div._val",
"throttle.max_downloads.div._val.set",
"throttle.max_downloads.div.set",
"throttle.max_downloads.global",
"throttle.max_downloads.global._val",
"throttle.max_downloads.global._val.set",
"throttle.max_downloads.global.set",
"throttle.max_downloads.set",
"throttle.max_peers.normal",
"throttle.max_peers.normal.set",
"throttle.max_peers.seed",
"throttle.max_peers.seed.set",
"throttle.max_unchoked_downloads",
"throttle.max_unchoked_uploads",
"throttle.max_uploads",
"throttle.max_uploads.div",
"throttle.max_uploads.div._val",
"throttle.max_uploads.div._val.set",
"throttle.max_uploads.div.set",
"throttle.max_uploads.global",
"throttle.max_uploads.global._val",
"throttle.max_uploads.global._val.set",
"throttle.max_uploads.global.set",
"throttle.max_uploads.set",
"throttle.min_downloads",
"throttle.min_downloads.set",
"throttle.min_peers.normal",
"throttle.min_peers.normal.set",
"throttle.min_peers.seed",
"throttle.min_peers.seed.set",
"throttle.min_uploads",
"throttle.min_uploads.set",
"throttle.unchoked_downloads",
"throttle.unchoked_uploads",
"throttle.up",
"throttle.up.max",
"throttle.up.rate",
"to_date",
"to_elapsed_time",
"to_gm_date",
"to_gm_time",
"to_kb",
"to_mb",
"to_throttle",
"to_time",
"to_xb",
"torrent_list_layout",
"trackers.disable",
"trackers.enable",
"trackers.numwant",
"trackers.numwant.set",
"trackers.use_udp",
"trackers.use_udp.set",
"true",
"try",
"try_import",
"ui.current_view",
"ui.current_view.set",
"ui.input.history.clear",
"ui.input.history.size",
"ui.input.history.size.set",
"ui.status.throttle.down.set",
"ui.status.throttle.up.set",
"ui.throttle.global.step.large",
"ui.throttle.global.step.large.set",
"ui.throttle.global.step.medium",
"ui.throttle.global.step.medium.set",
"ui.throttle.global.step.small",
"ui.throttle.global.step.small.set",
"ui.torrent_list.layout",
"ui.torrent_list.layout.set",
"ui.unfocus_download",
"upload_rate",
"value",
"view.add",
"view.event_added",
"view.event_removed",
"view.filter",
"view.filter.temp",
"view.filter.temp.excluded",
"view.filter.temp.excluded.set",
"view.filter.temp.log",
"view.filter.temp.log.set",
"view.filter_all",
"view.filter_download",
"view.filter_on",
"view.list",
"view.persistent",
"view.set",
"view.set_not_visible",
"view.set_visible",
"view.size",
"view.size_not_visible",
"view.sort",
"view.sort_current",
"view.sort_new",
"system.startup_time",
"d.data_path",
"d.session_file",
"cfg.scrape_interval.active",
"cfg.scrape_interval.active.set",
"cfg.scrape_interval.idle",
"cfg.scrape_interval.idle.set",
"d.last_scrape.send_set",
]
class _SCGIServerProxy(xmlrpc.client.ServerProxy):
def __init__(
self,
uri: str,
transport: xmlrpc.client.Transport | None = None,
**kwargs: Any,
):
u = urllib.parse.urlparse(uri)
if u.scheme != "scgi":
raise OSError("SCGIServerProxy Only Support XML-RPC over SCGI protocol")
# Feed some junk in here, but we'll fix it afterwards
super().__init__(
cast("str", urllib.parse.urlunparse(u._replace(scheme="http"))),
transport=transport,
**kwargs,
)
def _real_iterator_of_str(s: Iterable[str]) -> Iterator[str]:
if isinstance(s, str):
yield s
else:
yield from s
def _encode_tags(tags: Iterable[str] | None) -> str:
if not tags:
return ""
if isinstance(tags, str):
return quote(tags.strip())
return ",".join(quote(t) for t in sorted({x.strip() for x in tags}) if t)