Browse Source

Updated rTorrent downloader to set ratio stop action, added new seeding methods and updated the rTorrent library

pull/1977/head
Dean Gardiner 12 years ago
parent
commit
7c680cac10
  1. 24
      couchpotato/core/downloaders/rtorrent/__init__.py
  2. 109
      couchpotato/core/downloaders/rtorrent/main.py
  3. 21
      libs/rtorrent/__init__.py
  4. 88
      libs/rtorrent/group.py
  5. 15
      libs/rtorrent/rpc/__init__.py
  6. 22
      libs/rtorrent/torrent.py

24
couchpotato/core/downloaders/rtorrent/__init__.py

@ -36,6 +36,30 @@ config = [{
'description': 'Label to apply on added torrents.', 'description': 'Label to apply on added torrents.',
}, },
{ {
'name': 'stop_complete',
'label': 'Stop torrent',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Stop the torrent after it finishes seeding'
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent after it finishes seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused', 'name': 'paused',
'type': 'bool', 'type': 'bool',
'default': False, 'default': False,

109
couchpotato/core/downloaders/rtorrent/main.py

@ -15,21 +15,64 @@ log = CPLog(__name__)
class rTorrent(Downloader): class rTorrent(Downloader):
type = ['torrent', 'torrent_magnet'] type = ['torrent', 'torrent_magnet']
rtorrent_api = None rt = None
def connect(self):
# Already connected?
if self.rt is not None:
return self.rt
# Ensure url is set
if not self.conf('url'):
log.error('Config properties are not filled in correctly, url is missing.')
return False
def get_conn(self):
if self.conf('username') and self.conf('password'): if self.conf('username') and self.conf('password'):
return RTorrent( self.rt = RTorrent(
self.conf('url'), self.conf('url'),
self.conf('username'), self.conf('username'),
self.conf('password') self.conf('password')
) )
else:
self.rt = RTorrent(self.conf('url'))
return self.rt
def _update_provider_group(self, name, data):
if data.get('seed_time') is not None:
log.info('seeding time ignored, not supported')
if name is None or data.get('seed_ratio') is None:
return False
if not self.connect():
return False
views = self.rt.get_views()
if name not in views:
self.rt.create_group(name)
log.debug('Updating provider ratio to %s, group name: %s', (data.get('seed_ratio'), name))
group = self.rt.get_group(name)
group.get_min(data.get('seed_ratio') * 100)
if self.conf('stop_complete'):
group.set_command('d.stop')
else:
group.set_command()
return RTorrent(self.conf('url'))
def download(self, data, movie, filedata=None): def download(self, data, movie, filedata=None):
log.debug('Sending "%s" (%s) to rTorrent.', (data.get('name'), data.get('type'))) log.debug('Sending "%s" (%s) to rTorrent.', (data.get('name'), data.get('type')))
if not self.connect():
return False
group_name = 'cp_' + data.get('provider').lower()
self._update_provider_group(group_name, data)
torrent_params = {} torrent_params = {}
if self.conf('label'): if self.conf('label'):
torrent_params['label'] = self.conf('label') torrent_params['label'] = self.conf('label')
@ -56,16 +99,16 @@ class rTorrent(Downloader):
# Send request to rTorrent # Send request to rTorrent
try: try:
if not self.rtorrent_api:
self.rtorrent_api = self.get_conn()
# Send torrent to rTorrent # Send torrent to rTorrent
torrent = self.rtorrent_api.load_torrent(filedata) torrent = self.rt.load_torrent(filedata)
# Set label # Set label
if self.conf('label'): if self.conf('label'):
torrent.set_custom(1, self.conf('label')) torrent.set_custom(1, self.conf('label'))
# Set Ratio Group
torrent.set_visible(group_name)
# Start torrent # Start torrent
if not self.conf('paused', default=0): if not self.conf('paused', default=0):
torrent.start() torrent.start()
@ -75,24 +118,30 @@ class rTorrent(Downloader):
log.error('Failed to send torrent to rTorrent: %s', err) log.error('Failed to send torrent to rTorrent: %s', err)
return False return False
def getAllDownloadStatus(self): def getAllDownloadStatus(self):
log.debug('Checking rTorrent download status.') log.debug('Checking rTorrent download status.')
try: if not self.connect():
if not self.rtorrent_api: return False
self.rtorrent_api = self.get_conn()
torrents = self.rtorrent_api.get_torrents() try:
torrents = self.rt.get_torrents()
statuses = StatusList(self) statuses = StatusList(self)
for item in torrents: for item in torrents:
status = 'busy'
if item.complete:
if item.active:
status = 'seeding'
else:
status = 'completed'
statuses.append({ statuses.append({
'id': item.info_hash, 'id': item.info_hash,
'name': item.name, 'name': item.name,
'status': 'completed' if item.complete else 'busy', 'status': status,
'seed_ratio': item.ratio,
'original_status': item.state, 'original_status': item.state,
'timeleft': str(timedelta(seconds=float(item.left_bytes) / item.down_rate)) 'timeleft': str(timedelta(seconds=float(item.left_bytes) / item.down_rate))
if item.down_rate > 0 else -1, if item.down_rate > 0 else -1,
@ -104,3 +153,33 @@ class rTorrent(Downloader):
except Exception, err: except Exception, err:
log.error('Failed to get status from rTorrent: %s', err) log.error('Failed to get status from rTorrent: %s', err)
return False return False
def pause(self, download_info, pause = True):
if not self.connect():
return False
torrent = self.rt.find_torrent(download_info['id'])
if torrent is None:
return False
if pause:
return torrent.pause()
return torrent.resume()
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
return self.processComplete(item, delete_files=True)
def processComplete(self, item, delete_files):
log.debug('Requesting rTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
torrent = self.rt.find_torrent(item['id'])
if torrent is None:
return False
if delete_files:
log.info('not deleting files, not supported')
return torrent.erase() # just removes the torrent, doesn't delete data

21
libs/rtorrent/__init__.py

@ -24,6 +24,7 @@ from rtorrent.lib.torrentparser import TorrentParser
from rtorrent.lib.xmlrpc.http import HTTPServerProxy from rtorrent.lib.xmlrpc.http import HTTPServerProxy
from rtorrent.rpc import Method, BasicAuthTransport from rtorrent.rpc import Method, BasicAuthTransport
from rtorrent.torrent import Torrent from rtorrent.torrent import Torrent
from rtorrent.group import Group
import os.path import os.path
import rtorrent.rpc # @UnresolvedImport import rtorrent.rpc # @UnresolvedImport
import time import time
@ -286,6 +287,26 @@ class RTorrent:
getattr(p, func_name)(finput) getattr(p, func_name)(finput)
def get_views(self):
p = self._get_conn()
return p.view_list()
def create_group(self, name, persistent=True, view=None):
p = self._get_conn()
if persistent is True:
p.group.insert_persistent_view('', name)
else:
assert view is not None, "view parameter required on non-persistent groups"
p.group.insert('', name, view)
def get_group(self, name):
assert name is not None, "group name required"
group = Group(self, name)
group.update()
return group
def set_dht_port(self, port): def set_dht_port(self, port):
"""Set DHT port """Set DHT port

88
libs/rtorrent/group.py

@ -0,0 +1,88 @@
# Copyright (c) 2013 Dean Gardiner, <gardiner91@gmail.com>
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import rtorrent.rpc
Method = rtorrent.rpc.Method
class Group:
__name__ = 'Group'
def __init__(self, _rt_obj, name):
self._rt_obj = _rt_obj
self.name = name
self.methods = [
# RETRIEVERS
Method(Group, 'get_max', 'group.' + self.name + '.ratio.max', varname='max'),
Method(Group, 'get_min', 'group.' + self.name + '.ratio.min', varname='min'),
Method(Group, 'get_upload', 'group.' + self.name + '.ratio.upload', varname='upload'),
# MODIFIERS
Method(Group, 'set_max', 'group.' + self.name + '.ratio.max.set', varname='max'),
Method(Group, 'set_min', 'group.' + self.name + '.ratio.min.set', varname='min'),
Method(Group, 'set_upload', 'group.' + self.name + '.ratio.upload.set', varname='upload')
]
rtorrent.rpc._build_rpc_methods(self, self.methods)
# Setup multicall_add method
caller = lambda multicall, method, *args: \
multicall.add(method, *args)
setattr(self, "multicall_add", caller)
def _get_prefix(self):
return 'group.' + self.name + '.ratio.'
def update(self):
multicall = rtorrent.rpc.Multicall(self)
retriever_methods = [m for m in self.methods
if m.is_retriever() and m.is_available(self._rt_obj)]
for method in retriever_methods:
multicall.add(method)
multicall.call()
def enable(self):
m = rtorrent.rpc.Multicall(self)
self.multicall_add(m, self._get_prefix() + 'enable')
return(m.call()[-1])
def disable(self):
m = rtorrent.rpc.Multicall(self)
self.multicall_add(m, self._get_prefix() + 'disable')
return(m.call()[-1])
def set_command(self, *methods):
methods = [m + '=' for m in methods]
m = rtorrent.rpc.Multicall(self)
self.multicall_add(
m, 'system.method.set',
self._get_prefix() + 'command',
*methods
)
return(m.call()[-1])

15
libs/rtorrent/rpc/__init__.py

@ -19,6 +19,7 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from base64 import encodestring from base64 import encodestring
import httplib import httplib
import inspect
import string import string
import rtorrent import rtorrent
@ -223,6 +224,8 @@ class Multicall:
result = process_result(method, r) result = process_result(method, r)
results_processed.append(result) results_processed.append(result)
# assign result to class_obj # assign result to class_obj
exists = hasattr(self.class_obj, method.varname)
if not exists or not inspect.ismethod(getattr(self.class_obj, method.varname)):
setattr(self.class_obj, method.varname, result) setattr(self.class_obj, method.varname, result)
return(tuple(results_processed)) return(tuple(results_processed))
@ -315,6 +318,11 @@ def process_result(method, result):
def _build_rpc_methods(class_, method_list): def _build_rpc_methods(class_, method_list):
"""Build glorified aliases to raw RPC methods""" """Build glorified aliases to raw RPC methods"""
instance = None
if not inspect.isclass(class_):
instance = class_
class_ = instance.__class__
for m in method_list: for m in method_list:
class_name = m.class_name class_name = m.class_name
if class_name != class_.__name__: if class_name != class_.__name__:
@ -337,6 +345,10 @@ def _build_rpc_methods(class_, method_list):
call_method(self, method, self.rpc_id, call_method(self, method, self.rpc_id,
bool_to_int(arg)) bool_to_int(arg))
elif class_name == "Group":
caller = lambda arg = None, method = m: \
call_method(instance, method, bool_to_int(arg))
if m.docstring is None: if m.docstring is None:
m.docstring = "" m.docstring = ""
@ -351,4 +363,7 @@ def _build_rpc_methods(class_, method_list):
caller.__doc__ = docstring caller.__doc__ = docstring
for method_name in [m.method_name] + list(m.aliases): for method_name in [m.method_name] + list(m.aliases):
if instance is None:
setattr(class_, method_name, caller) setattr(class_, method_name, caller)
else:
setattr(instance, method_name, caller)

22
libs/rtorrent/torrent.py

@ -190,6 +190,20 @@ class Torrent:
self.active = m.call()[-1] self.active = m.call()[-1]
return(self.active) return(self.active)
def pause(self):
"""Pause the torrent"""
m = rtorrent.rpc.Multicall(self)
self.multicall_add(m, "d.pause")
return(m.call()[-1])
def resume(self):
"""Resume the torrent"""
m = rtorrent.rpc.Multicall(self)
self.multicall_add(m, "d.resume")
return(m.call()[-1])
def close(self): def close(self):
"""Close the torrent and it's files""" """Close the torrent and it's files"""
m = rtorrent.rpc.Multicall(self) m = rtorrent.rpc.Multicall(self)
@ -305,6 +319,14 @@ class Torrent:
return(m.call()[-1]) return(m.call()[-1])
def set_visible(self, view, visible=True):
p = self._rt_obj._get_conn()
if visible:
return p.view.set_visible(self.info_hash, view)
else:
return p.view.set_not_visible(self.info_hash, view)
############################################################################ ############################################################################
# CUSTOM METHODS (Not part of the official rTorrent API) # CUSTOM METHODS (Not part of the official rTorrent API)
########################################################################## ##########################################################################

Loading…
Cancel
Save