├── .gitignore ├── BUGS ├── COPYING ├── README ├── TODO ├── bin ├── πϱ └── πϱ.pth ├── cli.py ├── cli.sh ├── config.py ├── flask-config.py ├── lib ├── __init__.py ├── a.py ├── baserequester.py ├── config_parser.py ├── decorators.py ├── filerequester.py ├── filetree.py ├── helper.py ├── multibase.py ├── peerrequester.py ├── rtorrentquery.py ├── torrentquery.py ├── torrentrequester.py ├── xmlrpc.py └── xmlrpc2scgi.py ├── model ├── __init__.py ├── peer.py ├── pyro │ ├── __init__.py │ └── user.py ├── rtorrent.py ├── torrent.py └── torrentfile.py ├── pyrotorrent.py ├── requirements.txt ├── rtorrentcommands.txt ├── sphinx ├── Makefile ├── baserequester.rst ├── conf.py ├── decorator.rst ├── develintro.rst ├── index.rst ├── multibase.rst ├── peer.rst ├── peerrequester.rst ├── rtorrent.rst ├── rtorrentquery.rst ├── setup.rst ├── torrent.rst ├── torrentquery.rst └── torrentrequester.rst ├── static ├── bg.png ├── favicon.ico ├── favicon.png ├── icons │ ├── add.png │ ├── arrow_down.png │ ├── arrow_up.png │ └── readme.txt ├── pylons-logo.gif └── style_old.css ├── templates ├── 404.html ├── base.jinja2 ├── download_list.html ├── error.html ├── loginform.html ├── style.css ├── torrent_add.html └── torrent_info.html ├── test.py ├── tests └── api.py └── tools └── gen_rtorrent_doc.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | -------------------------------------------------------------------------------- /BUGS: -------------------------------------------------------------------------------- 1 | - login / sessions are broken when using built-in httpd. 2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | πϱTorrent (pronounced/also pyroTorrent) 2 | 3 | will be a lean web interface to rTorrent written in Python, 4 | thereby getting rid of PHP. 5 | 6 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Admin panel: 2 | - User Management [ ] 3 | - Shutdown rtorrent? (and possibly start rtorrent?) [ ] 4 | - Change encryption [ ] 5 | 6 | Configuration: 7 | - Clean up [ ] 8 | - Move to SQLite? (SQLAlchemy!) [ ] 9 | CLI tools + admin interface. 10 | 11 | Download Management: 12 | - List torrents. [X] 13 | - Create custom views [ ] 14 | - Add/Remove torrents from views. [ ] 15 | - Add a torrent [X] 16 | 17 | Events and Schedulers: 18 | - Schedule stuff 19 | - Manage events. (Provide default mail scripts?) 20 | 21 | Statistics: 22 | - Disk Usage [ ] 23 | - Memory Usage [ ] 24 | - CPU Usage [ ] 25 | - Current upload / download rate [X] 26 | - Current active torrents [ ] 27 | - Complete Torrents [ ] 28 | - Incomplete Torrents [ ] 29 | 30 | API: 31 | - pyroTorrent handlers [X] 32 | - Initial specification [X] 33 | - Fine grained access per user over the API [ ] 34 | 35 | - AJAX/JS support on the web page: [ ] 36 | - Use AJAX for the main page. Fetch parallel from 37 | several rtorrent instances. [ ] 38 | 39 | - Torrent action: start,stop,pause,resume,delete [ ] 40 | 41 | - Change rtorrent up/down throttle [ ] 42 | - Change other rtorrent settings [ ] 43 | 44 | Front page design: 45 | - Needs a lot more icons. [ ] 46 | - Display when a torrent is no longer functional / 47 | active with colours. A different colour when the 48 | ``downloaded file'' is gone. [ ] 49 | 50 | Version 0.0: 51 | - Show pages of torrents with: speed, completeness [X] 52 | - Add torrent link [X] 53 | - Show current up and down speed [X] 54 | - Add torrents [X] 55 | - Add support for views [X] 56 | - Global progress bar [ ] 57 | - Torrents Page [X] 58 | - List torrent peers [ ] 59 | - List trackers. [ ] 60 | 61 | - List torrent files [X] 62 | - Clean up the root name of the file tree [ ] 63 | - Fix markup for files tree [ ] 64 | 65 | - Interface should work without a rtorrent connection [X] 66 | - Implement XMLRPC Exceptions, own wrapper? [X] 67 | 68 | - Download torrent files [X] 69 | 70 | Debug: 71 | - RTorrent ``info'' page, where we show all the available rtorrent 72 | information. (eg, all rtorrent calls that return info) [ ] 73 | 74 | 75 | 76 | Far fetched TODO: 77 | 78 | - Prioritize torrent files [ ] 79 | - Possiblity to set (and manage) directory per torrent file. 80 | (Where it has to be copied or downloaded to) 81 | 82 | - Graphs 83 | - Database support 84 | - FTP Access / Download torrent files via web interface, 85 | - Advanced security. Download limit per user, etc. 86 | - Private views per user? 87 | 88 | -------------------------------------------------------------------------------- /bin/πϱ: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import os 5 | import sys 6 | import argparse 7 | import re 8 | 9 | conf_path = unicode(os.getenv('XDG_CONFIG_HOME', os.getenv('HOME') + \ 10 | '/.config')) 11 | conf_path += u'/πϱ' 12 | #print conf_path 13 | 14 | sys.path.append(conf_path) 15 | 16 | try: 17 | from pyroconfig import rtorrent_config, lib_path 18 | except ImportError: 19 | print 'pyroconfig.py not found in', conf_path 20 | sys.exit(1) 21 | 22 | sys.path.append(lib_path) 23 | 24 | from lib.config_parser import parse_config_part, \ 25 | RTorrentConfigException, CONNECTION_SCGI, CONNECTION_HTTP 26 | 27 | from model.rtorrent import RTorrent 28 | from model.torrent import Torrent 29 | 30 | from lib.torrentrequester import TorrentRequester 31 | 32 | from lib.filerequester import TorrentFileRequester 33 | from lib.filetree import FileTree 34 | 35 | import xmlrpclib 36 | from lib.xmlrpc import RTorrentXMLRPC 37 | 38 | parser = argparse.ArgumentParser(description='πϱ command tool', 39 | epilog='Beter een torrent op de harddisk dan 10 in de cloud') 40 | parser.add_argument('-l', '--list', type=str, 41 | action='append', default=[], 42 | help='List torrents.') 43 | parser.add_argument('-t', '--torrent', type=str, 44 | action='append', default=[], 45 | help='Torrent actions. Requires hash.'\ 46 | 'Valid actions are: open, close, start, stop, pause,'\ 47 | 'resume, erase, info, files. Default action=\'info\'') 48 | 49 | for x in ('open', 'close', 'start', 'stop', 'pause','resume', 'erase', 'files'): 50 | parser.add_argument('--' + x + '-torrent', type=str, 51 | action='append', default=[], 52 | help='Same as --torrent, but action is implied. Argument is a hash') 53 | 54 | parser.add_argument('--add-file', type=str, 55 | action='append', default=[], 56 | help='Add torrent file specified by path; requires an explicit target.') 57 | 58 | parser.add_argument('--add-stdin', 59 | action='store_true', 60 | help='Add torrent file from stdin; requires an explicit target.') 61 | 62 | parser.add_argument('--add-magnet', type=str, 63 | action='append', default=[], 64 | help='Add torrent specified by magnet; requires an explicit target.') 65 | 66 | parser.add_argument('--target', type=str, 67 | default=None, 68 | help='Target for target-specific operations. You want to set this'\ 69 | 'if you\'re not just listing torrents and files') 70 | parser.add_argument('--view', type=str, 71 | default='default', 72 | help='view for target-specific operations. you only need to set this'\ 73 | 'if you do not want to use the default view for --list') 74 | 75 | parser.add_argument('--raw', type=str, 76 | action='append', default=[], 77 | help='Raw command to execute on specified target.') 78 | 79 | parser.add_argument('--pretty', action='store_true', 80 | help='Pretty print') 81 | 82 | args = parser.parse_args() 83 | 84 | def parse_config(): 85 | targets = [] 86 | for x in rtorrent_config: 87 | try: 88 | info = parse_config_part(rtorrent_config[x], x) 89 | except RTorrentConfigException, e: 90 | print 'Invalid config: ', e 91 | sys.exit(1) 92 | 93 | targets.append(info) 94 | 95 | return targets 96 | 97 | def handle_list(lists, view, pretty=False): 98 | for l in lists: 99 | reg = re.compile(l, re.IGNORECASE) 100 | for x in targets: 101 | if target and x != target: 102 | continue 103 | if pretty: 104 | print '-' * 80 105 | print x['name'] 106 | print '-' * 80 107 | treq = TorrentRequester(x, view) 108 | treq.get_name().get_hash() 109 | treq = treq.all() 110 | 111 | for t in treq: 112 | if reg.findall(t.get_name): 113 | if pretty: 114 | s = '| %s: ' % t.get_hash 115 | s += t.get_name.encode('utf-8')[:(74 - 42)] 116 | s += (78 - len(s)) * ' ' 117 | s += '|' 118 | print s 119 | else: 120 | print t.get_hash, t.get_name.encode('utf-8') 121 | 122 | def dfs(node, dfs_depth=0): 123 | print (' ' * dfs_depth * 2) + node.name 124 | 125 | if hasattr(node, 'children'): 126 | for x in node.children: 127 | dfs(x, dfs_depth+1) 128 | 129 | def handle_torrent(torrents): 130 | def info(target, t): 131 | t = Torrent(target, t) 132 | q = t.query() 133 | q.get_hash().get_name().get_size_bytes().get_download_total().\ 134 | get_loaded_file().get_message().is_active().get_full_path() 135 | t = q.first() 136 | print 'Hash:', t.get_hash 137 | print 'Name:', t.get_name.encode('utf-8') 138 | print 'Active:', t.is_active 139 | print 'Size:', t.get_size_bytes 140 | print 'Loaded file:', t.get_loaded_file 141 | print 'Message:', t.get_message 142 | print 'Full path:', t.get_full_path.encode('utf-8') 143 | 144 | def files(target, t): 145 | t = Torrent(target, t) 146 | files = TorrentFileRequester(target, t._hash)\ 147 | .get_path_components().get_size_chunks()\ 148 | .get_completed_chunks().all() 149 | root_node = FileTree(files).root 150 | dfs(root_node) 151 | 152 | 153 | a = {'info' : info, 'files' : files} 154 | 155 | for x in targets: 156 | if target and x != target: 157 | continue 158 | for t in torrents: 159 | try: 160 | _hash, action = t.split(',') 161 | except ValueError: 162 | _hash = t 163 | action = 'info' 164 | 165 | if action not in ('open', 'close', 'start', 'stop', 'pause', \ 166 | 'resume', 'erase', 'info', 'files'): 167 | print >>sys.stderr, 'Invalid action:', action 168 | continue 169 | print _hash, action 170 | try: 171 | a[action](x, _hash) 172 | except KeyError: 173 | t = Torrent(x, _hash) 174 | getattr(t, action)() 175 | 176 | def handle_stdin(): 177 | if not target: 178 | print 'Adding torrent requires a specific target!' 179 | return 180 | try: 181 | import xmlrpclib 182 | except ImportError: 183 | print 'xmlrpclib wasn\'t found. Not adding file!' 184 | return 185 | 186 | f = sys.stdin 187 | s = f.read() 188 | b = xmlrpclib.Binary(s) 189 | 190 | print target 191 | 192 | rtorrent = RTorrent(target) 193 | return_code = rtorrent.add_torrent_raw_start(b) 194 | if return_code == 0: 195 | print 'Successfully added torrent from stdin.' 196 | 197 | def handle_magnet(magnet_links): 198 | if not target: 199 | print 'Adding torrent requires a specific target!' 200 | return 201 | for magnet_link in magnet_links: 202 | torrent = 'd10:magnet-uri' + str(len(magnet_link)) + ':' + magnet_link + 'e' 203 | rtorrent = RTorrent(target) 204 | return_code = rtorrent.add_torrent_raw(torrent) 205 | if return_code == 0: 206 | print 'Successfully added torrent from magnet', magnet_link 207 | 208 | 209 | def handle_file(filenames): 210 | if not target: 211 | print 'Adding torrent requires a specific target!' 212 | return 213 | for filename in filenames: 214 | try: 215 | torrent_raw = open(filename).read() 216 | except IOError: 217 | print 'File not found:', filename 218 | continue 219 | 220 | torrent_raw_bin = xmlrpclib.Binary(torrent_raw) 221 | 222 | rtorrent = RTorrent(target) 223 | return_code = rtorrent.add_torrent_raw_start(torrent_raw_bin) 224 | 225 | if return_code == 0: 226 | print 'Succesfully added torrent from file', filename 227 | 228 | def handle_raw(cmds): 229 | for x in targets: 230 | if target and x != target: 231 | continue 232 | 233 | rpc = RTorrentXMLRPC(x) 234 | m = xmlrpclib.MultiCall(rpc) 235 | 236 | for raw_cmd in cmds: 237 | cmd = raw_cmd.split('=', 2) 238 | 239 | if len(cmd) > 1: 240 | getattr(m, cmd[0])(cmd[1]) 241 | else: 242 | getattr(m, cmd[0])() 243 | 244 | for cmd, ret in zip(cmds, m()): 245 | print cmd, ':', ret 246 | 247 | 248 | targets = parse_config() 249 | target = None 250 | 251 | if args.target: 252 | for x in targets: 253 | if x['name'] == args.target: 254 | target = x 255 | if target == None: 256 | raise Exception('Invalid target: %s' % args.target) 257 | 258 | handle_list(args.list, args.view) 259 | 260 | handle_torrent(args.torrent) 261 | 262 | for x in ('open', 'close', 'start', 'stop', 'pause','resume', 'erase', 'files'): 263 | torr = getattr(args, x + '_torrent') 264 | for y in torr: 265 | handle_torrent([y + ',' + x]) 266 | 267 | if args.add_stdin: 268 | handle_stdin() 269 | 270 | if args.add_magnet: 271 | handle_magnet(args.add_magnet) 272 | 273 | if args.add_file: 274 | handle_file(args.add_file) 275 | 276 | if args.raw: 277 | handle_raw(args.raw) 278 | 279 | sys.exit(0) 280 | -------------------------------------------------------------------------------- /bin/πϱ.pth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/bin/πϱ.pth -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from model.torrent import Torrent 3 | from model.rtorrent import RTorrent 4 | from model.peer import Peer 5 | 6 | from lib.torrentrequester import TorrentRequester 7 | from lib.peerrequester import PeerRequester 8 | 9 | from config import rtorrent_config 10 | from lib.config_parser import parse_config_part, RTorrentConfigException 11 | 12 | targets = [] 13 | for x in rtorrent_config: 14 | try: 15 | info = parse_config_part(rtorrent_config[x], x) 16 | except RTorrentConfigException as e: 17 | print('Invalid config: ', e) 18 | sys.exit(1) 19 | 20 | targets.append(info) 21 | 22 | if len(x) == 0: 23 | print('No targets') 24 | sys.exit(1) 25 | 26 | r = RTorrent(targets[0]) 27 | 28 | print('''Welcome to the pyroTorrent CLI interface. 29 | We have created a RTorrent() object called 'r'. 30 | 31 | Use the help() commands to find out how to use the client interface; 32 | help(RTorrent), help(Torrent) might be helpful''') 33 | 34 | print('RTorrent object using target: ', targets[0]) 35 | -------------------------------------------------------------------------------- /cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/env python -i -c 'from cli import *' 4 | 5 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # Place all your globals here 2 | 3 | # ``Base'' URL for your HTTP website 4 | # This URL should not end with a trailing slash 5 | # doing so would break redirect behaviour. 6 | BASE_URL = '/torrent' 7 | # HTTP URL for the static files 8 | STATIC_URL = BASE_URL + '/static' 9 | USE_OWN_HTTPD = False 10 | 11 | # Default downloads blocksize 12 | FILE_BLOCK_SIZE = 4096 13 | 14 | # Default background 15 | BACKGROUND_IMAGE = 'cat.jpg' 16 | 17 | USE_AUTH = True 18 | 19 | ENABLE_API = False 20 | 21 | CACHE_TIMEOUT=10 22 | 23 | torrent_users = { 24 | 'test' : { 25 | 'targets' : [], 26 | 'background-image' : 'space1.png', 27 | 'password' : 'test' 28 | } 29 | } 30 | 31 | ## Exemplary SCGI setup using unix socket 32 | #rtorrent_config = { 33 | # 'sheeva' : { 34 | # 'scgi' : { 35 | # 'unix-socket' : '/tmp/rtorrent.sock' 36 | # } 37 | # } 38 | #} 39 | # 40 | ## Exemplary SCGI setup using scgi over network 41 | #rtorrent_config = { 42 | # 'sheeva' : { 43 | # 'scgi' : { 44 | # 'host' : '192.168.1.70', 45 | # 'port' : 80 46 | # } 47 | # } 48 | #} 49 | 50 | # Exemplary HTTP setup using remote XMLRPC server. (SCGI is handled by the HTTPD 51 | # in this case) 52 | rtorrent_config = { 53 | 'sheeva' : { 54 | 'http' : { 55 | 'host' : '192.168.1.70', 56 | 'port' : 80, 57 | 'url' : '/RPC2', 58 | } 59 | } 60 | # , 61 | # 'sheevareborn' : { 62 | # 'http' : { 63 | # 'host' : '42.42.42.42', 64 | # 'port' : 80, 65 | # 'url' : '/RPC2', 66 | # } 67 | # } 68 | } 69 | -------------------------------------------------------------------------------- /flask-config.py: -------------------------------------------------------------------------------- 1 | # ``Base'' URL for your HTTP website 2 | # This URL should not end with a trailing slash 3 | # doing so would break redirect behaviour. 4 | 5 | # Set this to '' when using the built-in HTTPD. 6 | #APPLICATION_ROOT = '' 7 | 8 | APPLICATION_ROOT = '/torrent' 9 | 10 | SECRET_KEY = 'development key' 11 | DEBUG = True 12 | USERNAME = 'admin' 13 | PASSWORD = 'default' 14 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/lib/__init__.py -------------------------------------------------------------------------------- /lib/a.py: -------------------------------------------------------------------------------- 1 | import simplejson as json 2 | 3 | """ 4 | [ 5 | { 6 | "attributes": [ 7 | [ 8 | "get_name", 9 | [] 10 | ], 11 | [ 12 | "get_download_rate", 13 | [] 14 | ], 15 | [ 16 | "get_upload_rate", 17 | [] 18 | ], 19 | [ 20 | "is_complete", 21 | [] 22 | ], 23 | [ 24 | "get_size_bytes", 25 | [] 26 | ], 27 | [ 28 | "get_download_total", 29 | [] 30 | ], 31 | [ 32 | "get_hash", 33 | [] 34 | ] 35 | ], 36 | "type": "torrentrequester", 37 | "target": "sheevareborn", 38 | "view": "" 39 | }, 40 | { 41 | "attributes": [ 42 | [ 43 | "set_upload_throttle", 44 | [ 45 | 20480 46 | ] 47 | ], 48 | [ 49 | "get_upload_throttle", 50 | [] 51 | ] 52 | ], 53 | "type": "rtorrent", 54 | "target": "sheevareborn" 55 | }, 56 | { 57 | "attributes": [ 58 | [ 59 | "set_upload_throttle", 60 | [ 61 | 40960 62 | ] 63 | ], 64 | [ 65 | "get_upload_throttle", 66 | [] 67 | ] 68 | ], 69 | "type": "rtorrent", 70 | "target": "sheevareborn" 71 | }, 72 | { 73 | "hash": "8EB5801B88D34D50A6E7594B6678A2CF6224766E", 74 | "type": "torrent", 75 | "target": "sheevareborn", 76 | "attributes": [ 77 | [ 78 | "get_hash", 79 | [] 80 | ], 81 | [ 82 | "get_name", 83 | [] 84 | ], 85 | [ 86 | "is_complete", 87 | [] 88 | ] 89 | ] 90 | } 91 | ] 92 | """ 93 | 94 | -------------------------------------------------------------------------------- /lib/baserequester.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _baserequester-class: 3 | 4 | BaseRequester 5 | ============= 6 | 7 | BaseRequester is a class created to provide an easy way to create classes that 8 | implement a lot of operations over XMLRPC. It is not the same as 9 | :ref:`multibase-class` in the sense that this performs operations on all the 10 | items in the view. (For example, :ref:`torrentrequester-class` implements a 11 | method to get the name of each torrent in a view by simply calling .get_name(). 12 | """ 13 | 14 | from lib.xmlrpc import RTorrentXMLRPC 15 | from lib.multibase import InvalidTorrentCommandException 16 | 17 | 18 | class BaseRequester(object): 19 | """ 20 | """ 21 | 22 | def __init__(self, target, *extra_args): 23 | """ 24 | """ 25 | self.s = RTorrentXMLRPC(target) 26 | self.target = target['name'] 27 | self.extra_args = extra_args 28 | 29 | # Stack to put commands on 30 | self.commandstack = [] 31 | 32 | # Same as commandstack, but stores the original names. 33 | # We need to for .all() 34 | self.commandistack = [] 35 | 36 | # Contains possible arguments. 37 | self.commands = {} 38 | 39 | def __hash__(self): 40 | h = 42 ^ hash(self.target) 41 | 42 | for x in self.commandstack: 43 | h ^= hash(x) 44 | for y in self.commands[x]: 45 | h ^= hash(y) 46 | 47 | for x in self.extra_args: 48 | h ^= hash(x) 49 | 50 | return h 51 | 52 | def __call__(self, *args): 53 | """ 54 | Return self so we can chain calls: 55 | """ 56 | if len(args): 57 | raise InvalidTorrentCommandException('No parameters are supported' \ 58 | ' yet') 59 | 60 | self.commands[self.commandstack[-1]] = args 61 | return self 62 | 63 | def __getattr__(self, attr): 64 | """ 65 | Used to add commands. 66 | """ 67 | try: 68 | self.append_command(attr) 69 | except AttributeError as e: 70 | raise InvalidTorrentCommandException(e.message) 71 | return self 72 | 73 | def append_command(self, command): 74 | """ 75 | Add commands to the stack. 76 | """ 77 | # TODO: Find out how set commands work. 78 | oldcommand = command 79 | command = self._convert_command(command) 80 | 81 | self.commands[command] = () 82 | self.commandstack.append(command) 83 | self.commandistack.append(oldcommand) 84 | 85 | def _fetch(self): 86 | """ 87 | Executes the current command stack. Stores results in the class. 88 | """ 89 | rpc_commands = [] 90 | for x in self.commandstack: 91 | rpc_commands.append('%s=' % x) 92 | if len(self.commands[x]): 93 | pass # TODO: Add args for set* 94 | 95 | res = self.dofetch(*rpc_commands) 96 | return res 97 | 98 | 99 | def all(self): 100 | """ 101 | Returns a list of the results. 102 | """ 103 | _res = self._fetch() 104 | self.__res = [DictAttribute(list(zip(self.commandistack, x))) for x in _res] 105 | return self.__res 106 | 107 | def as_list(self): 108 | """ 109 | """ 110 | _res = self._fetch() 111 | return _res 112 | 113 | # XXX: When do we use this? Do we use it all? Do we need it at all? 114 | # Do we have it here just as a convenience for the user? 115 | def flush(self): 116 | del self.commandstack 117 | del self.commands 118 | del self.commandistack 119 | self.commandstack = [] 120 | self.commandistack = [] 121 | self.commands = {} 122 | 123 | class DictAttribute(dict): 124 | def __getattr__(self, attr): 125 | if attr in self: 126 | return self[attr] 127 | else: 128 | raise AttributeError('%s not in dict' % attr) 129 | 130 | -------------------------------------------------------------------------------- /lib/config_parser.py: -------------------------------------------------------------------------------- 1 | from model.pyro.user import PyroUser 2 | 3 | CONNECTION_SCGI, CONNECTION_HTTP = list(range(2)) 4 | 5 | class RTorrentConfigException(Exception): 6 | pass 7 | 8 | def _parse_config_part_connection(config_dict, name): 9 | """ 10 | Parse and verify rtorrent connection configuration. 11 | """ 12 | 13 | if 'scgi' in config_dict and 'http' in config_dict: 14 | raise RTorrentConfigException('Ambigious configuration for: %s\n' 15 | 'You cannot have both a \'scgi\' line and a \'http\' key.' \ 16 | % name) 17 | 18 | if 'scgi' in config_dict: 19 | if 'unix-socket' in config_dict['scgi']: 20 | # TODO: Check path 21 | return \ 22 | { 23 | 'type' : CONNECTION_SCGI, 24 | 'fd' : 'scgi://%s' % config_dict['scgi']['unix-socket'], 25 | 'name' : name 26 | } 27 | elif 'host' in config_dict['scgi'] and \ 28 | 'port' in config_dict['scgi']: 29 | return \ 30 | { 31 | 'type' : CONNECTION_SCGI, 32 | 'fd' : 'scgi://%s:%d' % (config_dict['scgi']['host'], \ 33 | config_dict['scgi']['port']), 34 | 'name' : name 35 | } 36 | else: 37 | raise RTorrentConfigException('Config lacks specific scgi' 38 | 'information. Needs host and port or unix-socket.') 39 | 40 | elif 'http' in config_dict: 41 | return \ 42 | { 43 | 'type' : CONNECTION_HTTP, 44 | 'host' : config_dict['http']['host'], 45 | 'port' : config_dict['http']['port'], 46 | 'url' : config_dict['http']['url'], 47 | 'name' : name 48 | } 49 | else: 50 | raise RTorrentConfigException('Config lacks scgi of http information') 51 | 52 | def _parse_config_part_storage(config_dict, info): 53 | """ 54 | Verify and parse any file storage configuration. 55 | """ 56 | 57 | if 'storage_mode' in config_dict: 58 | storage_mode = config_dict['storage_mode'] 59 | if 'remote_path' in storage_mode: 60 | if 'local_path' not in storage_mode: 61 | raise RTorrentConfigException('Remote storage mode, ' + 62 | 'missing local_path to mount point') 63 | elif 'local_path' in storage_mode: 64 | if storage_mode['local_path'] != '/': 65 | raise RTorrentConfigException('Local storage mode, not ' + 66 | 'pointed to root') 67 | else: 68 | raise RTorrentConfigException('Storage mode configured to ' + 69 | 'invalid/unsupported state.') 70 | 71 | info['storage_mode'] = config_dict['storage_mode'] 72 | 73 | return info 74 | 75 | def parse_config_part(config_dict, name): 76 | """ 77 | Parse target configuration. 78 | """ 79 | 80 | info = _parse_config_part_connection(config_dict, name) 81 | info = _parse_config_part_storage(config_dict, info) 82 | 83 | return info 84 | 85 | def parse_user_part(config_dict, name): 86 | user = PyroUser() 87 | 88 | user.name = name 89 | 90 | if 'targets' not in config_dict: 91 | raise RTorrentConfigException('User %s has no ``targets'' entry' % name) 92 | elif type(config_dict['targets']) not in (list,): 93 | raise RTorrentConfigException('User %s ``targets'' needs to be a list'\ 94 | % name) 95 | 96 | user.targets = config_dict['targets'] 97 | 98 | if 'background-image' not in config_dict: 99 | user.background_image = None 100 | elif type(config_dict['background-image']) not in (str,): 101 | raise RTorrentConfigException('User %s ``background-image'' must be a str'\ 102 | % name) 103 | else: 104 | user.background_image = config_dict['background-image'] 105 | 106 | if 'password' not in config_dict: 107 | raise RTorrentConfigException('User %s has no ``password'' entry' % name) 108 | elif type(config_dict['password']) not in (str,): 109 | raise RTorrentConfigException('User %s ``password'' must be a str'\ 110 | % name) 111 | 112 | user.password = config_dict['password'] 113 | 114 | return user 115 | -------------------------------------------------------------------------------- /lib/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyroTorrent decorators - providing validation and lookup routines 3 | """ 4 | 5 | # 'inspect' is used for function standards validation. 6 | import inspect 7 | 8 | from functools import wraps 9 | 10 | # pyro's web framework 11 | from flask import session, g, abort 12 | 13 | # pyro imports 14 | from config import USE_AUTH 15 | from lib.helper import lookup_user, lookup_target, \ 16 | detach_deep_func, attach_deep_func, loggedin_and_require 17 | from model.torrent import Torrent 18 | from model.rtorrent import RTorrent 19 | 20 | ################################### 21 | # This section contains decorator # 22 | # helpers. # 23 | ################################### 24 | 25 | class AppStandardsViolation(Exception): 26 | """ 27 | Function breaks application conventions 28 | 29 | dec: Name of decorator that found the offending functions 30 | func: Offending function 31 | args: List of tuples, where each tuple is a pair of 32 | (arg_nr, arg_name) 33 | arg_nr: offending argument number 34 | arg_name: expected name 35 | """ 36 | def __init__(self, dec, func, args): 37 | self.py_file = inspect.getsourcefile(func) 38 | self.py_line = inspect.getsourcelines(func)[1] 39 | self.py_name = func.__name__ 40 | self.args = args 41 | self.dec = dec 42 | 43 | arg_error = '' 44 | 45 | for arg in args: 46 | if arg[0] == -1: 47 | arg_error += "\n missing argument '%s'" % arg[1] 48 | else: 49 | arg_error += "\n argument %i, expecting '%s'" % arg 50 | Exception.__init__(self, 51 | '%s:%s: @%s: Function \'%s\' breaks application semantics%s' % 52 | (self.py_file, self.py_line, dec, self.py_name, arg_error)) 53 | 54 | class AppBugException(Exception): 55 | """ 56 | Thrown upon encountering a state that should not occur. 57 | """ 58 | 59 | #################################################### 60 | ####################################### # # ######## 61 | # From here on decorators only! ######## # # ####### 62 | ####################################### # # ######## 63 | #################################################### 64 | 65 | # Basic lookup method, wrapping all pyroTorrent views. 66 | def pyroview(func = None, require_login = True, do_lookup_user = False): 67 | """ 68 | pyroTorrent - standard view wrapper 69 | 70 | When calling this function without keyword arguments, 71 | you will want to keep the first argument None since this is a 72 | hack to make the function multisyntax compatible, see note at 73 | end of function code. 74 | 75 | require_login: 76 | When enabled, page will reject users without authorisation. 77 | Opposingly disabling will render the page without problems. 78 | lookup_user: 79 | When enabled, decorator will lookup the current user, and 80 | provide it as an additional argument. 81 | 82 | provides the following variables: 83 | g.user = Result of lookup_user operation. 84 | """ 85 | 86 | # All hail closures 87 | 88 | ################################ 89 | ### Optional arguments hack ### 90 | ################################ 91 | def owhy(func): 92 | 93 | ################################ 94 | ### Actual wrapping function ### 95 | ################################ 96 | # Use wraps here since we skip the decorator module 97 | # in this function. 98 | @wraps(func) 99 | def pyroview_func(*args, **key_args): 100 | """ 101 | pyroview internal function wrapper 102 | """ 103 | 104 | print("pyroview_func") 105 | print(args, key_args) 106 | 107 | # Force a login if necessary 108 | if require_login: 109 | if not loggedin_and_require(): 110 | # If you wonder why the definition of the 111 | # following function is nowhere to be found, check 112 | # '/pyrotorrent.py'. 113 | return handle_login() 114 | 115 | # Lookup user 116 | try: 117 | user_name = session['user_name'] 118 | user = lookup_user(user_name) 119 | except KeyError as e: 120 | user = None 121 | g.user = user 122 | 123 | # Pass argument if requested 124 | if do_lookup_user: 125 | key_args['user'] = user 126 | 127 | return func(*args, **key_args) 128 | 129 | # FIXME: pyroview does not preserve signature to fix 130 | #some unexpected decorator behaviour 131 | #return decorator(pyroview_func, func) 132 | return pyroview_func 133 | 134 | # pyroview was called using '@pyroview' 135 | if func: 136 | return owhy(func) 137 | 138 | # pyroview was called using '@pyroview(...)' 139 | # I would say this is a weakness in the PEP318 spec, but 140 | # there is proabably a reason for this.. 141 | # It makes decorators taking optional arguments a pain to 142 | # implement. 143 | # http://www.python.org/dev/peps/pep-0318/ 144 | return owhy 145 | 146 | def require_target(func): 147 | """ 148 | Wrap a function requiring a target client. Implements 149 | automatic rejection semantics to ensure user authorisation. 150 | 151 | provides: 152 | target argument 153 | """ 154 | 155 | # Validate target argument 156 | #if 'target' not in inspect.getargspec(func)[0]: 157 | # raise AppStandardsViolation('require_target', func, [(-1, 'target')]) 158 | 159 | ################################ 160 | ### Actual wrapping function ### 161 | ################################ 162 | #def target_func(func, target, *args, **key_args): 163 | @wraps(func) 164 | def target_func(target, *args, **key_args): 165 | """ 166 | require_target internal target argument wrapper. 167 | """ 168 | 169 | print("target_func") 170 | print(key_args) 171 | 172 | # Perform target lookup for callback function 173 | target = lookup_target(target) 174 | 175 | # Return 404 on target not found 176 | if target is None: 177 | return abort(404) 178 | 179 | # Reject user if not allowed to view this target 180 | if USE_AUTH: 181 | if g.user == None or target['name'] not in g.user.targets: 182 | return abort(404) # 404 183 | 184 | print(target, args, key_args) 185 | return func(target = target, *args, **key_args) 186 | 187 | #return decorator(target_func, func) 188 | return target_func 189 | 190 | def require_torrent(func): 191 | """ 192 | Wrap a function working on a specific torrent. 193 | This decorator expects Flask to pass a 'torrent_hash' argument. 194 | 195 | converts: 196 | torrent_hash into a torrent object. 197 | provides: 198 | torrent argument. 199 | """ 200 | 201 | print('@require_torrent') 202 | # Validate torrent argument 203 | #if 'torrent' not in inspect.getargspec(func)[0]: 204 | # raise AppStandardsViolation('require_torrent', func, [(-1, 'torrent')]) 205 | 206 | ################################ 207 | ### Actual wrapping function ### 208 | ################################ 209 | #def torrent_func(func, target, torrent, *args, **key_args): 210 | @wraps(func) 211 | def torrent_func(target, *args, **key_args): 212 | """ 213 | require_torrent internal torrent argument wrapper 214 | """ 215 | 216 | print("torrent_func") 217 | print(key_args) 218 | 219 | # Grab torrent_hash 220 | if 'torrent' not in key_args: 221 | bugstr = "!!! BUG: route is not passing a 'torrent' " + \ 222 | "argument like it should" 223 | print(bugstr) 224 | raise AppBugException(bugstr) 225 | 226 | # Build torrent object 227 | torrent_hash = key_args['torrent'] 228 | t = Torrent(target, torrent_hash) 229 | 230 | # torrent_hash is contained by torrent object and thus superfluous 231 | #del key_args['torrent_hash'] 232 | key_args['torrent'] = t 233 | print(key_args) 234 | 235 | return func(target = target, *args, **key_args) 236 | 237 | #return decorator(torrent_func, func) 238 | return torrent_func 239 | 240 | def require_rtorrent(func): 241 | """ 242 | Wrap a function requiring an RTorrent object. 243 | 244 | expects: 245 | target argument 246 | provides: 247 | rtorrent argument 248 | """ 249 | 250 | ## Validate torrent argument 251 | #if 'rtorrent' not in inspect.getargspec(func)[0]: 252 | # raise AppStandardsViolation('require_rtorrent', func, [(-1, 'rtorrent')]) 253 | 254 | ################################ 255 | ### Actual wrapping function ### 256 | ################################ 257 | #def rtorrent_func(func, *args, **key_args): 258 | @wraps(func) 259 | def rtorrent_func(*args, **key_args): 260 | """ 261 | require_rtorrent internal torrent argument wrapper 262 | """ 263 | 264 | # Setup torrent object. 265 | r = RTorrent(key_args['target']) 266 | key_args['rtorrent'] = r 267 | 268 | return func(*args, **key_args) 269 | 270 | #return decorator(rtorrent_func, func) 271 | return rtorrent_func 272 | 273 | -------------------------------------------------------------------------------- /lib/filerequester.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _torrentrequester-class: 3 | 4 | TorrentRequester 5 | ================ 6 | 7 | The TorrentRequester is a class created to quickly and efficiently query all the 8 | torrents in a view. It only uses one XMLRPC request. All the methods you can 9 | perform on TorrentRequester are identical to the methods on 10 | :ref:`torrent-class`. (Although set* methods have not been implemented) 11 | 12 | Example usage: 13 | 14 | .. code-block:: python 15 | 16 | t = TorrentRequester('hostname') 17 | t.get_name().get_hash() # Chaining commands is possible 18 | t.get_upload_throttle() # As well as calling another method on it. 19 | print t.all() 20 | 21 | """ 22 | # Also change return type? not list of list but perhaps a dict or class? 23 | # Properly implement flush? 24 | 25 | import xmlrpc.client 26 | from model import torrentfile 27 | from lib.baserequester import BaseRequester, \ 28 | InvalidTorrentCommandException 29 | 30 | from config import rtorrent_config 31 | 32 | # XXX: Create baseclass for rtorrent-multicall's. BaseRequester 33 | 34 | class TorrentFileRequester(BaseRequester): 35 | """ 36 | """ 37 | def __init__(self, target, *first_args): 38 | BaseRequester.__init__(self, target, first_args) 39 | self.first_args = first_args 40 | 41 | def dofetch(self, *rpc_commands): 42 | return self.s.f.multicall(*(self.first_args + ('',) + rpc_commands)) 43 | 44 | def _convert_command(self, command): 45 | """ 46 | Convert command based on torrent._rpc_methods to rtorrent command. 47 | """ 48 | if command in torrentfile._rpc_methods: 49 | return torrentfile._rpc_methods[command][0] 50 | else: 51 | raise InvalidTorrentCommandException("%s is not a valid command" % 52 | command) 53 | -------------------------------------------------------------------------------- /lib/filetree.py: -------------------------------------------------------------------------------- 1 | """ 2 | File tree representation of torrent. 3 | 4 | The classes in this file can be used to convert a list of 5 | file paths in a torrent to a tree representation. 6 | """ 7 | 8 | class Leaf(object): 9 | def __init__(self, name, path, obj): 10 | self.name = name 11 | self.path = path 12 | self.obj = obj 13 | 14 | def get_path(self): 15 | return self.path 16 | 17 | def get_path_no_root(self): 18 | """ 19 | Remove preceding / 20 | """ 21 | return self.path[1:] 22 | 23 | def repr(self): 24 | return 'Leaf(%s, path)' % (self.name, self.path) 25 | 26 | class Node(Leaf): 27 | def __init__(self, name, path): 28 | Leaf.__init__(self, name, path, None) 29 | self.children = [] 30 | 31 | def find(self, name): 32 | # Not a nested search 33 | for x in self.children: 34 | if x.name == name: 35 | return x 36 | 37 | return None 38 | 39 | def add(self, name, path, leaf=False, obj=None): 40 | if self.find(name): 41 | raise Exception('Invalid') # FIXME 42 | 43 | if leaf: 44 | n = Leaf(name, path, obj) 45 | else: 46 | n = Node(name, path) 47 | self.children.append(n) 48 | return n 49 | 50 | def repr(self): 51 | return 'Node(%s, %s)' % (self.name, self.path) 52 | 53 | class FileTree(object): 54 | def __init__(self, files): 55 | """ 56 | Files should be a list of lists. Each list contains a path with each 57 | item in the path as a new item. 58 | 59 | >>> [['a', 'e'], ['a', 'b', 'c', 'd']] (a/e, a/b/c/d) 60 | """ 61 | self.root = self.build_tree(files) 62 | 63 | def build_tree(self, files): 64 | root = Node('Files', '/') 65 | 66 | for file_ in files: 67 | 68 | x = file_.get_path_components 69 | 70 | last_node = root 71 | path = '' 72 | 73 | # Traverse directories 74 | while len(x) > 1: 75 | y = x[0] 76 | n = last_node.find(y) 77 | if n: 78 | last_node = n 79 | path = n.get_path() 80 | else: 81 | path += '/' + y 82 | n = last_node.add(y, path, leaf=False) 83 | last_node = n 84 | x = x[1:] 85 | 86 | # Add file 87 | if not len(path): 88 | path = '/' 89 | else: 90 | path += '/' 91 | path += x[0] 92 | last_node.add(x[0], path, leaf=True, obj=file_) 93 | 94 | return root 95 | 96 | -------------------------------------------------------------------------------- /lib/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various helper functions 3 | """ 4 | 5 | import sys 6 | 7 | # flask webframework stuff 8 | from flask import redirect, g, session, url_for, render_template 9 | 10 | # pyro imports 11 | from config import FILE_BLOCK_SIZE, BACKGROUND_IMAGE, \ 12 | USE_AUTH, ENABLE_API, rtorrent_config, torrent_users 13 | from lib.config_parser import parse_config_part, parse_user_part, \ 14 | RTorrentConfigException, CONNECTION_SCGI, CONNECTION_HTTP 15 | from lib.multibase import InvalidTorrentException, InvalidConnectionException, \ 16 | InvalidTorrentCommandException 17 | from model.rtorrent import RTorrent 18 | 19 | # Pulled somewhere from the net. Used in jinja. 20 | def wiz_normalise(a): 21 | a = float(a) 22 | if a >= 1099511627776: 23 | terabytes = a / 1099511627776 24 | size = '%.2fT' % terabytes 25 | elif a >= 1073741824: 26 | gigabytes = a / 1073741824 27 | size = '%.2fG' % gigabytes 28 | elif a >= 1048576: 29 | megabytes = a / 1048576 30 | size = '%.2fM' % megabytes 31 | elif a >= 1024: 32 | kilobytes = a / 1024 33 | size = '%.2fK' % kilobytes 34 | else: 35 | size = '%.2fb' % a 36 | return size 37 | 38 | def error_page(error='No error?'): 39 | """ 40 | Called on exceptions, when something goes wrong. 41 | """ 42 | rtorrent_data = fetch_global_info() 43 | tmpl = jinjaenv.get_template('error.html') 44 | return template_render(tmpl, {'error' : error, 45 | 'rtorrent_data' : rtorrent_data }) 46 | 47 | # Is session in login state? 48 | def loggedin(): 49 | return 'user_name' in session 50 | 51 | # Logged in when required? 52 | # XXX: 'authorized' might be a better name for this function 53 | # since having authorisation does not imply being logged in. 54 | def loggedin_and_require(): 55 | """ 56 | Return False whenever the user is not logged in and 57 | this is a requirement. 58 | True otherwise. 59 | """ 60 | return loggedin() if USE_AUTH else True 61 | 62 | def lookup_target(name): 63 | """ 64 | Simple helper to find the target with the name ``name``. 65 | """ 66 | 67 | for x in targets: 68 | if x['name'] == name: 69 | return x 70 | return None 71 | 72 | def lookup_user(name): 73 | """ 74 | Verify username is in configfile. 75 | """ 76 | 77 | for x in users: 78 | if x.name == name: 79 | return x 80 | return None 81 | 82 | # The following 2 functions are used by the decorator subsystem 83 | # to handle passing deep function objects. Deep functions in pyroTorrent are the 84 | # original undecorated functions. 85 | 86 | # XXX These functions have been superseded by the use of the wraps decorator 87 | # and will be removed in the (near) future. 88 | 89 | def detach_deep_func(func): 90 | """ 91 | This function retrieves a deep function object asociated with a returned 92 | decorated function object. This function is called 'detach' and not 'fetch' 93 | because it also removes the asociation after retrieving. We do this to 94 | decrease the number of references to the original function and have only 95 | the last returned function point directly to the deep function. 96 | 97 | At the moment deep functions are stored in the _PYRO_deep_func attribute. 98 | 99 | returns: deep function (Python function object) 100 | """ 101 | 102 | if hasattr(func, '_PYRO_deep_func'): 103 | deep_func = func._PYRO_deep_func 104 | del func._PYRO_deep_func 105 | return deep_func 106 | 107 | # This function has no deep function attribute and is therefore 108 | # most likely the original function and thus 'deep function' 109 | return func 110 | 111 | def attach_deep_func(func, deep_func): 112 | """ 113 | Attach a deep function object to a decorated function. 114 | 115 | returns: 'func' argument. 116 | """ 117 | 118 | # Attaching a deep function to a deep function.. 119 | # not such a good idea, return unmodified function. 120 | if func is not deep_func: 121 | func._PYRO_deep_func = deep_func 122 | 123 | return func 124 | 125 | # Function to render the jinja template and pass some simple vars / functions. 126 | def pyro_render_template(template, **kw): 127 | """ 128 | Template Render is a helper that initialises basic template variables 129 | and handles unicode encoding. 130 | """ 131 | #XXX Base URL not needed any more since Flask 132 | kw['use_auth'] = USE_AUTH 133 | kw['wn'] = wiz_normalise 134 | kw['trans'] = 0.4 135 | kw['nm_background'] = BACKGROUND_IMAGE 136 | kw['login'] = session['user_name'] if \ 137 | 'user_name' in session else None 138 | 139 | #ret = unicode(template.render(vars)).encode('utf8') 140 | 141 | return render_template(template, **kw) 142 | 143 | # Fetch some useful rtorrent info from all targets. 144 | def fetch_global_info(): 145 | """ 146 | Fetch global stuff (always displayed): 147 | - Down/Up Rate. 148 | - IP (perhaps move to static global) 149 | """ 150 | res = {} 151 | for target in targets: 152 | rtorrent = RTorrent(target) 153 | try: 154 | r = rtorrent.query().get_upload_rate().get_download_rate()\ 155 | .get_upload_throttle().get_download_throttle().get_ip()\ 156 | .get_hostname().get_memory_usage().get_max_memory_usage()\ 157 | .get_libtorrent_version().get_view_list() 158 | 159 | h = hash(r) 160 | 161 | res[target['name']] = cache.get(h) 162 | if res[target['name']]: 163 | continue 164 | 165 | res[target['name']] = r.first() 166 | 167 | cache.set(h, res[target['name']], timeout=60) 168 | 169 | except InvalidConnectionException as e: 170 | print('InvalidConnectionException:', e) 171 | # Do we want to return or just not get data for this target? 172 | # I'd say return for now. 173 | return {} 174 | 175 | return res 176 | 177 | def parse_config(): 178 | """ 179 | Use lib.config_parser to parse each target in the rtorrent_config dict. 180 | I suppose it's more like verifying than parsing. Returns a list of dicts, 181 | one dict per target. 182 | """ 183 | 184 | targets = [] 185 | for x in rtorrent_config: 186 | try: 187 | info = parse_config_part(rtorrent_config[x], x) 188 | except RTorrentConfigException as e: 189 | print('Invalid config: ', e) 190 | sys.exit(1) 191 | 192 | targets.append(info) 193 | 194 | return targets 195 | 196 | def parse_users(): 197 | """ 198 | """ 199 | users = [] 200 | for x in torrent_users: 201 | try: 202 | user = parse_user_part(torrent_users[x], x) 203 | except RTorrentConfigException as e: 204 | print('Invalid config: ', e) 205 | sys.exit(1) 206 | 207 | users.append(user) 208 | 209 | return users 210 | 211 | def fetch_user(): 212 | """ 213 | Unconditionally fetch credentials from the flask session, 214 | and verify against config file. 215 | returns: 216 | A valid user string, or None if no valid user could be found. 217 | """ 218 | try: 219 | user_name = session['user_name'] 220 | user = lookup_user(user_name) 221 | except KeyError as e: 222 | user = None 223 | return user 224 | 225 | def parse_args_to_url(endpoint=None, url=None, **kw): 226 | """ 227 | This function parses the arguments accepted by the pyroTorrent 228 | redirect helpers. 229 | 230 | Arguments: 231 | This function accepts only keyword arguments or a single 232 | endpoint string as first argument. 233 | 234 | enpoint: 235 | A Flask/Werkzeug endpoint to be redirected to. 236 | 237 | url: 238 | A URL to be redirected to in any format you fancy. 239 | 240 | **kw: 241 | Any variables provided to the endpoint. 242 | For instance a route of: '/view/' would 243 | accept a 'name' keyword. 244 | 245 | Returns: 246 | A URL string. 247 | 248 | Raises: 249 | ValueError: 250 | When either both endpoint and url specify something 251 | or whenever both a URL and additional keyword arguments 252 | are provided. 253 | """ 254 | 255 | if endpoint and url: 256 | raise ValueError("Both 'endpoint' and 'URL' have valid values," + 257 | "only specify one of them.") 258 | 259 | if endpoint: 260 | return url_for(endpoint, **kw) 261 | 262 | if url and len(kw): 263 | raise ValueError('Cannot specify keyword arguments for fixed URL') 264 | 265 | return url 266 | 267 | def redirect_client_prg(endpoint=None, **kw): 268 | """ 269 | Return a HTTP 303 response, effectively redirecting 270 | the client to the given URL in a Post/Redirect/Get manor. 271 | 272 | Arguments: 273 | This function accepts only keyword arguments or a single 274 | endpoint string as first argument. 275 | 276 | enpoint: 277 | A Flask/Werkzeug endpoint to be redirected to. 278 | 279 | url: 280 | A URL to be redirected to in any format you fancy. 281 | 282 | **kw: 283 | Any variables provided to the endpoint. 284 | For instance a route of: '/view/' would 285 | accept a 'name' keyword. 286 | 287 | Returns: 288 | A Flask Response object. 289 | 290 | Raises: 291 | ValueError: 292 | When either both endpoint and url specify something 293 | or whenever both a URL and additional keyword arguments 294 | are provided. 295 | """ 296 | 297 | url = parse_args_to_url(endpoint, **kw) 298 | print('redirect_client_prg:', url) 299 | 300 | # Tell flask to redirect using HTTP 303 See Other. 301 | # A 303 should not result in resubmission of POST data 302 | # to the given location. 303 | return redirect(url, code=303) 304 | 305 | def redirect_client(endpoint=None, **kw): 306 | """ 307 | Return a HTTP 307 response, effectively redirecting 308 | the client to the given URL. 309 | 310 | Arguments: 311 | This function accepts only keyword arguments or a single 312 | endpoint string as first argument. 313 | 314 | enpoint: 315 | A Flask/Werkzeug endpoint to be redirected to. 316 | 317 | url: 318 | A URL to be redirected to in any format you fancy. 319 | 320 | **kw: 321 | Any variables provided to the endpoint. 322 | For instance a route of: '/view/' would 323 | accept a 'name' keyword. 324 | 325 | Returns: 326 | A Flask Response object. 327 | 328 | Raises: 329 | ValueError: 330 | When either both endpoint and url specify something 331 | or whenever both a URL and additional keyword arguments 332 | are provided. 333 | """ 334 | 335 | url = parse_args_to_url(endpoint, **kw) 336 | print('redirect_client:', url) 337 | 338 | # Tell flask to redirect using HTTP 307 Temporary Redirect. 339 | # A 307 should not be cached unless explicitely stated so 340 | # by the HTTP headers. 341 | return redirect(url, code=307) 342 | 343 | -------------------------------------------------------------------------------- /lib/multibase.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _multibase-class: 3 | 4 | MultiBase 5 | ========= 6 | 7 | MultiBase is a class that can be inherited to easily create a class that can 8 | send multiple XMLRPC commands in one request, using the xmlrpclib.MultiCall 9 | class. RTorrentQuery and TorrentQuery implement this class. 10 | 11 | It also overrides several functions like __getattr__ and __call__ to 12 | make usage simple: 13 | 14 | .. code-block:: python 15 | 16 | t = Torrent.query() # Returns a TorrentQuery object which inherits from 17 | # MultiBase 18 | t.get_name().get_hash() 19 | t.get_upload_rate() 20 | 21 | print t.all() 22 | """ 23 | 24 | import xmlrpc.client 25 | import socket 26 | 27 | from lib.xmlrpc import RTorrentXMLRPC 28 | 29 | class MultiBase(object): 30 | """ 31 | """ 32 | def __init__(self, target, *args): 33 | """ 34 | """ 35 | self.s = RTorrentXMLRPC(target) 36 | self.m = xmlrpc.client.MultiCall(self.s) 37 | self.target = target['name'] 38 | 39 | # Stack to put commands on 40 | self._groups = [[]] 41 | 42 | # We keep these purely for hash functionality 43 | self._groups_args = [[]] 44 | self._def_group_args = args 45 | 46 | def __hash__(self): 47 | h = 42 ^ hash(self.target) 48 | for y in zip(self._groups, self._groups_args): 49 | for x in zip(y[0], y[1]): 50 | h ^= hash(x[0]) 51 | for z in x[1]: 52 | h ^= hash(z) 53 | 54 | return h 55 | 56 | def __call__(self, attr, *args): 57 | """ 58 | Add the attribute ``attr'' to the list we want to fetch. 59 | 60 | Return self so we can chain calls. 61 | """ 62 | _attr = self._convert_command(attr) 63 | 64 | total_args = list(self._def_group_args) 65 | total_args.extend(args) 66 | getattr(self.m, _attr)(*total_args) 67 | self._groups[-1].append(attr) 68 | self._groups_args[-1].append(total_args) 69 | 70 | return self 71 | 72 | def __getattr__(self, attr): 73 | """ 74 | Used to add commands. 75 | """ 76 | return lambda *args: self(attr, *args) # calls __call__ 77 | 78 | def new_group(self, *args): 79 | """ 80 | Use this to create a new group. You can chain calls as well. 81 | """ 82 | self._groups.append([]) 83 | self._groups_args.append([]) 84 | self._def_group_args = args 85 | return self 86 | 87 | def all(self, _type=None): 88 | """ 89 | Returns a list of the results. 90 | 91 | _type can be 'list' or AttributeDictMultiResult. 92 | """ 93 | if _type is None: 94 | _type = AttributeDictMultiResult 95 | if _type not in (AttributeDictMultiResult, list): 96 | raise InvalidTorrentCommandException('Invalid _type: %s' % 97 | str(_type)) 98 | 99 | try: 100 | xmlres = list(self.m()) 101 | except xmlrpc.client.Fault as e: 102 | raise InvalidTorrentException(e) 103 | except socket.error as s: 104 | raise InvalidConnectionException(s) 105 | 106 | if _type is list: 107 | self._flush() 108 | return xmlres 109 | 110 | xmlres.reverse() 111 | 112 | result = [] 113 | for group in self._groups: 114 | res = [] 115 | for command in group: 116 | res.append(xmlres.pop()) 117 | 118 | result.append(AttributeDictMultiResult(list(zip(group, res)))) 119 | 120 | self._flush() 121 | 122 | return result 123 | 124 | def first(self, _type=None): 125 | """ 126 | Return the first result. 127 | """ 128 | res = self.all(_type) 129 | if len(res): 130 | return res[0] 131 | else: 132 | return None 133 | 134 | def _flush(self): 135 | pass 136 | 137 | def _convert_command(self, command): 138 | """ 139 | Convert command based on self._rpc_methods to rtorrent command. 140 | """ 141 | if command in self._rpc_methods: 142 | return self._rpc_methods[command][0] 143 | else: 144 | raise InvalidTorrentCommandException("%s is not a valid command" % 145 | command) 146 | 147 | class InvalidTorrentCommandException(Exception): 148 | """ 149 | Thrown on an invalid command. 150 | """ 151 | 152 | class InvalidTorrentException(Exception): 153 | """ 154 | Thrown on xmlrpc error. 155 | """ 156 | 157 | class InvalidConnectionException(Exception): 158 | """ 159 | Thrown on xmlrpc error. 160 | """ 161 | 162 | class AttributeDictMultiResult(dict): 163 | """ 164 | AttributeDictMultiResult is used by MultiBase .all() calls to return 165 | data in a somewhat usable manner. It's basically a dict with an extra 166 | feature to access the dict values via . instead of [name]. 167 | """ 168 | def __getattr__(self, attr): 169 | if attr in self: 170 | return self[attr] 171 | else: 172 | raise AttributeError('%s not in dict' % attr) 173 | 174 | -------------------------------------------------------------------------------- /lib/peerrequester.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _peerrequester-class: 3 | 4 | PeerRequester 5 | ================ 6 | """ 7 | # Also change return type? not list of list but perhaps a dict or class? 8 | # Properly implement flush? 9 | 10 | import xmlrpc.client 11 | from model import peer 12 | from lib.baserequester import BaseRequester, InvalidTorrentCommandException 13 | 14 | class PeerRequester(BaseRequester): 15 | """ 16 | """ 17 | def __init__(self, target, *first_args): 18 | BaseRequester.__init__(self, target, first_args) 19 | self.first_args = first_args 20 | 21 | def dofetch(self, *rpc_commands): 22 | return self.s.p.multicall(*(self.first_args + (' ',) + rpc_commands)) 23 | 24 | def _convert_command(self, command): 25 | """ 26 | Convert command based on torrent._rpc_methods to rtorrent command. 27 | """ 28 | if command in peer._rpc_methods: 29 | return peer._rpc_methods[command][0] 30 | else: 31 | raise InvalidTorrentCommandException("%s is not a valid command" % 32 | command) 33 | 34 | -------------------------------------------------------------------------------- /lib/rtorrentquery.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _rtorrentquery-class: 4 | 5 | RTorrentQuery 6 | ============= 7 | 8 | The RTorrent query class can be used to send multiple queries over one 9 | XMLRPC command, thus heavily decreasing loading times. 10 | 11 | It is created in RTorrent.query() or can be manually created using the 12 | RTorrentQuery() constructor. 13 | 14 | It extends the MultiBase class, which is used in all the *Query classes. 15 | Head over to :ref:`multibase-class` to find more about the general *Query classes. 16 | 17 | .. code-block:: python 18 | 19 | # Create a RTorrent class (or use an existing one) 20 | r = RTorrent(host, port, url) 21 | 22 | # Create the query. 23 | rq = r.query().get_upload_rate().get_download_rate(\ 24 | ).get_librtorrent_version() 25 | 26 | res = rq.first() 27 | 28 | print res.get_upload_rate # Note that this is an attribute. 29 | 30 | """ 31 | from lib.multibase import MultiBase 32 | from model import rtorrent 33 | 34 | class RTorrentQuery(MultiBase): 35 | """ 36 | The RTorrent query class can be used to send multiple queries over one 37 | XMLRPC command, thus heavily decreasing loading times. 38 | """ 39 | 40 | def __init__(self, target, *args): 41 | """ 42 | Pass the host, port, url and possible default arguments. 43 | """ 44 | 45 | MultiBase.__init__(self, target, *args) 46 | self._rpc_methods = rtorrent._rpc_methods 47 | 48 | -------------------------------------------------------------------------------- /lib/torrentquery.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _torrentquery-class: 4 | 5 | TorrentQuery 6 | ============ 7 | 8 | The Torrent query class can be used to send multiple queries over one 9 | XMLRPC command, thus heavily decreasing loading times. 10 | 11 | """ 12 | from lib.multibase import MultiBase 13 | from model import torrent 14 | 15 | class TorrentQuery(MultiBase): 16 | """ 17 | The Torrent query class can be used to send multiple queries over one 18 | XMLRPC command, thus heavily decreasing loading times. 19 | """ 20 | 21 | def __init__(self, target, *args): 22 | """ 23 | Pass the host, port, url and possible default arguments. 24 | *args is usually only the torrent hash or just undefined. 25 | """ 26 | 27 | MultiBase.__init__(self, target, *args) 28 | self._rpc_methods = torrent._rpc_methods 29 | 30 | -------------------------------------------------------------------------------- /lib/torrentrequester.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _torrentrequester-class: 3 | 4 | TorrentRequester 5 | ================ 6 | 7 | The TorrentRequester is a class created to quickly and efficiently query all the 8 | torrents in a view. It only uses one XMLRPC request. All the methods you can 9 | perform on TorrentRequester are identical to the methods on 10 | :ref:`torrent-class`. (Although set* methods have not been implemented) 11 | 12 | Example usage: 13 | 14 | .. code-block:: python 15 | 16 | t = TorrentRequester('hostname') 17 | t.get_name().get_hash() # Chaining commands is possible 18 | t.get_upload_throttle() # As well as calling another method on it. 19 | print t.all() 20 | 21 | """ 22 | # Also change return type? not list of list but perhaps a dict or class? 23 | # Properly implement flush? 24 | 25 | import xmlrpc.client 26 | from model import torrent 27 | from lib.baserequester import BaseRequester, InvalidTorrentCommandException 28 | 29 | from config import rtorrent_config 30 | 31 | class TorrentRequester(BaseRequester): 32 | """ 33 | """ 34 | def __init__(self, target, *first_args): 35 | BaseRequester.__init__(self, target, first_args) 36 | self.first_args = first_args 37 | 38 | def dofetch(self, *rpc_commands): 39 | return self.s.d.multicall2(*(self.first_args + rpc_commands)) 40 | 41 | def _convert_command(self, command): 42 | """ 43 | Convert command based on torrent._rpc_methods to rtorrent command. 44 | """ 45 | if command in torrent._rpc_methods: 46 | return torrent._rpc_methods[command][0] 47 | else: 48 | raise InvalidTorrentCommandException("%s is not a valid command" % 49 | command) 50 | 51 | -------------------------------------------------------------------------------- /lib/xmlrpc.py: -------------------------------------------------------------------------------- 1 | """ 2 | XMLRPCLib Wrapper with support for SCGI over unix:// and network(ed) sockets. 3 | (Also supports http://) 4 | """ 5 | 6 | import xmlrpc.client 7 | from . import xmlrpc2scgi 8 | 9 | from .config_parser import CONNECTION_HTTP, CONNECTION_SCGI 10 | 11 | class RTorrentXMLRPCException(Exception): 12 | pass 13 | 14 | class RTorrentXMLRPC(object): 15 | 16 | def __init__(self, target): 17 | self.info = target 18 | 19 | if self.info['type'] == CONNECTION_HTTP: 20 | self.c = xmlrpc.client.ServerProxy('http://%s:%i%s' % \ 21 | (self.info['host'], self.info['port'], \ 22 | self.info['url'])) 23 | else: 24 | self.c = xmlrpc2scgi.RTorrentXMLRPCClient(self.info['fd']) 25 | 26 | 27 | def __getattr__(self, attr, **args): 28 | return getattr(self.c, attr) 29 | -------------------------------------------------------------------------------- /lib/xmlrpc2scgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2005-2007, Glenn Washburn 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | # 19 | # In addition, as a special exception, the copyright holders give 20 | # permission to link the code of portions of this program with the 21 | # OpenSSL library under certain conditions as described in each 22 | # individual source file, and distribute linked combinations 23 | # including the two. 24 | # 25 | # You must obey the GNU General Public License in all respects for 26 | # all of the code used other than OpenSSL. If you modify file(s) 27 | # with this exception, you may extend this exception to your version 28 | # of the file(s), but you are not obligated to do so. If you do not 29 | # wish to do so, delete this exception statement from your version. 30 | # If you delete this exception statement from all source files in the 31 | # program, then also delete it here. 32 | # 33 | # Contact: Glenn Washburn 34 | 35 | import sys, io as StringIO 36 | import xmlrpc.client, urllib.request, urllib.parse, urllib.error, urllib.parse, socket 37 | 38 | # this allows us to parse scgi urls just like http ones 39 | from urllib.parse import uses_netloc 40 | uses_netloc.append('scgi') 41 | 42 | def do_scgi_xmlrpc_request(host, methodname, params=()): 43 | """ 44 | Send an xmlrpc request over scgi to host. 45 | host: scgi://host:port/path 46 | methodname: xmlrpc method name 47 | params: tuple of simple python objects 48 | returns: xmlrpc response 49 | """ 50 | xmlreq = xmlrpc.client.dumps(params, methodname) 51 | xmlresp = SCGIRequest(host).send(xmlreq) 52 | #~ print xmlresp 53 | 54 | return xmlresp 55 | 56 | def do_scgi_xmlrpc_request_py(host, methodname, params=()): 57 | """ 58 | Send an xmlrpc request over scgi to host. 59 | host: scgi://host:port/path 60 | methodname: xmlrpc method name 61 | params: tuple of simple python objects 62 | returns: xmlrpc response converted to python 63 | """ 64 | xmlresp = do_scgi_xmlrpc_request(host, methodname, params) 65 | return xmlrpc.client.loads(xmlresp)[0][0] 66 | 67 | class SCGIRequest(object): 68 | """ See spec at: http://python.ca/scgi/protocol.txt 69 | Send an SCGI request. 70 | 71 | Use tcp socket 72 | SCGIRequest('scgi://host:port').send(data) 73 | 74 | Or use the named unix domain socket 75 | SCGIRequest('scgi:///tmp/rtorrent.sock').send(data) 76 | """ 77 | 78 | def __init__(self, url): 79 | self.url=url 80 | self.resp_headers=[] 81 | 82 | def __send(self, scgireq): 83 | scheme, netloc, path, query, frag = urllib.parse.urlsplit(self.url) 84 | host, port = urllib.parse.splitport(netloc) 85 | #~ print '>>>', (netloc, host, port) 86 | 87 | if netloc: 88 | addrinfo = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM) 89 | 90 | assert len(addrinfo) == 1, "There's more than one? %r"%addrinfo 91 | #~ print addrinfo 92 | 93 | sock = socket.socket(*addrinfo[0][:3]) 94 | sock.connect(addrinfo[0][4]) 95 | else: 96 | # if no host then assume unix domain socket 97 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 98 | sock.connect(path) 99 | 100 | sock.send(bytes(scgireq, 'utf-8')) 101 | recvdata = resp = sock.recv(1024) 102 | while recvdata != bytes(): 103 | recvdata = sock.recv(1024) 104 | #print('Trying to receive more: %r'%recvdata) 105 | resp += recvdata 106 | sock.close() 107 | return str(resp, 'utf-8') 108 | 109 | def send(self, data): 110 | "Send data over scgi to url and get response" 111 | scgiresp = self.__send(self.add_required_scgi_headers(data)) 112 | resp, self.resp_headers = self.get_scgi_resp(scgiresp) 113 | return resp 114 | 115 | @staticmethod 116 | def encode_netstring(string): 117 | "Encode string as netstring" 118 | return '%d:%s,'%(len(string), string) 119 | 120 | @staticmethod 121 | def make_headers(headers): 122 | "Make scgi header list" 123 | #~ return '\x00'.join(headers)+'\x00' 124 | return '\x00'.join(['%s\x00%s'%t for t in headers])+'\x00' 125 | 126 | @staticmethod 127 | def add_required_scgi_headers(data, headers=[]): 128 | "Wrap data in an scgi request,\nsee spec at: http://python.ca/scgi/protocol.txt" 129 | # See spec at: http://python.ca/scgi/protocol.txt 130 | headers = SCGIRequest.make_headers([ 131 | ('CONTENT_LENGTH', str(len(data))), 132 | ('SCGI', '1'), 133 | ] + headers) 134 | 135 | enc_headers = SCGIRequest.encode_netstring(headers) 136 | 137 | return enc_headers+data 138 | 139 | @staticmethod 140 | def gen_headers(file): 141 | "Get header lines from scgi response" 142 | line = file.readline().rstrip() 143 | while line.strip(): 144 | yield line 145 | line = file.readline().rstrip() 146 | 147 | @staticmethod 148 | def get_scgi_resp(resp): 149 | "Get xmlrpc response from scgi response" 150 | fresp = StringIO.StringIO(resp) 151 | headers = [] 152 | for line in SCGIRequest.gen_headers(fresp): 153 | #~ print "Header: %r"%line 154 | headers.append(line.split(': ', 1)) 155 | 156 | xmlresp = fresp.read() 157 | return (xmlresp, headers) 158 | 159 | class RTorrentXMLRPCClient(object): 160 | """ 161 | The following is an exmple of how to use this class. 162 | rtorrent_host='http://localhost:33000' 163 | rtc = RTorrentXMLRPCClient(rtorrent_host) 164 | for infohash in rtc.download_list('complete'): 165 | if rtc.d.get_ratio(infohash) > 500: 166 | print "%s has a ratio of over 0.5"%(rtc.d.get_name(infohash)) 167 | """ 168 | 169 | def __init__(self, url, methodname=''): 170 | self.url = url 171 | self.methodname = methodname 172 | 173 | def __call__(self, *args): 174 | #~ print "%s%r"%(self.methodname, args) 175 | scheme, netloc, path, query, frag = urllib.parse.urlsplit(self.url) 176 | xmlreq = xmlrpc.client.dumps(args, self.methodname) 177 | if scheme == 'scgi': 178 | xmlresp = SCGIRequest(self.url).send(xmlreq) 179 | return xmlrpc.client.loads(xmlresp)[0][0] 180 | #~ return do_scgi_xmlrpc_request_py(self.url, self.methodname, args) 181 | elif scheme == 'http': 182 | raise Exception('Unsupported protocol') 183 | elif scheme == '': 184 | raise Exception('Unsupported protocol') 185 | else: 186 | raise Exception('Unsupported protocol') 187 | 188 | def __getattr__(self, attr): 189 | methodname = self.methodname and '.'.join([self.methodname,attr]) or attr 190 | return RTorrentXMLRPCClient(self.url, methodname) 191 | 192 | def convert_params_to_native(params): 193 | "Parse xmlrpc-c command line arg syntax" 194 | #~ print 'convert_params_to_native', params 195 | cparams = [] 196 | # parse parameters 197 | for param in params: 198 | if len(param) < 2 or param[1] != '/': 199 | cparams.append(param) 200 | continue 201 | 202 | if param[0] == 'i': 203 | ptype = int 204 | elif param[0] == 'b': 205 | ptype = bool 206 | elif param[0] == 's': 207 | ptype = str 208 | else: 209 | cparams.append(param) 210 | continue 211 | 212 | cparams.append(ptype(param[2:])) 213 | 214 | return tuple(cparams) 215 | 216 | def main(argv): 217 | output_python=False 218 | 219 | if argv[0] == '-p': 220 | output_python=True 221 | argv.pop(0) 222 | 223 | host, methodname = argv[:2] 224 | 225 | respxml = do_scgi_xmlrpc_request(host, methodname, 226 | convert_params_to_native(argv[2:])) 227 | #~ respxml = RTorrentXMLRPCClient(host, methodname)(convert_params_to_native(argv[2:])) 228 | 229 | if not output_python: 230 | print(respxml) 231 | else: 232 | print(xmlrpc.client.loads(respxml)[0][0]) 233 | 234 | if __name__ == "__main__": 235 | main(sys.argv[1:]) 236 | -------------------------------------------------------------------------------- /model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/model/__init__.py -------------------------------------------------------------------------------- /model/peer.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _peer-class: 3 | 4 | Peer 5 | ==== 6 | 7 | The Peer Model. 8 | 9 | It took me some time to figure out what was initially wrong with the peer 10 | multicall - http://libtorrent.rakshasa.no/ticket/1308. If only rtorrent had been 11 | documented a *little* better. 12 | """ 13 | 14 | from lib.xmlrpc import RTorrentXMLRPC 15 | 16 | class Peer(object): 17 | """ 18 | Peer class. 19 | """ 20 | 21 | # FIXME: If we leave URL at '' xmlrpclib will default to /RPC2 as well. 22 | def __init__(self, target): 23 | """ 24 | Initialise the Peer object. 25 | """ 26 | self.target = target 27 | self.s = RTorrentXMLRPC(target) 28 | 29 | self.hacks() 30 | 31 | def hacks(self): 32 | for x, y in _rpc_methods.items(): 33 | caller = (lambda name: lambda self, *args: getattr(self.s, name)(*args))(y[0]) 34 | caller.__doc__ = y[1] + '\nOriginal libTorrent method: ``%s``' % y[0] 35 | setattr(Peer, x, types.MethodType(caller, self)) 36 | 37 | del caller 38 | 39 | import types 40 | 41 | _rpc_methods = { 42 | 'is_obfuscated' : ('p.is_obfuscated', 43 | """ 44 | Returns if the client is obfuscated. 45 | """), # XXX: What is obfuscated in peer context? 46 | 'is_snubbed' : ('p.is_snubbed', 47 | """ 48 | """), # XXX: What is obfuscated in peer context? 49 | 'is_encrypted' : ('p.is_encrypted', 50 | """ 51 | Returns if the peer is encrypted. 52 | """), 53 | 'is_incoming' : ('p.is_incoming', 54 | """ 55 | Returns if the peer is an incoming peer. 56 | """), 57 | 'get_address' : ('p.address', 58 | """ 59 | Returns the IP address of the peer. 60 | """), 61 | 'get_port' : ('p.port', 62 | """ 63 | Returns the port of the peer. 64 | """), 65 | 'get_client_version' : ('p.client_version', 66 | """ 67 | Returns the client version string of the peer. 68 | """), 69 | 'get_completed_percent' : ('p.completed_percent', 70 | """ 71 | Returns the completed percent of the peer. 72 | """), 73 | 'get_id' : ('p.id', 74 | """ 75 | Returns the ID of the peer. 76 | """), 77 | 'get_upload_rate' : ('p.up_rate', 78 | """ 79 | Returns the upload rate of the peer. 80 | """), 81 | 'get_upload_total' : ('p.up_total', 82 | """ 83 | Returns the upload total of the peer. 84 | """), 85 | 'get_download_rate' : ('p.down_rate', 86 | """ 87 | Returns the download rate of the peer. 88 | """), 89 | 'get_download_total' : ('p.down_total', 90 | """ 91 | Returns the download total of the peer. 92 | """), 93 | 'get_rate' : ('p.peer_rate', 94 | """ 95 | """), # XXX: Rate? 96 | 'get_total' : ('p.peer_total', 97 | """ 98 | """) # XXX: Total? 99 | } 100 | -------------------------------------------------------------------------------- /model/pyro/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/model/pyro/__init__.py -------------------------------------------------------------------------------- /model/pyro/user.py: -------------------------------------------------------------------------------- 1 | 2 | class PyroUser(object): 3 | 4 | def __init__(self): 5 | pass 6 | -------------------------------------------------------------------------------- /model/rtorrent.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _rtorrent-class: 3 | 4 | RTorrent 5 | ======== 6 | 7 | The RTorrent class serves as an interface to a remote RTorrent instance. 8 | It implements a lot of the functionality that RTorrent exposes over XMLRPC; 9 | currently only XMLRPC over HTTP is supported; but support for direct SCGI is 10 | planned. Basically, HTTP support requires a web server to direct requests to 11 | RTorrent, whereas SCGI talks directly to RTorrent. (The web server also uses 12 | SCGI to talk to RTorrent) 13 | 14 | The RTorrent constructor requires a host and optionally a port and url. 15 | 16 | Some of the functions documented in here are in fact auto generated (at 17 | runtime); We did this for a few reasons: extendability and ease of use. 18 | (We can easily chain calls this way) 19 | They will only have one argument in the documentation: *args. 20 | Obviously some do not take any arguments; the docstring should 21 | (in the near future, anyway) explain exactly what variables 22 | should be passed. 23 | 24 | A simple test: 25 | 26 | .. code-block: python 27 | 28 | x = RTorrent('sheeva') 29 | 30 | # Simple test. 31 | old = x.get_upload_throttle() 32 | print 'Throttle:', old 33 | print 'Return:', x.set_upload_throttle(20000) 34 | print 'Throttle:', x.get_upload_throttle() 35 | print 'Return:', x.set_upload_throttle(old) 36 | print 'Throttle:', x.get_upload_throttle() 37 | 38 | print 'Download list', x.get_download_list() 39 | 40 | """ 41 | 42 | from lib.xmlrpc import RTorrentXMLRPC 43 | 44 | class RTorrent(object): 45 | """ 46 | RTorrent class. This wraps most of the RTorrent *main* functionality 47 | (read: global functionality) in a class. Think of, current upload and 48 | download, libTorrent version. 49 | 50 | Methods specific to a Torrent can be found in the :ref:`torrent-class` 51 | class. 52 | """ 53 | 54 | # FIXME: If we leave URL at '' xmlrpclib will default to /RPC2 as well. 55 | def __init__(self, target): 56 | """ 57 | Initialise the RTorrent object. 58 | ``target`` is target dict as parsed by parse_config (pyrotorrent.py). 59 | """ 60 | self.target = target 61 | self.s = RTorrentXMLRPC(target) 62 | 63 | self.hacks() 64 | 65 | def hacks(self): 66 | # Hack in all the methods in _rpc_methods! 67 | for x, y in _rpc_methods.items(): 68 | 69 | # caller = create_caller(y[0], create_argcheck(y[2])) # belongs to the 70 | # argument checking 71 | 72 | caller = (lambda name: lambda self, *args: getattr(self.s, name)(*args))(y[0]) 73 | caller.__doc__ = y[1] + '\nOriginal libTorrent method: ``%s``' % y[0] 74 | setattr(RTorrent, x, types.MethodType(caller, self)) 75 | 76 | del caller 77 | 78 | def __repr__(self): 79 | return 'RTorrent(%s)' % self.target['name'] 80 | 81 | def get_download_list(self, _type=''): 82 | """ 83 | Returns a list of torrents. 84 | _type defines what is returned. Valid: 85 | 86 | * '' (Empty string), 'default' 87 | * 'complete' 88 | * 'incomplete' 89 | * 'started' 90 | * 'stopped' 91 | * 'active' 92 | * 'hashing' 93 | * 'seeding' 94 | 95 | Plus all customly defined views. 96 | """ 97 | # FIXME: List is not complete(?) + exception should be raised. 98 | if _type not in ('complete', 'incomplete', 'started', 'stopped', 99 | 'active', 'hashing', 'seeding', '', 'default'): 100 | return None 101 | 102 | res = self.s.download_list(_type) 103 | 104 | # FIXME: We now only have the hashes. Do we also want to fetch all the 105 | # data per torrent? Or perhaps only the basic info? 106 | 107 | return res 108 | 109 | def query(self): 110 | """ 111 | Query returns a new RTorrentQuery object with the target 112 | from the current RTorrent object. 113 | 114 | Use this to execute several (different) calls on the RTorrent class in 115 | one request. This can increase performance and reduce latency and load. 116 | 117 | See :ref:`rtorrentquery-class` on how to use it. 118 | """ 119 | from lib.rtorrentquery import RTorrentQuery 120 | return RTorrentQuery(self.target) 121 | 122 | # XXX: Begin hacks 123 | 124 | import types 125 | 126 | _rpc_methods = { 127 | 'get_upload_throttle' : ('throttle.global_up.max_rate', 128 | """ 129 | Returns the current upload throttle. 130 | """), 131 | 'set_upload_throttle' : ('throttle.global_up.max_rate.set', 132 | """ 133 | Set the upload throttle. 134 | Pass the new throttle size in bytes. 135 | """), 136 | 'get_download_throttle' : ('throttle.global_down.max_rate', 137 | """ 138 | Returns the download upload throttle. 139 | """), 140 | 'set_download_throttle' : ('throttle.global_down.max_rate.set', 141 | """ 142 | Set the current download throttle. 143 | Pass the new throttle size in bytes. 144 | """), 145 | 'get_upload_rate' : ('throttle.global_up.rate', 146 | """ 147 | Returns the current upload rate. 148 | """), 149 | 'get_upload_rate_total' : ('throttle.global_up.total', 150 | """ 151 | Returns the total uploaded data. 152 | """), # XXX ^ Unsure about comment 153 | 'get_download_rate' : ('throttle.global_down.rate', 154 | """ 155 | Returns the current download rate. 156 | """), 157 | 'get_download_rate_total' : ('throttle.global_down.total', 158 | """ 159 | Returns the total downloaded data. 160 | """), # XXX ^ Unsure about comment 161 | 'get_memory_usage' : ('pieces.memory.current', 162 | """ 163 | Returns rtorrent memory usage. 164 | """), 165 | 'get_max_memory_usage' : ('pieces.memory.max', 166 | """ 167 | Returns rtorrent maximum memory usage. 168 | """), 169 | 'get_libtorrent_version' : ('system.library_version', 170 | """ 171 | Returns the libTorrent version. 172 | """), 173 | 'get_client_version' : ('system.client_version', 174 | """ 175 | Returns the rTorrent version. 176 | """), 177 | 'get_hostname' : ('system.hostname', 178 | """ 179 | Returns the hostname. 180 | """), 181 | 'add_torrent' : ('load.normal', 182 | """ 183 | Loads a torrent into rtorrent from the torrent path. 184 | """), 185 | 'add_torrent_start' : ('load.start', 186 | """ 187 | Loads a torrent into rtorrent from the torrent path. 188 | Will also start the download immediately. 189 | """), 190 | 'add_torrent_raw' : ('load.raw', 191 | """ 192 | Loads a torrent into rtorrent from a given string. 193 | """), 194 | 'add_torrent_raw_start' : ('load.raw_start', 195 | """ 196 | Loads a torrent into rtorrent from a given string. 197 | Will also start the download immediately. 198 | """), 199 | 'get_ip' : ('network.local_address', 200 | """ 201 | Returns the IP rtorrent is bound to. (For XMLRPC?) 202 | """), # XXX:For XMLRPC? ^ 203 | 'get_view_list' : ('view.list', 204 | """ 205 | Returns a list of all views. 206 | """), 207 | 'create_view' : ('view.add', 208 | """ 209 | Creates a view; requires a single argument: A name for the view. 210 | WARNING: If you add an already existing view; rtorrent will simply crash 211 | (at least 0.8.6 does). 212 | """), 213 | 'get_process_id' : ('system.pid', 214 | """ 215 | Returns the process ID. 216 | """), 217 | 'get_cwd' : ('system.get_cwd', 218 | """ 219 | Returns the current working directory. 220 | """), 221 | 'get_xmlrpc_size_limit' : ('network.xmlrpc.size_limit', 222 | """ 223 | Returns the XMLRPC Size Limit 224 | """), 225 | 'set_xmlrpc_size_limit' : ('network.xmlrpc.size_limit.set', 226 | """ 227 | Set the XMLRPC size limit. 228 | """), 229 | 'execute_command' : ('execute.capture', 230 | """ 231 | Execute command as rtorrent user and return output as string. 232 | """) 233 | } 234 | -------------------------------------------------------------------------------- /model/torrent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | .. _torrent-class: 4 | 5 | Torrent 6 | ======= 7 | 8 | The Torrent class defines a single torrent. 9 | It is instantiated with a Torrent specific hash, 10 | and connection information similar to :ref:`rtorrent-class`. 11 | """ 12 | 13 | from lib.xmlrpc import RTorrentXMLRPC 14 | import types 15 | 16 | #from lib.peerrequester import PeerRequester 17 | 18 | class Torrent(object): 19 | """ 20 | Torrent class. This class contains most of the methods specific to a 21 | torrent, such as get_name, get_hash, is_complete, get_download_rate, etc. 22 | """ 23 | 24 | def __init__(self, target, _hash): 25 | """ 26 | Initialise the Torrent object; pass a target dict (parsed by 27 | parse_config in pyrotorrentpy) and the torrent hash. 28 | """ 29 | self.target = target 30 | self.s = RTorrentXMLRPC(target) 31 | self._hash = _hash 32 | self.hacks() 33 | 34 | def hacks(self): 35 | for x, y in _rpc_methods.items(): 36 | 37 | # Passing self._hash as first (default) argument. This may be easier in 38 | # most cases. If you don't want to pass a default (hash) argument; use the 39 | # _rpc_methods_noautoarg variable. 40 | 41 | #caller = (lambda name: lambda self, *args: getattr(self.s, name)(*args))(y[0]) 42 | 43 | caller = (lambda name: lambda self, *args: getattr(self.s, name)(self._hash, *args))(y[0]) 44 | caller.__doc__ = y[1] + '\nOriginal libTorrent method: ``%s``' % y[0] 45 | setattr(Torrent, x, types.MethodType(caller, self)) 46 | 47 | del caller 48 | 49 | def __repr__(self): 50 | return 'Torrent(%s): %s' % (self._hash, self.get_name()) 51 | 52 | def query(self): 53 | """ 54 | Query returns a new TorrentQuery object with the host, port, url and 55 | hash(!) from the current Torrent object. The hash will be used as 56 | default argument in the TorrentQuery class. 57 | See :ref:`torrentquery-class` 58 | """ 59 | from lib.torrentquery import TorrentQuery 60 | return TorrentQuery(self.target, self._hash) 61 | 62 | def get_peers(self): 63 | pass 64 | 65 | # XXX: Begin hacks 66 | 67 | # RPC Methods for Torrent. You don't have to pass the Torrent hash; it is 68 | # automatically passed. When you invoke one of these methods on a Torrent 69 | # instance. 70 | _rpc_methods = { 71 | 'get_name' : ('d.name', # XXX: get_base_filename is the same? 72 | """ 73 | Returns the name of the Torrent. 74 | """), 75 | 'get_full_path' : ('d.base_path', 76 | """ 77 | Returns the full path to the Torrent. 78 | """), 79 | 'get_bytes_done' : ('d.bytes_done', 80 | """ 81 | Returns the amount of bytes done. 82 | """), 83 | 'is_complete' : ('d.complete', 84 | """ 85 | Returns 1 if torrent is complete; 0 if it is not complete. 86 | """), 87 | 'get_download_rate' : ('d.down.rate', 88 | """ 89 | Returns the current download rate for Torrent. 90 | """), 91 | 'get_download_total' : ('d.down.total', 92 | """ 93 | Returns the total downloaded data for torrent. 94 | """), 95 | 'get_upload_rate' : ('d.up.rate', 96 | """ 97 | Returns the current upload rate for Torrent. 98 | """), 99 | 'get_upload_total' : ('d.up.total', 100 | """ 101 | Returns the total uploaded data for torrent. 102 | """), 103 | 'get_bytes_left' : ('d.left_bytes', 104 | """ 105 | Returns the amounts of bytes left to download. 106 | """), 107 | 'get_ratio' : ('d.ratio', 108 | """ 109 | Returns the ratio for this Torrent. (Download / Upload) 110 | """), 111 | 'get_size_bytes' : ('d.size_bytes', 112 | """ 113 | Returns the size of the torrent in bytes. 114 | """), 115 | 'get_size_chucks' : ('d.get_size_chucks', 116 | """ 117 | Returns the size of the torrent in chucks. 118 | """), 119 | 'get_size_files' : ('d.size_files', 120 | """ 121 | Returns the size of the torrent in files. 122 | """), 123 | 'get_loaded_file' : ('d.loaded_file', 124 | """ 125 | Returns absolute path to .torrent file. 126 | """), 127 | 128 | 'get_hash' : ('d.hash', 129 | """ 130 | Returns the torrent hash. Very useful. 131 | """), 132 | 'is_hashing' : ('d.hashing', 133 | """ 134 | """), 135 | 'hashing_failed' : ('d.hashing_failed', 136 | """ 137 | """), 138 | 'perform_hash_check' : ('d.check_hash', 139 | """ 140 | Performs a hash check. Returns 0 immediately. 141 | """), 142 | 'open' : ('d.open', 143 | """ 144 | Open a torrent. 145 | """), 146 | 'close' : ('d.close', 147 | """ 148 | Close a torrent. 149 | """), 150 | 'start' : ('d.start', 151 | """ 152 | Start a torrent. 153 | """), 154 | 'stop' : ('d.stop', 155 | """ 156 | Stop a torrent. 157 | """), 158 | 'pause' : ('d.pause', 159 | """ 160 | Pause a torrent. 161 | """), 162 | 'resume' : ('d.resume', 163 | """ 164 | Resume a torrent. 165 | """), 166 | 'erase' : ('d.erase', 167 | """ 168 | Erase a torrent. 169 | """), 170 | 'is_active' : ('d.is_active', 171 | """ 172 | Returns 1 if the torrent is active; 0 when it is not active. 173 | """), 174 | 'is_open' : ('d.is_open', 175 | """ 176 | Returns 1 if the torrent is open, 0 otherwise. 177 | """ 178 | ), 179 | 'is_hash_checked' : ('d.is_hash_checked', 180 | """ 181 | Returns 1 if the hash has been checked, 0 is otherwise. 182 | """), 183 | 'is_hash_checking' : ('d.is_hash_checking', 184 | """ 185 | Returns 1 if the hash is currently being checked, 0 otherwise? 186 | TODO 187 | """), # TODO 188 | 'get_state' : ('d.state', 189 | """ 190 | No clue as to what this returns yet. 191 | TODO 192 | """),# TODO 193 | 'get_message' : ('d.message', 194 | """ 195 | Returns the torrent *message*. 196 | """), 197 | 'get_views' : ('d.views', 198 | """ 199 | """) 200 | } 201 | -------------------------------------------------------------------------------- /model/torrentfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _torrentfile-class: 3 | 4 | TorrentFile 5 | =========== 6 | 7 | """ 8 | 9 | from lib.xmlrpc import RTorrentXMLRPC 10 | import types 11 | 12 | class TorrentFile(object): 13 | """ 14 | """ 15 | 16 | def __init__(self, target): 17 | 18 | self.target = target 19 | self.s = RTorrentXMLRPC(target) 20 | self.hacks() 21 | 22 | def hacks(self): 23 | for x, y in _rpc_methods.items(): 24 | caller = (lambda name: lambda self, *args: getattr(self.s, name)(self._hash, *args))(y[0]) 25 | caller.__doc__ = y[1] 26 | setattr(TorrentFile, x, types.MethodType(caller, None, TorrentFile)) 27 | 28 | del caller 29 | 30 | def query(self): 31 | """ 32 | """ 33 | raise Exception('TODO') 34 | 35 | _rpc_methods = { 36 | 'get_path' : ('f.path', 37 | """ 38 | Returns the path. 39 | """), 40 | 'get_path_components' : ('f.path_components', 41 | """ 42 | Returns the path components. 43 | """), 44 | 'get_frozen_path' : ('f.frozen_path', 45 | """ 46 | Returns the *frozen* path. 47 | """), # XXX: What is the frozen path? 48 | 'get_size_bytes' : ('f.size_bytes', 49 | """ 50 | Returns the total size of the file. 51 | """), 52 | 'get_size_chunks' : ('f.size_chunks', 53 | """ 54 | Returns the size of the file in chunks. 55 | """), 56 | 'get_completed_chunks' : ('f.completed_chunks', 57 | """ 58 | Get the amount of completed chunks. 59 | """) 60 | } 61 | -------------------------------------------------------------------------------- /pyrotorrent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | TODO: 4 | - Default arguments for jinja (wn, etc) 5 | """ 6 | 7 | ############################################################################### 8 | # PYROTORRENT # 9 | ############################################################################### 10 | # To configure pyrotorrent, check the documentation as well as config.py and # 11 | # flask-config.py. These two files will be merged later. # 12 | ############################################################################### 13 | 14 | 15 | from flask import Flask, request, session, g, redirect, url_for, \ 16 | abort, render_template, flash 17 | 18 | from flask import Response 19 | 20 | # TODO http://flask.pocoo.org/docs/config/ 21 | 22 | 23 | app = Flask(__name__) 24 | app.config.from_object(__name__) 25 | app.config.from_pyfile('flask-config.py') 26 | 27 | from config import FILE_BLOCK_SIZE, BACKGROUND_IMAGE, \ 28 | USE_AUTH, ENABLE_API, rtorrent_config, torrent_users, USE_OWN_HTTPD, \ 29 | CACHE_TIMEOUT 30 | 31 | from lib.config_parser import parse_config_part, parse_user_part, \ 32 | RTorrentConfigException, CONNECTION_SCGI, CONNECTION_HTTP 33 | 34 | from model.rtorrent import RTorrent 35 | from model.torrent import Torrent 36 | 37 | from lib.multibase import InvalidTorrentException, InvalidConnectionException, \ 38 | InvalidTorrentCommandException 39 | 40 | from lib.torrentrequester import TorrentRequester 41 | from lib.peerrequester import PeerRequester 42 | 43 | from lib.filerequester import TorrentFileRequester 44 | from lib.filetree import FileTree 45 | 46 | # TODO REMOVE? 47 | from lib.helper import wiz_normalise, pyro_render_template, error_page, loggedin, \ 48 | loggedin_and_require, parse_config, parse_users, fetch_user, \ 49 | fetch_global_info, lookup_user, lookup_target, redirect_client_prg, \ 50 | redirect_client 51 | from lib.decorators import pyroview, require_torrent, \ 52 | require_rtorrent, require_target 53 | 54 | # For MIME 55 | import mimetypes 56 | 57 | # For stat and path services 58 | import os 59 | import stat 60 | 61 | # For fetching http .torrents 62 | import urllib.request, urllib.error, urllib.parse 63 | import xmlrpc.client # for .Binary 64 | 65 | # For serving .torrent files 66 | import base64 67 | 68 | import datetime 69 | import time 70 | 71 | import json 72 | 73 | @app.route('/') 74 | @app.route('/view/') 75 | @pyroview 76 | def main_view_page(view='default'): 77 | rtorrent_data = fetch_global_info() 78 | 79 | # if view not in rtorrent_data.get_view_list: 80 | # return error_page(env, 'Invalid view: %s' % view) 81 | 82 | torrents = {} 83 | for target in targets: 84 | if g.user == None and USE_AUTH: 85 | continue 86 | if g.user and (target['name'] not in g.user.targets): 87 | continue 88 | 89 | try: 90 | t = TorrentRequester(target, '', view) 91 | 92 | t.get_name().get_download_rate().get_upload_rate() \ 93 | .is_complete().get_size_bytes().get_download_total().get_hash() 94 | 95 | h = hash(t) 96 | 97 | torrents[target['name']] = cache.get(h) 98 | if torrents[target['name']]: 99 | continue 100 | 101 | torrents[target['name']] = cache.get(target['name']) 102 | 103 | if torrents[target['name']] is not None: 104 | continue 105 | 106 | torrents[target['name']] = t.all() 107 | 108 | cache.set(h, torrents[target['name']], timeout=CACHE_TIMEOUT) 109 | 110 | except InvalidTorrentException as e: 111 | return error_page(env, str(e)) 112 | 113 | return pyro_render_template('download_list.html', 114 | torrents_list=torrents, rtorrent_data=rtorrent_data, view=view 115 | # TODO 116 | ) 117 | 118 | @app.route('/target//torrent/') 119 | @app.route('/target//torrent//') 120 | @pyroview 121 | @require_target 122 | @require_torrent 123 | def torrent_info(target, torrent, action=None): 124 | print(torrent, action) 125 | if action in ('open', 'close', 'start', 'stop', 'pause', 'resume'): 126 | print('Executing action', action) 127 | print(getattr(torrent, action)()) 128 | flash('Executed %s on torrent %s' % (action, torrent.get_name())) 129 | 130 | try: 131 | q = torrent.query() 132 | q.get_hash().get_name().get_size_bytes().get_download_total().\ 133 | get_loaded_file().get_message().is_active() 134 | h = hash(q) 135 | 136 | torrentinfo = cache.get(h) 137 | if torrentinfo is None: 138 | torrentinfo = q.all()[0] # .first() ? 139 | cache.set(h, torrentinfo, CACHE_TIMEOUT) 140 | 141 | except InvalidTorrentException as e: 142 | return error_page(env, str(e)) 143 | 144 | p = PeerRequester(torrent.target, torrent._hash) 145 | 146 | p.get_address().get_client_version().is_encrypted().get_id() 147 | 148 | peers = p.all() 149 | 150 | files = TorrentFileRequester(target, torrent._hash)\ 151 | .get_path_components().get_size_chunks().get_completed_chunks() 152 | 153 | h = hash(files) 154 | f = cache.get(h) 155 | if f is None: 156 | f = files.all() 157 | cache.set(h, f, CACHE_TIMEOUT) 158 | 159 | tree = FileTree(f).root 160 | 161 | rtorrent_data = fetch_global_info() 162 | 163 | return pyro_render_template('torrent_info.html', torrent=torrentinfo, tree=tree, 164 | rtorrent_data=rtorrent_data, target=target, 165 | file_downloads='storage_mode' in target, 166 | peers=peers 167 | 168 | # TODO FIX ME 169 | ,wn=wiz_normalise 170 | ) 171 | 172 | @app.route('/target//torrent//peer/') 173 | @pyroview 174 | @require_target 175 | @require_torrent 176 | def peer_info(target, torrent, peer_id): 177 | return 'Peer info page will be here' 178 | 179 | @app.route('/target//torrent/.torrent') 180 | @pyroview 181 | @require_target 182 | @require_torrent 183 | @require_rtorrent 184 | def torrent_file(target, torrent, rtorrent): 185 | try: 186 | filepath = torrent.get_loaded_file() 187 | 188 | # TODO: Check for errors. (Permission denied, non existing file, etc) 189 | contents = rtorrent.execute_command('sh', '-c', 'cat ' + filepath + 190 | ' | base64') 191 | 192 | except InvalidTorrentException as e: 193 | return error_page(env, str(e)) 194 | 195 | r = Response(base64.b64decode(contents), 196 | mimetype='application/x-bittorrent') 197 | r.status_code = 200 198 | return r 199 | 200 | @app.route('/target//torrent//get_file/') 201 | @pyroview 202 | @require_target 203 | @require_torrent 204 | def torrent_get_file(target, torrent, filename): 205 | # Is file fetching enabled? 206 | print(target) 207 | if 'storage_mode' not in target: 208 | print("File fetching disabled, 404") 209 | abort(404) 210 | 211 | s_mode = target['storage_mode'] 212 | 213 | print("Requested file:", filename) 214 | 215 | # Fetch absolute path to torrent 216 | try: 217 | # FIXME: rtorrent get_full_path() apparently isn't always ``full''. 218 | t_path = torrent.get_full_path() 219 | except InvalidTorrentException as e: 220 | return error_page(env, str(e)) 221 | 222 | # rtorrent is running on a remote fs mounted 223 | # on this machine? 224 | if 'remote_path' in s_mode: 225 | # Transform remote path to locally mounted path 226 | t_path = t_path.replace(s_mode['remote_path'], s_mode['local_path'], 1) 227 | 228 | # Compute absolute file path 229 | try: 230 | if stat.S_ISDIR(os.stat(t_path).st_mode): 231 | print("Multi file torrent.") 232 | file_path = os.path.abspath(t_path + '/' + filename) 233 | else: 234 | print("Single file torrent.") 235 | file_path = os.path.abspath(t_path) 236 | except OSError as e: 237 | print("Exception performing stat:", e) 238 | abort(500) 239 | 240 | print("Computed path:", file_path) 241 | 242 | # Now verify this path is within torrent path 243 | if file_path.find(t_path) != 0: 244 | print("Path rejected..") 245 | abort(403) 246 | 247 | print("Path accepted.") 248 | 249 | HTTP_RANGE_REQUEST = False 250 | try: 251 | _bytes = request.headers['Range'] 252 | print('HTTP_RANGE Found') 253 | _bytes = _bytes.split('=')[1] 254 | _start, _end = _bytes.split('-') 255 | _start = int(_start) 256 | if _end: 257 | _end = int(_end) 258 | 259 | HTTP_RANGE_REQUEST = True 260 | 261 | except KeyError as e: 262 | print('No HTTP_RANGE passed') 263 | except (ValueError, IndexError) as e: 264 | print('Invalid HTTP_RANGE:', e) 265 | 266 | 267 | # Open file for reading 268 | try: 269 | f = open(file_path, 'r') 270 | if HTTP_RANGE_REQUEST: 271 | print('Seeking...') 272 | f.seek(_start) 273 | except IOError as e: 274 | print('Exception opening file:', e) 275 | return None 276 | print('File open.') 277 | 278 | # Setup response 279 | f_size = os.path.getsize(file_path) 280 | if HTTP_RANGE_REQUEST: 281 | # We can't set _end earlier. 282 | if not _end: 283 | _end = f_size 284 | 285 | mimetype = mimetypes.guess_type(file_path) 286 | if mimetype[0] == None: 287 | mimetype = 'application/x-binary' 288 | else: 289 | mimetype = mimetype[0] 290 | 291 | headers = [] 292 | headers.append(('Content-Type', mimetype)) 293 | 294 | if HTTP_RANGE_REQUEST: 295 | headers.append(('Content-length', str(_end-_start))) 296 | else: 297 | headers.append(('Content-length', str(f_size))) 298 | 299 | # Let browser figure out filename 300 | # See also: http://greenbytes.de/tech/tc2231/ 301 | # and: http://stackoverflow.com/questions/1361604/how-to-encode-utf8-filename-for-http-headers-python-django 302 | headers.append(('Content-Disposition', 'attachment;'\ 303 | 'filename="'+os.path.split(file_path)[1]+'"')) 304 | 305 | # Useful code for returning files 306 | # http://stackoverflow.com/questions/3622675/returning-a-file-to-a-wsgi-get-request 307 | # if 'wsgi.file_wrapper' in env: 308 | # f_ret = env['wsgi.file_wrapper'](f, FILE_BLOCK_SIZE) 309 | # else: 310 | f_ret = iter(lambda: f.read(FILE_BLOCK_SIZE), '') 311 | 312 | 313 | if HTTP_RANGE_REQUEST: 314 | # Date, Content-Location/ETag, Expires/Cache-Control 315 | # Either a Content-Range header field (section 14.16) indicating 316 | # the range included with this response, or a multipart/byteranges 317 | # Content-Type including Content-Range fields for each part. If a 318 | # Content-Length header field is present in the response, its 319 | # value MUST match the actual number of OCTETs transmitted in the 320 | # message-body. 321 | d = datetime.datetime.now() 322 | 323 | headers.append(('Date', d.strftime('%a, %d %b %Y %H:%M:%S GMT'))) 324 | headers.append(('Content-Range', 'bytes: %d-%d/%d' % (_start, _end-1, 325 | f_size-1))) 326 | headers.append(('Cache-Control', 'max-age=3600')) 327 | headers.append(('Content-Location', filename)) 328 | 329 | r = Response(f_ret) 330 | r.status_code = 206 331 | r.headers = headers 332 | return r 333 | else: 334 | r = Response(f_ret) 335 | r.status_code = 200 336 | r.headers = headers 337 | return r 338 | 339 | 340 | @app.route('/target//add_torrent', methods=['GET', 'POST']) 341 | @pyroview 342 | @require_target 343 | def add_torrent_page(target): 344 | if request.method == 'POST': 345 | if 'torrent_file' in request.files: 346 | 347 | torrent_raw = request.files['torrent_file'].read() 348 | 349 | torrent_raw_bin = xmlrpc.client.Binary(torrent_raw) 350 | 351 | rtorrent = RTorrent(target) 352 | return_code = rtorrent.add_torrent_raw_start('', torrent_raw_bin) 353 | elif 'torrent_url' in request.form: 354 | 355 | torrent_url = request.form['torrent_url'] 356 | 357 | response = urllib.request.urlopen(torrent_url) 358 | torrent_raw = response.read() 359 | 360 | torrent_raw_bin = xmlrpc.client.Binary(torrent_raw) 361 | 362 | rtorrent = RTorrent(target) 363 | return_code = rtorrent.add_torrent_raw_start('', torrent_raw_bin) 364 | elif 'magnet' in request.form: 365 | magnet_link = request.form['magnet'] 366 | 367 | torrent = 'd10:magnet-uri' + str(len(magnet_link)) + ':' + magnet_link + 'e' 368 | 369 | rtorrent = RTorrent(target) 370 | return_code = rtorrent.add_torrent_raw('', torrent) 371 | 372 | flash('Succesfully added torrent' if return_code == 0 else 'Failed to add torrent') 373 | 374 | rtorrent_data = fetch_global_info() 375 | 376 | return pyro_render_template('torrent_add.html', 377 | rtorrent_data=rtorrent_data, target=target['name']) 378 | 379 | def handle_api_method(method, keys): 380 | if not ENABLE_API: 381 | return None 382 | if method not in known_methods: 383 | raise Exception('Unknown method') 384 | 385 | if 'target' in keys: 386 | target = keys['target'] 387 | target = lookup_target(target) 388 | if target is None: 389 | print('Returning null, target is invalid') 390 | return None 391 | 392 | # u = fetch_user(env) 393 | # # User can't be none, since we've already passed login. 394 | # if USE_AUTH and target not in user.targets: 395 | # print 'User is not allowed to use target: %s!' % target 396 | 397 | 398 | if method == 'torrentrequester': 399 | return known_methods[method](keys, target) 400 | else: 401 | return known_methods[method](keys, method, target) 402 | 403 | 404 | def handle_torrentrequester(k, target): 405 | if 'view' not in k or 'attributes' not in k: 406 | return None 407 | 408 | view = k['view'] 409 | attributes = k['attributes'] 410 | 411 | try: 412 | tr = TorrentRequester(target, view) 413 | for method, args in attributes: 414 | getattr(tr, method) 415 | 416 | h = hash(tr) 417 | r = cache.get(h) 418 | if r is None: 419 | r = tr.all() 420 | cache.set(h, r, timeout=CACHE_TIMEOUT) 421 | 422 | return r 423 | 424 | except (InvalidTorrentCommandException,): 425 | return None 426 | 427 | 428 | def handle_rtorrent_torrent(k, m, target): 429 | if 'attributes' not in k: 430 | return None 431 | 432 | attributes = k['attributes'] 433 | 434 | try: 435 | if m == 'rtorrent': 436 | a = RTorrent(target).query() 437 | else: 438 | if 'hash' not in k: 439 | return None 440 | 441 | _hash = k['hash'] 442 | 443 | a = Torrent(target, _hash).query() 444 | 445 | for method, args in attributes: 446 | getattr(a, method)(*args) 447 | 448 | h = hash(a) 449 | r = cache.get(h) 450 | if r is None: 451 | r = a.first() 452 | cache.set(h, r, timeout=CACHE_TIMEOUT) 453 | 454 | return r 455 | except (InvalidTorrentException, InvalidTorrentCommandException) as e: 456 | print(e) 457 | return None 458 | 459 | known_methods = { 460 | 'torrentrequester' : handle_torrentrequester, 461 | 'rtorrent' : handle_rtorrent_torrent, 462 | 'torrent' : handle_rtorrent_torrent 463 | } 464 | 465 | 466 | @app.route('/api', methods=['POST']) 467 | def api(): 468 | if not ENABLE_API: 469 | abort(403) 470 | #if not loggedin_and_require(env): 471 | # r = Response(json.dumps(None, indent=' ' * 4), mimetype='application/json') 472 | # r.status_code = 403 473 | # return r 474 | 475 | # Get JSON request via POST data 476 | try: 477 | req = request.form['request'] 478 | except KeyError as e: 479 | abort(500) 480 | 481 | d = json.loads(req) 482 | resp = [] 483 | 484 | for x in d: 485 | if 'type' in x: 486 | print('Method:', x['type']) 487 | resp.append(handle_api_method(x['type'], x)) 488 | 489 | s = json.dumps(resp, indent=4) 490 | 491 | r = Response(s, mimetype='application/json') 492 | r.status_code = 200 493 | return r 494 | 495 | @app.route('/style.css') 496 | def style_serve(): 497 | """ 498 | TODO: Re-add user lookup and user specific image: 499 | 500 | if user and user.background_image != None: 501 | background = user.background_image 502 | 503 | """ 504 | background = BACKGROUND_IMAGE 505 | 506 | return Response(pyro_render_template('style.css', 507 | trans=0.6), 508 | mimetype='text/css') 509 | 510 | @app.route('/login', methods=['GET', 'POST']) 511 | @pyroview(require_login=False) 512 | def handle_login(): 513 | print('Handle das login') 514 | #tmpl = jinjaenv.get_template('loginform.html') 515 | _login_fail = lambda: pyro_render_template('loginform.html', loginfail=True) 516 | print('Request method:', request.method) 517 | 518 | if request.method == 'POST': 519 | if 'user' not in request.form or 'pass' not in request.form: 520 | return _login_fail() 521 | 522 | user = request.form['user'] 523 | passwd = request.form['pass'] 524 | 525 | # pass = hashlib.sha256(pass).hexdigest() 526 | u = lookup_user(user) 527 | if u is None: 528 | return _login_fail() 529 | 530 | if u.password == passwd: 531 | print('Login succes.') 532 | session['user_name'] = user 533 | 534 | # Redirect user to original page, or if not possible 535 | # to the main page. 536 | redir_url = session.pop('login_redirect_url', None) 537 | if redir_url: 538 | print('Redirecting to:', redir_url) 539 | return redirect_client_prg(url=redir_url) 540 | 541 | return redirect_client_prg('main_view_page') 542 | 543 | else: 544 | return _login_fail() 545 | 546 | # Not logged in? 547 | # Render login page, and store 548 | # current URL in beaker session. 549 | if not loggedin(): 550 | print('Not logged in, storing session data:', request.base_url) 551 | session['login_redirect_url'] = request.base_url 552 | 553 | return pyro_render_template('loginform.html') 554 | 555 | # User already loggedin, redirect to main page. 556 | else: 557 | return redirect_client('main_view_page') 558 | 559 | @app.route('/logout') 560 | @pyroview(require_login=False) 561 | def handle_logout(): 562 | if loggedin(): 563 | session.pop('user_name', None) 564 | else: 565 | return error_page(env, 'Not logged in') 566 | 567 | return redirect_client('main_view_page') 568 | 569 | class PrefixWith(object): 570 | def __init__(self, app): 571 | self.app = app 572 | 573 | def __call__(self, environ, start_response): 574 | app_root = app.config['APPLICATION_ROOT'] 575 | if environ['PATH_INFO'].startswith(app_root): 576 | environ['PATH_INFO'] = environ['PATH_INFO'][len(app_root):] 577 | environ['SCRIPT_NAME'] = app_root 578 | else: 579 | environ['PATH_INFO'] = '/GENERIC-404' 580 | environ['SCRIPT_NAME'] = '' 581 | #start_response('404 Not Found', [('Content-Type', 'text/plain')]) 582 | #return '404' 583 | 584 | return app(environ, start_response) 585 | 586 | 587 | 588 | class SimpleCache(object): 589 | def __init__(self): 590 | self.kv = {} 591 | 592 | def get(self, name): 593 | if name in self.kv: 594 | v = self.kv[name] 595 | tt = (time.time() - v[1]) 596 | if tt > v[2]: 597 | return None 598 | return v[0] 599 | else: 600 | return None 601 | 602 | def set(self, name, value, timeout=1): 603 | self.kv[name] = (value, time.time(), timeout) 604 | 605 | 606 | 607 | if __name__ == '__main__': 608 | targets = parse_config() 609 | users = parse_users() 610 | 611 | from cachelib import SimpleCache 612 | cache = SimpleCache() 613 | 614 | import lib.helper 615 | lib.helper.targets = targets 616 | lib.helper.users = users 617 | lib.helper.cache = cache 618 | lib.decorators.handle_login = handle_login 619 | 620 | 621 | if USE_OWN_HTTPD: 622 | app.run() # Run with host='0.0.0.0' if you want pyro to be 623 | # remotely accessible 624 | else: 625 | application = PrefixWith(app) 626 | from flup.server.fcgi import WSGIServer 627 | WSGIServer(application).run() 628 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | cachelib 3 | flup 4 | -------------------------------------------------------------------------------- /rtorrentcommands.txt: -------------------------------------------------------------------------------- 1 | [ ] system.listMethods # Returns all supported methods. 2 | 3 | [ ] system.methodExist 4 | [ ] system.methodHelp 5 | [ ] system.methodSignature 6 | [ ] system.multicall 7 | [ ] system.shutdown 8 | [ ] system.capabilities 9 | [ ] system.getCapabilities 10 | [ ] and 11 | [ ] argument.0 12 | [ ] argument.1 13 | [ ] argument.2 14 | [ ] argument.3 15 | [ ] branch 16 | [ ] call_download 17 | [ ] cat 18 | [ ] close_low_diskspace 19 | [ ] close_untied 20 | [ ] create_link 21 | [ ] d.add_peer 22 | [/] d.check_hash # Partially implemented. (Need to figure out how to use it, see 23 | # when it is done etc) 24 | [X] d.close 25 | [ ] d.create_link 26 | [ ] d.delete_link 27 | [ ] d.delete_tied 28 | [X] d.erase 29 | [ ] d.get_base_filename 30 | [X] d.get_base_path # Returns the base path. Torrent.get_full_path 31 | [ ] d.get_bitfield 32 | [X] d.get_bytes_done # Torrent.get_bytes_done 33 | [ ] d.get_chunk_size 34 | [ ] d.get_chunks_hashed 35 | [X] d.get_complete 36 | [X] d.get_completed_bytes # Same as get_bytes_done? 37 | [ ] d.get_completed_chunks 38 | [ ] d.get_connection_current 39 | [ ] d.get_connection_leech 40 | [ ] d.get_connection_seed 41 | [ ] d.get_creation_date 42 | [ ] d.get_custom 43 | [ ] d.get_custom1 44 | [ ] d.get_custom2 45 | [ ] d.get_custom3 46 | [ ] d.get_custom4 47 | [ ] d.get_custom5 48 | [ ] d.get_custom_throw 49 | [ ] d.get_directory 50 | [ ] d.get_directory_base 51 | [X] d.get_down_rate # Torrent.get_download_rate 52 | [X] d.get_down_total # Torrent.get_download_total 53 | [ ] d.get_free_diskspace 54 | [X] d.get_hash # Torrent.get_hash 55 | [X] d.get_hashing # Torrent.is_hashing 56 | [X] d.get_hashing_failed # Torrent.hashing_failed 57 | [ ] d.get_ignore_commands 58 | [X] d.get_left_bytes 59 | [X] d.get_loaded_file 60 | [ ] d.get_local_id 61 | [ ] d.get_local_id_html 62 | [ ] d.get_max_file_size 63 | [ ] d.get_max_size_pex 64 | [X] d.get_message 65 | [ ] d.get_mode 66 | [X] d.get_name # Torrent.get_name 67 | [ ] d.get_peer_exchange 68 | [ ] d.get_peers_accounted 69 | [ ] d.get_peers_complete 70 | [ ] d.get_peers_connected 71 | [ ] d.get_peers_max 72 | [ ] d.get_peers_min 73 | [ ] d.get_peers_not_connected 74 | [ ] d.get_priority 75 | [ ] d.get_priority_str 76 | [X] d.get_ratio 77 | [X] d.get_size_bytes # Torrent.get_size_bytes 78 | [X] d.get_size_chunks # Torrent.get_size_chucks 79 | [X] d.get_size_files # Torrent.get_size_files 80 | [ ] d.get_size_pex 81 | [ ] d.get_skip_rate 82 | [ ] d.get_skip_total 83 | [X] d.get_state 84 | [ ] d.get_state_changed 85 | [ ] d.get_state_counter 86 | [ ] d.get_throttle_name 87 | [ ] d.get_tied_to_file 88 | [ ] d.get_tracker_focus 89 | [ ] d.get_tracker_numwant 90 | [ ] d.get_tracker_size 91 | [X] d.get_up_rate # Torrent.get_upload_rate 92 | [X] d.get_up_total # Torrent.get_upload_total 93 | [ ] d.get_uploads_max 94 | [ ] d.initialize_logs 95 | [X] d.is_active 96 | [X] d.is_hash_checked 97 | [X] d.is_hash_checking 98 | [ ] d.is_multi_file 99 | [X] d.is_open 100 | [ ] d.is_pex_active 101 | [ ] d.is_private 102 | [X] d.multicall 103 | [X] d.open 104 | [X] d.pause 105 | [X] d.resume 106 | [ ] d.save_session 107 | [ ] d.set_connection_current 108 | [ ] d.set_custom 109 | [ ] d.set_custom1 110 | [ ] d.set_custom2 111 | [ ] d.set_custom3 112 | [ ] d.set_custom4 113 | [ ] d.set_custom5 114 | [ ] d.set_directory 115 | [ ] d.set_directory_base 116 | [ ] d.set_hashing_failed 117 | [ ] d.set_ignore_commands 118 | [ ] d.set_max_file_size 119 | [ ] d.set_message 120 | [ ] d.set_peer_exchange 121 | [ ] d.set_peers_max 122 | [ ] d.set_peers_min 123 | [ ] d.set_priority 124 | [ ] d.set_throttle_name 125 | [ ] d.set_tied_to_file 126 | [ ] d.set_tracker_numwant 127 | [ ] d.set_uploads_max 128 | [X] d.start 129 | [X] d.stop 130 | [ ] d.try_close 131 | [ ] d.try_start 132 | [ ] d.try_stop 133 | [ ] d.update_priorities 134 | [T] d.views 135 | [ ] d.views.has 136 | [ ] d.views.push_back 137 | [ ] d.views.push_back_uniqu. 138 | [ ] d.views.remove 139 | [ ] delete_link 140 | [ ] dht 141 | [ ] dht_add_node 142 | [ ] dht_statistics 143 | [X] download_list # Returns list of hashes. See RTorrent.get_download_list 144 | [ ] enable_trackers 145 | [ ] encoding_list 146 | [ ] encryption 147 | [ ] event.download.closed 148 | [ ] event.download.erased 149 | [ ] event.download.finished 150 | [ ] event.download.hash_done 151 | [ ] event.download.hash_queued 152 | [ ] event.download.hash_removed 153 | [ ] event.download.inserted 154 | [ ] event.download.inserted_new 155 | [ ] event.download.inserted_session 156 | [ ] event.download.opened 157 | [ ] event.download.paused 158 | [ ] event.download.resumed 159 | [ ] execute 160 | [ ] execute_capture 161 | [ ] execute_capture_nothrow 162 | [ ] execute_nothrow 163 | [ ] execute_raw 164 | [ ] execute_raw_nothrow 165 | [X] f.get_completed_chunks 166 | [X] f.get_frozen_path 167 | [ ] f.get_last_touched 168 | [ ] f.get_match_depth_next 169 | [ ] f.get_match_depth_prev 170 | [ ] f.get_offset 171 | [X] f.get_path 172 | [X] f.get_path_components 173 | [ ] f.get_path_depth 174 | [ ] f.get_priority 175 | [ ] f.get_range_first 176 | [ ] f.get_range_second 177 | [X] f.get_size_bytes 178 | [X] f.get_size_chunks 179 | [ ] f.is_create_queued 180 | [ ] f.is_created 181 | [ ] f.is_open 182 | [ ] f.is_resize_queued 183 | [X] f.multicall # First arg is hash, Second argument is empty ''... third is 184 | # methods. 185 | [ ] f.set_create_queued 186 | [ ] f.set_priority 187 | [ ] f.set_resize_queued 188 | [ ] f.unset_create_queued 189 | [ ] f.unset_resize_queued 190 | [ ] false 191 | [ ] fi.get_filename_last 192 | [ ] fi.is_file 193 | [ ] get_active_bg_color 194 | [ ] get_active_fg_color 195 | [ ] get_bind 196 | [ ] get_check_hash 197 | [ ] get_connection_leech 198 | [ ] get_connection_seed 199 | [ ] get_dht_port 200 | [ ] get_dht_throttle 201 | [ ] get_directory 202 | [ ] get_done_bg_color 203 | [ ] get_done_fg_color 204 | [X] get_down_rate # Current download rate. RTorrent.get_download_rate 205 | [X] get_down_total # Total download. RTorrent.get_download_rate_total(0 206 | [X] get_download_rate # Download throttle. RTorrent.get_download_throttle 207 | [ ] get_handshake_log 208 | [ ] get_hash_interval 209 | [ ] get_hash_max_tries 210 | [ ] get_hash_read_ahead 211 | [ ] get_http_cacert 212 | [ ] get_http_capath 213 | [ ] get_http_proxy 214 | [X] get_ip 215 | [ ] get_key_layout 216 | [ ] get_log.tracker 217 | [ ] get_max_downloads_div 218 | [ ] get_max_downloads_global 219 | [ ] get_max_file_size 220 | [X] get_max_memory_usage # RTorrent.get_max_memory_usage 221 | [ ] get_max_open_files 222 | [ ] get_max_open_http 223 | [ ] get_max_open_sockets 224 | [ ] get_max_peers 225 | [ ] get_max_peers_seed 226 | [ ] get_max_uploads 227 | [ ] get_max_uploads_div 228 | [ ] get_max_uploads_global 229 | [X] get_memory_usage 230 | [ ] get_min_peers 231 | [ ] get_min_peers_seed 232 | [ ] get_name 233 | [ ] get_peer_exchange 234 | [ ] get_port_open 235 | [ ] get_port_random 236 | [ ] get_port_range 237 | [ ] get_preload_min_size 238 | [ ] get_preload_required_rate 239 | [ ] get_preload_type 240 | [ ] get_proxy_address 241 | [ ] get_receive_buffer_size 242 | [ ] get_safe_free_diskspace 243 | [ ] get_safe_sync 244 | [ ] get_scgi_dont_route 245 | [ ] get_send_buffer_size 246 | [ ] get_session 247 | [ ] get_session_lock 248 | [ ] get_session_on_completion 249 | [ ] get_split_file_size 250 | [ ] get_split_suffix 251 | [ ] get_stats_not_preloaded 252 | [ ] get_stats_preloaded 253 | [ ] get_throttle_down_max 254 | [ ] get_throttle_down_rate 255 | [ ] get_throttle_up_max 256 | [ ] get_throttle_up_rate 257 | [ ] get_timeout_safe_sync 258 | [ ] get_timeout_sync 259 | [ ] get_tracker_numwant 260 | [X] get_up_rate # current upload rate (not throttle) 261 | [X] get_up_total # total uploaded data RTorrent.get_upload_rate_total 262 | [X] get_upload_rate # current upload _throttle_ RTorrent.get_upload_throttle 263 | [ ] get_use_udp_trackers 264 | [X] get_xmlrpc_size_limit 265 | [ ] greater 266 | [ ] group.insert 267 | [ ] group.insert_persistent_view 268 | [ ] group.seeding.ratio.command 269 | [ ] group.seeding.ratio.disable 270 | [ ] group.seeding.ratio.enable 271 | [ ] group.seeding.ratio.max 272 | [ ] group.seeding.ratio.max.set 273 | [ ] group.seeding.ratio.min 274 | [ ] group.seeding.ratio.min.set 275 | [ ] group.seeding.ratio.upload 276 | [ ] group.seeding.ratio.upload.set 277 | [ ] group.seeding.view 278 | [ ] group.seeding.view.set 279 | [ ] if 280 | [ ] import 281 | [ ] less 282 | [X] load 283 | [X] load_raw 284 | [X] load_raw_start 285 | [ ] load_raw_verbose 286 | [X] load_start 287 | [ ] load_start_verbose 288 | [ ] load_verbose 289 | [ ] log.execute 290 | [ ] log.xmlrpc 291 | [ ] not 292 | [ ] on_close 293 | [ ] on_erase 294 | [ ] on_finished 295 | [ ] on_hash_queued 296 | [ ] on_hash_removed 297 | [ ] on_insert 298 | [ ] on_open 299 | [ ] on_ratio 300 | [ ] on_start 301 | [ ] on_stop 302 | [ ] or 303 | [X] p.get_address 304 | [X] p.get_client_version 305 | [X] p.get_completed_percent 306 | [X] p.get_down_rate 307 | [X] p.get_down_total 308 | [X] p.get_id 309 | [ ] p.get_id_html 310 | [ ] p.get_options_str 311 | [X] p.get_peer_rate 312 | [X] p.get_peer_total 313 | [X] p.get_port 314 | [X] p.get_up_rate 315 | [X] p.get_up_total 316 | [X] p.is_encrypted 317 | [X] p.is_incoming 318 | [X] p.is_obfuscated 319 | [X] p.is_snubbed 320 | [X] p.multicall 321 | [ ] print 322 | [ ] ratio.disable 323 | [ ] ratio.enable 324 | [ ] ratio.max 325 | [ ] ratio.max.set 326 | [ ] ratio.min 327 | [ ] ratio.min.set 328 | [ ] ratio.upload 329 | [ ] ratio.upload.set 330 | [ ] remove_untied 331 | [ ] scgi_local 332 | [ ] scgi_port 333 | [ ] schedule 334 | [ ] schedule_remove 335 | [ ] scheduler.max_active 336 | [ ] scheduler.max_active.set 337 | [ ] scheduler.simple.added 338 | [ ] scheduler.simple.removed 339 | [ ] scheduler.simple.update 340 | [ ] session_save 341 | [ ] set_active_bg_color 342 | [ ] set_active_fg_color 343 | [ ] set_bind 344 | [ ] set_check_hash 345 | [ ] set_connection_leech 346 | [ ] set_connection_seed 347 | [ ] set_dht_port 348 | [ ] set_dht_throttle 349 | [ ] set_directory 350 | [ ] set_done_bg_color 351 | [ ] set_done_fg_color 352 | [X] set_download_rate # Download throttle. RTorrent.set_download_throttle 353 | [ ] set_handshake_log 354 | [ ] set_hash_interval 355 | [ ] set_hash_max_tries 356 | [ ] set_hash_read_ahead 357 | [ ] set_http_cacert 358 | [ ] set_http_capath 359 | [ ] set_http_proxy 360 | [ ] set_ip 361 | [ ] set_key_layout 362 | [ ] set_log.tracker 363 | [ ] set_max_downloads_div 364 | [ ] set_max_downloads_global 365 | [ ] set_max_file_size 366 | [ ] set_max_memory_usage 367 | [ ] set_max_open_files 368 | [ ] set_max_open_http 369 | [ ] set_max_open_sockets 370 | [ ] set_max_peers 371 | [ ] set_max_peers_seed 372 | [ ] set_max_uploads 373 | [ ] set_max_uploads_div 374 | [ ] set_max_uploads_global 375 | [ ] set_min_peers 376 | [ ] set_min_peers_seed 377 | [ ] set_name 378 | [ ] set_peer_exchange 379 | [ ] set_port_open 380 | [ ] set_port_random 381 | [ ] set_port_range 382 | [ ] set_preload_min_size 383 | [ ] set_preload_required_rate 384 | [ ] set_preload_type 385 | [ ] set_proxy_address 386 | [ ] set_receive_buffer_size 387 | [ ] set_safe_sync 388 | [ ] set_scgi_dont_route 389 | [ ] set_send_buffer_size 390 | [ ] set_session 391 | [ ] set_session_lock 392 | [ ] set_session_on_completion 393 | [ ] set_split_file_size 394 | [ ] set_split_suffix 395 | [ ] set_timeout_safe_sync 396 | [ ] set_timeout_sync 397 | [ ] set_tracker_numwant 398 | [X] set_upload_rate # Set the upload throttle. RTorrent.set_upload_throttle. 399 | [ ] set_use_udp_trackers 400 | [X] set_xmlrpc_size_limit 401 | [ ] start_tied 402 | [ ] stop_untied 403 | [X] system.client_version 404 | [ ] system.file_allocate 405 | [ ] system.file_allocate.set 406 | [ ] system.file_status_cache.prune 407 | [ ] system.file_status_cache.size 408 | [X] system.get_cwd 409 | [X] system.hostname # RTorrent.get_hostname() 410 | [X] system.library_version 411 | [ ] system.method.erase 412 | [ ] system.method.get 413 | [ ] system.method.has_key 414 | [ ] system.method.insert 415 | [ ] system.method.list_keys 416 | [ ] system.method.set 417 | [ ] system.method.set_key 418 | [X] system.pid 419 | [ ] system.set_cwd 420 | [ ] system.set_umask 421 | [ ] system.time 422 | [ ] system.time_seconds 423 | [ ] system.time_usec 424 | [ ] t.get_group 425 | [ ] t.get_id 426 | [ ] t.get_min_interval 427 | [ ] t.get_normal_interval 428 | [ ] t.get_scrape_complete 429 | [ ] t.get_scrape_downloaded 430 | [ ] t.get_scrape_incomplete 431 | [ ] t.get_scrape_time_last 432 | [ ] t.get_type 433 | [ ] t.get_url 434 | [ ] t.is_enabled 435 | [ ] t.is_open 436 | [ ] t.multicall 437 | [ ] t.set_enabled 438 | [ ] test.method.simple 439 | [ ] throttle_down 440 | [ ] throttle_ip 441 | [ ] throttle_up 442 | [ ] to_date 443 | [ ] to_elapsed_time 444 | [ ] to_gm_date 445 | [ ] to_gm_time 446 | [ ] to_kb 447 | [ ] to_mb 448 | [ ] to_throttle 449 | [ ] to_time 450 | [ ] to_xb 451 | [ ] tos 452 | [ ] try_import 453 | [ ] ui.current_view.set 454 | [ ] ui.unfocus_download 455 | [ ] view.event_added 456 | [ ] view.event_removed 457 | [ ] view.filter_download 458 | [ ] view.persistent 459 | [ ] view.set_not_visible 460 | [ ] view.set_visible 461 | [ ] view.size 462 | [ ] view.size_not_visible 463 | [X] view_add 464 | [ ] view_filter 465 | [ ] view_filter_on 466 | [X] view_list 467 | [ ] view_set 468 | [ ] view_sort 469 | [ ] view_sort_current 470 | [ ] view_sort_new 471 | [ ] xmlrpc_dialect 472 | 473 | -------------------------------------------------------------------------------- /sphinx/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyroTorrent.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyroTorrent.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyroTorrent" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyroTorrent" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /sphinx/baserequester.rst: -------------------------------------------------------------------------------- 1 | .. _baserequester: 2 | 3 | BaseRequester 4 | ============= 5 | 6 | .. automodule:: baserequester 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyroTorrent documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Jan 23 16:32:39 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | # XXX: Change this to model path later. 21 | sys.path.insert(0, os.path.abspath('../model')) 22 | sys.path.insert(0, os.path.abspath('../')) 23 | sys.path.insert(0, os.path.abspath('../lib')) 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = 'πϱTorrent' 48 | copyright = '2011, Merlijn Wajer, Bas Weelinck' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = '0.10' 56 | # The full version, including alpha/beta/rc tags. 57 | release = '0.10' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = ['_build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | html_theme = 'nature' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | #html_theme_path = [] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'pyroTorrentdoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | # The paper size ('letter' or 'a4'). 177 | #latex_paper_size = 'letter' 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #latex_font_size = '10pt' 181 | 182 | # Grouping the document tree into LaTeX files. List of tuples 183 | # (source start file, target name, title, author, documentclass [howto/manual]). 184 | latex_documents = [ 185 | ('index', 'pyroTorrent.tex', 'pyroTorrent Documentation', 186 | 'Merlijn Wajer, Bas Weelinck', 'manual'), 187 | ] 188 | 189 | # The name of an image file (relative to this directory) to place at the top of 190 | # the title page. 191 | #latex_logo = None 192 | 193 | # For "manual" documents, if this is true, then toplevel headings are parts, 194 | # not chapters. 195 | #latex_use_parts = False 196 | 197 | # If true, show page references after internal links. 198 | #latex_show_pagerefs = False 199 | 200 | # If true, show URL addresses after external links. 201 | #latex_show_urls = False 202 | 203 | # Additional stuff for the LaTeX preamble. 204 | #latex_preamble = '' 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'pyroTorrent', 'pyroTorrent Documentation', 219 | ['Merlijn Wajer, Bas Weelinck'], 1) 220 | ] 221 | -------------------------------------------------------------------------------- /sphinx/decorator.rst: -------------------------------------------------------------------------------- 1 | .. _decorator: 2 | 3 | Webtool decorators 4 | ================== 5 | 6 | .. automodule:: lib.decorator 7 | :members: 8 | :undoc-members: 9 | 10 | -------------------------------------------------------------------------------- /sphinx/develintro.rst: -------------------------------------------------------------------------------- 1 | Introduction for Developers 2 | =========================== 3 | 4 | This page presents an introduction to the pyroTorrent design goals. 5 | 6 | Understanding pyroTorrent 7 | ------------------------- 8 | 9 | pyroTorrent's codebase might look complex at first, but it's ultimitely trivial 10 | once you understand *why* we made certain design decisions. (Let's hope they 11 | turn out to be right ones, it seems like they are so far). 12 | 13 | The easiest way to help you understand our design decisions, is to simply share 14 | our experiences and thoughts when we were planning to write pyroTorrent. 15 | 16 | Since I currently suck as a writer, don't worry if you at some point don't 17 | understand what I'm trying to say, just keep on reading. It will probably make 18 | sense after you've read the entire page. If not, read it again. If you still 19 | don't get it, it's either a case of PEBCAK or I just really suck at writing. :-) 20 | 21 | Code design goal 22 | ---------------- 23 | 24 | After trying several rTorrent web frontends such as `wTorrent 25 | `_ and `rTWi 26 | `_, we were rather disappointed by the speed offered by 27 | both these frontends, aside from the fact that they used PHP. (Which may be one 28 | of the reasons they were so unresponsive) 29 | 30 | Here's a very flawed and probably unfair comparison, with PHP admittedly compiled with -O1 (because 31 | -O2 broke compilation...) 32 | 33 | Loading the main/overview page with about ~150 torrents in queue: 34 | 35 | ==================== ============ ============= 36 | pyroTorrent rTWi wTorrent 37 | ==================== ============ ============= 38 | ~400 milli seconds 10+ seconds 8 seconds 39 | ==================== ============ ============= 40 | 41 | All tests are done on a Sheevaplug with 1,2 Ghz ARM processor, softfloat. 42 | 43 | One of pyroTorrent's design goals is to be fast. One of the ways to achieve this 44 | is to minimise the amount of XMLRPC calls with so called *multicalls*. 45 | 46 | Multicalls 47 | ---------- 48 | 49 | ``libTorrent`` and the Python module ``xmlrpclib`` both have support for a so 50 | called *multicall*. A multicall in xmlrpclib typically encapsulates several 51 | XMLRPC requests in one request, thus decreasing overhead a lot. 52 | libTorrent multicalls perform an action on all items of a specific type, say 53 | all torrents - simply with the call ``d.multicall``. 54 | If you don't use multicall you'll rapidly find yourself opening over 500 55 | connections per page load; and since XMLRPC is stateless you'll have to actually 56 | do 500 requests, each with their own connection. 57 | 58 | pyroTorrent makes use of both these multicall mechanisms. Typically it should 59 | not open more than a few connections per page load. Current in release 0.04, 60 | pyroTorrent does *only 2* XMLRPC requests to load the main overview page. 61 | 62 | 63 | xmlrpclib Multicall 64 | ~~~~~~~~~~~~~~~~~~~ 65 | 66 | Below is some code from pyroTorrent release-0.03. 67 | 68 | Fetching some rTorrent information: 69 | 70 | .. code-block:: python 71 | 72 | try: 73 | r = global_rtorrent.query().get_upload_rate().get_download_rate().get_ip()\ 74 | .get_hostname().get_memory_usage().get_max_memory_usage()\ 75 | .get_libtorrent_version() 76 | return r.first() 77 | except InvalidConnectionException, e: 78 | return {} 79 | 80 | As illustrated in an interactive python shell using pyroTorrent's ``cli.sh``: 81 | 82 | >>> r.query().get_upload_rate().get_download_rate().get_ip()\ 83 | ... .get_hostname().get_memory_usage().get_max_memory_usage()\ 84 | ... .get_libtorrent_version().first() 85 | {'get_memory_usage': 30408704, 'get_ip': '0.0.0.0', 'get_upload_rate': 16303, 86 | 'get_max_memory_usage': 858993459, 'get_hostname': 'sheeva', 87 | 'get_download_rate': 4932, 'get_libtorrent_version': '0.12.6'} 88 | 89 | And all this information is retrieved in one XMLRPC call. 90 | 91 | Note how we call ``.query()`` on the object ``r``. ``r`` is a :ref:`rtorrent` 92 | instance; and the ``.query()`` method returns a :ref:`rtorrentquery` object. 93 | The :ref:`rtorrentquery` object contains all the libTorrent calls that the 94 | rtorrent object supports, but it remembers what calls you've done on the object, 95 | and then returns them all when you tell it to. (The ``.first()`` call). 96 | Also note how the :ref:`rtorrentquery` object allows you to chain calls, by 97 | returning itself. 98 | 99 | The :ref:`rtorrentquery` inherits from on the :ref:`multibase` class, which 100 | takes care of all the underlying tasks. You'll find that :ref:`rtorrentquery` 101 | is no more than 40 lines of code, of which 80% is documentation. 102 | 103 | Apart from :ref:`rtorrentquery`, we also have :ref:`torrentquery`, which does 104 | the same, but for the :ref:`torrent` model instead of the :ref:`rtorrent` model. 105 | 106 | 107 | libTorrent Multicall 108 | ~~~~~~~~~~~~~~~~~~~~ 109 | 110 | Getting certain information of all torrents: 111 | 112 | .. code-block:: python 113 | 114 | try: 115 | t = TorrentRequester('') 116 | 117 | t.get_name().get_download_rate().get_upload_rate() \ 118 | .is_complete().get_size_bytes().get_download_total().get_hash() 119 | 120 | torrents = t.all() 121 | 122 | except InvalidTorrentException, e: 123 | return error_page(env, str(e)) 124 | 125 | Basic example in ``cli.sh``: 126 | 127 | >>> t = TorrentRequester('') 128 | >>> t.get_name().get_download_rate().get_upload_rate() \ 129 | ... .is_complete().get_size_bytes().get_download_total().get_hash() 130 | 131 | >>> torrents = t.all() 132 | >>> len(torrents) 133 | 83 134 | >>> torrents[:1] 135 | [{'get_size_bytes': 41907644, 'get_upload_rate': 0, 'get_name': 136 | 'RevengeOfTheTitansSoundtrack.zip', 'get_hash': 137 | '6709A6306E2FB4EEF89455DFC8C26CA4DB316E6F', 'get_download_total': 0, 138 | 'get_download_rate': 0, 'is_complete': 0}] 139 | 140 | The :ref:`torrentrequester` works somewhat similar to :ref:`rtorrentquery` in 141 | the sense that it also uses multicalls; but in this case the libTorrent 142 | multicall. The TorrentRequester inherits most of its functionality from 143 | the :ref:`baserequester`. 144 | 145 | pyroTorrent Model API 146 | --------------------- 147 | 148 | libTorrent offers an API to program most if not all tasks; but the API is rather 149 | undocumented and awkward to be used without any wrapper or model. 150 | 151 | It would however become increasingly cumbersome to write a method for *each* 152 | libTorrent method, so we've come up with a solution. 153 | 154 | In the file ``model/rtorrent.py`` all the RPC methods are stored in a dict: 155 | 156 | .. code-block:: python 157 | 158 | _rpc_methods = { 159 | 'get_upload_throttle' : ('get_upload_rate', 160 | """ 161 | Returns the current upload throttle. 162 | """), 163 | 'set_upload_throttle' : ('set_upload_rate', 164 | """ 165 | Set the upload throttle. 166 | Pass the new throttle size in bytes. 167 | """), 168 | 'get_ip' : ('get_ip', 169 | """ 170 | Returns the IP rtorrent is bound to. (For XMLRPC?) 171 | """) 172 | } 173 | 174 | For each entry in the dictionary, a method is generated and added to the 175 | :ref:`rtorrent` class, along with a ``__doc__`` entry: 176 | 177 | .. code-block:: python 178 | 179 | for x, y in _rpc_methods.iteritems(): 180 | caller = (lambda name: lambda self, *args: getattr(self.s, name)(*args))(y[0]) 181 | caller.__doc__ = y[1] + '\nOriginal libTorrent method: ``%s``' % y[0] 182 | setattr(RTorrent, x, types.MethodType(caller, None, RTorrent)) 183 | 184 | del caller 185 | 186 | .. GETRIDOFVIMHIGHLIGHTBUG* 187 | 188 | We do something similar for the :ref:`torrent` class. 189 | 190 | pyroTorrent JSON API 191 | ==================== 192 | 193 | PyroTorrent features a JSON API which can be used to extract most (if not all) 194 | information using the provided Python classes. 195 | 196 | Below is our API test, written in Python. It does a POST request to the 197 | ``/torrent/api`` URL, where ``/torrent`` is the base url, the API resides 198 | at ``/api``. The POST request should contain only one variable called 199 | ``request``; and ``request`` should contain a JSON object containing the data to 200 | be requested and executed. 201 | 202 | JSON API example 203 | ---------------- 204 | 205 | .. literalinclude:: ../tests/api.py 206 | -------------------------------------------------------------------------------- /sphinx/index.rst: -------------------------------------------------------------------------------- 1 | .. pyroTorrent documentation master file, created by 2 | sphinx-quickstart on Sun Jan 23 16:32:39 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | Introduction to πϱTorrent 8 | =========================== 9 | 10 | .. figure:: pyrotorrent.png 11 | :scale: 25% 12 | 13 | A picture is worth a thousand words. 14 | 15 | What is πϱTorrent? 16 | ------------------ 17 | 18 | πϱTorrent is a web interface to rTorrent, the popular bittorrent client. 19 | πϱTorrent was created to rid servers of that awful project called PHP. It is a 20 | disgrace to programming languages in general. Anyway, enough ranting, as the 21 | code of this project sucks too, just less than any PHP project. pyroTorrent is 22 | still very much work in progress. It can only show some basic information about 23 | torrents, list them and you can add torrents by passing the URL. The design 24 | sucks. But it is quite fast. Faster than most rtorrent (did I mention it's for 25 | rtorrent? Now I did.) web interfaces. 26 | 27 | Features 28 | -------- 29 | 30 | Its features include but are not limited to: 31 | 32 | - View your torrents and upload/download rate. 33 | - Efficient XMLRPC usage. 34 | - Browse the files of your torrents. 35 | - Add torrents via direct links to .torrent files 36 | - Basic support for rtorrent views. 37 | - Direct SCGI communication over Unix sockets as well as HTTP XMLRPC. 38 | - Multiple rtorrent sources. (``targets``) 39 | - Support for basic user management. (config file + per target) 40 | - Download (in)complete files using your browser. 41 | - Support for resuming aforementioned downloads. (HTTP 206) 42 | 43 | Planned features: 44 | 45 | - Select/Change/Create/Delete views 46 | - Advanced user management. 47 | - Add events / schedulers. 48 | - Encryption policy management. 49 | - Manage lots of rtorrent settings. 50 | - Move torrents to views 51 | - Add statistics. (graphs) 52 | - And a lot more... 53 | 54 | Far fetched: 55 | 56 | - Support for transmission 57 | - Support for other clients. (uTorrent, Azureus) 58 | 59 | Additionally, pyroTorrent tries to document most of the rTorrent XMLRPC methods 60 | it uses. Its documentation of the rTorrent XMLRPC methods is probably far more 61 | complete than rTorrent's own documentation. We hope to send our documentation to 62 | the rTorrent project at some point and make the world a less chaotic place. 63 | 64 | Download / Source code 65 | ---------------------- 66 | 67 | Git source code: 68 | 69 | .. code-block:: bash 70 | 71 | git clone git://github.com/MerlijnWajer/pyroTorrent.git 72 | 73 | Web view (mirror) https://git.wizzup.org/pyroTorrent.git/. 74 | 75 | πϱTorrent's documentation 76 | ========================= 77 | 78 | .. toctree:: 79 | :maxdepth: 2 80 | 81 | setup.rst 82 | develintro.rst 83 | rtorrent.rst 84 | torrent.rst 85 | peer.rst 86 | baserequester.rst 87 | torrentrequester.rst 88 | peerrequester.rst 89 | multibase.rst 90 | torrentquery.rst 91 | rtorrentquery.rst 92 | decorator.rst 93 | 94 | 95 | Indices and tables 96 | ================== 97 | 98 | * :ref:`genindex` 99 | * :ref:`modindex` 100 | * :ref:`search` 101 | 102 | -------------------------------------------------------------------------------- /sphinx/multibase.rst: -------------------------------------------------------------------------------- 1 | .. _multibase: 2 | 3 | MultiBase 4 | ========= 5 | 6 | .. automodule:: multibase 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/peer.rst: -------------------------------------------------------------------------------- 1 | .. _peer: 2 | 3 | Peer 4 | ==== 5 | 6 | .. automodule:: peer 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/peerrequester.rst: -------------------------------------------------------------------------------- 1 | .. _peerrequester: 2 | 3 | PeerRequester 4 | ============= 5 | 6 | .. automodule:: peerrequester 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/rtorrent.rst: -------------------------------------------------------------------------------- 1 | .. _rtorrent: 2 | 3 | RTorrent 4 | ======== 5 | 6 | .. automodule:: rtorrent 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/rtorrentquery.rst: -------------------------------------------------------------------------------- 1 | .. _rtorrentquery: 2 | 3 | RTorrentQuery 4 | ============= 5 | 6 | .. automodule:: rtorrentquery 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/setup.rst: -------------------------------------------------------------------------------- 1 | .. _introduction: 2 | 3 | Setting up πϱTorrent 4 | ==================== 5 | 6 | Requirements 7 | ------------ 8 | 9 | πϱTorrent is written in Python. Aside from Python (2.6 or 2.7), 10 | you'll need the following python packages, depending on your setup: 11 | 12 | - flask (with all the dependencies, (werkzeug, jinja, etc) 13 | - decorator 14 | 15 | as well as rtorrent with XMLRPC support. πϱTorrent has only been tested on 16 | GNU/Linux, so this would be an advantage as well. 17 | 18 | .. TERRIBLE NAME vvvvvv 19 | 20 | Deciding on your setup 21 | ---------------------- 22 | 23 | πϱTorrent supports two ways of connecting to rTorrent. Through a HTTPD (such 24 | as `lighttpd `_ or directly via SCGI. 25 | 26 | To run the web interface, πϱTorrent can either serve pages using a FastCGI-aware 27 | HTTPD (`lighttpd`_, but also Apache and Nginx) or it can simply run it's 28 | own built-in *basic* HTTPD. A professional HTTPD such as `lighttpd`_ is 29 | recommended, but the built-in HTTPD works well if you don't need extreme 30 | performance and is a lot easier to set up. 31 | 32 | Now that all the option have been layed out, you have a few options. 33 | 34 | HTTPD for everything 35 | ~~~~~~~~~~~~~~~~~~~~ 36 | 37 | This approach uses a professional-grade HTTPD for 38 | everything: Serving web pages and providing a HTTP XMLRPC interface to rTorrent. 39 | 40 | HTTPD for serving webpages, πϱTorrent for SCGI 41 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | 43 | This uses a HTTPD to serve πϱTorrent pages, 44 | and uses πϱTorrent's direct SCGI capabilities to communicate with rTorrent. 45 | 46 | πϱTorrent for everything (serving webpages and SCGI) 47 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | This uses πϱTorrent's built-in HTTPD to serve web pages and uses πϱTorrent's 50 | direct SCGI ability to talk to rTorrent. 51 | 52 | πϱTorrent for serving webpages, HTTPD for communication 53 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | This uses a HTTPD to talk to rTorrent, but use πϱTorrent's built-in HTTPD to 56 | serve web pages. 57 | 58 | Notes 59 | ~~~~~ 60 | 61 | Personally I suggest using a professional HTTPD (like `lighttpd`_) to serve 62 | the pages and using πϱTorrent's direct SCGI capabilities to talk to rTorrent 63 | directly over a unix socket file. But really, there's not a huge difference. If 64 | you just want to try out pyroTorrent, the built-in HTTPD is fine. 65 | 66 | Alternatively you can use `lighttpd`_'s SCGI capabilities to act as middle man 67 | for the communication between rtorrent and πϱTorrent. 68 | (πϱTorrent will then talk over HTTP using XMLRPC instead of SCGI) 69 | 70 | Getting started 71 | --------------- 72 | 73 | Throughout the entire setup manual we will make the following assumptions: 74 | 75 | - You know your way around the terminal - at least a bit. 76 | - You are smart enough to adjust exemplary paths to your own. 77 | 78 | Our setup is as follows: (Compare it to your own, or how you will be wanting to 79 | set it up) 80 | 81 | - The *user* ``rtorrent`` runs ``rtorrent``. 82 | - The *user* ``rtorrent`` has a folder called ``pyroTorrent`` in it's home 83 | directory, (*/home/rtorrent*) this is the directory containing the 84 | pyroTorrent source code. 85 | 86 | Now, depending on your setup, you may or may not use a professional HTTPD: 87 | 88 | - The HTTPD users and groups are *lighttpd* (at least in the lighttpd 89 | example) 90 | - You know how to configure your HTTPD (lighttpd in our case); your 91 | webroot directory is assumed to be */var/www*. It doesn't matter to 92 | πϱTorrent but the examples use this directory. 93 | 94 | AT ALL TIMES make sure you use the appropriate paths. 95 | 96 | The setup process can be divided chronologically into a few parts: 97 | 98 | - Configuring rTorrent. 99 | - Configuring communication for rTorrent. 100 | - Testing a basic πϱTorrent. 101 | - Configuring how to serve the πϱTorrent webpages. 102 | 103 | rTorrent configuration 104 | ---------------------- 105 | 106 | To communicate with rTorrent, rTorrent needs to expose a XMLRPC interface. 107 | Most likely this feature is already compiled into your rTorrent, and you only 108 | need to enable it. 109 | 110 | SCGI 111 | ~~~~ 112 | 113 | In your *.rtorrent.rc* file, you need at least this line: 114 | 115 | .. code-block:: bash 116 | 117 | scgi_local = /tmp/rtorrent.sock 118 | 119 | Where */tmp/rtorrent.sock* is the path to the socket file rtorrent will 120 | create for communication. If you want to use the HTTPD as a *middle man* for 121 | communication, you'll need to make sure the socket is writable by the HTTPD as 122 | well. An interesting problem is that you have to make it writable every time 123 | you restart rTorrent. (or find a nice way to set up the permissions) 124 | 125 | Or, if you prefer a network socket to a unix socket: 126 | 127 | .. code-block:: bash 128 | 129 | scgi_port = localhost:5000 130 | 131 | Although this is typically not the most safe way, as any local user can connect 132 | to rTorrent now. 133 | 134 | Encoding 135 | ~~~~~~~~ 136 | 137 | Having this option in your *.rtorrent.rc* is also recommended: 138 | 139 | .. code-block:: bash 140 | 141 | encoding_list = UTF-8 142 | 143 | to ensure all the encoding is in UTF-8. 144 | 145 | Wrapping up 146 | ~~~~~~~~~~~ 147 | 148 | Restart rtorrent once you've changed the configuration. 149 | 150 | If the socket file is created (and you're using the ``scgi_local`` option) 151 | then you've set up your *.rtorrent.rc* correctly. 152 | 153 | Now, don't forget to make it writable by the web server if you want to use the 154 | HTTPD to communicate. 155 | 156 | Further reading 157 | ~~~~~~~~~~~~~~~ 158 | 159 | rTorrent also has a page on how to `Set up XMLRPC 160 | `_. 161 | 162 | SCGI communication 163 | ------------------ 164 | 165 | If you are going to use πϱTorrent to directly to talk rTorrent instead of via 166 | a HTTPD, you can skip this chapter. 167 | 168 | Lighttpd 169 | ~~~~~~~~ 170 | 171 | Lighttpd is known to work well with πϱTorrent. 172 | 173 | Setting up SCGI 174 | ``````````````` 175 | 176 | We need ``mod_scgi`` for the rtorrent <-> HTTPD connection. 177 | 178 | We need to include ``mod_scgi``, so put this in your configuration file: 179 | 180 | .. code-block:: lua 181 | 182 | server.modules += ("mod_scgi") 183 | 184 | Add this to your configuration file: 185 | 186 | .. code-block:: lua 187 | 188 | scgi.server = ( 189 | "/RPC2" => 190 | ( "127.0.0.1" => 191 | ( 192 | "socket" => "/home/rtorrent/rtorrentsock/rpc.socket", 193 | "disable-time" => 0, 194 | "check-local" => "disable" 195 | ) 196 | ) 197 | ) 198 | 199 | Again, make notice of the path */home/rtorrent/rtorrentsock/rpc.socket* that you 200 | set in `rTorrent configuration`_ (or, alternatively a host + port, have a look 201 | at lighttpd's official documentation on how to set this up, it'll be very 202 | similar) 203 | 204 | Now we can test your SCGI setup. Don't forget to restart lighttpd to make sure 205 | the configuration changes have been loaded. 206 | 207 | Apache 208 | ~~~~~~ 209 | 210 | TODO. 211 | 212 | Nginx 213 | ~~~~~ 214 | 215 | TODO. 216 | 217 | Testing SCGI 218 | ------------ 219 | 220 | Onto the testing of the communication. 221 | πϱTorrent offers a little test file called ``test.py``: 222 | 223 | .. code-block:: python 224 | 225 | from model.rtorrent import RTorrent 226 | import socket 227 | import sys 228 | 229 | from config import rtorrent_config 230 | from lib.config_parser import parse_config_part, RTorrentConfigException 231 | 232 | targets = [] 233 | for x in rtorrent_config: 234 | try: 235 | info = parse_config_part(rtorrent_config[x], x) 236 | except RTorrentConfigException, e: 237 | print 'Invalid config: ', e 238 | sys.exit(1) 239 | 240 | targets.append(info) 241 | 242 | for x in targets: 243 | r = RTorrent(x) 244 | 245 | try: 246 | print '[', x['name'], '] libTorrent version:', r.get_libtorrent_version() 247 | except socket.error, e: 248 | print 'Failed to connect to libTorrent:', str(e) 249 | 250 | Which should return your rTorrent version on success, and otherwise will tell 251 | you what went wrong. However, we cannot yet test our connection with πϱTorrent 252 | since we did not yet create a basic πϱTorrent configuration file. 253 | See `Basic πϱTorrent configuration`_ on how to do this. 254 | 255 | Once you've done this, verify that πϱTorrent works: 256 | 257 | .. code-block:: bash 258 | 259 | $ python test.py 260 | [ sheeva ] libTorrent version: 0.12.6 261 | 262 | Serving webpages 263 | ---------------- 264 | 265 | To actually view any content, we still need to set up the page serving. 266 | 267 | Using the built-in HTTPD 268 | ~~~~~~~~~~~~~~~~~~~~~~~~ 269 | 270 | Anyway, you'll typically have to select that you want to use the built-in HTTPD 271 | in the config file, and just run ``πϱtorrent.py``. 272 | 273 | To enable the built-in HTTPD, make sure the value ``USE_OWN_HTTPD`` in 274 | ``config.py`` is set to ``True``: 275 | 276 | .. code-block:: python 277 | 278 | USE_OWN_HTTPD = True 279 | 280 | Lighttpd 281 | ~~~~~~~~ 282 | 283 | Serving the webpages with `lighttpd`_ is recommended, as it has recieved a lot 284 | more testing than the built-in HTTPD, along with many other reasons. 285 | It is however, more complicated to set up. 286 | 287 | Setting up FCGI 288 | ``````````````` 289 | 290 | We need to include ``mod_fastcgi``, so put this in your configuration file: 291 | 292 | .. code-block:: lua 293 | 294 | server.modules += ("mod_fastcgi") 295 | 296 | Somewhere on top, but below the *server.modules =* line, (or just add it to your 297 | standard set of modules). In some cases a mod_fastcgi.conf file is shipped with 298 | your distribution instead. You can use this file by including it, but make sure 299 | it doesn't do any weird stuff like set up PHP. (Who would want that anyway?) 300 | 301 | .. code-block:: lua 302 | 303 | include "mod_fastcgi.conf" 304 | 305 | There. Now we should have fastcgi support for lighttpd. If this went too fast, 306 | have a look at the lighttpd documentation. 307 | 308 | Setting up FCGI to talk to πϱTorrent 309 | ```````````````````````````````````` 310 | 311 | This is the tricky part. You'll need to ensure that a couple of things work: 312 | 313 | - An empty file is required in your document root to prevent 404's before 314 | the FCGI contact is made. 315 | - You have the appropriate *rewrite-once* rule. 316 | - You have the correct *fastcgi.server* line. 317 | 318 | .. code-block:: lua 319 | 320 | url.rewrite-once = ( 321 | "^/torrent(/.*)?$" => "torrent.tfcgi" 322 | ) 323 | 324 | fastcgi.server += ( ".tfcgi" => 325 | ( "torrentfcgi" => 326 | ( 327 | "socket" => "/tmp/torrent.sock-1", 328 | "docroot" => "/home/rtorrent/pyrotorrent" 329 | ) 330 | ) 331 | ) 332 | 333 | And don't forget to create the empty file: 334 | 335 | .. code-block:: lua 336 | 337 | touch /var/www/torrent.tfcgi 338 | 339 | Where */var/www* is my *var.basedir* in the lighttpd configuration file. 340 | 341 | Using spawn-fcgi 342 | ```````````````` 343 | 344 | To spawn an instance of πϱTorrent, we use the program called *spawn-fcgi*. 345 | It's probably in your package manager; install it. Run the following command as 346 | root, obviously again adjust whatever parameters you need to adjust. 347 | 348 | .. code-block:: bash 349 | 350 | /usr/bin/spawn-fcgi /home/rtorrent/pyrotorrent/pyrotorrent.py \ 351 | -s /tmp/torrent.sock-1 \ 352 | -u lighttpd -g lighttpd \ 353 | -d /home/rtorrent/pyrotorrent/ 354 | 355 | Where the socket path is defined by *-s*, the user and group of the pid 356 | are set with *-u* and *-g*, and finally, the directory to change to is 357 | defined by *-d*. 358 | 359 | Now that you've spawned a πϱTorrent process, let's check that it's still 360 | alive: 361 | 362 | .. code-block:: bash 363 | 364 | # ps xua | grep python 365 | lighttpd 31639 84.5 1.6 12276 8372 ? Rs 19:57 0:01 /usr/bin/python2.6 /home/rtorrent/pyrotorrent/pyrotorrent.py 366 | 367 | 368 | πϱTorrent configuration 369 | ----------------------- 370 | 371 | 372 | The πϱTorrent configuration file is trivial. 373 | 374 | Basic πϱTorrent configuration 375 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 376 | 377 | A basic configuration file (just enough for the famous ``test.py``) looks like 378 | this: 379 | 380 | .. code-block:: python 381 | 382 | ## Exemplary SCGI setup using unix socket 383 | #rtorrent_config = { 384 | # 'sheeva': { 385 | # 'scgi' : { 386 | # 'unix-socket' : '/tmp/rtorrent.sock' 387 | # } 388 | # } 389 | #} 390 | # 391 | ## Exemplary SCGI setup using scgi over network 392 | #rtorrent_config = { 393 | # 'sheeva': { 394 | # 'scgi' : { 395 | # 'host' : '192.168.1.70', 396 | # 'port' : 80 397 | # } 398 | # } 399 | #} 400 | 401 | # Exemplary HTTP setup using remote XMLRPC server. (SCGI is handled by the HTTPD 402 | # in this case) 403 | rtorrent_config = { 404 | 'sheeva' : { 405 | 'http' : { 406 | 'host' : '192.168.1.70', 407 | 'port' : 80, 408 | 'url' : '/RPC2', 409 | } 410 | } 411 | } 412 | 413 | 414 | With examples for all of the three communication methods, uncomment the one you 415 | want to use and comment the other ones. (And make sure you adjust the 416 | information such as host, port or path) 417 | 418 | πϱTorrent configuration for webpages 419 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 420 | 421 | To actually serve webpages over FCGI, we need to extend the configuration file a 422 | bit: 423 | 424 | .. code-block:: python 425 | 426 | # Place all your globals here 427 | 428 | # Base URL for your HTTP website 429 | BASE_URL = '/torrent' 430 | # HTTP URL for the static files 431 | STATIC_URL = BASE_URL + '/static' 432 | 433 | # Use built-in HTTPD? 434 | USE_OWN_HTTPD = False 435 | 436 | # Default background 437 | BACKGROUND_IMAGE = 'cat.jpg' 438 | 439 | USE_AUTH = True 440 | 441 | torrent_users = { 442 | 'USER NAME' : { 443 | 'targets' : ['sheeva', 'sheevareborn'], 444 | 'background-image' : 'space1.png', 445 | 'password' : 'FILL IN PASSWORD' 446 | } 447 | } 448 | 449 | ## Exemplary SCGI setup using unix socket 450 | #rtorrent_config = { 451 | # 'sheeva' : { 452 | # 'scgi' : { 453 | # 'unix-socket' : '/tmp/rtorrent.sock' 454 | # } 455 | # } 456 | #} 457 | # 458 | ## Exemplary SCGI setup using scgi over network 459 | #rtorrent_config = { 460 | # 'sheeva' : { 461 | # 'scgi' : { 462 | # 'host' : '192.168.1.70', 463 | # 'port' : 80 464 | # } 465 | # } 466 | #} 467 | 468 | # Exemplary HTTP setup using remote XMLRPC server. (SCGI is handled by the HTTPD 469 | # in this case) 470 | rtorrent_config = { 471 | 'sheeva' : { 472 | 'http' : { 473 | 'host' : '192.168.1.70', 474 | 'port' : 80, 475 | 'url' : '/RPC2', 476 | } 477 | } 478 | , 479 | 'sheevareborn' : { 480 | 'http' : { 481 | 'host' : '42.42.42.42', 482 | 'port' : 80, 483 | 'url' : '/RPC2', 484 | } 485 | } 486 | } 487 | 488 | 489 | Make sure the *BASE_URL* matches the URL you set in your HTTPD setup. 490 | 491 | πϱTorrent configuration for serving downloaded files 492 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 493 | 494 | πϱTorrent can also act as direct download server for torrent files. 495 | Upon clicking on a torrent you will be presented with a list of files 496 | contained by that torrent. With the configuration below, the files will become 497 | clickable as well, upon which you will be offered to download the file. 498 | 499 | .. code-block:: python 500 | 501 | # Example configuration with file downloading enabled 502 | rtorrent_config = { 503 | 'woensdag' : { 504 | 'http' : { 505 | 'host' : '127.0.0.1', 506 | 'port' : 8080, 507 | 'url' : '/RPC2', 508 | }, 509 | 'storage_mode' : { 510 | 'local_path' : '/home/torrent/woensdag', 511 | 'remote_path' : '/home/rtorrent' 512 | } 513 | } 514 | 515 | As you can see, the rtorrent entry ``woensdag`` has an extra ``storage_mode`` 516 | property. There are currently 2 possible ways of configuring downloads. 517 | 518 | 1. rtorrent runs on the same machine as πϱTorrent 519 | 520 | 2. rtorrent runs on another machine, but πϱTorrent can access this 521 | machine's filesystem through a mounted directory. 522 | 523 | local setup 524 | ``````````` 525 | 526 | As you no doubt have guessed, the configuration above is the remote setup. 527 | To configure a locally running rtorrent simply drop the ``remote_path`` 528 | from the configuration, and set the ``local_path`` property to ``/``. 529 | 530 | remote setup 531 | ```````````` 532 | 533 | In this configuration ``local_path`` represents the path to the local 534 | mount point. ``remote_path`` is the path on the remote host running 535 | rtorrent, mounted by the local machine. 536 | To summerize, in the above configuration we have mounted the remote directory 537 | ``/home/rtorrent`` on the local directory ``/home/torrent/woensdag`` 538 | 539 | When you're done 540 | ---------------- 541 | 542 | Congratulations. (Some stuff here on what to do if you ran into problems, and 543 | also hint that people can now start looking at the code to add features, or how 544 | to request features) 545 | 546 | Oh, and enjoy πϱTorrent. 547 | -------------------------------------------------------------------------------- /sphinx/torrent.rst: -------------------------------------------------------------------------------- 1 | .. _torrent: 2 | 3 | Torrent Model 4 | ============= 5 | 6 | .. automodule:: torrent 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/torrentquery.rst: -------------------------------------------------------------------------------- 1 | .. _torrentquery: 2 | 3 | TorrentQuery 4 | ============ 5 | 6 | .. automodule:: torrentquery 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /sphinx/torrentrequester.rst: -------------------------------------------------------------------------------- 1 | .. _torrentrequester: 2 | 3 | TorrentRequester 4 | ================ 5 | 6 | .. automodule:: torrentrequester 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /static/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/static/bg.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/static/favicon.png -------------------------------------------------------------------------------- /static/icons/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/static/icons/add.png -------------------------------------------------------------------------------- /static/icons/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/static/icons/arrow_down.png -------------------------------------------------------------------------------- /static/icons/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/static/icons/arrow_up.png -------------------------------------------------------------------------------- /static/icons/readme.txt: -------------------------------------------------------------------------------- 1 | Silk icon set 1.3 2 | 3 | _________________________________________ 4 | Mark James 5 | http://www.famfamfam.com/lab/icons/silk/ 6 | _________________________________________ 7 | 8 | This work is licensed under a 9 | Creative Commons Attribution 2.5 License. 10 | [ http://creativecommons.org/licenses/by/2.5/ ] 11 | 12 | This means you may use it for any purpose, 13 | and make any changes you like. 14 | All I ask is that you include a link back 15 | to this page in your credits. 16 | 17 | Are you using this icon set? Send me an email 18 | (including a link or picture if available) to 19 | mjames@gmail.com 20 | 21 | Any other questions about this icon set please 22 | contact mjames@gmail.com -------------------------------------------------------------------------------- /static/pylons-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerlijnWajer/pyroTorrent/6dadb13a8c49f0a843ad5e0e66d54dafb3a10fdd/static/pylons-logo.gif -------------------------------------------------------------------------------- /static/style_old.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* background-color: #EEEEEE; */ 3 | /* background-image: url('/torrent/static/fox.png');*/ 4 | background-image: url('/torrent/static/cat.jpg'); 5 | color: #555555; 6 | font: 12px/18px "Liberation Sans", Arial, Verdana, Sans; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | header { 12 | background-color: rgba(190,212,235, 0.4); 13 | color: #212224; 14 | font-weight: bold; 15 | 16 | -moz-border-radius: 15px; 17 | border-radius: 15px; 18 | 19 | margin: 1em; 20 | padding-left: 1em; 21 | } 22 | 23 | span.logo { 24 | font-size: 22px; 25 | line-height: 33px; 26 | margin-right: 10px; 27 | } 28 | 29 | #torrent_list { 30 | /* margin: 20px 0 0 0; */ 31 | margin: 1em; 32 | padding: 0; 33 | 34 | } 35 | 36 | div.torrent_list_wiz { 37 | background-color: #D8DEE3; 38 | background-color: rgba(216, 222, 227, 0.4); 39 | padding: 1em; 40 | 41 | -moz-border-radius: 15px; 42 | border-radius: 15px; 43 | } 44 | 45 | #torrent_list ul { 46 | margin: 0em; 47 | padding: 0; 48 | 49 | } 50 | 51 | #torrent_list li { 52 | margin-top: 0.5em; 53 | margin-bottom: 0.5em; 54 | padding: 2px 6px; 55 | list-style: none; 56 | 57 | 58 | /* 59 | border: 2em; 60 | -moz-border-radius: 15px; 61 | border-radius: 15px; 62 | */ 63 | } 64 | 65 | 66 | span.up { 67 | color: #1D1; 68 | } 69 | 70 | span.down { 71 | color: #D11; 72 | } 73 | 74 | h1.target_header { 75 | padding-top: 1em; 76 | } 77 | 78 | div.target_header_bottom { 79 | padding-bottom: 1em; 80 | } 81 | 82 | span.ip { 83 | color: #fff; 84 | font-weight: bold; 85 | } 86 | 87 | li.torrent_complete { 88 | background-color: rgba(120, 120, 200, 0.4); 89 | } 90 | 91 | li.torrent_incomplete { 92 | background-color: rgba(190, 190, 240, 0.4); 93 | } 94 | 95 | span.upload_rate, span.download_rate, span.progress { 96 | clear: none; 97 | display: inline-block; 98 | margin-top: -17px; 99 | position: absolute; 100 | right: 0%; 101 | text-align: right; 102 | } 103 | 104 | span.progress { 105 | border: 1px solid #272; 106 | width: 140px; 107 | height: 14px; 108 | right: 30px; 109 | line-height: 16px; 110 | 111 | /* 112 | -moz-border-radius: 15px; 113 | border-radius: 15px; 114 | */ 115 | } 116 | 117 | span.download_rate { 118 | margin-right: 210px; 119 | } 120 | 121 | span.upload_rate { 122 | margin-right: 152px; 123 | } 124 | 125 | span.progress { 126 | color: #000; 127 | margin-right: 0px; 128 | } 129 | 130 | a { 131 | color: #000; 132 | text-decoration: none; 133 | } 134 | 135 | a:link { 136 | color: #000; 137 | } 138 | 139 | a:hover { 140 | color: #000; 141 | text-decoration: underline; 142 | } 143 | 144 | div.target_info { 145 | border: 1px; 146 | /* background-color: #C8D5E3; */ 147 | background-color: rgba(200, 213, 227, 0.4); 148 | color: #212224; 149 | 150 | font-size: 14px; 151 | font-weight: bold; 152 | 153 | -moz-border-radius: 15px; 154 | border-radius: 15px; 155 | border: 2em; 156 | } 157 | 158 | div.inner_target_info { 159 | margin: 2em; 160 | } 161 | 162 | 163 | li.torrent_complete span.progress { 164 | color: #000; 165 | border: 0; 166 | padding: 1px 5px; 167 | } 168 | 169 | li.torrent_incomplete span.progress_bar { 170 | /* Made the background colour more visible. I probably lack taste so 171 | * feel free to change it to a more fitting (but still noticable!) colour 172 | */ 173 | background-color: rgba(50, 200, 60, 0.4); 174 | width: 20%; 175 | height: 14px; 176 | display: block; 177 | float: left; 178 | position: absolute; 179 | z-index: 1; 180 | } 181 | 182 | li.torrent_incomplete span.progress_text { 183 | display: inline-block; 184 | float: right; 185 | position: absolute; 186 | text-align: right; 187 | right: 2px; 188 | width: 100%; 189 | z-index: 2; 190 | 191 | color: #000; 192 | } 193 | 194 | li.torrent_incomplete:hover, li.torrent_complete:hover { 195 | background-color: rgba(0, 0, 0, 0.0); /* Fully transparent background */ 196 | } 197 | 198 | span.download_icon, span.upload_icon { 199 | float: right; 200 | height: 16px; 201 | width: 16px; 202 | margin-left: 0em; 203 | margin-right: 1em; 204 | } 205 | 206 | span.download_icon { 207 | background: transparent url(/torrent/static/icons/arrow_down.png); 208 | } 209 | span.upload_icon { 210 | background: transparent url(/torrent/static/icons/arrow_up.png); 211 | } 212 | 213 | span.add_torrent_icon { 214 | height: 16px; 215 | width: 16px; 216 | display: inline-block; 217 | } 218 | 219 | span.add_torrent_icon { 220 | background: transparent url('/torrent/static/icons/add.png'); 221 | } 222 | 223 | div.torrent-def-div { 224 | background-color: rgba(100, 100, 100, 0.4); 225 | /* background-color: rgba(34, 34, 34, 0.4);*/ 226 | color: #fff; 227 | 228 | margin-top: 2em; 229 | padding: 1em; 230 | border: 2em; 231 | -moz-border-radius: 15px; 232 | border-radius: 15px; 233 | } 234 | 235 | /* Form styling */ 236 | 237 | input, input.file { 238 | background-color: rgba(0, 0, 0, 0.0); 239 | color: #222222; 240 | } 241 | 242 | /* Hack from 243 | http://www.dreamincode.net/forums/topic/15621-styling-a-file-browse-button/ 244 | */ 245 | input.hide { 246 | position: absolute; 247 | left: -137px; 248 | opacity: 0; 249 | z-index: 2; 250 | } 251 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | {% block title %} 3 | 404 Not Found! 4 | {% endblock %} 5 | 6 | {% block main %} 7 |
8 |

