deluge-web-client 0.1.0a2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of deluge-web-client might be problematic. Click here for more details.
- deluge_web_client-0.1.0a2/PKG-INFO +86 -0
- deluge_web_client-0.1.0a2/README.md +70 -0
- deluge_web_client-0.1.0a2/deluge_web_client/__init__.py +3 -0
- deluge_web_client-0.1.0a2/deluge_web_client/client.py +554 -0
- deluge_web_client-0.1.0a2/deluge_web_client/exceptions.py +2 -0
- deluge_web_client-0.1.0a2/deluge_web_client/response.py +10 -0
- deluge_web_client-0.1.0a2/pyproject.toml +22 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: deluge-web-client
|
|
3
|
+
Version: 0.1.0a2
|
|
4
|
+
Summary: Deluge Web Client
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: jlw_4049
|
|
7
|
+
Author-email: jessielw4049@gmail.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
<div align="center">
|
|
17
|
+
|
|
18
|
+
Deluge Web API Client
|
|
19
|
+
==========================
|
|
20
|
+
|
|
21
|
+
Python client implementation for Deluge Web API
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
Currently supports qBittorrent [v2.1.1](https://deluge-torrent.org).
|
|
27
|
+
|
|
28
|
+
User Guide and API Reference available on [Read the Docs](https://deluge-web-client.readthedocs.io).
|
|
29
|
+
|
|
30
|
+
Features
|
|
31
|
+
------------
|
|
32
|
+
* Access to most of the Web API as well as most of the **core** methods (accessed via RPC). [Web API](https://deluge.readthedocs.io/en/deluge-2.0.1/reference/webapi.html) / [RPC API](https://deluge.readthedocs.io/en/deluge-2.0.1/reference/api.html)
|
|
33
|
+
* Allows you to use direct **http** connections, allowing access via **reverse proxy** or any **direct url**.
|
|
34
|
+
|
|
35
|
+
Installation
|
|
36
|
+
------------
|
|
37
|
+
Install via pip from [PyPI](https://pypi.org/project/qbittorrent-api/)
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install qbittorrent-api
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Getting Started
|
|
43
|
+
---------------
|
|
44
|
+
```python
|
|
45
|
+
import qbittorrentapi
|
|
46
|
+
|
|
47
|
+
# instantiate a Client using the appropriate WebUI configuration
|
|
48
|
+
conn_info = dict(
|
|
49
|
+
host="localhost",
|
|
50
|
+
port=8080,
|
|
51
|
+
username="admin",
|
|
52
|
+
password="adminadmin",
|
|
53
|
+
)
|
|
54
|
+
qbt_client = qbittorrentapi.Client(**conn_info)
|
|
55
|
+
|
|
56
|
+
# the Client will automatically acquire/maintain a logged-in state
|
|
57
|
+
# in line with any request. therefore, this is not strictly necessary;
|
|
58
|
+
# however, you may want to test the provided login credentials.
|
|
59
|
+
try:
|
|
60
|
+
qbt_client.auth_log_in()
|
|
61
|
+
except qbittorrentapi.LoginFailed as e:
|
|
62
|
+
print(e)
|
|
63
|
+
|
|
64
|
+
# if the Client will not be long-lived or many Clients may be created
|
|
65
|
+
# in a relatively short amount of time, be sure to log out:
|
|
66
|
+
qbt_client.auth_log_out()
|
|
67
|
+
|
|
68
|
+
# or use a context manager:
|
|
69
|
+
with qbittorrentapi.Client(**conn_info) as qbt_client:
|
|
70
|
+
if qbt_client.torrents_add(urls="...") != "Ok.":
|
|
71
|
+
raise Exception("Failed to add torrent.")
|
|
72
|
+
|
|
73
|
+
# display qBittorrent info
|
|
74
|
+
print(f"qBittorrent: {qbt_client.app.version}")
|
|
75
|
+
print(f"qBittorrent Web API: {qbt_client.app.web_api_version}")
|
|
76
|
+
for k, v in qbt_client.app.build_info.items():
|
|
77
|
+
print(f"{k}: {v}")
|
|
78
|
+
|
|
79
|
+
# retrieve and show all torrents
|
|
80
|
+
for torrent in qbt_client.torrents_info():
|
|
81
|
+
print(f"{torrent.hash[-6:]}: {torrent.name} ({torrent.state})")
|
|
82
|
+
|
|
83
|
+
# stop all torrents
|
|
84
|
+
qbt_client.torrents.stop.all()
|
|
85
|
+
```
|
|
86
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
Deluge Web API Client
|
|
4
|
+
==========================
|
|
5
|
+
|
|
6
|
+
Python client implementation for Deluge Web API
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
Currently supports qBittorrent [v2.1.1](https://deluge-torrent.org).
|
|
12
|
+
|
|
13
|
+
User Guide and API Reference available on [Read the Docs](https://deluge-web-client.readthedocs.io).
|
|
14
|
+
|
|
15
|
+
Features
|
|
16
|
+
------------
|
|
17
|
+
* Access to most of the Web API as well as most of the **core** methods (accessed via RPC). [Web API](https://deluge.readthedocs.io/en/deluge-2.0.1/reference/webapi.html) / [RPC API](https://deluge.readthedocs.io/en/deluge-2.0.1/reference/api.html)
|
|
18
|
+
* Allows you to use direct **http** connections, allowing access via **reverse proxy** or any **direct url**.
|
|
19
|
+
|
|
20
|
+
Installation
|
|
21
|
+
------------
|
|
22
|
+
Install via pip from [PyPI](https://pypi.org/project/qbittorrent-api/)
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install qbittorrent-api
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Getting Started
|
|
28
|
+
---------------
|
|
29
|
+
```python
|
|
30
|
+
import qbittorrentapi
|
|
31
|
+
|
|
32
|
+
# instantiate a Client using the appropriate WebUI configuration
|
|
33
|
+
conn_info = dict(
|
|
34
|
+
host="localhost",
|
|
35
|
+
port=8080,
|
|
36
|
+
username="admin",
|
|
37
|
+
password="adminadmin",
|
|
38
|
+
)
|
|
39
|
+
qbt_client = qbittorrentapi.Client(**conn_info)
|
|
40
|
+
|
|
41
|
+
# the Client will automatically acquire/maintain a logged-in state
|
|
42
|
+
# in line with any request. therefore, this is not strictly necessary;
|
|
43
|
+
# however, you may want to test the provided login credentials.
|
|
44
|
+
try:
|
|
45
|
+
qbt_client.auth_log_in()
|
|
46
|
+
except qbittorrentapi.LoginFailed as e:
|
|
47
|
+
print(e)
|
|
48
|
+
|
|
49
|
+
# if the Client will not be long-lived or many Clients may be created
|
|
50
|
+
# in a relatively short amount of time, be sure to log out:
|
|
51
|
+
qbt_client.auth_log_out()
|
|
52
|
+
|
|
53
|
+
# or use a context manager:
|
|
54
|
+
with qbittorrentapi.Client(**conn_info) as qbt_client:
|
|
55
|
+
if qbt_client.torrents_add(urls="...") != "Ok.":
|
|
56
|
+
raise Exception("Failed to add torrent.")
|
|
57
|
+
|
|
58
|
+
# display qBittorrent info
|
|
59
|
+
print(f"qBittorrent: {qbt_client.app.version}")
|
|
60
|
+
print(f"qBittorrent Web API: {qbt_client.app.web_api_version}")
|
|
61
|
+
for k, v in qbt_client.app.build_info.items():
|
|
62
|
+
print(f"{k}: {v}")
|
|
63
|
+
|
|
64
|
+
# retrieve and show all torrents
|
|
65
|
+
for torrent in qbt_client.torrents_info():
|
|
66
|
+
print(f"{torrent.hash[-6:]}: {torrent.name} ({torrent.state})")
|
|
67
|
+
|
|
68
|
+
# stop all torrents
|
|
69
|
+
qbt_client.torrents.stop.all()
|
|
70
|
+
```
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import requests
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from .exceptions import DelugeWebClientError
|
|
9
|
+
from .response import Response
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DelugeWebClient:
|
|
13
|
+
HEADERS = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
14
|
+
LOGIN_RETRIES = 3
|
|
15
|
+
|
|
16
|
+
def __init__(self, url: str, password: str) -> None:
|
|
17
|
+
self.session = requests.Session()
|
|
18
|
+
self.url = self._build_url(url)
|
|
19
|
+
self.password = password
|
|
20
|
+
|
|
21
|
+
def __enter__(self) -> "DelugeWebClient":
|
|
22
|
+
"""Login and connect to client while using with statement."""
|
|
23
|
+
self.login()
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
27
|
+
"""End of with statement."""
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def login(self, timeout: int = 30) -> Response:
|
|
31
|
+
"""
|
|
32
|
+
Log in to Web UI and connects to the first available host, retrying if needed.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
timeout (int): Timeout for the login and connection attempts.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Response: A summary response indicating the success or failure of the login and connection attempts.
|
|
39
|
+
"""
|
|
40
|
+
login_response = self._attempt_login(timeout)
|
|
41
|
+
if not login_response.result:
|
|
42
|
+
return Response(result=False, error="Login failed", id=None)
|
|
43
|
+
|
|
44
|
+
# check if already connected
|
|
45
|
+
if self.check_connected(timeout).result:
|
|
46
|
+
return Response(result=True, error=None, id=None)
|
|
47
|
+
|
|
48
|
+
# attempt to connect to the first available host with retries
|
|
49
|
+
connection_response = self._connect_to_first_host(timeout)
|
|
50
|
+
if not connection_response.get(
|
|
51
|
+
"connected_now", Response(None, None, None)
|
|
52
|
+
).result:
|
|
53
|
+
return Response(result=False, error="Failed to connect to host", id=None)
|
|
54
|
+
|
|
55
|
+
return Response(result=True, error=None, id=None)
|
|
56
|
+
|
|
57
|
+
def _attempt_login(self, timeout: int) -> Response:
|
|
58
|
+
"""Attempt to log in to the Web UI"""
|
|
59
|
+
login_payload = {"method": "auth.login", "params": [self.password], "id": 1}
|
|
60
|
+
return self.execute_call(login_payload, timeout)
|
|
61
|
+
|
|
62
|
+
def _connect_to_first_host(self, timeout: int) -> dict[str, Response]:
|
|
63
|
+
"""Connect to the first available host with retry logic"""
|
|
64
|
+
hosts = self.get_hosts()
|
|
65
|
+
|
|
66
|
+
if hosts.result:
|
|
67
|
+
for _ in range(self.LOGIN_RETRIES):
|
|
68
|
+
host_id = hosts.result[0][0]
|
|
69
|
+
connect_response = self.connect_to_host(host_id)
|
|
70
|
+
|
|
71
|
+
if connect_response.result:
|
|
72
|
+
return {"connected_now": self.check_connected(timeout)}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"connected_now": Response(
|
|
76
|
+
result=False, error="All attempts failed", id=None
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def disconnect(self, timeout: int = 30) -> Response:
|
|
81
|
+
"""
|
|
82
|
+
Disconnects from the Web UI.
|
|
83
|
+
Note: This disconnects from all of your logged in instances outside of this program as well
|
|
84
|
+
that is tied to that user/password. Only use this IF needed not on each call.
|
|
85
|
+
"""
|
|
86
|
+
payload = {"method": "web.disconnect", "params": [], "id": 1}
|
|
87
|
+
return self.execute_call(payload, timeout)
|
|
88
|
+
|
|
89
|
+
def upload_torrent(
|
|
90
|
+
self,
|
|
91
|
+
torrent_path: Union[PathLike[str], Path],
|
|
92
|
+
add_paused: bool = False,
|
|
93
|
+
seed_mode: bool = False,
|
|
94
|
+
auto_managed: bool = False,
|
|
95
|
+
save_directory: str = None,
|
|
96
|
+
label: str = None,
|
|
97
|
+
timeout: int = 30,
|
|
98
|
+
) -> Response:
|
|
99
|
+
"""
|
|
100
|
+
Opens the torrent path building out the payload as needed to
|
|
101
|
+
upload a single torrent to the client.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
torrent_path (PathLike[str], Path): Path to torrent file (example.torrent).
|
|
105
|
+
save_directory (str, optional): Defined path where the file should go on the host. Defaults to None.
|
|
106
|
+
label (str, optional): Label to apply to uploaded torrent. Defaults to None.
|
|
107
|
+
timeout (int): Time to timeout.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Response: Response object.
|
|
111
|
+
"""
|
|
112
|
+
torrent_path = Path(torrent_path)
|
|
113
|
+
with open(torrent_path, "rb") as tf:
|
|
114
|
+
args = {
|
|
115
|
+
"add_paused": add_paused,
|
|
116
|
+
"seed_mode": seed_mode,
|
|
117
|
+
"auto_managed": auto_managed,
|
|
118
|
+
}
|
|
119
|
+
if save_directory:
|
|
120
|
+
args["download_location"] = str(save_directory)
|
|
121
|
+
params = [
|
|
122
|
+
str(torrent_path),
|
|
123
|
+
str(base64.b64encode(tf.read()), encoding="utf-8"),
|
|
124
|
+
args,
|
|
125
|
+
]
|
|
126
|
+
payload = {"method": "core.add_torrent_file", "params": params, "id": 1}
|
|
127
|
+
return self._upload_helper(payload, label, timeout)
|
|
128
|
+
|
|
129
|
+
def upload_torrents(
|
|
130
|
+
self,
|
|
131
|
+
torrents: Iterable[Union[PathLike[str], Path]],
|
|
132
|
+
save_directory: str = None,
|
|
133
|
+
label: str = None,
|
|
134
|
+
timeout: int = 30,
|
|
135
|
+
) -> dict[str, Response]:
|
|
136
|
+
"""
|
|
137
|
+
Uploads multiple torrents.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
torrents (Iterable[Union[PathLike[str], Path]]): A list or other iterable of torrent file paths.
|
|
141
|
+
save_directory (str, optional): Defined path where the file should go on the host. Defaults to None.
|
|
142
|
+
label (str, optional): Label to apply to uploaded torrents. Defaults to None.
|
|
143
|
+
timeout (int): Time to timeout.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
dict[str, Response]: A dictionary of torrent name and Response objects for each torrent.
|
|
147
|
+
"""
|
|
148
|
+
results = {}
|
|
149
|
+
for torrent_path in torrents:
|
|
150
|
+
torrent_path = Path(torrent_path)
|
|
151
|
+
try:
|
|
152
|
+
response = self.upload_torrent(
|
|
153
|
+
torrent_path, save_directory, label, timeout
|
|
154
|
+
)
|
|
155
|
+
results[torrent_path.stem] = response
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise DelugeWebClientError(
|
|
158
|
+
f"Failed to upload {torrent_path.name}:\n{e}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return results
|
|
162
|
+
|
|
163
|
+
def add_torrent_magnet(
|
|
164
|
+
self,
|
|
165
|
+
uri: str,
|
|
166
|
+
add_paused: bool = False,
|
|
167
|
+
seed_mode: bool = False,
|
|
168
|
+
auto_managed: bool = False,
|
|
169
|
+
save_directory: str = None,
|
|
170
|
+
label: str = None,
|
|
171
|
+
timeout: int = 30,
|
|
172
|
+
):
|
|
173
|
+
"""Adds a torrent from a magnet link.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
uri (str): Magnet input
|
|
177
|
+
save_directory (str, optional): Defined path where the file should go on the host. Defaults to None.
|
|
178
|
+
label (str, optional): Label to apply to uploaded torrent. Defaults to None.
|
|
179
|
+
timeout (int): Time to timeout.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Response: Response object.
|
|
183
|
+
"""
|
|
184
|
+
args = {
|
|
185
|
+
"add_paused": add_paused,
|
|
186
|
+
"seed_mode": seed_mode,
|
|
187
|
+
"auto_managed": auto_managed,
|
|
188
|
+
}
|
|
189
|
+
if save_directory:
|
|
190
|
+
args["download_location"] = str(save_directory)
|
|
191
|
+
payload = {
|
|
192
|
+
"method": "core.add_torrent_magnet",
|
|
193
|
+
"params": [str(uri), args],
|
|
194
|
+
"id": 1,
|
|
195
|
+
}
|
|
196
|
+
return self._upload_helper(payload, label, timeout)
|
|
197
|
+
|
|
198
|
+
def add_torrent_url(
|
|
199
|
+
self,
|
|
200
|
+
url: str,
|
|
201
|
+
add_paused: bool = False,
|
|
202
|
+
seed_mode: bool = False,
|
|
203
|
+
auto_managed: bool = False,
|
|
204
|
+
save_directory: str = None,
|
|
205
|
+
label: str = None,
|
|
206
|
+
timeout: int = 30,
|
|
207
|
+
):
|
|
208
|
+
"""Adds a torrent from a URL.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
url (str): URL input
|
|
212
|
+
save_directory (str, optional): Defined path where the file should go on the host. Defaults to None.
|
|
213
|
+
label (str, optional): Label to apply to uploaded torrent. Defaults to None.
|
|
214
|
+
timeout (int): Time to timeout.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Response: Response object.
|
|
218
|
+
"""
|
|
219
|
+
args = {
|
|
220
|
+
"add_paused": add_paused,
|
|
221
|
+
"seed_mode": seed_mode,
|
|
222
|
+
"auto_managed": auto_managed,
|
|
223
|
+
}
|
|
224
|
+
if save_directory:
|
|
225
|
+
args["download_location"] = str(save_directory)
|
|
226
|
+
payload = {
|
|
227
|
+
"method": "core.add_torrent_url",
|
|
228
|
+
"params": [str(url), args],
|
|
229
|
+
"id": 1,
|
|
230
|
+
}
|
|
231
|
+
return self._upload_helper(payload, label, timeout)
|
|
232
|
+
|
|
233
|
+
def _upload_helper(self, payload: dict, label: str, timeout: int) -> Response:
|
|
234
|
+
with self.session.post(
|
|
235
|
+
self.url, headers=self.HEADERS, json=payload, timeout=timeout
|
|
236
|
+
) as response:
|
|
237
|
+
if response.ok:
|
|
238
|
+
result = response.json()
|
|
239
|
+
info_hash = str(result["result"])
|
|
240
|
+
if label:
|
|
241
|
+
self._apply_label(info_hash, str(label), timeout)
|
|
242
|
+
self.resume_torrent(info_hash, timeout)
|
|
243
|
+
return Response(result=info_hash, error=None, id=1)
|
|
244
|
+
else:
|
|
245
|
+
response.raise_for_status()
|
|
246
|
+
|
|
247
|
+
def _apply_label(
|
|
248
|
+
self, info_hash: str, label: str, timeout: int
|
|
249
|
+
) -> tuple[bool, bool]:
|
|
250
|
+
"""
|
|
251
|
+
Used internally to add and apply labels as needed for
|
|
252
|
+
the `upload_torrent` method
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
info_hash (str): Info has of torrent.
|
|
256
|
+
label (str): Label to apply it to (automatically set to lowercase internally).
|
|
257
|
+
timeout (int): Time to timeout.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
tuple (bool, bool): add_label(), set_label().
|
|
261
|
+
"""
|
|
262
|
+
add_label = self.add_label(label, timeout)
|
|
263
|
+
set_label = self.set_label(info_hash, label, timeout)
|
|
264
|
+
return add_label, set_label
|
|
265
|
+
|
|
266
|
+
def get_free_space(self, path: PathLike[str] = None, timeout: int = 30) -> Response:
|
|
267
|
+
"""Gets free space"""
|
|
268
|
+
payload = {
|
|
269
|
+
"method": "core.get_free_space",
|
|
270
|
+
"params": [str(path)] if path else [],
|
|
271
|
+
"id": 1,
|
|
272
|
+
}
|
|
273
|
+
return self.execute_call(payload, timeout)
|
|
274
|
+
|
|
275
|
+
def get_path_size(self, path: PathLike[str] = None, timeout: int = 30) -> Response:
|
|
276
|
+
"""
|
|
277
|
+
Gets path size.
|
|
278
|
+
|
|
279
|
+
Returns the size of the file or folder `path` and `-1` if the path is
|
|
280
|
+
unaccessible (non-existent or insufficient privs)
|
|
281
|
+
"""
|
|
282
|
+
payload = {
|
|
283
|
+
"method": "core.get_path_size",
|
|
284
|
+
"params": [str(path)] if path else [],
|
|
285
|
+
"id": 1,
|
|
286
|
+
}
|
|
287
|
+
return self.execute_call(payload, timeout)
|
|
288
|
+
|
|
289
|
+
def get_labels(self, timeout: int = 30) -> Response:
|
|
290
|
+
"""Gets defined labels"""
|
|
291
|
+
payload = {"method": "label.get_labels", "params": [], "id": 1}
|
|
292
|
+
return self.execute_call(payload, timeout)
|
|
293
|
+
|
|
294
|
+
def set_label(self, info_hash: str, label: str, timeout: int = 30) -> Response:
|
|
295
|
+
"""Sets the label for a specific torrent"""
|
|
296
|
+
payload = {
|
|
297
|
+
"method": "label.set_torrent",
|
|
298
|
+
"params": [info_hash, label.lower()],
|
|
299
|
+
"id": 1,
|
|
300
|
+
}
|
|
301
|
+
return self.execute_call(payload, timeout)
|
|
302
|
+
|
|
303
|
+
def add_label(self, label: str, timeout: int = 30) -> Response:
|
|
304
|
+
"""Adds a label to the client, ignoring labels if they already exist"""
|
|
305
|
+
payload = {
|
|
306
|
+
"method": "label.add",
|
|
307
|
+
"params": [label.lower()],
|
|
308
|
+
"id": 1,
|
|
309
|
+
}
|
|
310
|
+
response = self.execute_call(payload, handle_error=False, timeout=timeout)
|
|
311
|
+
if response.error != None:
|
|
312
|
+
if "Label already exists" not in response.error.get("message"):
|
|
313
|
+
raise DelugeWebClientError(f"Error adding label:\n{response.error}")
|
|
314
|
+
return response
|
|
315
|
+
|
|
316
|
+
def get_libtorrent_version(self, timeout: int = 30) -> Response:
|
|
317
|
+
"""Gets libtorrent version"""
|
|
318
|
+
payload = {
|
|
319
|
+
"method": "core.get_libtorrent_version",
|
|
320
|
+
"params": [],
|
|
321
|
+
"id": 1,
|
|
322
|
+
}
|
|
323
|
+
return self.execute_call(payload, timeout)
|
|
324
|
+
|
|
325
|
+
def get_listen_port(self, timeout: int = 30) -> Response:
|
|
326
|
+
"""Gets listen port"""
|
|
327
|
+
payload = {
|
|
328
|
+
"method": "core.get_listen_port",
|
|
329
|
+
"params": [],
|
|
330
|
+
"id": 1,
|
|
331
|
+
}
|
|
332
|
+
return self.execute_call(payload, timeout)
|
|
333
|
+
|
|
334
|
+
def get_plugins(self, timeout: int = 30) -> Response:
|
|
335
|
+
"""Gets plugins"""
|
|
336
|
+
payload = {"method": "web.get_plugins", "params": [], "id": 1}
|
|
337
|
+
return self.execute_call(payload, timeout)
|
|
338
|
+
|
|
339
|
+
def get_torrent_files(self, torrent_id: str, timeout: int = 30) -> Response:
|
|
340
|
+
"""Gets the files for a torrent in tree format"""
|
|
341
|
+
payload = {"method": "web.get_torrent_files", "params": [torrent_id], "id": 1}
|
|
342
|
+
return self.execute_call(payload, timeout)
|
|
343
|
+
|
|
344
|
+
def get_torrent_status(
|
|
345
|
+
self,
|
|
346
|
+
torrent_id: str,
|
|
347
|
+
keys: list[str] = [],
|
|
348
|
+
diff: bool = False,
|
|
349
|
+
timeout: int = 30,
|
|
350
|
+
) -> Response:
|
|
351
|
+
"""
|
|
352
|
+
Gets the status for a torrent.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
torrent_id (str): Torrent hash of for a single torrent.
|
|
356
|
+
keys (dict): Filtering criteria for torrents.
|
|
357
|
+
diff (bool): Whether to return the status difference.
|
|
358
|
+
timeout (int): Time to timeout for the call.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Response: The response from the API call.
|
|
362
|
+
"""
|
|
363
|
+
payload = {
|
|
364
|
+
"method": "core.get_torrent_status",
|
|
365
|
+
"params": [torrent_id, keys, diff],
|
|
366
|
+
"id": 1,
|
|
367
|
+
}
|
|
368
|
+
return self.execute_call(payload, timeout)
|
|
369
|
+
|
|
370
|
+
def get_torrents_status(
|
|
371
|
+
self,
|
|
372
|
+
filter_dict: dict = {},
|
|
373
|
+
keys: list[str] = [],
|
|
374
|
+
diff: bool = False,
|
|
375
|
+
timeout: int = 30,
|
|
376
|
+
) -> Response:
|
|
377
|
+
"""
|
|
378
|
+
Gets the status for multiple torrents, returns all torrents,
|
|
379
|
+
optionally filtered by filter_dict
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
filter_dict (dict): Filtering criteria for torrents.
|
|
383
|
+
keys (list[str]): List of specific torrent IDs to fetch status for.
|
|
384
|
+
diff (bool): Whether to return the status difference.
|
|
385
|
+
timeout (int): Time to timeout for the call.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Response: The response from the API call.
|
|
389
|
+
"""
|
|
390
|
+
payload = {
|
|
391
|
+
"method": "core.get_torrents_status",
|
|
392
|
+
"params": [filter_dict, keys, diff],
|
|
393
|
+
"id": 1,
|
|
394
|
+
}
|
|
395
|
+
return self.execute_call(payload, timeout)
|
|
396
|
+
|
|
397
|
+
def check_connected(self, timeout: int = 30) -> Response:
|
|
398
|
+
"""
|
|
399
|
+
Use the `web.connected` method to get a boolean response if the Web UI is
|
|
400
|
+
connected to a deluged host
|
|
401
|
+
"""
|
|
402
|
+
payload = {"method": "web.connected", "params": [], "id": 1}
|
|
403
|
+
return self.execute_call(payload, timeout)
|
|
404
|
+
|
|
405
|
+
def get_hosts(self, timeout: int = 30) -> Response:
|
|
406
|
+
"""Returns hosts we're connected to currently"""
|
|
407
|
+
payload = {"method": "web.get_hosts", "params": [], "id": 1}
|
|
408
|
+
return self.execute_call(payload, timeout)
|
|
409
|
+
|
|
410
|
+
def get_host_status(self, host_id: str, timeout: int = 30) -> Response:
|
|
411
|
+
"""Get the deluged host status `<hostID>`"""
|
|
412
|
+
payload = {"method": "web.get_host_status", "params": [host_id], "id": 1}
|
|
413
|
+
return self.execute_call(payload, timeout)
|
|
414
|
+
|
|
415
|
+
def connect_to_host(self, host_id: str, timeout: int = 30) -> Response:
|
|
416
|
+
"""To connect to deluged with `<hostID>`"""
|
|
417
|
+
payload = {"method": "web.connect", "params": [host_id], "id": 1}
|
|
418
|
+
return self.execute_call(payload, timeout)
|
|
419
|
+
|
|
420
|
+
def test_listen_port(self, timeout: int = 30) -> bool:
|
|
421
|
+
"""Checks if the active port is open
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
bool: If active port is opened or closed
|
|
425
|
+
"""
|
|
426
|
+
payload = {"method": "core.test_listen_port", "params": [], "id": 1}
|
|
427
|
+
return self.execute_call(payload, timeout).result
|
|
428
|
+
|
|
429
|
+
def pause_torrent(self, torrent_id: str, timeout: int = 30) -> Response:
|
|
430
|
+
"""Pause a specific torrent"""
|
|
431
|
+
payload = {
|
|
432
|
+
"method": "core.pause_torrent",
|
|
433
|
+
"params": [torrent_id],
|
|
434
|
+
"id": 1,
|
|
435
|
+
}
|
|
436
|
+
return self.execute_call(payload, timeout)
|
|
437
|
+
|
|
438
|
+
def pause_torrents(self, torrent_ids: list, timeout: int = 30) -> Response:
|
|
439
|
+
"""Pause a list of torrents"""
|
|
440
|
+
if torrent_ids:
|
|
441
|
+
payload = {
|
|
442
|
+
"method": "core.pause_torrents",
|
|
443
|
+
"params": [torrent_ids],
|
|
444
|
+
"id": 1,
|
|
445
|
+
}
|
|
446
|
+
return self.execute_call(payload, timeout)
|
|
447
|
+
|
|
448
|
+
def remove_torrent(self, torrent_id: str, timeout: int = 30) -> Response:
|
|
449
|
+
"""Removes a specific torrent"""
|
|
450
|
+
payload = {
|
|
451
|
+
"method": "core.remove_torrent",
|
|
452
|
+
"params": [torrent_id],
|
|
453
|
+
"id": 1,
|
|
454
|
+
}
|
|
455
|
+
return self.execute_call(payload, timeout)
|
|
456
|
+
|
|
457
|
+
def remove_torrents(self, torrent_ids: list, timeout: int = 30) -> Response:
|
|
458
|
+
"""Removes a list of torrents"""
|
|
459
|
+
if torrent_ids:
|
|
460
|
+
payload = {
|
|
461
|
+
"method": "core.remove_torrents",
|
|
462
|
+
"params": [torrent_ids],
|
|
463
|
+
"id": 1,
|
|
464
|
+
}
|
|
465
|
+
return self.execute_call(payload, timeout)
|
|
466
|
+
|
|
467
|
+
def resume_torrent(self, torrent_id: str, timeout: int = 30) -> Response:
|
|
468
|
+
"""Resumes a specific torrent"""
|
|
469
|
+
payload = {
|
|
470
|
+
"method": "core.resume_torrent",
|
|
471
|
+
"params": [torrent_id],
|
|
472
|
+
"id": 1,
|
|
473
|
+
}
|
|
474
|
+
return self.execute_call(payload, timeout)
|
|
475
|
+
|
|
476
|
+
def resume_torrents(self, torrent_ids: list, timeout: int = 30) -> Response:
|
|
477
|
+
"""Resumes a list of torrents"""
|
|
478
|
+
if torrent_ids:
|
|
479
|
+
payload = {
|
|
480
|
+
"method": "core.resume_torrents",
|
|
481
|
+
"params": [torrent_ids],
|
|
482
|
+
"id": 1,
|
|
483
|
+
}
|
|
484
|
+
return self.execute_call(payload, timeout)
|
|
485
|
+
|
|
486
|
+
def set_torrent_trackers(
|
|
487
|
+
self, torrent_id: str, trackers: list[dict[str, any]], timeout: int = 30
|
|
488
|
+
) -> Response:
|
|
489
|
+
"""Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``"""
|
|
490
|
+
if torrent_id:
|
|
491
|
+
payload = {
|
|
492
|
+
"method": "core.set_torrent_trackers",
|
|
493
|
+
"params": [torrent_id, trackers],
|
|
494
|
+
"id": 1,
|
|
495
|
+
}
|
|
496
|
+
return self.execute_call(payload, timeout)
|
|
497
|
+
|
|
498
|
+
def execute_call(
|
|
499
|
+
self, payload: dict, handle_error: bool = True, timeout: int = 30
|
|
500
|
+
) -> Response:
|
|
501
|
+
"""
|
|
502
|
+
Helper method to execute most calls to the Web UI as needed.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
payload (dict): Payload object to be called.
|
|
506
|
+
handle_error (bool, optional): Handle errors here or allow the caller to handle
|
|
507
|
+
the error. Defaults to True.
|
|
508
|
+
timeout (int): Time to timeout.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Response: Response object for each call.
|
|
512
|
+
"""
|
|
513
|
+
with self.session.post(
|
|
514
|
+
self.url, headers=self.HEADERS, json=payload, timeout=timeout
|
|
515
|
+
) as response:
|
|
516
|
+
if response.ok:
|
|
517
|
+
response_json = response.json()
|
|
518
|
+
data = Response(
|
|
519
|
+
result=response_json.get("result"),
|
|
520
|
+
error=self._normalize_exception(response_json.get("error")),
|
|
521
|
+
id=response_json.get("id"),
|
|
522
|
+
)
|
|
523
|
+
if handle_error and data.error:
|
|
524
|
+
raise DelugeWebClientError(
|
|
525
|
+
f"Payload: {payload}, Error: {data.error}"
|
|
526
|
+
)
|
|
527
|
+
return data
|
|
528
|
+
else:
|
|
529
|
+
raise DelugeWebClientError(
|
|
530
|
+
f"Failed to execute call. Response code: {response.status_code}. Reason: {response.reason}"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
@staticmethod
|
|
534
|
+
def _normalize_exception(exc_str: any) -> Union[str, None]:
|
|
535
|
+
"""
|
|
536
|
+
Removes the un-needed ending square bracket and stripping extra white
|
|
537
|
+
space if input is a string
|
|
538
|
+
"""
|
|
539
|
+
if exc_str:
|
|
540
|
+
if isinstance(exc_str, str):
|
|
541
|
+
return exc_str.rstrip("]").strip()
|
|
542
|
+
else:
|
|
543
|
+
return exc_str
|
|
544
|
+
|
|
545
|
+
@staticmethod
|
|
546
|
+
def _build_url(url: str) -> str:
|
|
547
|
+
"""Automatically fixes urls as needed to access the json api endpoint"""
|
|
548
|
+
if not url.endswith("/"):
|
|
549
|
+
url += "/"
|
|
550
|
+
|
|
551
|
+
if "json" not in url:
|
|
552
|
+
url += "json/"
|
|
553
|
+
|
|
554
|
+
return url.rstrip("/")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "deluge-web-client"
|
|
3
|
+
version = "0.1.0-alpha2"
|
|
4
|
+
description = "Deluge Web Client"
|
|
5
|
+
authors = ["jlw_4049 <jessielw4049@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.11"
|
|
11
|
+
requests = "^2.32.3"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
[tool.poetry.group.dev.dependencies]
|
|
15
|
+
black = "^24.8.0"
|
|
16
|
+
sphinx = "7.4.7"
|
|
17
|
+
sphinx-autodoc-typehints = "2.3.0"
|
|
18
|
+
sphinx-rtd-theme = "2.0.0"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|