Skip to content

Commit

Permalink
Merge pull request #70 from jerrymakesjelly/dev
Browse files Browse the repository at this point in the history
Version 1.5.2
  • Loading branch information
jerrymakesjelly committed Mar 28, 2020
2 parents 7f3b0fd + f8d7f05 commit 23e12cc
Show file tree
Hide file tree
Showing 25 changed files with 793 additions and 318 deletions.
7 changes: 7 additions & 0 deletions README-cn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@

更新日志
----------
**2020-03-27 周五**:1.5.2 版本。

* 支持 Deluge (#8);
* 使用批量删除提升删除效率;
* 修复配置文件中的多语言支持问题(#69);
* 客户端名称不再对大小写敏感。

**2020-02-29 周六**:1.5.1 版本。

* 修复了 1.5.0 版本中丢失的状态 ``StalledUpload`` 和 ``StalledDownload``。(#66)
Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ Screenshot

Changelog
----------
**Fri, 27 Mar 2020**: Version 1.5.2.

* Support Deluge. (#8)
* Use batch delete to improve efficiency.
* Fix multi-language support in config file. (#69)
* Set the client names to be case-insensitive.

**Sat, 29 Feb 2020**: Version 1.5.1.

* Fix missing status ``StalledUpload`` and ``StalledDownload`` in version 1.5.0. (#66)
Expand Down
173 changes: 173 additions & 0 deletions autoremovetorrents/client/deluge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import time
from deluge_client import DelugeRPCClient
from deluge_client.client import DelugeClientException
from ..torrent import Torrent
from ..torrentstatus import TorrentStatus
from ..exception.loginfailure import LoginFailure
from ..exception.remotefailure import RemoteFailure

# Default port of Delgue
DEFAULT_PORT = 58846

class Deluge(object):
def __init__(self, host):
# Host
self._host = host
# RPC Client
self._client = None
# Torrent Properties Cache
self._torrent_cache = {}
# Cache Valid Time
self._refresh_expire_time = 30
# Last Time of Refreshing Cache
self._last_refresh = 0

# Login to Deluge
def login(self, username, password):
# Split IP(or domain name) and port
splits = self._host.split(':')
host = splits[0] if len(splits) > 0 else ''
port = int(splits[1]) if len(splits) > 1 else DEFAULT_PORT

# Create RPC client and connect to Deluge
self._client = DelugeRPCClient(host, port, username, password, decode_utf8 = True)
try:
self._client.connect()
except DelugeClientException as e:
# Display class name of the exception if there is no error messages
raise LoginFailure(e.args[0].split('\n')[0] if len(e.args) > 0 else e.__class__.__name__)

# A caller to call deluge api; includes exception processing
def _call(self, method, *args, **kwargs):
try:
return self._client.call(method, *args, **kwargs)
except DelugeClientException as e:
# Raise our own exception
raise RemoteFailure(e.args[0].split('\n')[0] if len(e.args) > 0 else e.__class__.__name__)

# Get Deluge version
def version(self):
funcs = {
1: 'daemon.info', # For Deluge 1.x, use daemon.info
2: 'daemon.get_version', # For Deluge 2.x, use daemon.get_version
}
ver = self._call(funcs[self._client.deluge_version])
return ('Deluge %s' % ver)

# Get API version
def api_version(self):
# Returns the protocol version
return self._client.deluge_protocol_version if self._client.deluge_protocol_version is not None else 'not provided'

# Get torrent list
def torrents_list(self):
# Save hashes
torrents_hash = []
# Get torrent list (and their properties)
torrent_list = self._call('core.get_torrents_status', {}, [
'active_time',
'all_time_download',
'download_payload_rate',
'finished_time',
'hash',
'label', # Available when the plugin 'label' is enabled
'name',
'num_peers',
'num_seeds',
'progress',
'ratio',
'seeding_time',
'state',
'time_added',
'time_since_transfer',
'total_peers',
'total_seeds',
'total_size',
'total_uploaded',
'trackers',
'upload_payload_rate',
])
# Save properties to cache
self._torrent_cache = torrent_list
self._last_refresh = time.time()
# Return torrent hashes
for h in torrent_list:
torrents_hash.append(h)
return torrents_hash

# Get Torrent Properties
def torrent_properties(self, torrent_hash):
# Check cache expiration
if time.time() - self._last_refresh > self._refresh_expire_time:
self.torrents_list()
# Extract properties
torrent = self._torrent_cache[torrent_hash]
# Create torrent object
torrent_obj = Torrent()
torrent_obj.hash = torrent['hash']
torrent_obj.name = torrent['name']
if 'label' in torrent:
torrent_obj.category = [torrent['label']] if len(torrent['label']) > 0 else []
torrent_obj.tracker = [tracker['url'] for tracker in torrent['trackers']]
torrent_obj.status = Deluge._judge_status(torrent['state'])
torrent_obj.size = torrent['total_size']
torrent_obj.ratio = torrent['ratio']
torrent_obj.uploaded = torrent['total_uploaded']
torrent_obj.create_time = int(torrent['time_added'])
torrent_obj.seeding_time = torrent['seeding_time']
torrent_obj.upload_speed = torrent['upload_payload_rate']
torrent_obj.download_speed = torrent['download_payload_rate']
torrent_obj.seeder = torrent['total_seeds']
torrent_obj.connected_seeder = torrent['num_seeds']
torrent_obj.leecher = torrent['total_peers']
torrent_obj.connected_leecher = torrent['num_peers']
torrent_obj.average_upload_speed = torrent['total_uploaded'] / torrent['active_time'] if torrent['active_time'] > 0 else 0
if 'finished_time' in torrent:
download_time = torrent['active_time'] - torrent['finished_time']
torrent_obj.average_download_speed = torrent['all_time_download'] / download_time if download_time > 0 else 0
if 'time_since_transfer' in torrent:
# Set the last active time of those never active torrents to timestamp 0
torrent_obj.last_activity = torrent['time_since_transfer'] if torrent['time_since_transfer'] > 0 else 0
torrent_obj.progress = torrent['progress'] / 100 # Accept Range: 0-1

return torrent_obj

# Judge Torrent Status
@staticmethod
def _judge_status(state):
return {
'Allocating': TorrentStatus.Unknown, # Ignore this state
'Checking': TorrentStatus.Checking,
'Downloading': TorrentStatus.Downloading,
'Error': TorrentStatus.Error,
'Moving': TorrentStatus.Unknown, # Ignore this state
'Paused': TorrentStatus.Paused,
'Queued': TorrentStatus.Queued,
'Seeding': TorrentStatus.Uploading,
}[state]

# Batch Remove Torrents
def remove_torrents(self, torrent_hash_list, remove_data):
if self._client.deluge_version >= 2: # Method 'core.remove_torrents' is only available in Deluge 2.x
failures = self._call('core.remove_torrents', torrent_hash_list, remove_data)
failed_hash = [torrent[0] for torrent in failures]
return (
[torrent for torrent in torrent_hash_list if torrent not in failed_hash],
[{
'hash': torrent[0],
'reason': torrent[1],
} for torrent in failures],
)
else: # For Deluge 1.x, remove torrents one by one
success_hash = []
failures = []
for torrent in torrent_hash_list:
try:
self._call('core.remove_torrent', torrent, remove_data)
success_hash.append(torrent)
except RemoteFailure as e:
failures.append({
'hash': torrent,
'reason': e.args[0],
})
return (success_hash, failures)
47 changes: 24 additions & 23 deletions autoremovetorrents/client/qbittorrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from ..torrent import Torrent
from ..torrentstatus import TorrentStatus
from ..exception.loginfailure import LoginFailure
from ..exception.deletionfailure import DeletionFailure
from ..exception.connectionfailure import ConnectionFailure
from ..exception.incompatibleapi import IncompatibleAPIVersion

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

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

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

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

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

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

def __init__(self, host):
# Torrents list cache
Expand Down Expand Up @@ -220,14 +219,16 @@ def _judge_status(state):
status = TorrentStatus.Unknown
return status

# Remove Torrent
def remove_torrent(self, torrent_hash):
request = self._request_handler.delete_torrent(torrent_hash)
# Batch Remove Torrents
# Return values: (success_hash_list, failed_list -> {hash: reason, ...})
def remove_torrents(self, torrent_hash_list, remove_data):
request = self._request_handler.delete_torrents_and_data(torrent_hash_list) if remove_data \
else self._request_handler.delete_torrents(torrent_hash_list)
if request.status_code != 200:
raise DeletionFailure('Cannot delete torrent %s. The server responses HTTP %d.' % (torrent_hash, request.status_code))

# Remove Torrent and Data
def remove_data(self, torrent_hash):
request = self._request_handler.delete_torrent_and_data(torrent_hash)
if request.status_code != 200:
raise DeletionFailure('Cannot delete torrent %s and its data. The server responses HTTP %d.' % (torrent_hash, request.status_code))
return ([], [{
'hash': torrent,
'reason': 'The server responses HTTP %d.' % request.status_code,
} for torrent in torrent_hash_list])
# Some of them may fail but we can't judge them,
# So we consider all of them as successful.
return (torrent_hash_list, [])
25 changes: 11 additions & 14 deletions autoremovetorrents/client/transmission.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#-*- coding:utf-8 -*-
import requests
from requests.auth import HTTPBasicAuth
from ..torrent import Torrent
from ..torrentstatus import TorrentStatus
from ..exception.connectionfailure import ConnectionFailure
from ..exception.deletionfailure import DeletionFailure
from ..exception.loginfailure import LoginFailure
from ..exception.nosuchclient import NoSuchClient
from ..exception.remotefailure import RemoteFailure
Expand Down Expand Up @@ -150,18 +148,17 @@ def _judge_status(state, errno):
TorrentStatus.Unknown # 7:ISOLATED(Torrent can't find peers)
][state]

# Remove Torrent
def remove_torrent(self, torrent_hash):
# Batch Remove Torrents
# Return values: (success_hash_list, failed_hash_list : {hash: reason, ...})
def remove_torrents(self, torrent_hash_list, remove_data):
try:
self._make_transmission_request('torrent-remove',
{'ids':[torrent_hash], 'delete-local-data':False})
{'ids': torrent_hash_list, 'delete-local-data': remove_data})
except Exception as e:
raise DeletionFailure('Cannot delete torrent %s. %s' % (torrent_hash, str(e)))

# Remove Data
def remove_data(self, torrent_hash):
try:
self._make_transmission_request('torrent-remove',
{'ids':[torrent_hash], 'delete-local-data':True})
except Exception as e:
raise DeletionFailure('Cannot delete torrent %s and its data. %s' % (torrent_hash, str(e)))
# We couldn't judge which torrents are removed and which aren't when an exception was raised
# Therefore we think all the deletion have been failed
return ([], [{
'hash': torrent,
'reason': str(e),
} for torrent in torrent_hash_list])
return (torrent_hash_list, [])
39 changes: 22 additions & 17 deletions autoremovetorrents/client/utorrent.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#-*- coding:utf-8 -*-
import re
import time
from requests.auth import HTTPBasicAuth
import requests
from ..torrent import Torrent
from autoremovetorrents.exception.connectionfailure import ConnectionFailure
from autoremovetorrents.exception.deletionfailure import DeletionFailure
from autoremovetorrents.exception.loginfailure import LoginFailure
from autoremovetorrents.exception.nosuchtorrent import NoSuchTorrent
from autoremovetorrents.exception.remotefailure import RemoteFailure
Expand Down Expand Up @@ -38,7 +36,7 @@ def login(self, username, password):

pattern = re.compile('<[^>]+>')
text = request.text
if request.status_code == 200:
if request.status_code == 200:
self._token = pattern.sub('', text)
elif request.status_code == 401: # Error
raise LoginFailure('401 Unauthorized.')
Expand Down Expand Up @@ -73,7 +71,7 @@ def torrents_list(self):
for torrent in result['torrents']:
torrents_hash.append(torrent[0])
return torrents_hash

# Get Torrent Job Properties
def _torrent_job_properties(self, torrent_hash):
request = self._session.get(self._host+'/gui/',
Expand Down Expand Up @@ -108,7 +106,7 @@ def torrent_properties(self, torrent_hash):
torrent_obj.leecher = torrent[13]
torrent_obj.connected_leecher = torrent[12]
torrent_obj.progress = torrent[4]

return torrent_obj
# Not Found
raise NoSuchTorrent('No such torrent.')
Expand All @@ -134,17 +132,24 @@ def _judge_status(state, progress):
else:
status = TorrentStatus.Unknown
return status

# Remove Torrent
def remove_torrent(self, torrent_hash):
request = self._session.get(self._host+'/gui/',
params={'action':'remove', 'token':self._token, 'hash':torrent_hash})
if request.status_code != 200:
raise DeletionFailure('Cannot delete torrent %s. The server responses HTTP %d.' % (torrent_hash, request.status_code))

# Remove Torrent and Data
def remove_data(self, torrent_hash):

# Batch Remove Torrents
# Return values: (success_hash_list, failed_hash_list : {hash: failed_reason, ...})
def remove_torrents(self, torrent_hash_list, remove_data):
actions = {
True: 'removedata',
False: 'remove',
}
# According to the tests, it looks like uTorrent can accept a very long URL
# (more than 10,000 torrents per request)
# Therefore we needn't to set a URL length limitation
request = self._session.get(self._host+'/gui/',
params={'action':'removedata', 'token':self._token, 'hash':torrent_hash})
params={'action': actions[remove_data], 'token': self._token, 'hash': torrent_hash_list})
# Note: uTorrent doesn't report the status of each torrent
# We think that all the torrents are removed when the request is sent successfully
if request.status_code != 200:
raise DeletionFailure('Cannot delete torrent %s and its data. The server responses HTTP %d.' % (torrent_hash, request.status_code))
return ([], [{
'hash': torrent,
'reason': 'The server responses HTTP %d.' % request.status_code,
} for torrent in torrent_hash_list])
return (torrent_hash_list, [])
Loading

0 comments on commit 23e12cc

Please sign in to comment.