Skip to content

Commit 23e12cc

Browse files
Merge pull request #70 from jerrymakesjelly/dev
Version 1.5.2
2 parents 7f3b0fd + f8d7f05 commit 23e12cc

25 files changed

Lines changed: 793 additions & 318 deletions

README-cn.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@
107107

108108
更新日志
109109
----------
110+
**2020-03-27 周五**:1.5.2 版本。
111+
112+
* 支持 Deluge (#8);
113+
* 使用批量删除提升删除效率;
114+
* 修复配置文件中的多语言支持问题(#69);
115+
* 客户端名称不再对大小写敏感。
116+
110117
**2020-02-29 周六**:1.5.1 版本。
111118

112119
* 修复了 1.5.0 版本中丢失的状态 ``StalledUpload`` 和 ``StalledDownload``。(#66)

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ Screenshot
111111

112112
Changelog
113113
----------
114+
**Fri, 27 Mar 2020**: Version 1.5.2.
115+
116+
* Support Deluge. (#8)
117+
* Use batch delete to improve efficiency.
118+
* Fix multi-language support in config file. (#69)
119+
* Set the client names to be case-insensitive.
120+
114121
**Sat, 29 Feb 2020**: Version 1.5.1.
115122

116123
* Fix missing status ``StalledUpload`` and ``StalledDownload`` in version 1.5.0. (#66)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import time
2+
from deluge_client import DelugeRPCClient
3+
from deluge_client.client import DelugeClientException
4+
from ..torrent import Torrent
5+
from ..torrentstatus import TorrentStatus
6+
from ..exception.loginfailure import LoginFailure
7+
from ..exception.remotefailure import RemoteFailure
8+
9+
# Default port of Delgue
10+
DEFAULT_PORT = 58846
11+
12+
class Deluge(object):
13+
def __init__(self, host):
14+
# Host
15+
self._host = host
16+
# RPC Client
17+
self._client = None
18+
# Torrent Properties Cache
19+
self._torrent_cache = {}
20+
# Cache Valid Time
21+
self._refresh_expire_time = 30
22+
# Last Time of Refreshing Cache
23+
self._last_refresh = 0
24+
25+
# Login to Deluge
26+
def login(self, username, password):
27+
# Split IP(or domain name) and port
28+
splits = self._host.split(':')
29+
host = splits[0] if len(splits) > 0 else ''
30+
port = int(splits[1]) if len(splits) > 1 else DEFAULT_PORT
31+
32+
# Create RPC client and connect to Deluge
33+
self._client = DelugeRPCClient(host, port, username, password, decode_utf8 = True)
34+
try:
35+
self._client.connect()
36+
except DelugeClientException as e:
37+
# Display class name of the exception if there is no error messages
38+
raise LoginFailure(e.args[0].split('\n')[0] if len(e.args) > 0 else e.__class__.__name__)
39+
40+
# A caller to call deluge api; includes exception processing
41+
def _call(self, method, *args, **kwargs):
42+
try:
43+
return self._client.call(method, *args, **kwargs)
44+
except DelugeClientException as e:
45+
# Raise our own exception
46+
raise RemoteFailure(e.args[0].split('\n')[0] if len(e.args) > 0 else e.__class__.__name__)
47+
48+
# Get Deluge version
49+
def version(self):
50+
funcs = {
51+
1: 'daemon.info', # For Deluge 1.x, use daemon.info
52+
2: 'daemon.get_version', # For Deluge 2.x, use daemon.get_version
53+
}
54+
ver = self._call(funcs[self._client.deluge_version])
55+
return ('Deluge %s' % ver)
56+
57+
# Get API version
58+
def api_version(self):
59+
# Returns the protocol version
60+
return self._client.deluge_protocol_version if self._client.deluge_protocol_version is not None else 'not provided'
61+
62+
# Get torrent list
63+
def torrents_list(self):
64+
# Save hashes
65+
torrents_hash = []
66+
# Get torrent list (and their properties)
67+
torrent_list = self._call('core.get_torrents_status', {}, [
68+
'active_time',
69+
'all_time_download',
70+
'download_payload_rate',
71+
'finished_time',
72+
'hash',
73+
'label', # Available when the plugin 'label' is enabled
74+
'name',
75+
'num_peers',
76+
'num_seeds',
77+
'progress',
78+
'ratio',
79+
'seeding_time',
80+
'state',
81+
'time_added',
82+
'time_since_transfer',
83+
'total_peers',
84+
'total_seeds',
85+
'total_size',
86+
'total_uploaded',
87+
'trackers',
88+
'upload_payload_rate',
89+
])
90+
# Save properties to cache
91+
self._torrent_cache = torrent_list
92+
self._last_refresh = time.time()
93+
# Return torrent hashes
94+
for h in torrent_list:
95+
torrents_hash.append(h)
96+
return torrents_hash
97+
98+
# Get Torrent Properties
99+
def torrent_properties(self, torrent_hash):
100+
# Check cache expiration
101+
if time.time() - self._last_refresh > self._refresh_expire_time:
102+
self.torrents_list()
103+
# Extract properties
104+
torrent = self._torrent_cache[torrent_hash]
105+
# Create torrent object
106+
torrent_obj = Torrent()
107+
torrent_obj.hash = torrent['hash']
108+
torrent_obj.name = torrent['name']
109+
if 'label' in torrent:
110+
torrent_obj.category = [torrent['label']] if len(torrent['label']) > 0 else []
111+
torrent_obj.tracker = [tracker['url'] for tracker in torrent['trackers']]
112+
torrent_obj.status = Deluge._judge_status(torrent['state'])
113+
torrent_obj.size = torrent['total_size']
114+
torrent_obj.ratio = torrent['ratio']
115+
torrent_obj.uploaded = torrent['total_uploaded']
116+
torrent_obj.create_time = int(torrent['time_added'])
117+
torrent_obj.seeding_time = torrent['seeding_time']
118+
torrent_obj.upload_speed = torrent['upload_payload_rate']
119+
torrent_obj.download_speed = torrent['download_payload_rate']
120+
torrent_obj.seeder = torrent['total_seeds']
121+
torrent_obj.connected_seeder = torrent['num_seeds']
122+
torrent_obj.leecher = torrent['total_peers']
123+
torrent_obj.connected_leecher = torrent['num_peers']
124+
torrent_obj.average_upload_speed = torrent['total_uploaded'] / torrent['active_time'] if torrent['active_time'] > 0 else 0
125+
if 'finished_time' in torrent:
126+
download_time = torrent['active_time'] - torrent['finished_time']
127+
torrent_obj.average_download_speed = torrent['all_time_download'] / download_time if download_time > 0 else 0
128+
if 'time_since_transfer' in torrent:
129+
# Set the last active time of those never active torrents to timestamp 0
130+
torrent_obj.last_activity = torrent['time_since_transfer'] if torrent['time_since_transfer'] > 0 else 0
131+
torrent_obj.progress = torrent['progress'] / 100 # Accept Range: 0-1
132+
133+
return torrent_obj
134+
135+
# Judge Torrent Status
136+
@staticmethod
137+
def _judge_status(state):
138+
return {
139+
'Allocating': TorrentStatus.Unknown, # Ignore this state
140+
'Checking': TorrentStatus.Checking,
141+
'Downloading': TorrentStatus.Downloading,
142+
'Error': TorrentStatus.Error,
143+
'Moving': TorrentStatus.Unknown, # Ignore this state
144+
'Paused': TorrentStatus.Paused,
145+
'Queued': TorrentStatus.Queued,
146+
'Seeding': TorrentStatus.Uploading,
147+
}[state]
148+
149+
# Batch Remove Torrents
150+
def remove_torrents(self, torrent_hash_list, remove_data):
151+
if self._client.deluge_version >= 2: # Method 'core.remove_torrents' is only available in Deluge 2.x
152+
failures = self._call('core.remove_torrents', torrent_hash_list, remove_data)
153+
failed_hash = [torrent[0] for torrent in failures]
154+
return (
155+
[torrent for torrent in torrent_hash_list if torrent not in failed_hash],
156+
[{
157+
'hash': torrent[0],
158+
'reason': torrent[1],
159+
} for torrent in failures],
160+
)
161+
else: # For Deluge 1.x, remove torrents one by one
162+
success_hash = []
163+
failures = []
164+
for torrent in torrent_hash_list:
165+
try:
166+
self._call('core.remove_torrent', torrent, remove_data)
167+
success_hash.append(torrent)
168+
except RemoteFailure as e:
169+
failures.append({
170+
'hash': torrent,
171+
'reason': e.args[0],
172+
})
173+
return (success_hash, failures)

autoremovetorrents/client/qbittorrent.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from ..torrent import Torrent
55
from ..torrentstatus import TorrentStatus
66
from ..exception.loginfailure import LoginFailure
7-
from ..exception.deletionfailure import DeletionFailure
87
from ..exception.connectionfailure import ConnectionFailure
98
from ..exception.incompatibleapi import IncompatibleAPIVersion
109

@@ -50,13 +49,13 @@ def torrent_generic_properties(self, torrent_hash):
5049
def torrent_trackers(self, torrent_hash):
5150
return self._session.get(self._host+'/query/propertiesTrackers/'+torrent_hash)
5251

53-
# Delete torrent
54-
def delete_torrent(self, torrent_hash):
55-
return self._session.post(self._host+'/command/delete', data={'hashes':torrent_hash})
52+
# Batch Delete torrents
53+
def delete_torrents(self, torrent_hash_list):
54+
return self._session.post(self._host+'/command/delete', data={'hashes':'|'.join(torrent_hash_list)})
5655

57-
# Delete torrent and data
58-
def delete_torrent_and_data(self, torrent_hash):
59-
return self._session.post(self._host+'/command/deletePerm', data={'hashes':torrent_hash})
56+
# Batch Delete torrents and data
57+
def delete_torrents_and_data(self, torrent_hash_list):
58+
return self._session.post(self._host+'/command/deletePerm', data={'hashes':'|'.join(torrent_hash_list)})
6059

6160
# API Handler for v2
6261
class qBittorrentAPIHandlerV2(object):
@@ -99,13 +98,13 @@ def torrent_generic_properties(self, torrent_hash):
9998
def torrent_trackers(self, torrent_hash):
10099
return self._session.get(self._host+'/api/v2/torrents/trackers', params={'hash':torrent_hash})
101100

102-
# Delete torrent
103-
def delete_torrent(self, torrent_hash):
104-
return self._session.get(self._host+'/api/v2/torrents/delete', params={'hashes':torrent_hash, 'deleteFiles': False})
101+
# Batch Delete torrents
102+
def delete_torrents(self, torrent_hash_list):
103+
return self._session.get(self._host+'/api/v2/torrents/delete', params={'hashes':'|'.join(torrent_hash_list), 'deleteFiles': False})
105104

106-
# Delete torrent and data
107-
def delete_torrent_and_data(self, torrent_hash):
108-
return self._session.get(self._host+'/api/v2/torrents/delete', params={'hashes':torrent_hash, 'deleteFiles': True})
105+
# Batch Delete torrents and data
106+
def delete_torrents_and_data(self, torrent_hash_list):
107+
return self._session.get(self._host+'/api/v2/torrents/delete', params={'hashes':'|'.join(torrent_hash_list), 'deleteFiles': True})
109108

110109
def __init__(self, host):
111110
# Torrents list cache
@@ -220,14 +219,16 @@ def _judge_status(state):
220219
status = TorrentStatus.Unknown
221220
return status
222221

223-
# Remove Torrent
224-
def remove_torrent(self, torrent_hash):
225-
request = self._request_handler.delete_torrent(torrent_hash)
222+
# Batch Remove Torrents
223+
# Return values: (success_hash_list, failed_list -> {hash: reason, ...})
224+
def remove_torrents(self, torrent_hash_list, remove_data):
225+
request = self._request_handler.delete_torrents_and_data(torrent_hash_list) if remove_data \
226+
else self._request_handler.delete_torrents(torrent_hash_list)
226227
if request.status_code != 200:
227-
raise DeletionFailure('Cannot delete torrent %s. The server responses HTTP %d.' % (torrent_hash, request.status_code))
228-
229-
# Remove Torrent and Data
230-
def remove_data(self, torrent_hash):
231-
request = self._request_handler.delete_torrent_and_data(torrent_hash)
232-
if request.status_code != 200:
233-
raise DeletionFailure('Cannot delete torrent %s and its data. The server responses HTTP %d.' % (torrent_hash, request.status_code))
228+
return ([], [{
229+
'hash': torrent,
230+
'reason': 'The server responses HTTP %d.' % request.status_code,
231+
} for torrent in torrent_hash_list])
232+
# Some of them may fail but we can't judge them,
233+
# So we consider all of them as successful.
234+
return (torrent_hash_list, [])

autoremovetorrents/client/transmission.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
#-*- coding:utf-8 -*-
22
import requests
3-
from requests.auth import HTTPBasicAuth
43
from ..torrent import Torrent
54
from ..torrentstatus import TorrentStatus
65
from ..exception.connectionfailure import ConnectionFailure
7-
from ..exception.deletionfailure import DeletionFailure
86
from ..exception.loginfailure import LoginFailure
97
from ..exception.nosuchclient import NoSuchClient
108
from ..exception.remotefailure import RemoteFailure
@@ -150,18 +148,17 @@ def _judge_status(state, errno):
150148
TorrentStatus.Unknown # 7:ISOLATED(Torrent can't find peers)
151149
][state]
152150

153-
# Remove Torrent
154-
def remove_torrent(self, torrent_hash):
151+
# Batch Remove Torrents
152+
# Return values: (success_hash_list, failed_hash_list : {hash: reason, ...})
153+
def remove_torrents(self, torrent_hash_list, remove_data):
155154
try:
156155
self._make_transmission_request('torrent-remove',
157-
{'ids':[torrent_hash], 'delete-local-data':False})
156+
{'ids': torrent_hash_list, 'delete-local-data': remove_data})
158157
except Exception as e:
159-
raise DeletionFailure('Cannot delete torrent %s. %s' % (torrent_hash, str(e)))
160-
161-
# Remove Data
162-
def remove_data(self, torrent_hash):
163-
try:
164-
self._make_transmission_request('torrent-remove',
165-
{'ids':[torrent_hash], 'delete-local-data':True})
166-
except Exception as e:
167-
raise DeletionFailure('Cannot delete torrent %s and its data. %s' % (torrent_hash, str(e)))
158+
# We couldn't judge which torrents are removed and which aren't when an exception was raised
159+
# Therefore we think all the deletion have been failed
160+
return ([], [{
161+
'hash': torrent,
162+
'reason': str(e),
163+
} for torrent in torrent_hash_list])
164+
return (torrent_hash_list, [])

autoremovetorrents/client/utorrent.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
#-*- coding:utf-8 -*-
22
import re
33
import time
4-
from requests.auth import HTTPBasicAuth
54
import requests
65
from ..torrent import Torrent
76
from autoremovetorrents.exception.connectionfailure import ConnectionFailure
8-
from autoremovetorrents.exception.deletionfailure import DeletionFailure
97
from autoremovetorrents.exception.loginfailure import LoginFailure
108
from autoremovetorrents.exception.nosuchtorrent import NoSuchTorrent
119
from autoremovetorrents.exception.remotefailure import RemoteFailure
@@ -38,7 +36,7 @@ def login(self, username, password):
3836

3937
pattern = re.compile('<[^>]+>')
4038
text = request.text
41-
if request.status_code == 200:
39+
if request.status_code == 200:
4240
self._token = pattern.sub('', text)
4341
elif request.status_code == 401: # Error
4442
raise LoginFailure('401 Unauthorized.')
@@ -73,7 +71,7 @@ def torrents_list(self):
7371
for torrent in result['torrents']:
7472
torrents_hash.append(torrent[0])
7573
return torrents_hash
76-
74+
7775
# Get Torrent Job Properties
7876
def _torrent_job_properties(self, torrent_hash):
7977
request = self._session.get(self._host+'/gui/',
@@ -108,7 +106,7 @@ def torrent_properties(self, torrent_hash):
108106
torrent_obj.leecher = torrent[13]
109107
torrent_obj.connected_leecher = torrent[12]
110108
torrent_obj.progress = torrent[4]
111-
109+
112110
return torrent_obj
113111
# Not Found
114112
raise NoSuchTorrent('No such torrent.')
@@ -134,17 +132,24 @@ def _judge_status(state, progress):
134132
else:
135133
status = TorrentStatus.Unknown
136134
return status
137-
138-
# Remove Torrent
139-
def remove_torrent(self, torrent_hash):
140-
request = self._session.get(self._host+'/gui/',
141-
params={'action':'remove', 'token':self._token, 'hash':torrent_hash})
142-
if request.status_code != 200:
143-
raise DeletionFailure('Cannot delete torrent %s. The server responses HTTP %d.' % (torrent_hash, request.status_code))
144-
145-
# Remove Torrent and Data
146-
def remove_data(self, torrent_hash):
135+
136+
# Batch Remove Torrents
137+
# Return values: (success_hash_list, failed_hash_list : {hash: failed_reason, ...})
138+
def remove_torrents(self, torrent_hash_list, remove_data):
139+
actions = {
140+
True: 'removedata',
141+
False: 'remove',
142+
}
143+
# According to the tests, it looks like uTorrent can accept a very long URL
144+
# (more than 10,000 torrents per request)
145+
# Therefore we needn't to set a URL length limitation
147146
request = self._session.get(self._host+'/gui/',
148-
params={'action':'removedata', 'token':self._token, 'hash':torrent_hash})
147+
params={'action': actions[remove_data], 'token': self._token, 'hash': torrent_hash_list})
148+
# Note: uTorrent doesn't report the status of each torrent
149+
# We think that all the torrents are removed when the request is sent successfully
149150
if request.status_code != 200:
150-
raise DeletionFailure('Cannot delete torrent %s and its data. The server responses HTTP %d.' % (torrent_hash, request.status_code))
151+
return ([], [{
152+
'hash': torrent,
153+
'reason': 'The server responses HTTP %d.' % request.status_code,
154+
} for torrent in torrent_hash_list])
155+
return (torrent_hash_list, [])

0 commit comments

Comments
 (0)