diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py index b04e6898..026a56c6 100755 --- a/couchpotato/core/downloaders/rtorrent/__init__.py +++ b/couchpotato/core/downloaders/rtorrent/__init__.py @@ -23,6 +23,8 @@ config = [{ { 'name': 'url', 'default': 'http://localhost:80/RPC2', + 'description': 'XML-RPC Endpoint URI. Usually scgi://localhost:5000 ' + 'or http://localhost:80/RPC2' }, { 'name': 'username', @@ -38,7 +40,7 @@ config = [{ { 'name': 'directory', 'type': 'directory', - 'description': 'Directory where rtorrent should download the files too.', + 'description': 'Download to this directory. Keep empty for default rTorrent download directory.', }, { 'name': 'remove_complete', @@ -48,14 +50,6 @@ config = [{ 'type': 'bool', 'description': 'Remove the torrent after it finishes seeding.', }, - { - 'name': 'append_label', - 'label': 'Append Label', - 'default': False, - 'advanced': True, - 'type': 'bool', - 'description': 'Append label to download location. Requires you to set the download location above.', - }, { 'name': 'delete_files', 'label': 'Remove files', @@ -64,6 +58,14 @@ config = [{ 'advanced': True, 'description': 'Also remove the leftover files.', }, + { + 'name': 'append_label', + 'label': 'Append Label', + 'default': False, + 'advanced': True, + 'type': 'bool', + 'description': 'Append label to download location. Requires you to set the download location above.', + }, { 'name': 'paused', 'type': 'bool', diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py index b6ff73a0..05add8d4 100755 --- a/libs/rtorrent/__init__.py +++ b/libs/rtorrent/__init__.py @@ -17,18 +17,21 @@ # 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 urllib +import os.path +import time +import xmlrpclib from rtorrent.common import find_torrent, \ is_valid_port, convert_version_tuple_to_str from rtorrent.lib.torrentparser import TorrentParser from rtorrent.lib.xmlrpc.http import HTTPServerProxy -from rtorrent.rpc import Method, BasicAuthTransport +from rtorrent.lib.xmlrpc.scgi import SCGIServerProxy +from rtorrent.rpc import Method +from rtorrent.lib.xmlrpc.basic_auth import BasicAuthTransport from rtorrent.torrent import Torrent from rtorrent.group import Group -import os.path import rtorrent.rpc # @UnresolvedImport -import time -import xmlrpclib __version__ = "0.2.9" __author__ = "Chris Lucas" @@ -43,13 +46,25 @@ class RTorrent: """ Create a new rTorrent connection """ rpc_prefix = None - def __init__(self, url, username=None, password=None, - verify=False, sp=HTTPServerProxy, sp_kwargs={}): - self.url = url # : From X{__init__(self, url)} + def __init__(self, uri, username=None, password=None, + verify=False, sp=None, sp_kwargs=None): + self.uri = uri # : From X{__init__(self, url)} + self.username = username self.password = password - self.sp = sp - self.sp_kwargs = sp_kwargs + + self.schema = urllib.splittype(uri)[0] + + if sp: + self.sp = sp + elif self.schema in ['http', 'https']: + self.sp = HTTPServerProxy + elif self.schema == 'scgi': + self.sp = SCGIServerProxy + else: + raise NotImplementedError() + + self.sp_kwargs = sp_kwargs or {} self.torrents = [] # : List of L{Torrent} instances self._rpc_methods = [] # : List of rTorrent RPC methods @@ -62,12 +77,16 @@ class RTorrent: def _get_conn(self): """Get ServerProxy instance""" if self.username is not None and self.password is not None: + if self.schema == 'scgi': + raise NotImplementedError() + return self.sp( - self.url, + self.uri, transport=BasicAuthTransport(self.username, self.password), **self.sp_kwargs ) - return self.sp(self.url, **self.sp_kwargs) + + return self.sp(self.uri, **self.sp_kwargs) def _verify_conn(self): # check for rpc methods that should be available diff --git a/libs/rtorrent/lib/xmlrpc/basic_auth.py b/libs/rtorrent/lib/xmlrpc/basic_auth.py new file mode 100644 index 00000000..20c02d9a --- /dev/null +++ b/libs/rtorrent/lib/xmlrpc/basic_auth.py @@ -0,0 +1,73 @@ +# +# 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. + +from base64 import encodestring +import string +import xmlrpclib + + +class BasicAuthTransport(xmlrpclib.Transport): + def __init__(self, username=None, password=None): + xmlrpclib.Transport.__init__(self) + + self.username = username + self.password = password + + def send_auth(self, h): + if self.username is not None and self.password is not None: + h.putheader('AUTHORIZATION', "Basic %s" % string.replace( + encodestring("%s:%s" % (self.username, self.password)), + "\012", "" + )) + + def single_request(self, host, handler, request_body, verbose=0): + # issue XML-RPC request + + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + try: + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_auth(h) + self.send_content(h, request_body) + + response = h.getresponse(buffering=True) + if response.status == 200: + self.verbose = verbose + return self.parse_response(response) + except xmlrpclib.Fault: + raise + except Exception: + self.close() + raise + + #discard any response data and raise exception + if response.getheader("content-length", 0): + response.read() + raise xmlrpclib.ProtocolError( + host + handler, + response.status, response.reason, + response.msg, + ) diff --git a/libs/rtorrent/lib/xmlrpc/scgi.py b/libs/rtorrent/lib/xmlrpc/scgi.py new file mode 100644 index 00000000..39866971 --- /dev/null +++ b/libs/rtorrent/lib/xmlrpc/scgi.py @@ -0,0 +1,201 @@ +#!/usr/bin/python + +# rtorrent_xmlrpc +# (c) 2011 Roger Que +# +# Python module for interacting with rtorrent's XML-RPC interface +# directly over SCGI, instead of through an HTTP server intermediary. +# Inspired by Glenn Washburn's xmlrpc2scgi.py [1], but subclasses the +# built-in xmlrpclib classes so that it is compatible with features +# such as MultiCall objects. +# +# [1] +# +# Usage: server = SCGIServerProxy('scgi://localhost:7000/') +# server = SCGIServerProxy('scgi:///path/to/scgi.sock') +# print server.system.listMethods() +# mc = xmlrpclib.MultiCall(server) +# mc.get_up_rate() +# mc.get_down_rate() +# print mc() +# +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the +# OpenSSL library under certain conditions as described in each +# individual source file, and distribute linked combinations +# including the two. +# +# You must obey the GNU General Public License in all respects for +# all of the code used other than OpenSSL. If you modify file(s) +# with this exception, you may extend this exception to your version +# of the file(s), but you are not obligated to do so. If you do not +# wish to do so, delete this exception statement from your version. +# If you delete this exception statement from all source files in the +# program, then also delete it here. +# +# +# +# Portions based on Python's xmlrpclib: +# +# Copyright (c) 1999-2002 by Secret Labs AB +# Copyright (c) 1999-2002 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. + +import re +import socket +import urllib +import xmlrpclib + + +class SCGITransport(xmlrpclib.Transport): + def single_request(self, host, handler, request_body, verbose=0): + # Add SCGI headers to the request. + headers = {'CONTENT_LENGTH': str(len(request_body)), 'SCGI': '1'} + header = '\x00'.join(('%s\x00%s' % item for item in headers.iteritems())) + '\x00' + header = '%d:%s' % (len(header), header) + request_body = '%s,%s' % (header, request_body) + + sock = None + + try: + if host: + host, port = urllib.splitport(host) + addrinfo = socket.getaddrinfo(host, port, socket.AF_INET, + socket.SOCK_STREAM) + sock = socket.socket(*addrinfo[0][:3]) + sock.connect(addrinfo[0][4]) + else: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(handler) + + self.verbose = verbose + + sock.send(request_body) + return self.parse_response(sock.makefile()) + finally: + if sock: + sock.close() + + def parse_response(self, response): + p, u = self.getparser() + + response_body = '' + while True: + data = response.read(1024) + if not data: + break + response_body += data + + # Remove SCGI headers from the response. + response_header, response_body = re.split(r'\n\s*?\n', response_body, + maxsplit=1) + + if self.verbose: + print 'body:', repr(response_body) + + p.feed(response_body) + p.close() + + return u.close() + + +class SCGIServerProxy(xmlrpclib.ServerProxy): + def __init__(self, uri, transport=None, encoding=None, verbose=False, + allow_none=False, use_datetime=False): + type, uri = urllib.splittype(uri) + if type not in ('scgi'): + raise IOError('unsupported XML-RPC protocol') + self.__host, self.__handler = urllib.splithost(uri) + if not self.__handler: + self.__handler = '/' + + if transport is None: + transport = SCGITransport(use_datetime=use_datetime) + self.__transport = transport + + self.__encoding = encoding + self.__verbose = verbose + self.__allow_none = allow_none + + def __close(self): + self.__transport.close() + + def __request(self, methodname, params): + # call a method on the remote server + + request = xmlrpclib.dumps(params, methodname, encoding=self.__encoding, + allow_none=self.__allow_none) + + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + + if len(response) == 1: + response = response[0] + + return response + + def __repr__(self): + return ( + "" % + (self.__host, self.__handler) + ) + + __str__ = __repr__ + + def __getattr__(self, name): + # magic method dispatcher + return xmlrpclib._Method(self.__request, name) + + # note: to call a remote object with an non-standard name, use + # result getattr(server, "strange-python-name")(args) + + def __call__(self, attr): + """A workaround to get special attributes on the ServerProxy + without interfering with the magic __getattr__ + """ + if attr == "close": + return self.__close + elif attr == "transport": + return self.__transport + raise AttributeError("Attribute %r not found" % (attr,)) diff --git a/libs/rtorrent/rpc/__init__.py b/libs/rtorrent/rpc/__init__.py index 034f4eef..116ca1c2 100755 --- a/libs/rtorrent/rpc/__init__.py +++ b/libs/rtorrent/rpc/__init__.py @@ -17,66 +17,16 @@ # 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. -from base64 import encodestring -import httplib -import inspect -import string +import inspect import rtorrent import re from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\ safe_repr -from rtorrent.err import RTorrentVersionError, MethodError +from rtorrent.err import MethodError from rtorrent.compat import xmlrpclib -class BasicAuthTransport(xmlrpclib.Transport): - def __init__(self, username=None, password=None): - xmlrpclib.Transport.__init__(self) - self.username = username - self.password = password - - def send_auth(self, h): - if self.username is not None and self.password is not None: - h.putheader('AUTHORIZATION', "Basic %s" % string.replace( - encodestring("%s:%s" % (self.username, self.password)), - "\012", "" - )) - - def single_request(self, host, handler, request_body, verbose=0): - # issue XML-RPC request - - h = self.make_connection(host) - if verbose: - h.set_debuglevel(1) - - try: - self.send_request(h, handler, request_body) - self.send_host(h, host) - self.send_user_agent(h) - self.send_auth(h) - self.send_content(h, request_body) - - response = h.getresponse(buffering=True) - if response.status == 200: - self.verbose = verbose - return self.parse_response(response) - except xmlrpclib.Fault: - raise - except Exception: - self.close() - raise - - #discard any response data and raise exception - if (response.getheader("content-length", 0)): - response.read() - raise xmlrpclib.ProtocolError( - host + handler, - response.status, response.reason, - response.msg, - ) - - def get_varname(rpc_call): """Transform rpc method into variable name. diff --git a/libs/rtorrent/torrent.py b/libs/rtorrent/torrent.py index c610e368..bd6bb689 100755 --- a/libs/rtorrent/torrent.py +++ b/libs/rtorrent/torrent.py @@ -172,6 +172,17 @@ class Torrent: self.directory = m.call()[-1] + def set_directory_base(self, d): + """Modify base download directory + + @note: Needs to stop torrent in order to change the directory. + Also doesn't restart after directory is set, that must be called + separately. + """ + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.try_stop") + self.multicall_add(m, "d.set_directory_base", d) + def start(self): """Start the torrent""" m = rtorrent.rpc.Multicall(self)