├── .gitignore ├── CHANGELOG.txt ├── DOCS.txt ├── MANIFEST.in ├── README.md ├── rtorrent ├── __init__.py ├── common.py ├── compat.py ├── err.py ├── file.py ├── group.py ├── lib │ ├── __init__.py │ ├── bencode.py │ ├── torrentparser.py │ └── xmlrpc │ │ ├── __init__.py │ │ ├── basic_auth.py │ │ ├── http.py │ │ └── scgi.py ├── peer.py ├── rpc │ └── __init__.py ├── torrent.py └── tracker.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*.komodoproject 2 | /*.*project 3 | /temp 4 | *.pyc 5 | *~ 6 | *.swp 7 | /__pycache__ 8 | /logs 9 | /tests 10 | /build 11 | /dist 12 | /*egg-info 13 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | v0.3.0 (not yet released) 2 | ------------------------- 3 | - added: support for torrent custom fields 4 | - added: support for ServerProxy subclasses 5 | - added: SCGIServerProxy subclass (thanks fuzeman) 6 | - fixed: unicode issues related to repr 7 | 8 | - rTorrent.RTorrent 9 | - changed: __init__() 10 | - added: sp and sp_kwargs args 11 | - removed: _verbose arg (specify in sp_kwargs instead) 12 | - changed: no longer calls update() and get_torrents() 13 | - renamed: get_rpc_methods() to _get_rpc_methods() 14 | - renamed: _get_xmlrpc_conn() to _get_conn() 15 | - changed: find_torrent() now returns None if torrent not found 16 | - added: verify_retries parameter to RTorrent.load_torrent() 17 | 18 | - rtorrent.Torrent 19 | - added: set_custom() 20 | - added: set_custom1() 21 | - added: set_custom2() 22 | - added: set_custom3() 23 | - added: set_custom4() 24 | - added: set_custom5() 25 | - added: get_custom() 26 | - added: get_custom1() 27 | - added: get_custom2() 28 | - added: get_custom3() 29 | - added: get_custom4() 30 | - added: get_custom5() 31 | - added: set_directory_base() 32 | 33 | - rtorrent.common 34 | - find_torrent() now returns None if torrent not found 35 | 36 | v0.2.9 (April 10, 2012) 37 | ----------------------- 38 | - General 39 | - improvement: rtorrent-python now creates a new http connection with every 40 | Multicall instance, this was to fix issues caused by calls being made 41 | simultaneously using the same RTorrent instance 42 | 43 | - fixed: an issue which prevented rtorrent.RTorrent rpc methods from 44 | being built (bug introduced in v0.2.8) 45 | 46 | - rtorrent.RTorrent 47 | - removed: all instances of RTorrent.download_list 48 | (replaced with [t.info_hash for t in RTorrent.torrents]) 49 | 50 | v0.2.8 (April 6, 2012) 51 | ---------------------- 52 | - General 53 | - Previously, instance methods for RTorrent and it's child classes weren't 54 | created if the version of rTorrent being connected to didn't support them. 55 | Now, all instance methods calls will be created, but ones that aren't 56 | supported by the connected rTorrent client will throw a MethodError when called. 57 | 58 | This also goes for methods being added to a Multicall instance via Multicall.add() 59 | 60 | - rtorrent.RTorrent 61 | - get_api_version() 62 | - get_system_time() 63 | 64 | - rtorrent.torrent.Torrent 65 | - added: announce() 66 | - added: accept_seeders() 67 | - added: is_accepting_seeders() 68 | - added: get_chunks_seen() 69 | - added: is_partially_done() 70 | - added: is_not_partially_done() 71 | - added: get_time_started() 72 | - added: get_num_chunks_wanted() 73 | - renamed: get_chunks_hashed() to get_num_chunks_hashed() 74 | 75 | - rtorrent.tracker.Tracker 76 | - added: get_activity_time_last() 77 | - added: get_activity_time_next() 78 | - added: get_failed_time_last() 79 | - added: get_failed_time_next() 80 | - added: get_success_time_last() 81 | - added: get_success_time_next() 82 | - added: can_scrape() 83 | - added: get_failed_counter() 84 | - added: get_scrape_counter() 85 | - added: get_success_counter() 86 | - added: is_usable() 87 | - added: is_busy() 88 | - added: is_extra_tracker() 89 | - added: get_latest_sum_peers() 90 | - added: get_latest_new_peers() 91 | 92 | - rtorrent.file.File 93 | - added: update() 94 | 95 | - rtorrent.rpc.Method 96 | - added support for aliases so there can be some backwards 97 | compatibility if methods are renamed 98 | 99 | - rtorrent.rpc.Multicall 100 | - Multicall now sets the results of the call to the instance object given (Issue #7) 101 | - Because Multicall.call() now assigns the result of a method call to the 102 | calling, Multicall now needs the calling object as an input instead of 103 | just the instance of RTorrent 104 | 105 | 106 | v0.2.7 (March 1, 2012) 107 | ---------------------- 108 | - fixed: RTorrent instances are no longer created if the connection to the XMLRPC server fails 109 | 110 | - API CHANGES 111 | - rtorrent.Torrent 112 | - added: Torrent.check_hash() 113 | 114 | 115 | v0.2.6 (February 11, 2012) 116 | -------------------------- 117 | - fixed: Torrent.get_ratio() wasn't converting to float properly 118 | - added: documentation 119 | 120 | - API CHANGES 121 | - rtorrent.Torrent 122 | - added: Torrent.is_started() 123 | - added: Torrent.is_paused() 124 | - added: Torrent.is_hash_checking_queued() 125 | 126 | v0.2.5 (February 05, 2012) 127 | -------------------------- 128 | - added: support for caching trackers/files/peers after calling get_torrents() (Issue #6) 129 | 130 | - API CHANGES 131 | - rtorrent.Torrent 132 | - added: Torrent.erase() 133 | - added: Torrent.close() 134 | - changed: ratio is now converted to a float (Issue #5) 135 | - rtorrent.rpc.Method 136 | - added: post_process_func - specify a custom post processing method (Issue #4) 137 | (look for get_ratio Method in rtorrent.torrent for an example) 138 | 139 | 140 | v0.2.4 (January 01, 2012) 141 | ------------------------- 142 | - fixed: critical bug that caused any "set" function to throw an AssertionError 143 | - fixed: critical bug in Torrent.start() and Torrent.stop() 144 | 145 | v0.2.3 (December 31, 2011) 146 | -------------------------- 147 | - added: min_version argument in rtorrent.rpc.Method, this will ensure 148 | the method will only be available when connected to an rTorrent 149 | client that can actually support it. 150 | 151 | - API CHANGES 152 | - General 153 | - RTorrent/Peer/Tracker/File/Torrent/Multicall now requires the actual 154 | instance of RTorrent as and input instead of the XMLRPC 155 | connection for that instance 156 | 157 | - instance methods that communicate with rTorrent will raise 158 | an exception if the said method is not supported by the version of 159 | rTorrent you're connected to: 160 | - will raise rtorrent.err.RTorrentVersionError if the method is newer 161 | than the rTorrent client 162 | - will raise MethodError if method isn't listed in RTorrent._rpc_methods 163 | 164 | - Methods for RTorrent/Peer/Torrent/etc instances will now be created 165 | dynamically based on the client version of rTorrent that it's connected 166 | to (as opposed to the old way, which deleted the original Method instance, 167 | and therefore was inaccessible to any future connection to rTorrent, even if 168 | the rTorrent client supported said method. This will ensure that methods 169 | aren't available if the client doesn't support it). You can use hasattr() 170 | in order to avoid AttributeError being raised. 171 | 172 | - rtorrent.RTorrent 173 | - renamed: get_commands to get_rpc_methods 174 | 175 | - rtorrent.rpc.Method 176 | - added: is_available(). This function will check if the Method instance is 177 | supported by the instance of RTorrent specified 178 | - renamed: class_name to _class 179 | - added: class_name, which is now just the name of the class, (_class.__name__) 180 | 181 | - rtorrent.peer.Peer 182 | - renamed: banned to is_banned; added support for boolean 183 | - renamed: client_version to get_client_version 184 | 185 | - rtorrent.torrent.Torrent 186 | - renamed: incomplete to is_incomplete; added support for boolean 187 | 188 | v0.2.2 (November 12, 2011) 189 | -------------------------- 190 | - TorrentParser now works with Python2 (Issue #2) 191 | - Fixed a bug in which if dict keys within the torrent had spaces, those spaces would not be converted 192 | to underscores when parsing the torrent, making the instance variables inaccessible (TorrentParser) 193 | - Fixed an issue in which creating a File instance would fail (Issue #3) 194 | 195 | - API CHANGES 196 | - RTorrent 197 | - renamed: get_download_rate to get_down_limit 198 | - renamed: get_upload_rate to get_up_limit 199 | - renamed: set_download_limit to set_down_limit 200 | - renamed: set_upload_limit to set_up_limit 201 | 202 | v0.2.1 (October 18, 2011) 203 | ------------------------- 204 | - Added: short delay before verifying if torrent was loaded to give rTorrent a chance to actually load it 205 | - Fixed: a bug that caused load_torrent() to not see that the torrent was added successfully (Issue #1) 206 | 207 | v0.2.0 (October 07, 2011) 208 | ------------------------- 209 | - Added: support for Peer rpc methods 210 | - Added: update methods for Peer, Tracker, Torrent and File classes 211 | - Added: int to boolean type casting on return values (only occurs if the Method's boolean flag is True) 212 | - Added: poll method for RTorrent class 213 | - Added: Multicall support (see rtorrent.rpc.Multicall) 214 | - Added: minimum rTorrent version check 215 | 216 | - Improvement: Completely rewrote how RPC methods are used 217 | 218 | - Removed: list_to_dict function in rtorrent.common (no longer needed) 219 | 220 | v0.1.0 (October 01, 2011) 221 | ------------------------- 222 | - Intial release 223 | -------------------------------------------------------------------------------- /DOCS.txt: -------------------------------------------------------------------------------- 1 | Documentation for rtorrent-python can be found here: 2 | http://cjlucas.github.com/rtorrent-python/ 3 | https://github.com/cjlucas/rtorrent-python/wiki 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md DOCS.txt CHANGELOG.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ABOUT THIS PROJECT 2 | ------------------ 3 | The xmlrpc interface to rTorrent is extremely unintuitive and has very little documentation, this project aims to make interfacing with rTorrent much easier. 4 | 5 | NOTE: This project is considered beta, so expect the API to change. API changes will be noted in the changelog. 6 | 7 | REQUIREMENTS 8 | ------------ 9 | - [Python](http://www.python.org/) 2.6 or higher, 3.0 or higher (might work on older versions, but these are the only ones I'm supporting). 10 | - [rTorrent](http://libtorrent.rakshasa.no/) 0.8.1 or later, with xmlrpc-c support (see [this guide](http://libtorrent.rakshasa.no/wiki/RTorrentXMLRPCGuide)). 11 | - [xmlrpc-c](http://xmlrpc-c.sourceforge.net/) 1.00 or later. 1.07 or later for 64bit integer support (highly recommended, download either the stable or advanced branches). 12 | - [Apache](http://www.apache.org/) or [lighttpd](http://www.lighttpd.net/) 13 | 14 | INSTALLATION 15 | ------------ 16 | 17 | If you're downloading the source: 18 | 19 | ```$ python setup.py install``` 20 | 21 | Or you can install directly from PyPI: 22 | 23 | ```$ pip install rtorrent-python``` 24 | 25 | ROADMAP 26 | ------- 27 | - v0.3.0 28 | - Add option for fast RTorrent init (skip paranoid check) 29 | 30 | - unknown 31 | - Logging support 32 | - Documentation and examples 33 | - Add support for views 34 | 35 | CREDITS 36 | ------- 37 | 38 | [fuzeman](https://github.com/fuzeman) 39 | -------------------------------------------------------------------------------- /rtorrent/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | try: 21 | import urllib.parse as urlparser 22 | except ImportError: 23 | import urllib as urlparser 24 | import os.path 25 | import time 26 | try: 27 | import xmlrpc.client as xmlrpclib 28 | except ImportError: 29 | import xmlrpclib 30 | 31 | from rtorrent.common import find_torrent, \ 32 | is_valid_port, convert_version_tuple_to_str 33 | from rtorrent.lib.torrentparser import TorrentParser 34 | from rtorrent.lib.xmlrpc.http import HTTPServerProxy 35 | from rtorrent.lib.xmlrpc.scgi import SCGIServerProxy 36 | from rtorrent.rpc import Method 37 | from rtorrent.lib.xmlrpc.basic_auth import BasicAuthTransport 38 | from rtorrent.torrent import Torrent 39 | from rtorrent.group import Group 40 | import rtorrent.rpc # @UnresolvedImport 41 | 42 | __version__ = "0.2.9" 43 | __author__ = "Chris Lucas" 44 | __contact__ = "chris@chrisjlucas.com" 45 | __license__ = "MIT" 46 | 47 | MIN_RTORRENT_VERSION = (0, 8, 1) 48 | MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION) 49 | 50 | 51 | class RTorrent: 52 | """ Create a new rTorrent connection """ 53 | rpc_prefix = None 54 | 55 | def __init__(self, uri, username=None, password=None, 56 | verify=False, sp=None, sp_kwargs=None): 57 | self.uri = uri # : From X{__init__(self, url)} 58 | 59 | self.username = username 60 | self.password = password 61 | 62 | self.schema = urlparser.splittype(uri)[0] 63 | 64 | if sp: 65 | self.sp = sp 66 | elif self.schema in ['http', 'https']: 67 | self.sp = HTTPServerProxy 68 | elif self.schema == 'scgi': 69 | self.sp = SCGIServerProxy 70 | else: 71 | raise NotImplementedError() 72 | 73 | self.sp_kwargs = sp_kwargs or {} 74 | 75 | self.torrents = [] # : List of L{Torrent} instances 76 | self._rpc_methods = [] # : List of rTorrent RPC methods 77 | self._torrent_cache = [] 78 | self._client_version_tuple = () 79 | 80 | if verify is True: 81 | self._verify_conn() 82 | 83 | def _get_conn(self): 84 | """Get ServerProxy instance""" 85 | if self.username is not None and self.password is not None: 86 | if self.schema == 'scgi': 87 | raise NotImplementedError() 88 | 89 | return self.sp( 90 | self.uri, 91 | transport=BasicAuthTransport(self.username, self.password), 92 | **self.sp_kwargs 93 | ) 94 | 95 | return self.sp(self.uri, **self.sp_kwargs) 96 | 97 | def _verify_conn(self): 98 | # check for rpc methods that should be available 99 | assert "system.client_version" in self._get_rpc_methods(), "Required RPC method not available." 100 | assert "system.library_version" in self._get_rpc_methods(), "Required RPC method not available." 101 | 102 | # minimum rTorrent version check 103 | assert self._meets_version_requirement() is True,\ 104 | "Error: Minimum rTorrent version required is {0}".format( 105 | MIN_RTORRENT_VERSION_STR) 106 | 107 | def _meets_version_requirement(self): 108 | return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION 109 | 110 | def _get_client_version_tuple(self): 111 | conn = self._get_conn() 112 | 113 | if not self._client_version_tuple: 114 | if not hasattr(self, "client_version"): 115 | setattr(self, "client_version", 116 | conn.system.client_version()) 117 | 118 | rtver = getattr(self, "client_version") 119 | self._client_version_tuple = tuple([int(i) for i in 120 | rtver.split(".")]) 121 | 122 | return self._client_version_tuple 123 | 124 | def _update_rpc_methods(self): 125 | self._rpc_methods = self._get_conn().system.listMethods() 126 | 127 | return self._rpc_methods 128 | 129 | def _get_rpc_methods(self): 130 | """ Get list of raw RPC commands 131 | 132 | @return: raw RPC commands 133 | @rtype: list 134 | """ 135 | 136 | return(self._rpc_methods or self._update_rpc_methods()) 137 | 138 | def get_torrents(self, view="main"): 139 | """Get list of all torrents in specified view 140 | 141 | @return: list of L{Torrent} instances 142 | 143 | @rtype: list 144 | 145 | @todo: add validity check for specified view 146 | """ 147 | self.torrents = [] 148 | methods = rtorrent.torrent.methods 149 | retriever_methods = [m for m in methods 150 | if m.is_retriever() and m.is_available(self)] 151 | 152 | m = rtorrent.rpc.Multicall(self) 153 | m.add("d.multicall", view, "d.get_hash=", 154 | *[method.rpc_call + "=" for method in retriever_methods]) 155 | 156 | results = m.call()[0] # only sent one call, only need first result 157 | 158 | for result in results: 159 | results_dict = {} 160 | # build results_dict 161 | for m, r in zip(retriever_methods, result[1:]): # result[0] is the info_hash 162 | results_dict[m.varname] = rtorrent.rpc.process_result(m, r) 163 | 164 | self.torrents.append( 165 | Torrent(self, info_hash=result[0], **results_dict) 166 | ) 167 | 168 | self._manage_torrent_cache() 169 | return(self.torrents) 170 | 171 | def _manage_torrent_cache(self): 172 | """Carry tracker/peer/file lists over to new torrent list""" 173 | for torrent in self._torrent_cache: 174 | new_torrent = rtorrent.common.find_torrent(torrent.info_hash, 175 | self.torrents) 176 | if new_torrent is not None: 177 | new_torrent.files = torrent.files 178 | new_torrent.peers = torrent.peers 179 | new_torrent.trackers = torrent.trackers 180 | 181 | self._torrent_cache = self.torrents 182 | 183 | def _get_load_function(self, file_type, start, verbose): 184 | """Determine correct "load torrent" RPC method""" 185 | func_name = None 186 | if file_type == "url": 187 | # url strings can be input directly 188 | if start and verbose: 189 | func_name = "load_start_verbose" 190 | elif start: 191 | func_name = "load_start" 192 | elif verbose: 193 | func_name = "load_verbose" 194 | else: 195 | func_name = "load" 196 | elif file_type in ["file", "raw"]: 197 | if start and verbose: 198 | func_name = "load_raw_start_verbose" 199 | elif start: 200 | func_name = "load_raw_start" 201 | elif verbose: 202 | func_name = "load_raw_verbose" 203 | else: 204 | func_name = "load_raw" 205 | 206 | return(func_name) 207 | 208 | def load_torrent(self, torrent, start=False, verbose=False, verify_load=True, verify_retries=3): 209 | """ 210 | Loads torrent into rTorrent (with various enhancements) 211 | 212 | @param torrent: can be a url, a path to a local file, or the raw data 213 | of a torrent file 214 | @type torrent: str 215 | 216 | @param start: start torrent when loaded 217 | @type start: bool 218 | 219 | @param verbose: print error messages to rTorrent log 220 | @type verbose: bool 221 | 222 | @param verify_load: verify that torrent was added to rTorrent successfully 223 | @type verify_load: bool 224 | 225 | @return: Depends on verify_load: 226 | - if verify_load is True, (and the torrent was 227 | loaded successfully), it'll return a L{Torrent} instance 228 | - if verify_load is False, it'll return None 229 | 230 | @rtype: L{Torrent} instance or None 231 | 232 | @raise AssertionError: If the torrent wasn't successfully added to rTorrent 233 | - Check L{TorrentParser} for the AssertionError's 234 | it raises 235 | 236 | 237 | @note: Because this function includes url verification (if a url was input) 238 | as well as verification as to whether the torrent was successfully added, 239 | this function doesn't execute instantaneously. If that's what you're 240 | looking for, use load_torrent_simple() instead. 241 | """ 242 | p = self._get_conn() 243 | tp = TorrentParser(torrent) 244 | torrent = xmlrpclib.Binary(tp._raw_torrent) 245 | info_hash = tp.info_hash 246 | 247 | func_name = self._get_load_function("raw", start, verbose) 248 | 249 | # load torrent 250 | getattr(p, func_name)(torrent) 251 | 252 | if verify_load: 253 | i = 0 254 | while i < verify_retries: 255 | self.get_torrents() 256 | if info_hash in [t.info_hash for t in self.torrents]: 257 | break 258 | 259 | # was still getting AssertionErrors, delay should help 260 | time.sleep(1) 261 | i += 1 262 | 263 | assert info_hash in [t.info_hash for t in self.torrents],\ 264 | "Adding torrent was unsuccessful." 265 | 266 | return(find_torrent(info_hash, self.torrents)) 267 | 268 | def load_torrent_simple(self, torrent, file_type, 269 | start=False, verbose=False): 270 | """Loads torrent into rTorrent 271 | 272 | @param torrent: can be a url, a path to a local file, or the raw data 273 | of a torrent file 274 | @type torrent: str 275 | 276 | @param file_type: valid options: "url", "file", or "raw" 277 | @type file_type: str 278 | 279 | @param start: start torrent when loaded 280 | @type start: bool 281 | 282 | @param verbose: print error messages to rTorrent log 283 | @type verbose: bool 284 | 285 | @return: None 286 | 287 | @raise AssertionError: if incorrect file_type is specified 288 | 289 | @note: This function was written for speed, it includes no enhancements. 290 | If you input a url, it won't check if it's valid. You also can't get 291 | verification that the torrent was successfully added to rTorrent. 292 | Use load_torrent() if you would like these features. 293 | """ 294 | p = self._get_conn() 295 | 296 | assert file_type in ["raw", "file", "url"], \ 297 | "Invalid file_type, options are: 'url', 'file', 'raw'." 298 | func_name = self._get_load_function(file_type, start, verbose) 299 | 300 | if file_type == "file": 301 | # since we have to assume we're connected to a remote rTorrent 302 | # client, we have to read the file and send it to rT as raw 303 | assert os.path.isfile(torrent), \ 304 | "Invalid path: \"{0}\"".format(torrent) 305 | torrent = open(torrent, "rb").read() 306 | 307 | if file_type in ["raw", "file"]: 308 | finput = xmlrpclib.Binary(torrent) 309 | elif file_type == "url": 310 | finput = torrent 311 | 312 | getattr(p, func_name)(finput) 313 | 314 | def get_views(self): 315 | p = self._get_conn() 316 | return p.view_list() 317 | 318 | def create_group(self, name, persistent=True, view=None): 319 | p = self._get_conn() 320 | 321 | if persistent is True: 322 | p.group.insert_persistent_view('', name) 323 | else: 324 | assert view is not None, "view parameter required on non-persistent groups" 325 | p.group.insert('', name, view) 326 | 327 | self._update_rpc_methods() 328 | 329 | def get_group(self, name): 330 | assert name is not None, "group name required" 331 | 332 | group = Group(self, name) 333 | group.update() 334 | return group 335 | 336 | def set_dht_port(self, port): 337 | """Set DHT port 338 | 339 | @param port: port 340 | @type port: int 341 | 342 | @raise AssertionError: if invalid port is given 343 | """ 344 | assert is_valid_port(port), "Valid port range is 0-65535" 345 | self.dht_port = self._p.set_dht_port(port) 346 | 347 | def enable_check_hash(self): 348 | """Alias for set_check_hash(True)""" 349 | self.set_check_hash(True) 350 | 351 | def disable_check_hash(self): 352 | """Alias for set_check_hash(False)""" 353 | self.set_check_hash(False) 354 | 355 | def find_torrent(self, info_hash): 356 | """Frontend for rtorrent.common.find_torrent""" 357 | return(rtorrent.common.find_torrent(info_hash, self.get_torrents())) 358 | 359 | def poll(self): 360 | """ poll rTorrent to get latest torrent/peer/tracker/file information 361 | 362 | @note: This essentially refreshes every aspect of the rTorrent 363 | connection, so it can be very slow if working with a remote 364 | connection that has a lot of torrents loaded. 365 | 366 | @return: None 367 | """ 368 | self.update() 369 | torrents = self.get_torrents() 370 | for t in torrents: 371 | t.poll() 372 | 373 | def update(self): 374 | """Refresh rTorrent client info 375 | 376 | @note: All fields are stored as attributes to self. 377 | 378 | @return: None 379 | """ 380 | multicall = rtorrent.rpc.Multicall(self) 381 | retriever_methods = [m for m in methods 382 | if m.is_retriever() and m.is_available(self)] 383 | for method in retriever_methods: 384 | multicall.add(method) 385 | 386 | multicall.call() 387 | 388 | 389 | def _build_class_methods(class_obj): 390 | # multicall add class 391 | caller = lambda self, multicall, method, *args:\ 392 | multicall.add(method, self.rpc_id, *args) 393 | 394 | caller.__doc__ = """Same as Multicall.add(), but with automatic inclusion 395 | of the rpc_id 396 | 397 | @param multicall: A L{Multicall} instance 398 | @type: multicall: Multicall 399 | 400 | @param method: L{Method} instance or raw rpc method 401 | @type: Method or str 402 | 403 | @param args: optional arguments to pass 404 | """ 405 | setattr(class_obj, "multicall_add", caller) 406 | 407 | 408 | def __compare_rpc_methods(rt_new, rt_old): 409 | from pprint import pprint 410 | rt_new_methods = set(rt_new._get_rpc_methods()) 411 | rt_old_methods = set(rt_old._get_rpc_methods()) 412 | print("New Methods:") 413 | pprint(rt_new_methods - rt_old_methods) 414 | print("Methods not in new rTorrent:") 415 | pprint(rt_old_methods - rt_new_methods) 416 | 417 | 418 | def __check_supported_methods(rt): 419 | from pprint import pprint 420 | supported_methods = set([m.rpc_call for m in 421 | methods + 422 | rtorrent.file.methods + 423 | rtorrent.torrent.methods + 424 | rtorrent.tracker.methods + 425 | rtorrent.peer.methods]) 426 | all_methods = set(rt._get_rpc_methods()) 427 | 428 | print("Methods NOT in supported methods") 429 | pprint(all_methods - supported_methods) 430 | print("Supported methods NOT in all methods") 431 | pprint(supported_methods - all_methods) 432 | 433 | methods = [ 434 | # RETRIEVERS 435 | Method(RTorrent, 'get_xmlrpc_size_limit', 'get_xmlrpc_size_limit'), 436 | Method(RTorrent, 'get_proxy_address', 'get_proxy_address'), 437 | Method(RTorrent, 'get_split_suffix', 'get_split_suffix'), 438 | Method(RTorrent, 'get_up_limit', 'get_upload_rate'), 439 | Method(RTorrent, 'get_max_memory_usage', 'get_max_memory_usage'), 440 | Method(RTorrent, 'get_max_open_files', 'get_max_open_files'), 441 | Method(RTorrent, 'get_min_peers_seed', 'get_min_peers_seed'), 442 | Method(RTorrent, 'get_use_udp_trackers', 'get_use_udp_trackers'), 443 | Method(RTorrent, 'get_preload_min_size', 'get_preload_min_size'), 444 | Method(RTorrent, 'get_max_uploads', 'get_max_uploads'), 445 | Method(RTorrent, 'get_max_peers', 'get_max_peers'), 446 | Method(RTorrent, 'get_timeout_sync', 'get_timeout_sync'), 447 | Method(RTorrent, 'get_receive_buffer_size', 'get_receive_buffer_size'), 448 | Method(RTorrent, 'get_split_file_size', 'get_split_file_size'), 449 | Method(RTorrent, 'get_dht_throttle', 'get_dht_throttle'), 450 | Method(RTorrent, 'get_max_peers_seed', 'get_max_peers_seed'), 451 | Method(RTorrent, 'get_min_peers', 'get_min_peers'), 452 | Method(RTorrent, 'get_tracker_numwant', 'get_tracker_numwant'), 453 | Method(RTorrent, 'get_max_open_sockets', 'get_max_open_sockets'), 454 | Method(RTorrent, 'get_session', 'get_session'), 455 | Method(RTorrent, 'get_ip', 'get_ip'), 456 | Method(RTorrent, 'get_scgi_dont_route', 'get_scgi_dont_route'), 457 | Method(RTorrent, 'get_hash_read_ahead', 'get_hash_read_ahead'), 458 | Method(RTorrent, 'get_http_cacert', 'get_http_cacert'), 459 | Method(RTorrent, 'get_dht_port', 'get_dht_port'), 460 | Method(RTorrent, 'get_handshake_log', 'get_handshake_log'), 461 | Method(RTorrent, 'get_preload_type', 'get_preload_type'), 462 | Method(RTorrent, 'get_max_open_http', 'get_max_open_http'), 463 | Method(RTorrent, 'get_http_capath', 'get_http_capath'), 464 | Method(RTorrent, 'get_max_downloads_global', 'get_max_downloads_global'), 465 | Method(RTorrent, 'get_name', 'get_name'), 466 | Method(RTorrent, 'get_session_on_completion', 'get_session_on_completion'), 467 | Method(RTorrent, 'get_down_limit', 'get_download_rate'), 468 | Method(RTorrent, 'get_down_total', 'get_down_total'), 469 | Method(RTorrent, 'get_up_rate', 'get_up_rate'), 470 | Method(RTorrent, 'get_hash_max_tries', 'get_hash_max_tries'), 471 | Method(RTorrent, 'get_peer_exchange', 'get_peer_exchange'), 472 | Method(RTorrent, 'get_down_rate', 'get_down_rate'), 473 | Method(RTorrent, 'get_connection_seed', 'get_connection_seed'), 474 | Method(RTorrent, 'get_http_proxy', 'get_http_proxy'), 475 | Method(RTorrent, 'get_stats_preloaded', 'get_stats_preloaded'), 476 | Method(RTorrent, 'get_timeout_safe_sync', 'get_timeout_safe_sync'), 477 | Method(RTorrent, 'get_hash_interval', 'get_hash_interval'), 478 | Method(RTorrent, 'get_port_random', 'get_port_random'), 479 | Method(RTorrent, 'get_directory', 'get_directory'), 480 | Method(RTorrent, 'get_port_open', 'get_port_open'), 481 | Method(RTorrent, 'get_max_file_size', 'get_max_file_size'), 482 | Method(RTorrent, 'get_stats_not_preloaded', 'get_stats_not_preloaded'), 483 | Method(RTorrent, 'get_memory_usage', 'get_memory_usage'), 484 | Method(RTorrent, 'get_connection_leech', 'get_connection_leech'), 485 | Method(RTorrent, 'get_check_hash', 'get_check_hash', 486 | boolean=True, 487 | ), 488 | Method(RTorrent, 'get_session_lock', 'get_session_lock'), 489 | Method(RTorrent, 'get_preload_required_rate', 'get_preload_required_rate'), 490 | Method(RTorrent, 'get_max_uploads_global', 'get_max_uploads_global'), 491 | Method(RTorrent, 'get_send_buffer_size', 'get_send_buffer_size'), 492 | Method(RTorrent, 'get_port_range', 'get_port_range'), 493 | Method(RTorrent, 'get_max_downloads_div', 'get_max_downloads_div'), 494 | Method(RTorrent, 'get_max_uploads_div', 'get_max_uploads_div'), 495 | Method(RTorrent, 'get_safe_sync', 'get_safe_sync'), 496 | Method(RTorrent, 'get_bind', 'get_bind'), 497 | Method(RTorrent, 'get_up_total', 'get_up_total'), 498 | Method(RTorrent, 'get_client_version', 'system.client_version'), 499 | Method(RTorrent, 'get_library_version', 'system.library_version'), 500 | Method(RTorrent, 'get_api_version', 'system.api_version', 501 | min_version=(0, 9, 1) 502 | ), 503 | Method(RTorrent, "get_system_time", "system.time", 504 | docstring="""Get the current time of the system rTorrent is running on 505 | 506 | @return: time (posix) 507 | @rtype: int""", 508 | ), 509 | 510 | # MODIFIERS 511 | Method(RTorrent, 'set_http_proxy', 'set_http_proxy'), 512 | Method(RTorrent, 'set_max_memory_usage', 'set_max_memory_usage'), 513 | Method(RTorrent, 'set_max_file_size', 'set_max_file_size'), 514 | Method(RTorrent, 'set_bind', 'set_bind', 515 | docstring="""Set address bind 516 | 517 | @param arg: ip address 518 | @type arg: str 519 | """, 520 | ), 521 | Method(RTorrent, 'set_up_limit', 'set_upload_rate', 522 | docstring="""Set global upload limit (in bytes) 523 | 524 | @param arg: speed limit 525 | @type arg: int 526 | """, 527 | ), 528 | Method(RTorrent, 'set_port_random', 'set_port_random'), 529 | Method(RTorrent, 'set_connection_leech', 'set_connection_leech'), 530 | Method(RTorrent, 'set_tracker_numwant', 'set_tracker_numwant'), 531 | Method(RTorrent, 'set_max_peers', 'set_max_peers'), 532 | Method(RTorrent, 'set_min_peers', 'set_min_peers'), 533 | Method(RTorrent, 'set_max_uploads_div', 'set_max_uploads_div'), 534 | Method(RTorrent, 'set_max_open_files', 'set_max_open_files'), 535 | Method(RTorrent, 'set_max_downloads_global', 'set_max_downloads_global'), 536 | Method(RTorrent, 'set_session_lock', 'set_session_lock'), 537 | Method(RTorrent, 'set_session', 'set_session'), 538 | Method(RTorrent, 'set_split_suffix', 'set_split_suffix'), 539 | Method(RTorrent, 'set_hash_interval', 'set_hash_interval'), 540 | Method(RTorrent, 'set_handshake_log', 'set_handshake_log'), 541 | Method(RTorrent, 'set_port_range', 'set_port_range'), 542 | Method(RTorrent, 'set_min_peers_seed', 'set_min_peers_seed'), 543 | Method(RTorrent, 'set_scgi_dont_route', 'set_scgi_dont_route'), 544 | Method(RTorrent, 'set_preload_min_size', 'set_preload_min_size'), 545 | Method(RTorrent, 'set_log.tracker', 'set_log.tracker'), 546 | Method(RTorrent, 'set_max_uploads_global', 'set_max_uploads_global'), 547 | Method(RTorrent, 'set_down_limit', 'set_download_rate', 548 | docstring="""Set global download limit (in bytes) 549 | 550 | @param arg: speed limit 551 | @type arg: int 552 | """, 553 | ), 554 | Method(RTorrent, 'set_preload_required_rate', 'set_preload_required_rate'), 555 | Method(RTorrent, 'set_hash_read_ahead', 'set_hash_read_ahead'), 556 | Method(RTorrent, 'set_max_peers_seed', 'set_max_peers_seed'), 557 | Method(RTorrent, 'set_max_uploads', 'set_max_uploads'), 558 | Method(RTorrent, 'set_session_on_completion', 'set_session_on_completion'), 559 | Method(RTorrent, 'set_max_open_http', 'set_max_open_http'), 560 | Method(RTorrent, 'set_directory', 'set_directory'), 561 | Method(RTorrent, 'set_http_cacert', 'set_http_cacert'), 562 | Method(RTorrent, 'set_dht_throttle', 'set_dht_throttle'), 563 | Method(RTorrent, 'set_hash_max_tries', 'set_hash_max_tries'), 564 | Method(RTorrent, 'set_proxy_address', 'set_proxy_address'), 565 | Method(RTorrent, 'set_split_file_size', 'set_split_file_size'), 566 | Method(RTorrent, 'set_receive_buffer_size', 'set_receive_buffer_size'), 567 | Method(RTorrent, 'set_use_udp_trackers', 'set_use_udp_trackers'), 568 | Method(RTorrent, 'set_connection_seed', 'set_connection_seed'), 569 | Method(RTorrent, 'set_xmlrpc_size_limit', 'set_xmlrpc_size_limit'), 570 | Method(RTorrent, 'set_xmlrpc_dialect', 'set_xmlrpc_dialect'), 571 | Method(RTorrent, 'set_safe_sync', 'set_safe_sync'), 572 | Method(RTorrent, 'set_http_capath', 'set_http_capath'), 573 | Method(RTorrent, 'set_send_buffer_size', 'set_send_buffer_size'), 574 | Method(RTorrent, 'set_max_downloads_div', 'set_max_downloads_div'), 575 | Method(RTorrent, 'set_name', 'set_name'), 576 | Method(RTorrent, 'set_port_open', 'set_port_open'), 577 | Method(RTorrent, 'set_timeout_sync', 'set_timeout_sync'), 578 | Method(RTorrent, 'set_peer_exchange', 'set_peer_exchange'), 579 | Method(RTorrent, 'set_ip', 'set_ip', 580 | docstring="""Set IP 581 | 582 | @param arg: ip address 583 | @type arg: str 584 | """, 585 | ), 586 | Method(RTorrent, 'set_timeout_safe_sync', 'set_timeout_safe_sync'), 587 | Method(RTorrent, 'set_preload_type', 'set_preload_type'), 588 | Method(RTorrent, 'set_check_hash', 'set_check_hash', 589 | docstring="""Enable/Disable hash checking on finished torrents 590 | 591 | @param arg: True to enable, False to disable 592 | @type arg: bool 593 | """, 594 | boolean=True, 595 | ), 596 | ] 597 | 598 | _all_methods_list = [methods, 599 | rtorrent.file.methods, 600 | rtorrent.torrent.methods, 601 | rtorrent.tracker.methods, 602 | rtorrent.peer.methods, 603 | ] 604 | 605 | class_methods_pair = { 606 | RTorrent: methods, 607 | rtorrent.file.File: rtorrent.file.methods, 608 | rtorrent.torrent.Torrent: rtorrent.torrent.methods, 609 | rtorrent.tracker.Tracker: rtorrent.tracker.methods, 610 | rtorrent.peer.Peer: rtorrent.peer.methods, 611 | } 612 | for c in class_methods_pair.keys(): 613 | rtorrent.rpc._build_rpc_methods(c, class_methods_pair[c]) 614 | _build_class_methods(c) 615 | -------------------------------------------------------------------------------- /rtorrent/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | from rtorrent.compat import is_py3 23 | 24 | 25 | def bool_to_int(value): 26 | """Translates python booleans to RPC-safe integers""" 27 | if value is True: 28 | return("1") 29 | elif value is False: 30 | return("0") 31 | else: 32 | return(value) 33 | 34 | 35 | def cmd_exists(cmds_list, cmd): 36 | """Check if given command is in list of available commands 37 | 38 | @param cmds_list: see L{RTorrent._rpc_methods} 39 | @type cmds_list: list 40 | 41 | @param cmd: name of command to be checked 42 | @type cmd: str 43 | 44 | @return: bool 45 | """ 46 | 47 | return(cmd in cmds_list) 48 | 49 | 50 | def find_torrent(info_hash, torrent_list): 51 | """Find torrent file in given list of Torrent classes 52 | 53 | @param info_hash: info hash of torrent 54 | @type info_hash: str 55 | 56 | @param torrent_list: list of L{Torrent} instances (see L{RTorrent.get_torrents}) 57 | @type torrent_list: list 58 | 59 | @return: L{Torrent} instance, or -1 if not found 60 | """ 61 | for t in torrent_list: 62 | if t.info_hash == info_hash: 63 | return t 64 | 65 | return None 66 | 67 | 68 | def is_valid_port(port): 69 | """Check if given port is valid""" 70 | return(0 <= int(port) <= 65535) 71 | 72 | 73 | def convert_version_tuple_to_str(t): 74 | return(".".join([str(n) for n in t])) 75 | 76 | 77 | def safe_repr(fmt, *args, **kwargs): 78 | """ Formatter that handles unicode arguments """ 79 | 80 | if not is_py3(): 81 | # unicode fmt can take str args, str fmt cannot take unicode args 82 | fmt = fmt.decode("utf-8") 83 | out = fmt.format(*args, **kwargs) 84 | return out.encode("utf-8") 85 | else: 86 | return fmt.format(*args, **kwargs) 87 | -------------------------------------------------------------------------------- /rtorrent/compat.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import sys 22 | 23 | 24 | def is_py3(): 25 | return sys.version_info[0] == 3 26 | 27 | if is_py3(): 28 | import xmlrpc.client as xmlrpclib 29 | else: 30 | import xmlrpclib 31 | -------------------------------------------------------------------------------- /rtorrent/err.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | from rtorrent.common import convert_version_tuple_to_str 22 | 23 | 24 | class RTorrentVersionError(Exception): 25 | def __init__(self, min_version, cur_version): 26 | self.min_version = min_version 27 | self.cur_version = cur_version 28 | self.msg = "Minimum version required: {0}".format( 29 | convert_version_tuple_to_str(min_version)) 30 | 31 | def __str__(self): 32 | return(self.msg) 33 | 34 | 35 | class MethodError(Exception): 36 | def __init__(self, msg): 37 | self.msg = msg 38 | 39 | def __str__(self): 40 | return(self.msg) 41 | -------------------------------------------------------------------------------- /rtorrent/file.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | # from rtorrent.rpc import Method 22 | import rtorrent.rpc 23 | 24 | from rtorrent.common import safe_repr 25 | 26 | Method = rtorrent.rpc.Method 27 | 28 | 29 | class File: 30 | """Represents an individual file within a L{Torrent} instance.""" 31 | 32 | def __init__(self, _rt_obj, info_hash, index, **kwargs): 33 | self._rt_obj = _rt_obj 34 | self.info_hash = info_hash # : info hash for the torrent the file is associated with 35 | self.index = index # : The position of the file within the file list 36 | for k in kwargs.keys(): 37 | setattr(self, k, kwargs.get(k, None)) 38 | 39 | self.rpc_id = "{0}:f{1}".format( 40 | self.info_hash, self.index) # : unique id to pass to rTorrent 41 | 42 | def update(self): 43 | """Refresh file data 44 | 45 | @note: All fields are stored as attributes to self. 46 | 47 | @return: None 48 | """ 49 | multicall = rtorrent.rpc.Multicall(self) 50 | retriever_methods = [m for m in methods 51 | if m.is_retriever() and m.is_available(self._rt_obj)] 52 | for method in retriever_methods: 53 | multicall.add(method, self.rpc_id) 54 | 55 | multicall.call() 56 | 57 | def __repr__(self): 58 | return safe_repr("File(index={0} path=\"{1}\")", self.index, self.path) 59 | 60 | methods = [ 61 | # RETRIEVERS 62 | Method(File, 'get_last_touched', 'f.get_last_touched'), 63 | Method(File, 'get_range_second', 'f.get_range_second'), 64 | Method(File, 'get_size_bytes', 'f.get_size_bytes'), 65 | Method(File, 'get_priority', 'f.get_priority'), 66 | Method(File, 'get_match_depth_next', 'f.get_match_depth_next'), 67 | Method(File, 'is_resize_queued', 'f.is_resize_queued', 68 | boolean=True, 69 | ), 70 | Method(File, 'get_range_first', 'f.get_range_first'), 71 | Method(File, 'get_match_depth_prev', 'f.get_match_depth_prev'), 72 | Method(File, 'get_path', 'f.get_path'), 73 | Method(File, 'get_completed_chunks', 'f.get_completed_chunks'), 74 | Method(File, 'get_path_components', 'f.get_path_components'), 75 | Method(File, 'is_created', 'f.is_created', 76 | boolean=True, 77 | ), 78 | Method(File, 'is_open', 'f.is_open', 79 | boolean=True, 80 | ), 81 | Method(File, 'get_size_chunks', 'f.get_size_chunks'), 82 | Method(File, 'get_offset', 'f.get_offset'), 83 | Method(File, 'get_frozen_path', 'f.get_frozen_path'), 84 | Method(File, 'get_path_depth', 'f.get_path_depth'), 85 | Method(File, 'is_create_queued', 'f.is_create_queued', 86 | boolean=True, 87 | ), 88 | 89 | 90 | # MODIFIERS 91 | ] 92 | -------------------------------------------------------------------------------- /rtorrent/group.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Dean Gardiner, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import rtorrent.rpc 22 | 23 | Method = rtorrent.rpc.Method 24 | 25 | 26 | class Group: 27 | __name__ = 'Group' 28 | 29 | def __init__(self, _rt_obj, name): 30 | self._rt_obj = _rt_obj 31 | self.name = name 32 | 33 | self.methods = [ 34 | # RETRIEVERS 35 | Method(Group, 'get_max', 'group.' + self.name + '.ratio.max', varname='max'), 36 | Method(Group, 'get_min', 'group.' + self.name + '.ratio.min', varname='min'), 37 | Method(Group, 'get_upload', 'group.' + self.name + '.ratio.upload', varname='upload'), 38 | 39 | # MODIFIERS 40 | Method(Group, 'set_max', 'group.' + self.name + '.ratio.max.set', varname='max'), 41 | Method(Group, 'set_min', 'group.' + self.name + '.ratio.min.set', varname='min'), 42 | Method(Group, 'set_upload', 'group.' + self.name + '.ratio.upload.set', varname='upload') 43 | ] 44 | 45 | rtorrent.rpc._build_rpc_methods(self, self.methods) 46 | 47 | # Setup multicall_add method 48 | caller = lambda multicall, method, *args: \ 49 | multicall.add(method, *args) 50 | setattr(self, "multicall_add", caller) 51 | 52 | def _get_prefix(self): 53 | return 'group.' + self.name + '.ratio.' 54 | 55 | def update(self): 56 | multicall = rtorrent.rpc.Multicall(self) 57 | 58 | retriever_methods = [m for m in self.methods 59 | if m.is_retriever() and m.is_available(self._rt_obj)] 60 | 61 | for method in retriever_methods: 62 | multicall.add(method) 63 | 64 | multicall.call() 65 | 66 | def enable(self): 67 | p = self._rt_obj._get_conn() 68 | return getattr(p, self._get_prefix() + 'enable')() 69 | 70 | def disable(self): 71 | p = self._rt_obj._get_conn() 72 | return getattr(p, self._get_prefix() + 'disable')() 73 | 74 | def set_command(self, *methods): 75 | methods = [m + '=' for m in methods] 76 | 77 | m = rtorrent.rpc.Multicall(self) 78 | self.multicall_add( 79 | m, 'system.method.set', 80 | self._get_prefix() + 'command', 81 | *methods 82 | ) 83 | 84 | return(m.call()[-1]) 85 | -------------------------------------------------------------------------------- /rtorrent/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjlucas/rtorrent-python/db4c76d2c6bcc9e98f0c5804320d0913f68433c7/rtorrent/lib/__init__.py -------------------------------------------------------------------------------- /rtorrent/lib/bencode.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011 by clueless 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | # 21 | # Version: 20111107 22 | # 23 | # Changelog 24 | # --------- 25 | # 2011-11-07 - Added support for Python2 (tested on 2.6) 26 | # 2011-10-03 - Fixed: moved check for end of list at the top of the while loop 27 | # in _decode_list (in case the list is empty) (Chris Lucas) 28 | # - Converted dictionary keys to str 29 | # 2011-04-24 - Changed date format to YYYY-MM-DD for versioning, bigger 30 | # integer denotes a newer version 31 | # - Fixed a bug that would treat False as an integral type but 32 | # encode it using the 'False' string, attempting to encode a 33 | # boolean now results in an error 34 | # - Fixed a bug where an integer value of 0 in a list or 35 | # dictionary resulted in a parse error while decoding 36 | # 37 | # 2011-04-03 - Original release 38 | 39 | import sys 40 | 41 | _py3 = sys.version_info[0] == 3 42 | 43 | if _py3: 44 | _VALID_STRING_TYPES = (str,) 45 | else: 46 | _VALID_STRING_TYPES = (str, unicode) # @UndefinedVariable 47 | 48 | _TYPE_INT = 1 49 | _TYPE_STRING = 2 50 | _TYPE_LIST = 3 51 | _TYPE_DICTIONARY = 4 52 | _TYPE_END = 5 53 | _TYPE_INVALID = 6 54 | 55 | # Function to determine the type of he next value/item 56 | # Arguments: 57 | # char First character of the string that is to be decoded 58 | # Return value: 59 | # Returns an integer that describes what type the next value/item is 60 | 61 | 62 | def _gettype(char): 63 | if not isinstance(char, int): 64 | char = ord(char) 65 | if char == 0x6C: # 'l' 66 | return _TYPE_LIST 67 | elif char == 0x64: # 'd' 68 | return _TYPE_DICTIONARY 69 | elif char == 0x69: # 'i' 70 | return _TYPE_INT 71 | elif char == 0x65: # 'e' 72 | return _TYPE_END 73 | elif char >= 0x30 and char <= 0x39: # '0' '9' 74 | return _TYPE_STRING 75 | else: 76 | return _TYPE_INVALID 77 | 78 | # Function to parse a string from the bendcoded data 79 | # Arguments: 80 | # data bencoded data, must be guaranteed to be a string 81 | # Return Value: 82 | # Returns a tuple, the first member of the tuple is the parsed string 83 | # The second member is whatever remains of the bencoded data so it can 84 | # be used to parse the next part of the data 85 | 86 | 87 | def _decode_string(data): 88 | end = 1 89 | # if py3, data[end] is going to be an int 90 | # if py2, data[end] will be a string 91 | if _py3: 92 | char = 0x3A 93 | else: 94 | char = chr(0x3A) 95 | 96 | while data[end] != char: # ':' 97 | end = end + 1 98 | strlen = int(data[:end]) 99 | return (data[end + 1:strlen + end + 1], data[strlen + end + 1:]) 100 | 101 | # Function to parse an integer from the bencoded data 102 | # Arguments: 103 | # data bencoded data, must be guaranteed to be an integer 104 | # Return Value: 105 | # Returns a tuple, the first member of the tuple is the parsed string 106 | # The second member is whatever remains of the bencoded data so it can 107 | # be used to parse the next part of the data 108 | 109 | 110 | def _decode_int(data): 111 | end = 1 112 | # if py3, data[end] is going to be an int 113 | # if py2, data[end] will be a string 114 | if _py3: 115 | char = 0x65 116 | else: 117 | char = chr(0x65) 118 | 119 | while data[end] != char: # 'e' 120 | end = end + 1 121 | return (int(data[1:end]), data[end + 1:]) 122 | 123 | # Function to parse a bencoded list 124 | # Arguments: 125 | # data bencoded data, must be guaranted to be the start of a list 126 | # Return Value: 127 | # Returns a tuple, the first member of the tuple is the parsed list 128 | # The second member is whatever remains of the bencoded data so it can 129 | # be used to parse the next part of the data 130 | 131 | 132 | def _decode_list(data): 133 | x = [] 134 | overflow = data[1:] 135 | while True: # Loop over the data 136 | if _gettype(overflow[0]) == _TYPE_END: # - Break if we reach the end of the list 137 | return (x, overflow[1:]) # and return the list and overflow 138 | 139 | value, overflow = _decode(overflow) # 140 | if isinstance(value, bool) or overflow == '': # - if we have a parse error 141 | return (False, False) # Die with error 142 | else: # - Otherwise 143 | x.append(value) # add the value to the list 144 | 145 | 146 | # Function to parse a bencoded list 147 | # Arguments: 148 | # data bencoded data, must be guaranted to be the start of a list 149 | # Return Value: 150 | # Returns a tuple, the first member of the tuple is the parsed dictionary 151 | # The second member is whatever remains of the bencoded data so it can 152 | # be used to parse the next part of the data 153 | def _decode_dict(data): 154 | x = {} 155 | overflow = data[1:] 156 | while True: # Loop over the data 157 | if _gettype(overflow[0]) != _TYPE_STRING: # - If the key is not a string 158 | return (False, False) # Die with error 159 | key, overflow = _decode(overflow) # 160 | if key == False or overflow == '': # - If parse error 161 | return (False, False) # Die with error 162 | value, overflow = _decode(overflow) # 163 | if isinstance(value, bool) or overflow == '': # - If parse error 164 | print("Error parsing value") 165 | print(value) 166 | print(overflow) 167 | return (False, False) # Die with error 168 | else: 169 | # don't use bytes for the key 170 | key = key.decode() 171 | x[key] = value 172 | if _gettype(overflow[0]) == _TYPE_END: 173 | return (x, overflow[1:]) 174 | 175 | # Arguments: 176 | # data bencoded data in bytes format 177 | # Return Values: 178 | # Returns a tuple, the first member is the parsed data, could be a string, 179 | # an integer, a list or a dictionary, or a combination of those 180 | # The second member is the leftover of parsing, if everything parses correctly this 181 | # should be an empty byte string 182 | 183 | 184 | def _decode(data): 185 | btype = _gettype(data[0]) 186 | if btype == _TYPE_INT: 187 | return _decode_int(data) 188 | elif btype == _TYPE_STRING: 189 | return _decode_string(data) 190 | elif btype == _TYPE_LIST: 191 | return _decode_list(data) 192 | elif btype == _TYPE_DICTIONARY: 193 | return _decode_dict(data) 194 | else: 195 | return (False, False) 196 | 197 | # Function to decode bencoded data 198 | # Arguments: 199 | # data bencoded data, can be str or bytes 200 | # Return Values: 201 | # Returns the decoded data on success, this coud be bytes, int, dict or list 202 | # or a combinatin of those 203 | # If an error occurs the return value is False 204 | 205 | 206 | def decode(data): 207 | # if isinstance(data, str): 208 | # data = data.encode() 209 | decoded, overflow = _decode(data) 210 | return decoded 211 | 212 | # Args: data as integer 213 | # return: encoded byte string 214 | 215 | 216 | def _encode_int(data): 217 | return b'i' + str(data).encode() + b'e' 218 | 219 | # Args: data as string or bytes 220 | # Return: encoded byte string 221 | 222 | 223 | def _encode_string(data): 224 | return str(len(data)).encode() + b':' + data 225 | 226 | # Args: data as list 227 | # Return: Encoded byte string, false on error 228 | 229 | 230 | def _encode_list(data): 231 | elist = b'l' 232 | for item in data: 233 | eitem = encode(item) 234 | if eitem == False: 235 | return False 236 | elist += eitem 237 | return elist + b'e' 238 | 239 | # Args: data as dict 240 | # Return: encoded byte string, false on error 241 | 242 | 243 | def _encode_dict(data): 244 | edict = b'd' 245 | keys = [] 246 | for key in data: 247 | if not isinstance(key, _VALID_STRING_TYPES) and not isinstance(key, bytes): 248 | return False 249 | keys.append(key) 250 | keys.sort() 251 | for key in keys: 252 | ekey = encode(key) 253 | eitem = encode(data[key]) 254 | if ekey == False or eitem == False: 255 | return False 256 | edict += ekey + eitem 257 | return edict + b'e' 258 | 259 | # Function to encode a variable in bencoding 260 | # Arguments: 261 | # data Variable to be encoded, can be a list, dict, str, bytes, int or a combination of those 262 | # Return Values: 263 | # Returns the encoded data as a byte string when successful 264 | # If an error occurs the return value is False 265 | 266 | 267 | def encode(data): 268 | if isinstance(data, bool): 269 | return False 270 | elif isinstance(data, (int, long)): 271 | return _encode_int(data) 272 | elif isinstance(data, bytes): 273 | return _encode_string(data) 274 | elif isinstance(data, _VALID_STRING_TYPES): 275 | return _encode_string(data.encode()) 276 | elif isinstance(data, list): 277 | return _encode_list(data) 278 | elif isinstance(data, dict): 279 | return _encode_dict(data) 280 | else: 281 | return False 282 | -------------------------------------------------------------------------------- /rtorrent/lib/torrentparser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | from rtorrent.compat import is_py3 22 | import os.path 23 | import re 24 | import rtorrent.lib.bencode as bencode 25 | import hashlib 26 | 27 | if is_py3(): 28 | from urllib.request import urlopen # @UnresolvedImport @UnusedImport 29 | else: 30 | from urllib2 import urlopen # @UnresolvedImport @Reimport 31 | 32 | 33 | class TorrentParser(): 34 | def __init__(self, torrent): 35 | """Decode and parse given torrent 36 | 37 | @param torrent: handles: urls, file paths, string of torrent data 38 | @type torrent: str 39 | 40 | @raise AssertionError: Can be raised for a couple reasons: 41 | - If _get_raw_torrent() couldn't figure out 42 | what X{torrent} is 43 | - if X{torrent} isn't a valid bencoded torrent file 44 | """ 45 | self.torrent = torrent 46 | self._raw_torrent = None # : testing yo 47 | self._torrent_decoded = None # : what up 48 | self.file_type = None 49 | 50 | self._get_raw_torrent() 51 | assert self._raw_torrent is not None, "Couldn't get raw_torrent." 52 | if self._torrent_decoded is None: 53 | self._decode_torrent() 54 | assert isinstance(self._torrent_decoded, dict), "Invalid torrent file." 55 | self._parse_torrent() 56 | 57 | def _is_raw(self): 58 | raw = False 59 | if isinstance(self.torrent, (str, bytes)): 60 | if isinstance(self._decode_torrent(self.torrent), dict): 61 | raw = True 62 | else: 63 | # reset self._torrent_decoded (currently equals False) 64 | self._torrent_decoded = None 65 | 66 | return(raw) 67 | 68 | def _get_raw_torrent(self): 69 | """Get raw torrent data by determining what self.torrent is""" 70 | # already raw? 71 | if self._is_raw(): 72 | self.file_type = "raw" 73 | self._raw_torrent = self.torrent 74 | return 75 | # local file? 76 | if os.path.isfile(self.torrent): 77 | self.file_type = "file" 78 | self._raw_torrent = open(self.torrent, "rb").read() 79 | # url? 80 | elif re.search("^(http|ftp)s?:\/\/", self.torrent, re.I): 81 | self.file_type = "url" 82 | self._raw_torrent = urlopen(self.torrent).read() 83 | 84 | def _decode_torrent(self, raw_torrent=None): 85 | if raw_torrent is None: 86 | raw_torrent = self._raw_torrent 87 | self._torrent_decoded = bencode.decode(raw_torrent) 88 | return(self._torrent_decoded) 89 | 90 | def _calc_info_hash(self): 91 | self.info_hash = None 92 | if "info" in self._torrent_decoded.keys(): 93 | info_encoded = bencode.encode(self._torrent_decoded["info"]) 94 | 95 | if info_encoded: 96 | self.info_hash = hashlib.sha1(info_encoded).hexdigest().upper() 97 | 98 | return(self.info_hash) 99 | 100 | def _parse_torrent(self): 101 | for k in self._torrent_decoded: 102 | key = k.replace(" ", "_").lower() 103 | setattr(self, key, self._torrent_decoded[k]) 104 | 105 | self._calc_info_hash() 106 | 107 | 108 | class NewTorrentParser(object): 109 | @staticmethod 110 | def _read_file(fp): 111 | return fp.read() 112 | 113 | @staticmethod 114 | def _write_file(fp): 115 | fp.write() 116 | return fp 117 | 118 | @staticmethod 119 | def _decode_torrent(data): 120 | return bencode.decode(data) 121 | 122 | def __init__(self, input): 123 | self.input = input 124 | self._raw_torrent = None 125 | self._decoded_torrent = None 126 | self._hash_outdated = False 127 | 128 | if isinstance(self.input, (str, bytes)): 129 | # path to file? 130 | if os.path.isfile(self.input): 131 | self._raw_torrent = self._read_file(open(self.input, "rb")) 132 | else: 133 | # assume input was the raw torrent data (do we really want 134 | # this?) 135 | self._raw_torrent = self.input 136 | 137 | # file-like object? 138 | elif self.input.hasattr("read"): 139 | self._raw_torrent = self._read_file(self.input) 140 | 141 | assert self._raw_torrent is not None, "Invalid input: input must be a path or a file-like object" 142 | 143 | self._decoded_torrent = self._decode_torrent(self._raw_torrent) 144 | 145 | assert isinstance( 146 | self._decoded_torrent, dict), "File could not be decoded" 147 | 148 | def _calc_info_hash(self): 149 | self.info_hash = None 150 | info_dict = self._torrent_decoded["info"] 151 | self.info_hash = hashlib.sha1(bencode.encode( 152 | info_dict)).hexdigest().upper() 153 | 154 | return(self.info_hash) 155 | 156 | def set_tracker(self, tracker): 157 | self._decoded_torrent["announce"] = tracker 158 | 159 | def get_tracker(self): 160 | return self._decoded_torrent.get("announce") 161 | -------------------------------------------------------------------------------- /rtorrent/lib/xmlrpc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjlucas/rtorrent-python/db4c76d2c6bcc9e98f0c5804320d0913f68433c7/rtorrent/lib/xmlrpc/__init__.py -------------------------------------------------------------------------------- /rtorrent/lib/xmlrpc/basic_auth.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2013 Dean Gardiner, 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | from base64 import encodestring 24 | import string 25 | try: 26 | import xmlrpc.client as xmlrpclib 27 | except: 28 | import xmlrpclib 29 | 30 | 31 | class BasicAuthTransport(xmlrpclib.Transport): 32 | def __init__(self, username=None, password=None): 33 | xmlrpclib.Transport.__init__(self) 34 | 35 | self.username = username 36 | self.password = password 37 | 38 | def send_auth(self, h): 39 | if self.username is not None and self.password is not None: 40 | h.putheader('AUTHORIZATION', "Basic %s" % string.replace( 41 | encodestring("%s:%s" % (self.username, self.password)), 42 | "\012", "" 43 | )) 44 | 45 | def single_request(self, host, handler, request_body, verbose=0): 46 | # issue XML-RPC request 47 | 48 | h = self.make_connection(host) 49 | if verbose: 50 | h.set_debuglevel(1) 51 | 52 | try: 53 | self.send_request(h, handler, request_body) 54 | self.send_host(h, host) 55 | self.send_user_agent(h) 56 | self.send_auth(h) 57 | self.send_content(h, request_body) 58 | 59 | response = h.getresponse(buffering=True) 60 | if response.status == 200: 61 | self.verbose = verbose 62 | return self.parse_response(response) 63 | except xmlrpclib.Fault: 64 | raise 65 | except Exception: 66 | self.close() 67 | raise 68 | 69 | #discard any response data and raise exception 70 | if response.getheader("content-length", 0): 71 | response.read() 72 | raise xmlrpclib.ProtocolError( 73 | host + handler, 74 | response.status, response.reason, 75 | response.msg, 76 | ) 77 | -------------------------------------------------------------------------------- /rtorrent/lib/xmlrpc/http.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | from rtorrent.compat import xmlrpclib 22 | 23 | HTTPServerProxy = xmlrpclib.ServerProxy 24 | -------------------------------------------------------------------------------- /rtorrent/lib/xmlrpc/scgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # rtorrent_xmlrpc 4 | # (c) 2011 Roger Que 5 | # 6 | # Modified portions: 7 | # (c) 2013 Dean Gardiner 8 | # 9 | # Python module for interacting with rtorrent's XML-RPC interface 10 | # directly over SCGI, instead of through an HTTP server intermediary. 11 | # Inspired by Glenn Washburn's xmlrpc2scgi.py [1], but subclasses the 12 | # built-in xmlrpclib classes so that it is compatible with features 13 | # such as MultiCall objects. 14 | # 15 | # [1] 16 | # 17 | # Usage: server = SCGIServerProxy('scgi://localhost:7000/') 18 | # server = SCGIServerProxy('scgi:///path/to/scgi.sock') 19 | # print server.system.listMethods() 20 | # mc = xmlrpclib.MultiCall(server) 21 | # mc.get_up_rate() 22 | # mc.get_down_rate() 23 | # print mc() 24 | # 25 | # 26 | # 27 | # This program is free software; you can redistribute it and/or modify 28 | # it under the terms of the GNU General Public License as published by 29 | # the Free Software Foundation; either version 2 of the License, or 30 | # (at your option) any later version. 31 | # 32 | # This program is distributed in the hope that it will be useful, 33 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 34 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 35 | # GNU General Public License for more details. 36 | # 37 | # You should have received a copy of the GNU General Public License 38 | # along with this program; if not, write to the Free Software 39 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 40 | # 41 | # In addition, as a special exception, the copyright holders give 42 | # permission to link the code of portions of this program with the 43 | # OpenSSL library under certain conditions as described in each 44 | # individual source file, and distribute linked combinations 45 | # including the two. 46 | # 47 | # You must obey the GNU General Public License in all respects for 48 | # all of the code used other than OpenSSL. If you modify file(s) 49 | # with this exception, you may extend this exception to your version 50 | # of the file(s), but you are not obligated to do so. If you do not 51 | # wish to do so, delete this exception statement from your version. 52 | # If you delete this exception statement from all source files in the 53 | # program, then also delete it here. 54 | # 55 | # 56 | # 57 | # Portions based on Python's xmlrpclib: 58 | # 59 | # Copyright (c) 1999-2002 by Secret Labs AB 60 | # Copyright (c) 1999-2002 by Fredrik Lundh 61 | # 62 | # By obtaining, using, and/or copying this software and/or its 63 | # associated documentation, you agree that you have read, understood, 64 | # and will comply with the following terms and conditions: 65 | # 66 | # Permission to use, copy, modify, and distribute this software and 67 | # its associated documentation for any purpose and without fee is 68 | # hereby granted, provided that the above copyright notice appears in 69 | # all copies, and that both that copyright notice and this permission 70 | # notice appear in supporting documentation, and that the name of 71 | # Secret Labs AB or the author not be used in advertising or publicity 72 | # pertaining to distribution of the software without specific, written 73 | # prior permission. 74 | # 75 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 76 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 77 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 78 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 79 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 80 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 81 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 82 | # OF THIS SOFTWARE. 83 | from __future__ import print_function 84 | 85 | try: 86 | import http.client as httplib 87 | except ImportError: 88 | import httplib 89 | import re 90 | import socket 91 | import sys 92 | try: 93 | import urllib.parse as urlparser 94 | except ImportError: 95 | import urllib as urlparser 96 | 97 | try: 98 | import xmlrpc.client as xmlrpclib 99 | except: 100 | import xmlrpclib 101 | 102 | import errno 103 | 104 | 105 | class SCGITransport(xmlrpclib.Transport): 106 | # Added request() from Python 2.7 xmlrpclib here to backport to Python 2.6 107 | def request(self, host, handler, request_body, verbose=0): 108 | #retry request once if cached connection has gone cold 109 | for i in (0, 1): 110 | try: 111 | return self.single_request(host, handler, request_body, verbose) 112 | except socket.error as e: 113 | if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE): 114 | raise 115 | except httplib.BadStatusLine: #close after we sent request 116 | if i: 117 | raise 118 | 119 | def single_request(self, host, handler, request_body, verbose=0): 120 | # Add SCGI headers to the request. 121 | headers = [('CONTENT_LENGTH', str(len(request_body))), ('SCGI', '1')] 122 | header = '\x00'.join(['%s\x00%s' % (key, value) for key, value in headers]) + '\x00' 123 | header = '%d:%s' % (len(header), header) 124 | request_body = '%s,%s' % (header, request_body) 125 | 126 | sock = None 127 | 128 | try: 129 | if host: 130 | host, port = urlparser.splitport(host) 131 | addrinfo = socket.getaddrinfo(host, int(port), socket.AF_INET, 132 | socket.SOCK_STREAM) 133 | sock = socket.socket(*addrinfo[0][:3]) 134 | sock.connect(addrinfo[0][4]) 135 | else: 136 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 137 | sock.connect(handler) 138 | 139 | self.verbose = verbose 140 | 141 | if sys.version_info[0] > 2: 142 | sock.send(bytes(request_body, "utf-8")) 143 | else: 144 | sock.send(request_body) 145 | return self.parse_response(sock.makefile()) 146 | finally: 147 | if sock: 148 | sock.close() 149 | 150 | def parse_response(self, response): 151 | p, u = self.getparser() 152 | 153 | response_body = '' 154 | while True: 155 | data = response.read(1024) 156 | if not data: 157 | break 158 | response_body += data 159 | 160 | # Remove SCGI headers from the response. 161 | 162 | if self.verbose: 163 | print('body:', repr(response_body)) 164 | 165 | try: 166 | response_header, response_body = re.split(r'\n\s*?\n', response_body, 167 | maxsplit=1) 168 | except ValueError: 169 | print("error in response: %s", response_body) 170 | p.close() 171 | u.close() 172 | 173 | p.feed(response_body) 174 | p.close() 175 | 176 | return u.close() 177 | 178 | 179 | class SCGIServerProxy(xmlrpclib.ServerProxy): 180 | def __init__(self, uri, transport=None, encoding=None, verbose=False, 181 | allow_none=False, use_datetime=False): 182 | type, uri = urlparser.splittype(uri) 183 | if type not in ('scgi'): 184 | raise IOError('unsupported XML-RPC protocol') 185 | self.__host, self.__handler = urlparser.splithost(uri) 186 | if not self.__handler: 187 | self.__handler = '/' 188 | 189 | if transport is None: 190 | transport = SCGITransport(use_datetime=use_datetime) 191 | self.__transport = transport 192 | 193 | self.__encoding = encoding 194 | self.__verbose = verbose 195 | self.__allow_none = allow_none 196 | 197 | def __close(self): 198 | self.__transport.close() 199 | 200 | def __request(self, methodname, params): 201 | # call a method on the remote server 202 | 203 | request = xmlrpclib.dumps(params, methodname, encoding=self.__encoding, 204 | allow_none=self.__allow_none) 205 | 206 | response = self.__transport.request( 207 | self.__host, 208 | self.__handler, 209 | request, 210 | verbose=self.__verbose 211 | ) 212 | 213 | if len(response) == 1: 214 | response = response[0] 215 | 216 | return response 217 | 218 | def __repr__(self): 219 | return ( 220 | "" % 221 | (self.__host, self.__handler) 222 | ) 223 | 224 | __str__ = __repr__ 225 | 226 | def __getattr__(self, name): 227 | # magic method dispatcher 228 | return xmlrpclib._Method(self.__request, name) 229 | 230 | # note: to call a remote object with an non-standard name, use 231 | # result getattr(server, "strange-python-name")(args) 232 | 233 | def __call__(self, attr): 234 | """A workaround to get special attributes on the ServerProxy 235 | without interfering with the magic __getattr__ 236 | """ 237 | if attr == "close": 238 | return self.__close 239 | elif attr == "transport": 240 | return self.__transport 241 | raise AttributeError("Attribute %r not found" % (attr,)) 242 | -------------------------------------------------------------------------------- /rtorrent/peer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | # from rtorrent.rpc import Method 22 | import rtorrent.rpc 23 | 24 | from rtorrent.common import safe_repr 25 | 26 | Method = rtorrent.rpc.Method 27 | 28 | 29 | class Peer: 30 | """Represents an individual peer within a L{Torrent} instance.""" 31 | def __init__(self, _rt_obj, info_hash, **kwargs): 32 | self._rt_obj = _rt_obj 33 | self.info_hash = info_hash # : info hash for the torrent the peer is associated with 34 | for k in kwargs.keys(): 35 | setattr(self, k, kwargs.get(k, None)) 36 | 37 | self.rpc_id = "{0}:p{1}".format( 38 | self.info_hash, self.id) # : unique id to pass to rTorrent 39 | 40 | def __repr__(self): 41 | return safe_repr("Peer(id={0})", self.id) 42 | 43 | def update(self): 44 | """Refresh peer data 45 | 46 | @note: All fields are stored as attributes to self. 47 | 48 | @return: None 49 | """ 50 | multicall = rtorrent.rpc.Multicall(self) 51 | retriever_methods = [m for m in methods 52 | if m.is_retriever() and m.is_available(self._rt_obj)] 53 | for method in retriever_methods: 54 | multicall.add(method, self.rpc_id) 55 | 56 | multicall.call() 57 | 58 | methods = [ 59 | # RETRIEVERS 60 | Method(Peer, 'is_preferred', 'p.is_preferred', 61 | boolean=True, 62 | ), 63 | Method(Peer, 'get_down_rate', 'p.get_down_rate'), 64 | Method(Peer, 'is_unwanted', 'p.is_unwanted', 65 | boolean=True, 66 | ), 67 | Method(Peer, 'get_peer_total', 'p.get_peer_total'), 68 | Method(Peer, 'get_peer_rate', 'p.get_peer_rate'), 69 | Method(Peer, 'get_port', 'p.get_port'), 70 | Method(Peer, 'is_snubbed', 'p.is_snubbed', 71 | boolean=True, 72 | ), 73 | Method(Peer, 'get_id_html', 'p.get_id_html'), 74 | Method(Peer, 'get_up_rate', 'p.get_up_rate'), 75 | Method(Peer, 'is_banned', 'p.banned', 76 | boolean=True, 77 | ), 78 | Method(Peer, 'get_completed_percent', 'p.get_completed_percent'), 79 | Method(Peer, 'completed_percent', 'p.completed_percent'), 80 | Method(Peer, 'get_id', 'p.get_id'), 81 | Method(Peer, 'is_obfuscated', 'p.is_obfuscated', 82 | boolean=True, 83 | ), 84 | Method(Peer, 'get_down_total', 'p.get_down_total'), 85 | Method(Peer, 'get_client_version', 'p.get_client_version'), 86 | Method(Peer, 'get_address', 'p.get_address'), 87 | Method(Peer, 'is_incoming', 'p.is_incoming', 88 | boolean=True, 89 | ), 90 | Method(Peer, 'is_encrypted', 'p.is_encrypted', 91 | boolean=True, 92 | ), 93 | Method(Peer, 'get_options_str', 'p.get_options_str'), 94 | Method(Peer, 'get_client_version', 'p.client_version'), 95 | Method(Peer, 'get_up_total', 'p.get_up_total'), 96 | 97 | # MODIFIERS 98 | ] 99 | -------------------------------------------------------------------------------- /rtorrent/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import inspect 22 | import rtorrent 23 | import re 24 | from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\ 25 | safe_repr 26 | from rtorrent.err import MethodError 27 | from rtorrent.compat import xmlrpclib 28 | 29 | 30 | def get_varname(rpc_call): 31 | """Transform rpc method into variable name. 32 | 33 | @newfield example: Example 34 | @example: if the name of the rpc method is 'p.get_down_rate', the variable 35 | name will be 'down_rate' 36 | """ 37 | # extract variable name from xmlrpc func name 38 | r = re.search( 39 | "([ptdf]\.|system\.|get\_|is\_|set\_)+([^=]*)", rpc_call, re.I) 40 | if r: 41 | return(r.groups()[-1]) 42 | else: 43 | return(None) 44 | 45 | 46 | def _handle_unavailable_rpc_method(method, rt_obj): 47 | msg = "Method isn't available." 48 | if rt_obj._get_client_version_tuple() < method.min_version: 49 | msg = "This method is only available in " \ 50 | "RTorrent version v{0} or later".format( 51 | convert_version_tuple_to_str(method.min_version)) 52 | 53 | raise MethodError(msg) 54 | 55 | 56 | class DummyClass: 57 | def __init__(self): 58 | pass 59 | 60 | 61 | class Method: 62 | """Represents an individual RPC method""" 63 | 64 | def __init__(self, _class, method_name, 65 | rpc_call, docstring=None, varname=None, **kwargs): 66 | self._class = _class # : Class this method is associated with 67 | self.class_name = _class.__name__ 68 | self.method_name = method_name # : name of public-facing method 69 | self.rpc_call = rpc_call # : name of rpc method 70 | self.docstring = docstring # : docstring for rpc method (optional) 71 | self.varname = varname # : variable for the result of the method call, usually set to self.varname 72 | self.min_version = kwargs.get("min_version", ( 73 | 0, 0, 0)) # : Minimum version of rTorrent required 74 | self.boolean = kwargs.get("boolean", False) # : returns boolean value? 75 | self.post_process_func = kwargs.get( 76 | "post_process_func", None) # : custom post process function 77 | self.aliases = kwargs.get( 78 | "aliases", []) # : aliases for method (optional) 79 | self.required_args = [] 80 | #: Arguments required when calling the method (not utilized) 81 | 82 | self.method_type = self._get_method_type() 83 | 84 | if self.varname is None: 85 | self.varname = get_varname(self.rpc_call) 86 | assert self.varname is not None, "Couldn't get variable name." 87 | 88 | def __repr__(self): 89 | return safe_repr("Method(method_name='{0}', rpc_call='{1}')", 90 | self.method_name, self.rpc_call) 91 | 92 | def _get_method_type(self): 93 | """Determine whether method is a modifier or a retriever""" 94 | if self.method_name[:4] == "set_": return('m') # modifier 95 | else: 96 | return('r') # retriever 97 | 98 | def is_modifier(self): 99 | if self.method_type == 'm': 100 | return(True) 101 | else: 102 | return(False) 103 | 104 | def is_retriever(self): 105 | if self.method_type == 'r': 106 | return(True) 107 | else: 108 | return(False) 109 | 110 | def is_available(self, rt_obj): 111 | if rt_obj._get_client_version_tuple() < self.min_version or \ 112 | self.rpc_call not in rt_obj._get_rpc_methods(): 113 | return(False) 114 | else: 115 | return(True) 116 | 117 | 118 | class Multicall: 119 | def __init__(self, class_obj, **kwargs): 120 | self.class_obj = class_obj 121 | if class_obj.__class__.__name__ == "RTorrent": 122 | self.rt_obj = class_obj 123 | else: 124 | self.rt_obj = class_obj._rt_obj 125 | self.calls = [] 126 | 127 | def add(self, method, *args): 128 | """Add call to multicall 129 | 130 | @param method: L{Method} instance or name of raw RPC method 131 | @type method: Method or str 132 | 133 | @param args: call arguments 134 | """ 135 | # if a raw rpc method was given instead of a Method instance, 136 | # try and find the instance for it. And if all else fails, create a 137 | # dummy Method instance 138 | if isinstance(method, str): 139 | result = find_method(method) 140 | # if result not found 141 | if result == -1: 142 | method = Method(DummyClass, method, method) 143 | else: 144 | method = result 145 | 146 | # ensure method is available before adding 147 | if not method.is_available(self.rt_obj): 148 | _handle_unavailable_rpc_method(method, self.rt_obj) 149 | 150 | self.calls.append((method, args)) 151 | 152 | def list_calls(self): 153 | for c in self.calls: 154 | print(c) 155 | 156 | def call(self): 157 | """Execute added multicall calls 158 | 159 | @return: the results (post-processed), in the order they were added 160 | @rtype: tuple 161 | """ 162 | m = xmlrpclib.MultiCall(self.rt_obj._get_conn()) 163 | for call in self.calls: 164 | method, args = call 165 | rpc_call = getattr(method, "rpc_call") 166 | getattr(m, rpc_call)(*args) 167 | 168 | results = m() 169 | results = tuple(results) 170 | results_processed = [] 171 | 172 | for r, c in zip(results, self.calls): 173 | method = c[0] # Method instance 174 | result = process_result(method, r) 175 | results_processed.append(result) 176 | # assign result to class_obj 177 | exists = hasattr(self.class_obj, method.varname) 178 | if not exists or not inspect.ismethod(getattr(self.class_obj, method.varname)): 179 | setattr(self.class_obj, method.varname, result) 180 | 181 | return(tuple(results_processed)) 182 | 183 | 184 | def call_method(class_obj, method, *args): 185 | """Handles single RPC calls 186 | 187 | @param class_obj: Peer/File/Torrent/Tracker/RTorrent instance 188 | @type class_obj: object 189 | 190 | @param method: L{Method} instance or name of raw RPC method 191 | @type method: Method or str 192 | """ 193 | if method.is_retriever(): 194 | args = args[:-1] 195 | else: 196 | assert args[-1] is not None, "No argument given." 197 | 198 | if class_obj.__class__.__name__ == "RTorrent": 199 | rt_obj = class_obj 200 | else: 201 | rt_obj = class_obj._rt_obj 202 | 203 | # check if rpc method is even available 204 | if not method.is_available(rt_obj): 205 | _handle_unavailable_rpc_method(method, rt_obj) 206 | 207 | m = Multicall(class_obj) 208 | m.add(method, *args) 209 | # only added one method, only getting one result back 210 | ret_value = m.call()[0] 211 | 212 | ####### OBSOLETE ########################################################## 213 | # if method.is_retriever(): 214 | # #value = process_result(method, ret_value) 215 | # value = ret_value #MultiCall already processed the result 216 | # else: 217 | # # we're setting the user's input to method.varname 218 | # # but we'll return the value that xmlrpc gives us 219 | # value = process_result(method, args[-1]) 220 | ########################################################################## 221 | 222 | return(ret_value) 223 | 224 | 225 | def find_method(rpc_call): 226 | """Return L{Method} instance associated with given RPC call""" 227 | method_lists = [ 228 | rtorrent.methods, 229 | rtorrent.file.methods, 230 | rtorrent.tracker.methods, 231 | rtorrent.peer.methods, 232 | rtorrent.torrent.methods, 233 | ] 234 | 235 | for l in method_lists: 236 | for m in l: 237 | if m.rpc_call.lower() == rpc_call.lower(): 238 | return(m) 239 | 240 | return(-1) 241 | 242 | 243 | def process_result(method, result): 244 | """Process given C{B{result}} based on flags set in C{B{method}} 245 | 246 | @param method: L{Method} instance 247 | @type method: Method 248 | 249 | @param result: result to be processed (the result of given L{Method} instance) 250 | 251 | @note: Supported Processing: 252 | - boolean - convert ones and zeros returned by rTorrent and 253 | convert to python boolean values 254 | """ 255 | # handle custom post processing function 256 | if method.post_process_func is not None: 257 | result = method.post_process_func(result) 258 | 259 | # is boolean? 260 | if method.boolean: 261 | if result in [1, '1']: 262 | result = True 263 | elif result in [0, '0']: 264 | result = False 265 | 266 | return(result) 267 | 268 | 269 | def _build_rpc_methods(class_, method_list): 270 | """Build glorified aliases to raw RPC methods""" 271 | instance = None 272 | if not inspect.isclass(class_): 273 | instance = class_ 274 | class_ = instance.__class__ 275 | 276 | for m in method_list: 277 | class_name = m.class_name 278 | if class_name != class_.__name__: 279 | continue 280 | 281 | if class_name == "RTorrent": 282 | caller = lambda self, arg = None, method = m:\ 283 | call_method(self, method, bool_to_int(arg)) 284 | elif class_name == "Torrent": 285 | caller = lambda self, arg = None, method = m:\ 286 | call_method(self, method, self.rpc_id, 287 | bool_to_int(arg)) 288 | elif class_name in ["Tracker", "File"]: 289 | caller = lambda self, arg = None, method = m:\ 290 | call_method(self, method, self.rpc_id, 291 | bool_to_int(arg)) 292 | 293 | elif class_name == "Peer": 294 | caller = lambda self, arg = None, method = m:\ 295 | call_method(self, method, self.rpc_id, 296 | bool_to_int(arg)) 297 | 298 | elif class_name == "Group": 299 | caller = lambda arg = None, method = m: \ 300 | call_method(instance, method, bool_to_int(arg)) 301 | 302 | if m.docstring is None: 303 | m.docstring = "" 304 | 305 | # print(m) 306 | docstring = """{0} 307 | 308 | @note: Variable where the result for this method is stored: {1}.{2}""".format( 309 | m.docstring, 310 | class_name, 311 | m.varname) 312 | 313 | caller.__doc__ = docstring 314 | 315 | for method_name in [m.method_name] + list(m.aliases): 316 | if instance is None: 317 | setattr(class_, method_name, caller) 318 | else: 319 | setattr(instance, method_name, caller) 320 | -------------------------------------------------------------------------------- /rtorrent/torrent.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import rtorrent.rpc 22 | # from rtorrent.rpc import Method 23 | import rtorrent.peer 24 | import rtorrent.tracker 25 | import rtorrent.file 26 | import rtorrent.compat 27 | 28 | from rtorrent.common import safe_repr 29 | 30 | Peer = rtorrent.peer.Peer 31 | Tracker = rtorrent.tracker.Tracker 32 | File = rtorrent.file.File 33 | Method = rtorrent.rpc.Method 34 | 35 | 36 | class Torrent: 37 | """Represents an individual torrent within a L{RTorrent} instance.""" 38 | 39 | def __init__(self, _rt_obj, info_hash, **kwargs): 40 | self._rt_obj = _rt_obj 41 | self.info_hash = info_hash # : info hash for the torrent 42 | self.rpc_id = self.info_hash # : unique id to pass to rTorrent 43 | for k in kwargs.keys(): 44 | setattr(self, k, kwargs.get(k, None)) 45 | 46 | self.peers = [] 47 | self.trackers = [] 48 | self.files = [] 49 | 50 | self._call_custom_methods() 51 | 52 | def __repr__(self): 53 | return safe_repr("Torrent(info_hash=\"{0}\" name=\"{1}\")", 54 | self.info_hash, self.name) 55 | 56 | def _call_custom_methods(self): 57 | """only calls methods that check instance variables.""" 58 | self._is_hash_checking_queued() 59 | self._is_started() 60 | self._is_paused() 61 | 62 | def get_peers(self): 63 | """Get list of Peer instances for given torrent. 64 | 65 | @return: L{Peer} instances 66 | @rtype: list 67 | 68 | @note: also assigns return value to self.peers 69 | """ 70 | self.peers = [] 71 | retriever_methods = [m for m in rtorrent.peer.methods 72 | if m.is_retriever() and m.is_available(self._rt_obj)] 73 | # need to leave 2nd arg empty (dunno why) 74 | m = rtorrent.rpc.Multicall(self) 75 | m.add("p.multicall", self.info_hash, "", 76 | *[method.rpc_call + "=" for method in retriever_methods]) 77 | 78 | results = m.call()[0] # only sent one call, only need first result 79 | 80 | for result in results: 81 | results_dict = {} 82 | # build results_dict 83 | for m, r in zip(retriever_methods, result): 84 | results_dict[m.varname] = rtorrent.rpc.process_result(m, r) 85 | 86 | self.peers.append(Peer( 87 | self._rt_obj, self.info_hash, **results_dict)) 88 | 89 | return(self.peers) 90 | 91 | def get_trackers(self): 92 | """Get list of Tracker instances for given torrent. 93 | 94 | @return: L{Tracker} instances 95 | @rtype: list 96 | 97 | @note: also assigns return value to self.trackers 98 | """ 99 | self.trackers = [] 100 | retriever_methods = [m for m in rtorrent.tracker.methods 101 | if m.is_retriever() and m.is_available(self._rt_obj)] 102 | 103 | # need to leave 2nd arg empty (dunno why) 104 | m = rtorrent.rpc.Multicall(self) 105 | m.add("t.multicall", self.info_hash, "", 106 | *[method.rpc_call + "=" for method in retriever_methods]) 107 | 108 | results = m.call()[0] # only sent one call, only need first result 109 | 110 | for result in results: 111 | results_dict = {} 112 | # build results_dict 113 | for m, r in zip(retriever_methods, result): 114 | results_dict[m.varname] = rtorrent.rpc.process_result(m, r) 115 | 116 | self.trackers.append(Tracker( 117 | self._rt_obj, self.info_hash, **results_dict)) 118 | 119 | return(self.trackers) 120 | 121 | def get_files(self): 122 | """Get list of File instances for given torrent. 123 | 124 | @return: L{File} instances 125 | @rtype: list 126 | 127 | @note: also assigns return value to self.files 128 | """ 129 | 130 | self.files = [] 131 | retriever_methods = [m for m in rtorrent.file.methods 132 | if m.is_retriever() and m.is_available(self._rt_obj)] 133 | # 2nd arg can be anything, but it'll return all files in torrent 134 | # regardless 135 | m = rtorrent.rpc.Multicall(self) 136 | m.add("f.multicall", self.info_hash, "", 137 | *[method.rpc_call + "=" for method in retriever_methods]) 138 | 139 | results = m.call()[0] # only sent one call, only need first result 140 | 141 | offset_method_index = retriever_methods.index( 142 | rtorrent.rpc.find_method("f.get_offset")) 143 | 144 | # make a list of the offsets of all the files, sort appropriately 145 | offset_list = sorted([r[offset_method_index] for r in results]) 146 | 147 | for result in results: 148 | results_dict = {} 149 | # build results_dict 150 | for m, r in zip(retriever_methods, result): 151 | results_dict[m.varname] = rtorrent.rpc.process_result(m, r) 152 | 153 | # get proper index positions for each file (based on the file 154 | # offset) 155 | f_index = offset_list.index(results_dict["offset"]) 156 | 157 | self.files.append(File(self._rt_obj, self.info_hash, 158 | f_index, **results_dict)) 159 | 160 | return(self.files) 161 | 162 | def set_directory(self, d): 163 | """Modify download directory 164 | 165 | @note: Needs to stop torrent in order to change the directory. 166 | Also doesn't restart after directory is set, that must be called 167 | separately. 168 | """ 169 | m = rtorrent.rpc.Multicall(self) 170 | self.multicall_add(m, "d.try_stop") 171 | self.multicall_add(m, "d.set_directory", d) 172 | 173 | self.directory = m.call()[-1] 174 | 175 | def set_directory_base(self, d): 176 | """Modify base download directory 177 | 178 | @note: Needs to stop torrent in order to change the directory. 179 | Also doesn't restart after directory is set, that must be called 180 | separately. 181 | """ 182 | m = rtorrent.rpc.Multicall(self) 183 | self.multicall_add(m, "d.try_stop") 184 | self.multicall_add(m, "d.set_directory_base", d) 185 | 186 | def start(self): 187 | """Start the torrent""" 188 | m = rtorrent.rpc.Multicall(self) 189 | self.multicall_add(m, "d.try_start") 190 | self.multicall_add(m, "d.is_active") 191 | 192 | self.active = m.call()[-1] 193 | return(self.active) 194 | 195 | def stop(self): 196 | """"Stop the torrent""" 197 | m = rtorrent.rpc.Multicall(self) 198 | self.multicall_add(m, "d.try_stop") 199 | self.multicall_add(m, "d.is_active") 200 | 201 | self.active = m.call()[-1] 202 | return(self.active) 203 | 204 | def pause(self): 205 | """Pause the torrent""" 206 | m = rtorrent.rpc.Multicall(self) 207 | self.multicall_add(m, "d.pause") 208 | 209 | return(m.call()[-1]) 210 | 211 | def resume(self): 212 | """Resume the torrent""" 213 | m = rtorrent.rpc.Multicall(self) 214 | self.multicall_add(m, "d.resume") 215 | 216 | return(m.call()[-1]) 217 | 218 | def close(self): 219 | """Close the torrent and it's files""" 220 | m = rtorrent.rpc.Multicall(self) 221 | self.multicall_add(m, "d.close") 222 | 223 | return(m.call()[-1]) 224 | 225 | def erase(self): 226 | """Delete the torrent 227 | 228 | @note: doesn't delete the downloaded files""" 229 | m = rtorrent.rpc.Multicall(self) 230 | self.multicall_add(m, "d.erase") 231 | 232 | return(m.call()[-1]) 233 | 234 | def check_hash(self): 235 | """(Re)hash check the torrent""" 236 | m = rtorrent.rpc.Multicall(self) 237 | self.multicall_add(m, "d.check_hash") 238 | 239 | return(m.call()[-1]) 240 | 241 | def poll(self): 242 | """poll rTorrent to get latest peer/tracker/file information""" 243 | self.get_peers() 244 | self.get_trackers() 245 | self.get_files() 246 | 247 | def update(self): 248 | """Refresh torrent data 249 | 250 | @note: All fields are stored as attributes to self. 251 | 252 | @return: None 253 | """ 254 | multicall = rtorrent.rpc.Multicall(self) 255 | retriever_methods = [m for m in methods 256 | if m.is_retriever() and m.is_available(self._rt_obj)] 257 | for method in retriever_methods: 258 | multicall.add(method, self.rpc_id) 259 | 260 | multicall.call() 261 | 262 | # custom functions (only call private methods, since they only check 263 | # local variables and are therefore faster) 264 | self._call_custom_methods() 265 | 266 | def accept_seeders(self, accept_seeds): 267 | """Enable/disable whether the torrent connects to seeders 268 | 269 | @param accept_seeds: enable/disable accepting seeders 270 | @type accept_seeds: bool""" 271 | if accept_seeds: 272 | call = "d.accepting_seeders.enable" 273 | else: 274 | call = "d.accepting_seeders.disable" 275 | 276 | m = rtorrent.rpc.Multicall(self) 277 | self.multicall_add(m, call) 278 | 279 | return(m.call()[-1]) 280 | 281 | def announce(self): 282 | """Announce torrent info to tracker(s)""" 283 | m = rtorrent.rpc.Multicall(self) 284 | self.multicall_add(m, "d.tracker_announce") 285 | 286 | return(m.call()[-1]) 287 | 288 | @staticmethod 289 | def _assert_custom_key_valid(key): 290 | assert type(key) == int and key > 0 and key < 6, \ 291 | "key must be an integer between 1-5" 292 | 293 | def get_custom(self, key): 294 | """ 295 | Get custom value 296 | 297 | @param key: the index for the custom field (between 1-5) 298 | @type key: int 299 | 300 | @rtype: str 301 | """ 302 | 303 | self._assert_custom_key_valid(key) 304 | m = rtorrent.rpc.Multicall(self) 305 | 306 | field = "custom{0}".format(key) 307 | self.multicall_add(m, "d.get_{0}".format(field)) 308 | setattr(self, field, m.call()[-1]) 309 | 310 | return (getattr(self, field)) 311 | 312 | def set_custom(self, key, value): 313 | """ 314 | Set custom value 315 | 316 | @param key: the index for the custom field (between 1-5) 317 | @type key: int 318 | 319 | @param value: the value to be stored 320 | @type value: str 321 | 322 | @return: if successful, value will be returned 323 | @rtype: str 324 | """ 325 | 326 | self._assert_custom_key_valid(key) 327 | m = rtorrent.rpc.Multicall(self) 328 | 329 | self.multicall_add(m, "d.set_custom{0}".format(key), value) 330 | 331 | return(m.call()[-1]) 332 | 333 | def set_visible(self, view, visible=True): 334 | p = self._rt_obj._get_conn() 335 | 336 | if visible: 337 | return p.view.set_visible(self.info_hash, view) 338 | else: 339 | return p.view.set_not_visible(self.info_hash, view) 340 | 341 | def add_tracker(self, group, tracker): 342 | """ 343 | Add tracker to torrent 344 | 345 | @param group: The group to add the tracker to 346 | @type group: int 347 | 348 | @param tracker: The tracker url 349 | @type tracker: str 350 | 351 | @return: if successful, 0 352 | @rtype: int 353 | """ 354 | m = rtorrent.rpc.Multicall(self) 355 | self.multicall_add(m, "d.tracker.insert", group, tracker) 356 | 357 | return (m.call()[-1]) 358 | 359 | ############################################################################ 360 | # CUSTOM METHODS (Not part of the official rTorrent API) 361 | ########################################################################## 362 | def _is_hash_checking_queued(self): 363 | """Only checks instance variables, shouldn't be called directly""" 364 | # if hashing == 3, then torrent is marked for hash checking 365 | # if hash_checking == False, then torrent is waiting to be checked 366 | self.hash_checking_queued = (self.hashing == 3 and 367 | self.hash_checking is False) 368 | 369 | return(self.hash_checking_queued) 370 | 371 | def is_hash_checking_queued(self): 372 | """Check if torrent is waiting to be hash checked 373 | 374 | @note: Variable where the result for this method is stored Torrent.hash_checking_queued""" 375 | m = rtorrent.rpc.Multicall(self) 376 | self.multicall_add(m, "d.get_hashing") 377 | self.multicall_add(m, "d.is_hash_checking") 378 | results = m.call() 379 | 380 | setattr(self, "hashing", results[0]) 381 | setattr(self, "hash_checking", results[1]) 382 | 383 | return(self._is_hash_checking_queued()) 384 | 385 | def _is_paused(self): 386 | """Only checks instance variables, shouldn't be called directly""" 387 | self.paused = (self.state == 0) 388 | return(self.paused) 389 | 390 | def is_paused(self): 391 | """Check if torrent is paused 392 | 393 | @note: Variable where the result for this method is stored: Torrent.paused""" 394 | self.get_state() 395 | return(self._is_paused()) 396 | 397 | def _is_started(self): 398 | """Only checks instance variables, shouldn't be called directly""" 399 | self.started = (self.state == 1) 400 | return(self.started) 401 | 402 | def is_started(self): 403 | """Check if torrent is started 404 | 405 | @note: Variable where the result for this method is stored: Torrent.started""" 406 | self.get_state() 407 | return(self._is_started()) 408 | 409 | 410 | methods = [ 411 | # RETRIEVERS 412 | Method(Torrent, 'is_hash_checked', 'd.is_hash_checked', 413 | boolean=True, 414 | ), 415 | Method(Torrent, 'is_hash_checking', 'd.is_hash_checking', 416 | boolean=True, 417 | ), 418 | Method(Torrent, 'get_peers_max', 'd.get_peers_max'), 419 | Method(Torrent, 'get_tracker_focus', 'd.get_tracker_focus'), 420 | Method(Torrent, 'get_skip_total', 'd.get_skip_total'), 421 | Method(Torrent, 'get_state', 'd.get_state'), 422 | Method(Torrent, 'get_peer_exchange', 'd.get_peer_exchange'), 423 | Method(Torrent, 'get_down_rate', 'd.get_down_rate'), 424 | Method(Torrent, 'get_connection_seed', 'd.get_connection_seed'), 425 | Method(Torrent, 'get_uploads_max', 'd.get_uploads_max'), 426 | Method(Torrent, 'get_priority_str', 'd.get_priority_str'), 427 | Method(Torrent, 'is_open', 'd.is_open', 428 | boolean=True, 429 | ), 430 | Method(Torrent, 'get_peers_min', 'd.get_peers_min'), 431 | Method(Torrent, 'get_peers_complete', 'd.get_peers_complete'), 432 | Method(Torrent, 'get_tracker_numwant', 'd.get_tracker_numwant'), 433 | Method(Torrent, 'get_connection_current', 'd.get_connection_current'), 434 | Method(Torrent, 'is_complete', 'd.get_complete', 435 | boolean=True, 436 | ), 437 | Method(Torrent, 'get_peers_connected', 'd.get_peers_connected'), 438 | Method(Torrent, 'get_chunk_size', 'd.get_chunk_size'), 439 | Method(Torrent, 'get_state_counter', 'd.get_state_counter'), 440 | Method(Torrent, 'get_base_filename', 'd.get_base_filename'), 441 | Method(Torrent, 'get_state_changed', 'd.get_state_changed'), 442 | Method(Torrent, 'get_peers_not_connected', 'd.get_peers_not_connected'), 443 | Method(Torrent, 'get_directory', 'd.get_directory'), 444 | Method(Torrent, 'is_incomplete', 'd.incomplete', 445 | boolean=True, 446 | ), 447 | Method(Torrent, 'get_tracker_size', 'd.get_tracker_size'), 448 | Method(Torrent, 'is_multi_file', 'd.is_multi_file', 449 | boolean=True, 450 | ), 451 | Method(Torrent, 'get_local_id', 'd.get_local_id'), 452 | Method(Torrent, 'get_ratio', 'd.get_ratio', 453 | post_process_func=lambda x: x / 1000.0, 454 | ), 455 | Method(Torrent, 'get_loaded_file', 'd.get_loaded_file'), 456 | Method(Torrent, 'get_max_file_size', 'd.get_max_file_size'), 457 | Method(Torrent, 'get_size_chunks', 'd.get_size_chunks'), 458 | Method(Torrent, 'is_pex_active', 'd.is_pex_active', 459 | boolean=True, 460 | ), 461 | Method(Torrent, 'get_hashing', 'd.get_hashing'), 462 | Method(Torrent, 'get_bitfield', 'd.get_bitfield'), 463 | Method(Torrent, 'get_local_id_html', 'd.get_local_id_html'), 464 | Method(Torrent, 'get_connection_leech', 'd.get_connection_leech'), 465 | Method(Torrent, 'get_peers_accounted', 'd.get_peers_accounted'), 466 | Method(Torrent, 'get_message', 'd.get_message'), 467 | Method(Torrent, 'is_active', 'd.is_active', 468 | boolean=True, 469 | ), 470 | Method(Torrent, 'get_size_bytes', 'd.get_size_bytes'), 471 | Method(Torrent, 'get_ignore_commands', 'd.get_ignore_commands'), 472 | Method(Torrent, 'get_creation_date', 'd.get_creation_date'), 473 | Method(Torrent, 'get_base_path', 'd.get_base_path'), 474 | Method(Torrent, 'get_left_bytes', 'd.get_left_bytes'), 475 | Method(Torrent, 'get_size_files', 'd.get_size_files'), 476 | Method(Torrent, 'get_size_pex', 'd.get_size_pex'), 477 | Method(Torrent, 'is_private', 'd.is_private', 478 | boolean=True, 479 | ), 480 | Method(Torrent, 'get_max_size_pex', 'd.get_max_size_pex'), 481 | Method(Torrent, 'get_num_chunks_hashed', 'd.get_chunks_hashed', 482 | aliases=("get_chunks_hashed",)), 483 | Method(Torrent, 'get_num_chunks_wanted', 'd.wanted_chunks'), 484 | Method(Torrent, 'get_priority', 'd.get_priority'), 485 | Method(Torrent, 'get_skip_rate', 'd.get_skip_rate'), 486 | Method(Torrent, 'get_completed_bytes', 'd.get_completed_bytes'), 487 | Method(Torrent, 'get_name', 'd.get_name'), 488 | Method(Torrent, 'get_completed_chunks', 'd.get_completed_chunks'), 489 | Method(Torrent, 'get_throttle_name', 'd.get_throttle_name'), 490 | Method(Torrent, 'get_free_diskspace', 'd.get_free_diskspace'), 491 | Method(Torrent, 'get_directory_base', 'd.get_directory_base'), 492 | Method(Torrent, 'get_hashing_failed', 'd.get_hashing_failed'), 493 | Method(Torrent, 'get_tied_to_file', 'd.get_tied_to_file'), 494 | Method(Torrent, 'get_down_total', 'd.get_down_total'), 495 | Method(Torrent, 'get_bytes_done', 'd.get_bytes_done'), 496 | Method(Torrent, 'get_up_rate', 'd.get_up_rate'), 497 | Method(Torrent, 'get_up_total', 'd.get_up_total'), 498 | Method(Torrent, 'is_accepting_seeders', 'd.accepting_seeders', 499 | boolean=True, 500 | ), 501 | Method(Torrent, "get_chunks_seen", "d.chunks_seen", 502 | min_version=(0, 9, 1), 503 | ), 504 | Method(Torrent, "is_partially_done", "d.is_partially_done", 505 | boolean=True, 506 | ), 507 | Method(Torrent, "is_not_partially_done", "d.is_not_partially_done", 508 | boolean=True, 509 | ), 510 | Method(Torrent, "get_time_started", "d.timestamp.started"), 511 | Method(Torrent, "get_custom1", "d.get_custom1"), 512 | Method(Torrent, "get_custom2", "d.get_custom2"), 513 | Method(Torrent, "get_custom3", "d.get_custom3"), 514 | Method(Torrent, "get_custom4", "d.get_custom4"), 515 | Method(Torrent, "get_custom5", "d.get_custom5"), 516 | 517 | # MODIFIERS 518 | Method(Torrent, 'set_uploads_max', 'd.set_uploads_max'), 519 | Method(Torrent, 'set_tied_to_file', 'd.set_tied_to_file'), 520 | Method(Torrent, 'set_tracker_numwant', 'd.set_tracker_numwant'), 521 | Method(Torrent, 'set_priority', 'd.set_priority'), 522 | Method(Torrent, 'set_peers_max', 'd.set_peers_max'), 523 | Method(Torrent, 'set_hashing_failed', 'd.set_hashing_failed'), 524 | Method(Torrent, 'set_message', 'd.set_message'), 525 | Method(Torrent, 'set_throttle_name', 'd.set_throttle_name'), 526 | Method(Torrent, 'set_peers_min', 'd.set_peers_min'), 527 | Method(Torrent, 'set_ignore_commands', 'd.set_ignore_commands'), 528 | Method(Torrent, 'set_max_file_size', 'd.set_max_file_size'), 529 | Method(Torrent, 'set_custom5', 'd.set_custom5'), 530 | Method(Torrent, 'set_custom4', 'd.set_custom4'), 531 | Method(Torrent, 'set_custom2', 'd.set_custom2'), 532 | Method(Torrent, 'set_custom1', 'd.set_custom1'), 533 | Method(Torrent, 'set_custom3', 'd.set_custom3'), 534 | Method(Torrent, 'set_connection_current', 'd.set_connection_current'), 535 | ] 536 | -------------------------------------------------------------------------------- /rtorrent/tracker.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Chris Lucas, 2 | # Permission is hereby granted, free of charge, to any person obtaining 3 | # a copy of this software and associated documentation files (the 4 | # "Software"), to deal in the Software without restriction, including 5 | # without limitation the rights to use, copy, modify, merge, publish, 6 | # distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so, subject to 8 | # the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | # from rtorrent.rpc import Method 22 | import rtorrent.rpc 23 | 24 | from rtorrent.common import safe_repr 25 | 26 | Method = rtorrent.rpc.Method 27 | 28 | 29 | class Tracker: 30 | """Represents an individual tracker within a L{Torrent} instance.""" 31 | 32 | def __init__(self, _rt_obj, info_hash, **kwargs): 33 | self._rt_obj = _rt_obj 34 | self.info_hash = info_hash # : info hash for the torrent using this tracker 35 | for k in kwargs.keys(): 36 | setattr(self, k, kwargs.get(k, None)) 37 | 38 | # for clarity's sake... 39 | self.index = self.group # : position of tracker within the torrent's tracker list 40 | self.rpc_id = "{0}:t{1}".format( 41 | self.info_hash, self.index) # : unique id to pass to rTorrent 42 | 43 | def __repr__(self): 44 | return safe_repr("Tracker(index={0}, url=\"{1}\")", 45 | self.index, self.url) 46 | 47 | def enable(self): 48 | """Alias for set_enabled("yes")""" 49 | self.set_enabled("yes") 50 | 51 | def disable(self): 52 | """Alias for set_enabled("no")""" 53 | self.set_enabled("no") 54 | 55 | def update(self): 56 | """Refresh tracker data 57 | 58 | @note: All fields are stored as attributes to self. 59 | 60 | @return: None 61 | """ 62 | multicall = rtorrent.rpc.Multicall(self) 63 | retriever_methods = [m for m in methods 64 | if m.is_retriever() and m.is_available(self._rt_obj)] 65 | for method in retriever_methods: 66 | multicall.add(method, self.rpc_id) 67 | 68 | multicall.call() 69 | 70 | def append_tracker(self, tracker): 71 | """ 72 | Append tracker to current tracker group 73 | 74 | @param tracker: The tracker url 75 | @type tracker: str 76 | 77 | @return: if successful, 0 78 | @rtype: int 79 | """ 80 | m = rtorrent.rpc.Multicall(self) 81 | self.multicall_add(m, "d.tracker.insert", self.index, tracker) 82 | 83 | return (m.call()[-1]) 84 | 85 | methods = [ 86 | # RETRIEVERS 87 | Method(Tracker, 'is_enabled', 't.is_enabled', boolean=True), 88 | Method(Tracker, 'get_id', 't.get_id'), 89 | Method(Tracker, 'get_scrape_incomplete', 't.get_scrape_incomplete'), 90 | Method(Tracker, 'is_open', 't.is_open', boolean=True), 91 | Method(Tracker, 'get_min_interval', 't.get_min_interval'), 92 | Method(Tracker, 'get_scrape_downloaded', 't.get_scrape_downloaded'), 93 | Method(Tracker, 'get_group', 't.get_group'), 94 | Method(Tracker, 'get_scrape_time_last', 't.get_scrape_time_last'), 95 | Method(Tracker, 'get_type', 't.get_type'), 96 | Method(Tracker, 'get_normal_interval', 't.get_normal_interval'), 97 | Method(Tracker, 'get_url', 't.get_url'), 98 | Method(Tracker, 'get_scrape_complete', 't.get_scrape_complete', 99 | min_version=(0, 8, 9), 100 | ), 101 | Method(Tracker, 'get_activity_time_last', 't.activity_time_last', 102 | min_version=(0, 8, 9), 103 | ), 104 | Method(Tracker, 'get_activity_time_next', 't.activity_time_next', 105 | min_version=(0, 8, 9), 106 | ), 107 | Method(Tracker, 'get_failed_time_last', 't.failed_time_last', 108 | min_version=(0, 8, 9), 109 | ), 110 | Method(Tracker, 'get_failed_time_next', 't.failed_time_next', 111 | min_version=(0, 8, 9), 112 | ), 113 | Method(Tracker, 'get_success_time_last', 't.success_time_last', 114 | min_version=(0, 8, 9), 115 | ), 116 | Method(Tracker, 'get_success_time_next', 't.success_time_next', 117 | min_version=(0, 8, 9), 118 | ), 119 | Method(Tracker, 'can_scrape', 't.can_scrape', 120 | min_version=(0, 9, 1), 121 | boolean=True 122 | ), 123 | Method(Tracker, 'get_failed_counter', 't.failed_counter', 124 | min_version=(0, 8, 9) 125 | ), 126 | Method(Tracker, 'get_scrape_counter', 't.scrape_counter', 127 | min_version=(0, 8, 9) 128 | ), 129 | Method(Tracker, 'get_success_counter', 't.success_counter', 130 | min_version=(0, 8, 9) 131 | ), 132 | Method(Tracker, 'is_usable', 't.is_usable', 133 | min_version=(0, 9, 1), 134 | boolean=True 135 | ), 136 | Method(Tracker, 'is_busy', 't.is_busy', 137 | min_version=(0, 9, 1), 138 | boolean=True 139 | ), 140 | Method(Tracker, 'is_extra_tracker', 't.is_extra_tracker', 141 | min_version=(0, 9, 1), 142 | boolean=True, 143 | ), 144 | Method(Tracker, "get_latest_sum_peers", "t.latest_sum_peers", 145 | min_version=(0, 9, 0) 146 | ), 147 | Method(Tracker, "get_latest_new_peers", "t.latest_new_peers", 148 | min_version=(0, 9, 0) 149 | ), 150 | 151 | # MODIFIERS 152 | Method(Tracker, 'set_enabled', 't.set_enabled'), 153 | ] 154 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os, sys 3 | 4 | version = __import__('rtorrent').__version__ 5 | 6 | def read(fname): 7 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 8 | 9 | required_pkgs = [] 10 | 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Natural Language :: English", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python", 18 | "Topic :: Communications :: File Sharing", 19 | "Topic :: Software Development :: Libraries :: Python Modules", 20 | ] 21 | 22 | setup( 23 | name="rtorrent-python", 24 | version=version, 25 | url='https://github.com/cjlucas/rtorrent-python', 26 | author='Chris Lucas', 27 | author_email='chris@chrisjlucas.com', 28 | maintainer='Chris Lucas', 29 | maintainer_email='chris@chrisjlucas.com', 30 | description='A simple rTorrent interface written in Python', 31 | long_description=read("README.md"), 32 | keywords="rtorrent p2p", 33 | license="MIT", 34 | packages=find_packages(), 35 | scripts=[], 36 | install_requires=required_pkgs, 37 | classifiers=classifiers, 38 | include_package_data=True, 39 | ) 40 | --------------------------------------------------------------------------------