We could not find the page you requested in our web space:

9 |
  • {{url}}
10 |

Click here to return home

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/base.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %} pyroTorrent {% endblock %} 8 | 9 | 10 | {% if g.user.background_image %} 11 | 12 | {% else %} 13 | 14 | {% endif %} 15 | 16 | 17 |
18 | 19 | {# 20 |
IP: {{ rtorrent_data.get_ip }}
21 |
Memory: {{ rtorrent_data.get_memory_usage }} / {{ 22 | rtorrent_data.get_max_memory_usage }}
23 |
Hostname: {{ rtorrent_data.get_hostname }}
24 | {% endif %} 25 | #} 26 | 27 | 28 | Home 29 | 30 | {% if login and use_auth %} 31 | 32 | Logout ( {{ login }} ) 33 | 34 | {% elif use_auth %} 35 | 36 | Login 37 | 38 | {% endif %} 39 | {% block nav %} 40 | {% endblock %} 41 | 42 |
43 | 44 | {% with messages = get_flashed_messages() %} 45 | {% if messages %} 46 |
47 |
    48 | {% for message in messages %} 49 |
  • {{ message }}
  • 50 | {% endfor %} 51 |
52 |
53 | {% endif %} 54 | {% endwith %} 55 | 56 |
57 | {% block main %} 58 |

O GOD WTF

59 | {% endblock %} 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /templates/download_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | 3 | {% block nav %} 4 | View: {{ view}} 5 | {% endblock %} 6 | 7 | {% block main %} 8 | 9 | {% if torrents_list %} 10 | {% for target in torrents_list %} 11 | {% set torrents = torrents_list[target] %} 12 | {% set rtorrent = rtorrent_data[target] %} 13 |
14 |
15 |

{{ target }}

16 |
17 | libTorrent version: {{ rtorrent.get_libtorrent_version }} 18 | 19 | Up: 20 | {{ wn(rtorrent.get_upload_rate) }} / 21 | {{ wn(rtorrent.get_upload_throttle) }} 22 | 23 | 24 | Down: 25 | {{ wn(rtorrent.get_download_rate) }} / 26 | {{ wn(rtorrent.get_download_throttle) }} 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 |
37 |
    38 | {% for x in torrents %} 39 |
  • 40 | 41 | {{ x.get_name }} 42 | 44 | 45 | {% if x.is_complete %} 46 | 47 | {% else %} 48 | 49 | {% endif %} 50 | 51 | {% if not x.is_complete %} 52 | 53 | 55 | 56 | {% endif %} 57 | 58 | {% if x.is_complete %} 59 | {{ wn(x.get_size_bytes )}} 60 | {% else %} 61 | {{ wn(x.get_download_total) }} / {{ wn(x.get_size_bytes )}} 62 | {% endif %} 63 | 64 | 65 | 66 | 67 | {% if not x.is_complete %} 68 | 69 | 70 | {% if x.get_download_rate > 0 %} 71 | 72 | {{ wn(x.get_download_rate) }} 73 | {% endif %} 74 | 75 | {% endif %} 76 | 77 | 78 | {% if x.get_upload_rate > 0 %} 79 | 80 | {{ wn(x.get_upload_rate) }} 81 | {% endif %} 82 | 83 | 84 |
  • 85 | {% endfor %} 86 |
87 |
88 | 89 |
90 | 91 | {% endfor %} 92 | {% endif %} 93 | {% endblock %} 94 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | {% block title %} 3 | Whoop! 4 | {% endblock %} 5 | 6 | {% block main %} 7 |

Something went wrong! 8 | Technical details:

9 |
{{error}}
10 |

Click here to return home

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/loginform.html: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | 3 | {% block title %} 4 | pyroTorrent - Login 5 | {% endblock %} 6 | 7 | {% block main %} 8 | 9 |
10 | 11 | {% if loginsuccess %} 12 | 13 |

You are now logged in.

14 | {# 15 |

Redirecting you to your own 16 | page 17 | in 5 seconds...

18 | 19 | #} 20 | 21 | {% elif loginfail %} 22 |

Invalid username or password.

23 | 24 | {% else %} 25 | {% if user_name %} 26 | 27 |

You are already logged in

28 | 29 | {% else %} 30 |
31 | 32 |

You can log in with your pyroTorrent user here.

33 | 34 |

User:

35 |

Pass:

36 |
37 | 38 |
39 | 40 | {% endif %} 41 | {% endif %} 42 | 43 |
44 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /templates/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* background-image: url('/torrent/static/space1.png');*/ 3 | background-color: rgb(68, 68 ,68); 4 | color: #fff; 5 | font: 12px/18px "Liberation Sans", Arial, Verdana, Sans; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | img.bg { 11 | background-size: 100%; 12 | width: 100%; 13 | height: 100%; 14 | position: fixed; 15 | z-index: -1; 16 | margin-top: -20px; 17 | } 18 | 19 | header { 20 | background-color: rgba(85, 85, 85, {{ trans }}); 21 | color: #fff; 22 | font-weight: bold; 23 | 24 | -moz-border-radius: 15px; 25 | border-radius: 15px; 26 | 27 | margin: 1em; 28 | padding-left: 1em; 29 | } 30 | 31 | span.logo { 32 | font-size: 22px; 33 | line-height: 33px; 34 | margin-right: 10px; 35 | } 36 | 37 | #torrent_list { 38 | /* margin: 20px 0 0 0; */ 39 | margin: 1em; 40 | padding: 0; 41 | 42 | } 43 | 44 | div.torrent_list_wiz { 45 | background-color: rgba(102, 102, 102, {{trans}}); 46 | padding: 1em; 47 | 48 | -moz-border-radius: 15px; 49 | border-radius: 15px; 50 | } 51 | 52 | #torrent_list ul { 53 | margin: 0em; 54 | padding: 0; 55 | 56 | } 57 | 58 | #torrent_list li { 59 | margin-top: 0.5em; 60 | margin-bottom: 0.5em; 61 | padding: 2px 6px; 62 | list-style: none; 63 | 64 | 65 | /* 66 | border: 2em; 67 | -moz-border-radius: 15px; 68 | border-radius: 15px; 69 | */ 70 | } 71 | 72 | 73 | span.up { 74 | color: #0CC; 75 | } 76 | 77 | span.down { 78 | color: #C90; 79 | } 80 | 81 | h1.target_header { 82 | padding-top: 1em; 83 | } 84 | 85 | div.target_header_bottom { 86 | padding-bottom: 1em; 87 | } 88 | 89 | span.ip { 90 | color: #fff; 91 | font-weight: bold; 92 | } 93 | 94 | span.upload_rate, span.download_rate, span.progress { 95 | clear: none; 96 | display: inline-block; 97 | right: 0%; 98 | text-align: right; 99 | float: right; 100 | } 101 | 102 | span.progress { 103 | border: 1px solid #272; 104 | width: 140px; 105 | height: 14px; 106 | line-height: 16px; 107 | 108 | /* 109 | -moz-border-radius: 15px; 110 | border-radius: 15px; 111 | */ 112 | 113 | /*color: #000; #aaa; */ 114 | margin-right: 0px; 115 | 116 | color: #eee; 117 | } 118 | 119 | span.progress_text { 120 | /* 121 | position: absolute; 122 | right: 30px; 123 | */ 124 | display: clear; 125 | /*text-align: right;*/ 126 | width: 100%; 127 | /* z-index: 2; */ 128 | 129 | /* meridion adds */ 130 | float: right; 131 | min-width: 100%; 132 | z-index: -1; 133 | 134 | color: #eee; 135 | } 136 | 137 | span.progress_bar { 138 | position: absolute; 139 | max-width: 140px; 140 | background-color: rgba(50, 200, 60, {{trans}}); 141 | /*width: 10px; */ 142 | height: 14px; 143 | /* float: left; */ 144 | /*z-index: 1; */ 145 | /* z-index: 1; */ 146 | } 147 | 148 | a { 149 | color: #fff; 150 | text-decoration: none; 151 | /* meridion adds */ 152 | z-index: 100; 153 | } 154 | 155 | a:link { 156 | color: #fff; 157 | } 158 | 159 | a:hover { 160 | color: #888; 161 | text-decoration: underline; 162 | } 163 | 164 | div.target_info { 165 | background-color: rgba(34, 34, 34, 0.2); 166 | color: #fff; 167 | 168 | border: 2em; 169 | -moz-border-radius: 15px; 170 | border-radius: 15px; 171 | 172 | margin-bottom: 2em; 173 | padding-left: 1em; 174 | padding-right: 1em; 175 | padding-bottom: 1em; 176 | } 177 | 178 | div.inner_target_info { 179 | margin: 2em; 180 | 181 | font-size: 14px; 182 | font-weight: bold; 183 | 184 | } 185 | 186 | li.torrent_complete { 187 | background-color: rgba(183, 74, 74, {{trans}}); 188 | /*background-color: rgba(34, 153, 34, {{trans}});*/ 189 | 190 | border: 0; 191 | padding: 1px 5px; 192 | } 193 | 194 | li.torrent_incomplete { 195 | background-color: rgba(190, 190, 240, {{trans}}); 196 | 197 | border: 0; 198 | z-index: 2; 199 | } 200 | 201 | li.torrent_incomplete:hover, li.torrent_complete:hover { 202 | background-color: rgba(0, 0, 0, 0.0); /* Fully transparent background */ 203 | } 204 | 205 | 206 | span.download_icon, span.upload_icon { 207 | float: right; 208 | height: 16px; 209 | width: 16px; 210 | margin-left: 0em; 211 | margin-right: 1em; 212 | } 213 | 214 | span.download_icon { 215 | background: transparent url({{ url_for('static', filename='icons/arrow_down.png') }} ); 216 | } 217 | span.upload_icon { 218 | background: transparent url({{ url_for('static', filename='icons/arrow_up.png') }} ); 219 | } 220 | 221 | span.add_torrent_icon { 222 | height: 16px; 223 | width: 16px; 224 | display: inline-block; 225 | 226 | background: transparent url({{ url_for('static', filename='icons/add.png') }} ); 227 | } 228 | 229 | div.torrent-def-div { 230 | background-color: rgba(100, 100, 100, {{trans}}); 231 | /* background-color: rgba(34, 34, 34, {{trans}});*/ 232 | color: #fff; 233 | 234 | margin-top: 2em; 235 | padding: 1em; 236 | border: 2em; 237 | -moz-border-radius: 15px; 238 | border-radius: 15px; 239 | } 240 | 241 | div.flashes { 242 | background-color: rgba(240, 240, 240, {{trans}}); 243 | 244 | margin: 1em; 245 | padding: 1em; 246 | border: 2em; 247 | -moz-border-radius: 15px; 248 | border-radius: 15px; 249 | 250 | } 251 | 252 | div.torrentfile-div { 253 | background-color: rgba(150, 150, 150, 0.1); 254 | /* background-color: rgba(34, 34, 34, {{trans}});*/ 255 | color: #fff; 256 | 257 | margin-top: 2em; 258 | padding: 1em; 259 | border: 2em; 260 | -moz-border-radius: 15px; 261 | border-radius: 15px; 262 | } 263 | 264 | /* Form styling */ 265 | 266 | input, input.file { 267 | background-color: rgba(0, 0, 0, 0.0); 268 | color: #FFFFFF; 269 | } 270 | 271 | /* Hack from 272 | http://www.dreamincode.net/forums/topic/15621-styling-a-file-browse-button/ 273 | */ 274 | input.hide { 275 | position: absolute; 276 | left: -137px; 277 | opacity: 0; 278 | z-index: 2; 279 | } 280 | -------------------------------------------------------------------------------- /templates/torrent_add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | 3 | {% block main %} 4 | 5 |
6 |

Add a torrent with a direct link

7 |
8 |

Enter the Torrent URL here:

10 | 11 |
12 |
13 | 14 |
15 |

Add a torrent via magnet link

16 |
17 |

Enter the magnet link here:

19 | 20 |
21 |
22 | 23 | 32 | 33 |
34 |

Add a local torrent file

35 |
37 | 38 |

39 | 40 | 41 | 45 |

46 | 47 |

48 |
49 |
50 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /templates/torrent_info.html: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | 3 | {% macro dfs(node) -%} 4 | 5 |
  • 6 | {% if node.children %} 7 | {{ node.name }} 8 | {% else %} 9 | {% if file_downloads %} 10 | {{ node.name }} 12 | 13 | (Chunks completed: {{ node.obj.get_completed_chunks }} / {{ 14 | node.obj.get_size_chunks }} ) 15 | {% else %} 16 | {{ node.name }} 17 | (Chunks completed: {{ node.obj.get_completed_chunks }} / {{ 18 | node.obj.get_size_chunks }} ) 19 | {% endif %} 20 | {% endif %} 21 |
  • 22 | 23 | {% if node.children %} 24 |
    25 |
      26 | {% for x in node.children %} 27 | {{ dfs(x) }} 28 | {% endfor %} 29 |
    30 |
    31 | {% endif %} 32 | {%- endmacro %} 33 | 34 | {% block main %} 35 | {% if torrent %} 36 |
    37 |

    38 | {% for action in ['open', 'close', 'start', 'stop', 'pause', 39 | 'resume', 'erase'] %} 40 | {{ action }} 42 | {% endfor %} 43 |

    44 |
    45 |
    46 |

    {{ torrent.get_name }} 47 | ({{ wn(torrent.get_download_total) }} / 48 | {{ wn(torrent.get_size_bytes) }})

    49 |

    Loaded file: 50 | 53 | {{ torrent.get_loaded_file }} 54 | 55 |

    56 | {% if torrent.get_message %} 57 |

    Message: {{ torrent.get_message }}

    58 | {% endif %} 59 |

    Active: 60 | {% if torrent.is_active %} 61 | True 62 | {% else %} 63 | False 64 | {% endif %} 65 |

    66 |
    67 |
    68 |

    69 |

      70 | {{ dfs(tree) }} 71 |
    72 |

    73 |
    74 | 75 | {% if peers %} 76 | 88 | {% endif %} 89 | {% endif %} 90 | {% endblock %} 91 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from model.rtorrent import RTorrent 2 | import socket 3 | import sys 4 | 5 | from config import rtorrent_config 6 | from lib.config_parser import parse_config_part, RTorrentConfigException 7 | 8 | targets = [] 9 | for x in rtorrent_config: 10 | try: 11 | info = parse_config_part(rtorrent_config[x], x) 12 | except RTorrentConfigException as e: 13 | print('Invalid config: ', e) 14 | sys.exit(1) 15 | 16 | targets.append(info) 17 | 18 | for x in targets: 19 | r = RTorrent(x) 20 | 21 | try: 22 | print('[', x['name'], '] libTorrent version:', r.get_libtorrent_version()) 23 | except socket.error as e: 24 | print('Failed to connect to libTorrent:', str(e)) 25 | -------------------------------------------------------------------------------- /tests/api.py: -------------------------------------------------------------------------------- 1 | import simplejson as json 2 | import urllib.request, urllib.parse, urllib.error 3 | import urllib.request, urllib.error, urllib.parse 4 | 5 | url = 'http://localhost/torrent/api' 6 | values = [ 7 | { 8 | 'target' : 'sheevareborn', 9 | 'type' : 'torrentrequester', 10 | 'view' : '', 11 | 'attributes' : [ 12 | ('get_name', []), 13 | ('get_download_rate', []), 14 | ('get_upload_rate', []), 15 | ('is_complete', []), 16 | ('get_size_bytes', []), 17 | ('get_download_total', []), 18 | ('get_hash', []) 19 | ] 20 | }, 21 | { 22 | 'target' : 'sheevareborn', 23 | 'type' : 'rtorrent', 24 | 'attributes' : [ 25 | ('set_upload_throttle', [20480]), 26 | ('get_upload_throttle', []) 27 | ] 28 | }, 29 | { 30 | 'target' : 'sheevareborn', 31 | 'type' : 'rtorrent', 32 | 'attributes' : [ 33 | ('set_upload_throttle', [20480*2]), 34 | ('get_upload_throttle', []) 35 | ] 36 | }, 37 | 38 | { 39 | 'target' : 'sheevareborn', 40 | 'type' : 'torrent', 41 | 'hash' : '8EB5801B88D34D50A6E7594B6678A2CF6224766E', 42 | 'attributes' : ( 43 | ('get_hash', []), 44 | ('get_name', []), 45 | ('is_complete', []) 46 | ) 47 | } 48 | ] 49 | 50 | data = urllib.parse.urlencode({'request' : json.dumps(values)}) 51 | req = urllib.request.Request(url, data) 52 | response = urllib.request.urlopen(req) 53 | the_page = response.read() 54 | print(the_page) 55 | 56 | 57 | -------------------------------------------------------------------------------- /tools/gen_rtorrent_doc.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, '..') 3 | 4 | import model.rtorrent as rtorrent 5 | import model.torrent as torrent 6 | import model.peer as peer 7 | import model.torrentfile as torrentfile 8 | 9 | 10 | for d in (rtorrent, torrent, peer, torrentfile): 11 | for x, y in d._rpc_methods.items(): 12 | print(y[0], y[1]) 13 | --------------------------------------------------------------------------------