From 7c680cac10b98861427407bee7af7c612113afe8 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Wed, 31 Jul 2013 22:41:20 +1200 Subject: [PATCH] Updated rTorrent downloader to set ratio stop action, added new seeding methods and updated the rTorrent library --- couchpotato/core/downloaders/rtorrent/__init__.py | 24 +++++ couchpotato/core/downloaders/rtorrent/main.py | 109 +++++++++++++++++++--- libs/rtorrent/__init__.py | 21 +++++ libs/rtorrent/group.py | 88 +++++++++++++++++ libs/rtorrent/rpc/__init__.py | 19 +++- libs/rtorrent/torrent.py | 22 +++++ 6 files changed, 266 insertions(+), 17 deletions(-) create mode 100755 libs/rtorrent/group.py diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py index f3944c7..b28f580 100755 --- a/couchpotato/core/downloaders/rtorrent/__init__.py +++ b/couchpotato/core/downloaders/rtorrent/__init__.py @@ -36,6 +36,30 @@ config = [{ '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', 'type': 'bool', 'default': False, diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index 7544af8..33243fe 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -15,21 +15,64 @@ log = CPLog(__name__) class rTorrent(Downloader): 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'): - return RTorrent( + self.rt = RTorrent( self.conf('url'), self.conf('username'), 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): 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 = {} if self.conf('label'): torrent_params['label'] = self.conf('label') @@ -56,16 +99,16 @@ class rTorrent(Downloader): # Send request to rTorrent try: - if not self.rtorrent_api: - self.rtorrent_api = self.get_conn() - # Send torrent to rTorrent - torrent = self.rtorrent_api.load_torrent(filedata) + torrent = self.rt.load_torrent(filedata) # Set label if self.conf('label'): torrent.set_custom(1, self.conf('label')) + # Set Ratio Group + torrent.set_visible(group_name) + # Start torrent if not self.conf('paused', default=0): torrent.start() @@ -75,24 +118,30 @@ class rTorrent(Downloader): log.error('Failed to send torrent to rTorrent: %s', err) return False - def getAllDownloadStatus(self): - log.debug('Checking rTorrent download status.') - try: - if not self.rtorrent_api: - self.rtorrent_api = self.get_conn() + if not self.connect(): + return False - torrents = self.rtorrent_api.get_torrents() + try: + torrents = self.rt.get_torrents() statuses = StatusList(self) for item in torrents: + status = 'busy' + if item.complete: + if item.active: + status = 'seeding' + else: + status = 'completed' + statuses.append({ 'id': item.info_hash, 'name': item.name, - 'status': 'completed' if item.complete else 'busy', + 'status': status, + 'seed_ratio': item.ratio, 'original_status': item.state, 'timeleft': str(timedelta(seconds=float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1, @@ -104,3 +153,33 @@ class rTorrent(Downloader): except Exception, err: log.error('Failed to get status from rTorrent: %s', err) 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 diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py index e427b65..d19c78b 100755 --- a/libs/rtorrent/__init__.py +++ b/libs/rtorrent/__init__.py @@ -24,6 +24,7 @@ from rtorrent.lib.torrentparser import TorrentParser from rtorrent.lib.xmlrpc.http import HTTPServerProxy from rtorrent.rpc import Method, BasicAuthTransport from rtorrent.torrent import Torrent +from rtorrent.group import Group import os.path import rtorrent.rpc # @UnresolvedImport import time @@ -286,6 +287,26 @@ class RTorrent: 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): """Set DHT port diff --git a/libs/rtorrent/group.py b/libs/rtorrent/group.py new file mode 100755 index 0000000..01f6bb3 --- /dev/null +++ b/libs/rtorrent/group.py @@ -0,0 +1,88 @@ +# Copyright (c) 2013 Dean Gardiner, +# 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]) diff --git a/libs/rtorrent/rpc/__init__.py b/libs/rtorrent/rpc/__init__.py index f83446c..034f4ee 100755 --- a/libs/rtorrent/rpc/__init__.py +++ b/libs/rtorrent/rpc/__init__.py @@ -19,6 +19,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from base64 import encodestring import httplib +import inspect import string import rtorrent @@ -223,7 +224,9 @@ class Multicall: result = process_result(method, r) results_processed.append(result) # assign result to class_obj - setattr(self.class_obj, method.varname, result) + 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) return(tuple(results_processed)) @@ -315,6 +318,11 @@ def process_result(method, result): def _build_rpc_methods(class_, method_list): """Build glorified aliases to raw RPC methods""" + instance = None + if not inspect.isclass(class_): + instance = class_ + class_ = instance.__class__ + for m in method_list: class_name = m.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, 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: m.docstring = "" @@ -351,4 +363,7 @@ def _build_rpc_methods(class_, method_list): caller.__doc__ = docstring for method_name in [m.method_name] + list(m.aliases): - setattr(class_, method_name, caller) + if instance is None: + setattr(class_, method_name, caller) + else: + setattr(instance, method_name, caller) diff --git a/libs/rtorrent/torrent.py b/libs/rtorrent/torrent.py index 1e06e1c..c610e36 100755 --- a/libs/rtorrent/torrent.py +++ b/libs/rtorrent/torrent.py @@ -190,6 +190,20 @@ class Torrent: self.active = m.call()[-1] 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): """Close the torrent and it's files""" m = rtorrent.rpc.Multicall(self) @@ -305,6 +319,14 @@ class Torrent: 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) ##########################################################################