blueluna-transmissionrpc-eb2a32720f8a/.hg_archival.txt0000644000000000000000000000020012227066501021132 0ustar 00000000000000repo: d3324aa18b92d84a07d07382ee6390854cb64cd4 node: eb2a32720f8a29869a2ac1ea381f4ce0acc181a2 branch: default tag: release-0.11 blueluna-transmissionrpc-eb2a32720f8a/.hgignore0000755000000000000000000000021212227066501017655 0ustar 00000000000000syntax: glob *.pyc *.pyo *.torrent *.wpr *.tmproj syntax: regexp ^build/* ^dist/* ^html/* .+\.egg-info/* ^doc/out/* ^\.idea/* ^\hgmessage blueluna-transmissionrpc-eb2a32720f8a/.hgtags0000755000000000000000000000126212227066501017336 0ustar 0000000000000043fc5953284c0103846c31685f2d319de260c565 release-0.1 f13f60a1b1b19ce3a1a03c022ebdb23fc2dde0bf release-0.2 327d3a89c94b3d0c8ed48d1f6da875c0325c5b15 release-0.2 113840953040db2732edecefb0ca89e7b57eb4d4 release-0.3 113840953040db2732edecefb0ca89e7b57eb4d4 release-0.3 e803459fa18de3f4c2ced990d246879595a2da74 release-0.3 78ed9f66c61bb858fd4448256cd358edb0fb8b64 release-0.4 3203f625e8aa9b09f2a7022e7b9f39e1ed5c697e release-0.5 89a5547e7acadb3b94b6cfc11ab15514c4ed0997 release-0.6 5ac8ac343b8fd098cdd15cfef0277c5afa2e6d37 release-0.7 5b5fad08460b90bd0e6a165a2ab372c77f0ed0a5 release-0.8 f51a451a42e693dbec90cb78f5b33b99ef9d8ee7 release-0.9 f74c8cbb721e79f1abd588e1c6935f669b6e341f release-0.10 blueluna-transmissionrpc-eb2a32720f8a/CHANGELOG0000755000000000000000000000224212227066501017271 0ustar 00000000000000Transmissionrpc 0.11 -------------------- * Added support for Transmission RPC version 15. * Minor fixes. Transmissionrpc 0.10 -------------------- * Support for Python 3 through "six". * Dropping support for Python 2.5. * Removed modification of default url opener. * Description of all Transmission RPC fields in Torrent and Session. Transmissionrpc 0.9 ------------------- * Fixed message handling in TransmissionError. Issue #35. Thanks Adam Rothman. * Made Client.start_all honour queue order. Thanks stephenharrell. * Changed interface for Client. * Added mutators to Torrent and Session. Transmissionrpc 0.8 ------------------- * Fixed argument "location" falsely named "ids" for Client.change. Issue #31. * Fixed Torrent.ratio to use response value uploadRatio. Issue #30. Transmissionrpc 0.7 ------------------- * Added support for Transmission RPC version 10 and 11. * Fixed an issue with Python 2.7 urllib2 handling. Issue #24. Transmissionrpc 0.6 ------------------- * Added support for Transmission RPC version 9. * Fixed handing of exceptions from urllib2.urlopen. Issues #21 and #22. * Fixed test suite to handle structure comparisons better. blueluna-transmissionrpc-eb2a32720f8a/LICENSE0000644000000000000000000000204612227066501017063 0ustar 00000000000000Copyright (c) 2008-2013 Erik Svensson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. blueluna-transmissionrpc-eb2a32720f8a/MANIFEST.in0000644000000000000000000000007412227066501017613 0ustar 00000000000000recursive-include test * recursive-exclude test *.pyc *.pyo blueluna-transmissionrpc-eb2a32720f8a/README0000755000000000000000000000164612227066501016746 0ustar 00000000000000TransmissionRPC Readme ###################### Introduction ============ transmissionrpc is a python module implementing the json-rpc client protocol for the bittorent client Transmission. transmissionrpc is licensed under the MIT license. Getting started =============== transmissionrpc 0.11 is compatible with Transmission 1.31 - 2.82. Requirements ------------ transmissionrpc requires: * Python >= 2.6 * Six >= 1.1.0, https://pypi.python.org/pypi/six/ Install ------- transmissionrpc is installed by running setup.py $ python setup.py install NOTE: You might need administrator privileges to install python modules. The setup program will take care of the simplejson requirement. Please refer to the Transmission documentation on how to install Transmission. Developer ========= transmissionrpc is hosted by bitbucket at http://www.bitbucket.org/blueluna/transmissionrpc/. Copyright (c) 2008-2013 Erik Svensson. blueluna-transmissionrpc-eb2a32720f8a/contrib/helical.py0000755000000000000000000003332212227066501021475 0ustar 00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 2008-07, Erik Svensson import sys, os, os.path, re, itertools import socket, urllib2, urlparse, base64, shlex import logging from optparse import OptionParser try: import readline except: pass import cmd import transmissionrpc from transmissionrpc.utils import * from transmissionrpc.constants import DEFAULT_PORT __author__ = u'Erik Svensson ' __version__ = u'0.2' __copyright__ = u'Copyright (c) 2008 Erik Svensson' __license__ = u'MIT' class Helical(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) self.intro = u'Helical %s' % (__version__) self.doc_leader = u''' Helical is a command line interface that communicates with Transmission bittorent client through json-rpc. To run helical in interactive mode start without a command. ''' def connect(self, address=None, port=None, username=None, password=None): self.tc = transmissionrpc.Client(address, port, username, password) urlo = urlparse.urlparse(self.tc.url) if urlo.port: self.prompt = u'Helical %s:%d> ' % (urlo.hostname, urlo.port) else: self.prompt = u'Helical %s> ' % (urlo.hostname) def arg_tokenize(self, argstr): return [unicode(token, 'utf-8') for token in shlex.split(argstr.encode('utf-8'))] or [''] def word_complete(self, text, words): suggestions = [] for word in words: if word.startswith(text): suggestions.append(word) return suggestions def _complete_torrent(self, name, offset): words = [torrent.name for id, torrent in self.tc.torrents.iteritems()] suggestions = [] cut_index = len(name) - offset for word in words: if word.startswith(name): suggestions.append(word[cut_index:]) return suggestions def _complete_torrent_command(self, text, line, begidx, endidx): args = self.arg_tokenize(line) item = args[-1] if len(args) > 1 else '' return self._complete_torrent(item, endidx - begidx) def help_quit(self): print(u'quit|exit\n') print(u'Exit to shell.\n') def do_quit(self, line): sys.exit('') #Alias do_exit = do_quit help_exit = help_quit do_EOF = do_quit def help_add(self): print(u'add [ paused=(yes|no) peer-limit=#]\n') print(u'Add a torrent to the transfer list.\n') def do_add(self, line): args = self.arg_tokenize(line) if len(args) == 0: print(u'Specify a torrent file or url') return torrent_url = args[0] args = args[1:] torrent_file = None if os.path.exists(torrent_url): torrent_file = open(torrent_url, 'r') else: try: torrent_file = urllib2.urlopen(torrent_url) except: torrent_file = None if not torrent_file: print(u'Couldn\'t find torrent "%s"' % torrent_url) return add_args = {} if len(args) > 0: for arg in args: try: (k,v) = arg.split('=') add_args[str(k)] = str(v) except: if 'download_dir' not in add_args: try: os.mkdir(arg) add_args['target'] = arg continue except: pass print(u'Unknown argument: "%s"' % arg) torrent_data = base64.b64encode(torrent_file.read()) try: self.tc.add(torrent_data, **add_args) except transmissionrpc.TransmissionError, e: print(u'Failed to add torrent "%s"' % e) def do_magnet(self, line): args = self.arg_tokenize(line) if len(args) == 0: print(u'Specify a torrent file or url') return torrent_url = args[0] try: self.tc.add_uri(torrent_url) except transmissionrpc.TransmissionError, e: print(u'Failed to add torrent "%s"' % e) def complete_remove(self, text, line, begidx, endidx): return self._complete_torrent_command(text, line, begidx, endidx) def help_remove(self): print(u'remove [,, ...]\n') print(u'Remove one or more torrents from the transfer list.\n') def do_remove(self, line): args = self.arg_tokenize(line) if len(args) == 0: raise ValueError(u'No torrent id') self.tc.remove(args) def complete_start(self, text, line, begidx, endidx): return self._complete_torrent_command(text, line, begidx, endidx) def help_start(self): print(u'start [,, ...]\n') print(u'Start one or more queued torrent transfers.\n') def do_start(self, line): args = self.arg_tokenize(line) if len(args) == 0: raise ValueError(u'No torrent id') self.tc.start(args) def complete_stop(self, text, line, begidx, endidx): return self._complete_torrent_command(text, line, begidx, endidx) def help_stop(self): print(u'stop [,, ...]\n') print(u'Stop one or more active torrent transfers.\n') def do_stop(self, line): args = self.arg_tokenize(line) if len(args) == 0: raise ValueError(u'No torrent id') self.tc.stop(args) def complete_verify(self, text, line, begidx, endidx): return self._complete_torrent_command(text, line, begidx, endidx) def help_verify(self): print(u'verify [,, ...]\n') print(u'Verify one or more torrent transfers.\n') def do_verify(self, line): args = self.arg_tokenize(line) if len(args) == 0: raise ValueError(u'No torrent id') self.tc.verify(args) def complete_info(self, text, line, begidx, endidx): return self._complete_torrent_command(text, line, begidx, endidx) def help_info(self): print(u'info [, ...]\n') print(u'Get details for a torrent. If no torrent id is provided, all torrents are displayed.\n') def do_info(self, line): args = self.arg_tokenize(line) if len(args) == 0: raise ValueError(u'No torrent id') result = self.tc.info(args) for id, torrent in result.iteritems(): print(self._torrent_detail(torrent)) def help_list(self): print(u'list\n') print(u'List all torrent transfers.\n') def do_list(self, line): args = self.arg_tokenize(line) result = self.tc.list() self._list_torrents(result) def help_files(self): print(u'files [, ...]\n') print(u'Get the file list for one or more torrents\n') def do_files(self, line): args = self.arg_tokenize(line) result = self.tc.get_files(args) for tid, files in result.iteritems(): print('torrent id: %d' % tid) for fid, file in files.iteritems(): print(' %d: %s' % (fid, file['name'])) def do_set(self, line): args = self.arg_tokenize(line) set_args = {} ids = [] add_ids = True if len(args) > 0: for arg in args: try: (k,v) = arg.split(u'=') set_args[str(k)] = str(v) add_ids = False except: if add_ids: ids.append(arg) else: print(u'Unknown argument: "%s"' % arg) if len(ids) > 0: result = self.tc.change(ids, **set_args) def complete_session(self, text, line, begidx, endidx): return self.word_complete(text, [u'get', u'set', u'stats']) def help_session(self): print(u'session (get|stats)\n') print(u'Get session parameters or session statistics.\n') def do_session(self, line): args = self.arg_tokenize(line) if len(args[0]) == 0 or args[0] == u'get': self.tc.get_session() print(self.tc.session) elif args[0] == u'stats': print(self.tc.session_stats()) def do_request(self, line): (method, sep, args) = line.partition(' ') try: args = eval(args) except SyntaxError: args = {} if not isinstance(args, dict): args = {} verbose = self.tc.verbose self.tc.verbose = True self.tc._request(method, args) self.tc.verbose = verbose def _list_torrents(self, torrents): if len(torrents) > 0: print(self._torrent_brief_header()) for tid, torrent in torrents.iteritems(): print(self._torrent_brief(torrent)) def _torrent_brief_header(self): return u' Id Done ETA Status Download Upload Ratio Name' def _torrent_brief(self, torrent): s = u'% 3d: ' % (torrent.id) try: s += u'%5.1f%%' % torrent.progress except: pass try: s += u' %- 13s' % torrent.format_eta() except: s += u' - ' try: s += u' %- 12s' % torrent.status except: s += u' -status ' pass try: s += u' %6.1f %- 5s' % format_speed(torrent.rateDownload) s += u' %6.1f %- 5s' % format_speed(torrent.rateUpload) except: s += u' -rate ' s += u' -rate ' pass try: s += u' %4.2f ' % torrent.ratio except: s += u' -ratio' pass s += u' ' + torrent.name return s def _torrent_detail(self, torrent): s = '' s += ' id: ' + str(torrent.fields['id']) s += '\n name: ' + torrent.fields['name'] s += '\n hash: ' + torrent.fields['hashString'] s += '\n' try: # size f = '' f += '\n progress: %.2f%%' % torrent.progress f += '\n total: %.2f %s' % format_size(torrent.totalSize) f += '\n reqested: %.2f %s' % format_size(torrent.sizeWhenDone) f += '\n remaining: %.2f %s' % format_size(torrent.leftUntilDone) f += '\n verified: %.2f %s' % format_size(torrent.haveValid) f += '\n not verified: %.2f %s' % format_size(torrent.haveUnchecked) s += f + '\n' except KeyError: pass try: # activity f = '' f += '\n status: ' + str(torrent.status) f += '\n download: %.2f %s' % format_speed(torrent.rateDownload) f += '\n upload: %.2f %s' % format_speed(torrent.rateUpload) f += '\n available: %.2f %s' % format_size(torrent.desiredAvailable) f += '\ndownload peers: ' + str(torrent.peersSendingToUs) f += '\n upload peers: ' + str(torrent.peersGettingFromUs) s += f + '\n' except KeyError: pass try: # history f = '' f += '\n ratio: %.2f' % torrent.ratio f += '\n downloaded: %.2f %s' % format_size(torrent.downloadedEver) f += '\n uploaded: %.2f %s' % format_size(torrent.uploadedEver) f += '\n active: ' + format_timestamp(torrent.activityDate) f += '\n added: ' + format_timestamp(torrent.addedDate) f += '\n started: ' + format_timestamp(torrent.startDate) f += '\n done: ' + format_timestamp(torrent.doneDate) s += f + '\n' except KeyError: pass return s def main(args=None): """Main entry point""" if sys.version_info[0] <= 2 and sys.version_info[1] <= 5: socket.setdefaulttimeout(30) if args is None: args = sys.argv[1:] parser = OptionParser(usage='Usage: %prog [options] [[address]:[port]] [command]') parser.add_option('-u', '--username', dest='username', help='Athentication username.') parser.add_option('-p', '--password', dest='password', help='Athentication password.') parser.add_option('-d', '--debug', dest='debug', help='Enable debug messages.', action="store_true") (values, args) = parser.parse_args(args) commands = [cmd[3:] for cmd in itertools.ifilter(lambda c: c[:3] == 'do_', dir(Helical))] address = 'localhost' port = DEFAULT_PORT command = None if values.debug: logger = logging.getLogger('transmissionrpc') logger.setLevel(logging.DEBUG) loghandler = logging.StreamHandler() loghandler.setLevel(logging.DEBUG) logging.getLogger('transmissionrpc').addHandler(loghandler) for arg in args: if arg in commands: command = arg break try: (address, port) = inet_address(arg, DEFAULT_PORT) except INetAddressError: address = arg port = None helical = Helical() try: helical.connect(address, port, values.username, values.password) except transmissionrpc.TransmissionError, error: print(error) parser.print_help() return if command: command_args = u' '.join([u'"%s"' % arg for arg in args[args.index(command)+1:]]) helical.onecmd(command + command_args) else: try: helical.cmdloop() except KeyboardInterrupt: helical.do_quit('') if __name__ == '__main__': sys.exit(main()) blueluna-transmissionrpc-eb2a32720f8a/doc/_static/style.css0000644000000000000000000000542112227066501022123 0ustar 00000000000000/* Copyright (c) 2008-2011 Erik Svensson Licensed under the MIT license. */ html { font-family: DejaVu Sans, Helvetica, sans-serif; font-size: 12pt; font-weight: normal; } body { background-color: #eee; color: #000; margin: 0; padding: 0; } h1, h2, h3, h4, h5, h6 { font-weight: 400; } a { text-decoration: none; } h1 > a, h2 > a, h3 > a, h4 > a, h5 > a, h6 > a { color: black; } ul { list-style: none; padding-left: 1em; } table, th, td { border: none; } table { border-spacing: 10px 2px; } th { border-bottom: solid 1px #aaa; text-align: left; } td { border-bottom: solid 1px #eee; text-align: left; } div.documentwrapper { float: right; width: 100%; } div.bodywrapper { margin: 0 230px 0 0; } div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } div.sphinxsidebar { float: right; width: 230px; margin-right: -100%; font-size: 90%; } div.related { padding: 5px; } div.related h3 { display: none; } div.related ul { margin: 0; padding: 0 0 0 0; } div.related li { display: inline; } div.related li.right { float: right; margin-right: 5px; } div.body { border: 1px solid #ddd; border-left: none; -webkit-border-top-right-radius: 1em; -moz-border-radius-topright: 1em; -webkit-border-bottom-right-radius: 1em; -moz-border-radius-bottomright: 1em; background-color: #fff; color: #000; padding: 20px; } a.headerlink { margin-left: 3px; padding: 0 4px 0 4px; text-decoration: none; visibility: hidden; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { color: #999; visibility: visible; text-decoration: none; } pre { font-size: 90%; padding: 10px; background-color: #f9f9f9; color: black; border: 1px solid #e2e2e2; -webkit-border-radius: 0.3em; -moz-border-radius: 0.3em; overflow: auto; } div.contents { background-color: #f3f3f3; padding: 10px; -webkit-border-radius: 0.3em; -moz-border-radius: 0.3em; } div.footer { font-size: 80%; margin: 10px; margin-top: 0px; } div.body ul { list-style: disc; padding-left: 3em; } div.body ul li { margin-top: 7px; } div.contents ul { list-style: none; padding-left: 2em; } div.admonition { margin: 0.5em; border: 1px solid #999; -webkit-border-radius: 0.3em; -moz-border-radius: 0.3em; } div.admonition p { margin: 0.5em; } p.admonition-title { font-weight: bold; } div.note { background: #ffffd8; border: 1px solid #dad503; } div.warning { background: #ffe38b; border: 1px solid #c2701a; } blueluna-transmissionrpc-eb2a32720f8a/doc/conf.py0000755000000000000000000001340612227066501020127 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. # # Vxl documentation build configuration file, created by # sphinx-quickstart on Tue Jul 15 14:28:42 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os, os.path sys.path.append(os.path.abspath(u'..')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. #sys.path.append(os.path.abspath('some/directory')) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'Transmission RPC' copyright = '2008-2013, Erik Svensson' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '0.11' # The full version, including alpha/beta/rc tags. release = '0.11' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. #exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'friendly' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'style.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'trdoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'Transmission.tex', 'Transmission Documentation', 'Erik Svensson', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True autoclass_content = 'both' blueluna-transmissionrpc-eb2a32720f8a/doc/index.rst0000755000000000000000000001123012227066501020462 0ustar 00000000000000.. Copyright (c) 2008-2013 Erik Svensson Licensed under the MIT license. Transmission RPC ################ Introduction ============ This is **transmissionrpc**. This module helps using Python to connect to a Transmission_ JSON-RPC service. transmissionrpc is compatible with Transmission 1.31 and later. transmissionrpc is licensed under the MIT license. .. _Transmission: http://www.transmissionbt.com/ Getting started =============== Transmission is available at `Python Package Index `_. To install the transmissionrpc python module use easy_install or pip. :: $ easy_install transmissionrpc .. NOTE:: You might need administrator privileges to install python modules. You may also download the tarball from `Python Package Index `_. Untar and install. :: $ tar -xzf transmissionrpc-0.11.tar.gz $ cd transmissionrpc-0.11 $ python setup.py install Dependecies ----------- transmissionrpc has the following dependencies. * Python >= 2.6 * Six >= 1.1.0, https://pypi.python.org/pypi/six/ Report a problem ---------------- Problems with transmissionrpc should be reported through the issue tracker at bitbucket_. Please look through the `existing issues`_ before opening a `new issue`_. .. _existing issues: http://bitbucket.org/blueluna/transmissionrpc/issues/ .. _new issue: http://bitbucket.org/blueluna/transmissionrpc/issues/new/ Installing from source ====================== The source code --------------- Transmission is hosted at bitbucket_ using mercurial_. To get a working copy, run :: $ hg clone http://www.bitbucket.org/blueluna/transmissionrpc/ The source code will be fetched and stored the directory transmissionrpc. Then install the module using :: $ python setup.py install Or if you wish to further develop transmissionrpc itself use :: $ python setup.py develop This will link this directory to the library as transmissionrpc. .. _bitbucket: http://www.bitbucket.org/blueluna/transmissionrpc/ .. _mercurial: http://www.selenic.com/mercurial Poking around ------------- Now that transmissionrpc has been installed, run python and start to poke around. Following will create a RPC client and list all torrents. :: >>> import transmissionrpc >>> tc = transmissionrpc.Client('localhost', port=9091) >>> tc.get_torrents() List will return a dictionary of Torrent object indexed by their id. You might not have any torrents yet. This can be remedied by adding an torrent. :: >>> tc.add_torrent('http://releases.ubuntu.com/8.10/ubuntu-8.10-desktop-i386.iso.torrent') {1: } >>> tc.get_torrent(1) {1: } As you saw, the add_url and info calls also returns a dictionary with ``{: , ...}``. More information about a torrent transfer can be found in the Torrent object. :: >>> torrent = tc.get_torrent(1)[1] >>> torrent.name 'ubuntu-8.10-desktop-i386.iso' >>> torrent.hashString '33820db6dd5e5928d23bc811bbac2f4ae94cb882' >>> torrent.status 'downloading' >>> torrent.eta datetime.timedelta(0, 750) Well, we weren't that interested in Ubuntu so lets stop the transfer and the remove it. :: >>> tc.stop_torrent(1) >>> tc.remove_torrent('33820db6dd5e5928d23bc811bbac2f4ae94cb882') See what we did there? most methods in transmissionrpc can take both torrent id and torrent hash when referring to a torrent. lists and sequences are also supported. >>> tc.get_torrents([2, 'caff87b88f50f46bc22da3a2712a6a4e9a98d91e']) {2: , 3: } >>> tc.get_torrents('1:3') {2: , 3: } Continue to explore and have fun! For more in depth information read the module reference. A note about debugging information ---------------------------------- If you ever need to see what's going on inside transmissionrpc, you can change the logging level of transmissionrpc. This is done with these easy steps :: >>> import logging >>> logging.getLogger('transmissionrpc').setLevel(logging.DEBUG) Note that this will produce a whole lot of output! Other levels are (listed by severity) * ``logging.ERROR`` * ``logging.WARNING`` * ``logging.INFO`` * ``logging.DEBUG`` The default logging level of transmissionrpc is ``logging.ERROR``. Module reference ================ .. toctree:: :maxdepth: 2 reference/transmissionrpc Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` blueluna-transmissionrpc-eb2a32720f8a/doc/reference/transmissionrpc.rst0000755000000000000000000003555612227066501024570 0ustar 00000000000000.. Copyright (c) 2008-2013 Erik Svensson Licensed under the MIT license. :mod:`transmissionrpc` --- Module reference ########################################### .. module:: transmissionrpc .. moduleauthor:: Erik Svensson This documentation will not describe all RPC fields in detail. Please refer to the `RPC specification`_ for more information on RPC data. .. _RPC specification: http://trac.transmissionbt.com/wiki/rpc .. contents:: :depth: 3 Exceptions ========== .. autoclass:: TransmissionError .. attribute:: original The original exception. .. autoclass:: HTTPHandlerError .. attribute:: url The requested url. .. attribute:: code HTTP error code. .. attribute:: message HTTP error message. .. attribute:: headers HTTP headers. .. attribute:: data HTTP data. Torrent object ============== Torrent is a class holding the information received from Transmission regarding a bittorrent transfer. Attributes ---------- All fetched torrent fields are accessible through this class using attributes. The attributes use underscore instead of hyphen in the names though. This class has a few convenience attributes using the torrent information. Example: :: >>> import transmissionrpc >>> t = transmissionrpc.Torrent(None, {'id': 1, 'comment': 'My torrent', 'addedDate': 1232281019}) >>> t.comment 'My torrent' >>> t.date_added datetime.datetime(2009, 1, 18, 13, 16, 59) >>> Transmission returns following fields. =========================== ====== ========================================================================================================== Argument RPC Description =========================== ====== ========================================================================================================== ``activityDate`` 1 - Last time of upload or download activity. ``addedDate`` 1 - The date when this torrent was first added. ``announceResponse`` 1 - 7 The announce message from the tracker. ``announceURL`` 1 - 7 Current announce URL. ``bandwidthPriority`` 5 - Bandwidth priority. Low (-1), Normal (0) or High (1). ``comment`` 1 - Torrent comment. ``corruptEver`` 1 - Number of bytes of corrupt data downloaded. ``creator`` 1 - Torrent creator. ``dateCreated`` 1 - Torrent creation date. ``desiredAvailable`` 1 - Number of bytes avalable and left to be downloaded. ``doneDate`` 1 - The date when the torrent finished downloading. ``downloadDir`` 4 - The directory path where the torrent is downloaded to. ``downloadLimit`` 1 - Download limit in Kbps. ``downloadLimitMode`` 1 - 5 Download limit mode. 0 means global, 1 means signle, 2 unlimited. ``downloadLimited`` 5 - Download limit is enabled ``downloadedEver`` 1 - Number of bytes of good data downloaded. ``downloaders`` 4 - 7 Number of downloaders. ``error`` 1 - Kind of error. 0 means OK, 1 means tracker warning, 2 means tracker error, 3 means local error. ``errorString`` 1 - Error message. ``eta`` 1 - Estimated number of seconds left when downloading or seeding. -1 means not available and -2 means unknown. ``fileStats`` 5 - Aray of file statistics containing bytesCompleted, wanted and priority. ``files`` 1 - Array of file object containing key, bytesCompleted, length and name. ``hashString`` 1 - Hashstring unique for the torrent even between sessions. ``haveUnchecked`` 1 - Number of bytes of partial pieces. ``haveValid`` 1 - Number of bytes of checksum verified data. ``honorsSessionLimits`` 5 - True if session upload limits are honored ``id`` 1 - Session unique torrent id. ``isFinished`` 9 - True if the torrent is finished. Downloaded and seeded. ``isPrivate`` 1 - True if the torrent is private. ``isStalled`` 14 - True if the torrent has stalled (been idle for a long time). ``lastAnnounceTime`` 1 - 7 The time of the last announcement. ``lastScrapeTime`` 1 - 7 The time af the last successful scrape. ``leechers`` 1 - 7 Number of leechers. ``leftUntilDone`` 1 - Number of bytes left until the download is done. ``magnetLink`` 7 - The magnet link for this torrent. ``manualAnnounceTime`` 1 - The time until you manually ask for more peers. ``maxConnectedPeers`` 1 - Maximum of connected peers. ``metadataPercentComplete`` 7 - Download progress of metadata. 0.0 to 1.0. ``name`` 1 - Torrent name. ``nextAnnounceTime`` 1 - 7 Next announce time. ``nextScrapeTime`` 1 - 7 Next scrape time. ``peer_limit`` 5 - Maximum number of peers. ``peers`` 2 - Array of peer objects. ``peersConnected`` 1 - Number of peers we are connected to. ``peersFrom`` 1 - Object containing download peers counts for different peer types. ``peersGettingFromUs`` 1 - Number of peers we are sending data to. ``peersKnown`` 1 - 13 Number of peers that the tracker knows. ``peersSendingToUs`` 1 - Number of peers sending to us ``percentDone`` 5 - Download progress of selected files. 0.0 to 1.0. ``pieceCount`` 1 - Number of pieces. ``pieceSize`` 1 - Number of bytes in a piece. ``pieces`` 5 - String with base64 encoded bitfield indicating finished pieces. ``priorities`` 1 - Array of file priorities. ``queuePosition`` 14 - The queue position. ``rateDownload`` 1 - Download rate in bps. ``rateUpload`` 1 - Upload rate in bps. ``recheckProgress`` 1 - Progress of recheck. 0.0 to 1.0. ``scrapeResponse`` 1 - 7 Scrape response message. ``scrapeURL`` 1 - 7 Current scrape URL ``seedIdleLimit`` 10 - Idle limit in minutes. ``seedIdleMode`` 10 - Use global (0), torrent (1), or unlimited (2) limit. ``seedRatioLimit`` 5 - Seed ratio limit. ``seedRatioMode`` 5 - Use global (0), torrent (1), or unlimited (2) limit. ``seeders`` 1 - 7 Number of seeders reported by the tracker. ``sizeWhenDone`` 1 - Size of the torrent download in bytes. ``startDate`` 1 - The date when the torrent was last started. ``status`` 1 - Current status, see source ``swarmSpeed`` 1 - 7 Estimated speed in Kbps in the swarm. ``timesCompleted`` 1 - 7 Number of successful downloads reported by the tracker. ``torrentFile`` 5 - Path to .torrent file. ``totalSize`` 1 - Total size of the torrent in bytes ``trackerStats`` 7 - Array of object containing tracker statistics. ``trackers`` 1 - Array of tracker objects. ``uploadLimit`` 1 - Upload limit in Kbps ``uploadLimitMode`` 1 - 5 Upload limit mode. 0 means global, 1 means signle, 2 unlimited. ``uploadLimited`` 5 - Upload limit enabled. ``uploadRatio`` 1 - Seed ratio. ``uploadedEver`` 1 - Number of bytes uploaded, ever. ``wanted`` 1 - Array of booleans indicated wanted files. ``webseeds`` 1 - Array of webseeds objects ``webseedsSendingToUs`` 1 - Number of webseeds seeding to us. =========================== ====== ========================================================================================================== Mutators -------- Some attributes can be changed, these are called mutators. These changes will be sent to the server when changed. To reload information from Transmission use ``update()``. Example: :: >>> import transmissionrpc >>> c = transmissionrpc.Client() >>> t = c.get_torrent(0) >>> t.peer_limit 10 >>> t.peer_limit = 20 >>> t.update() >>> t.peer_limit 20 Reference --------- .. autoclass:: Torrent :members: Session object ============== Session is a class holding the session data for a Transmission session. Attributes ---------- Access the session field can be done through attributes. The attributes available are the same as the session arguments in the Transmission RPC specification, but with underscore instead of hyphen. ``download-dir`` -> ``download_dir``. Transmission returns following fields. ================================ ===== ================= ====================================================================== Argument RPC Replaced by Description ================================ ===== ================= ====================================================================== ``alt_speed_down`` 5 - Alternate session download speed limit (in Kib/s). ``alt_speed_enabled`` 5 - True if alternate global download speed limiter is ebabled. ``alt_speed_time_begin`` 5 - Time when alternate speeds should be enabled. Minutes after midnight. ``alt_speed_time_day`` 5 - Days alternate speeds scheduling is enabled. ``alt_speed_time_enabled`` 5 - True if alternate speeds scheduling is enabled. ``alt_speed_time_end`` 5 - Time when alternate speeds should be disabled. Minutes after midnight. ``alt_speed_up`` 5 - Alternate session upload speed limit (in Kib/s) ``blocklist_enabled`` 5 - True when blocklist is enabled. ``blocklist_size`` 5 - Number of rules in the blocklist ``blocklist_url`` 11 - Location of the block list. Updated with blocklist-update. ``cache_size_mb`` 10 - The maximum size of the disk cache in MB ``config_dir`` 8 - location of transmissions configuration directory ``dht_enabled`` 6 - True if DHT enabled. ``download_dir`` 1 - The download directory. ``download_dir_free_space`` 12 - Free space in the download directory, in bytes ``download_queue_enabled`` 14 - True if the download queue is enabled. ``download_queue_size`` 14 - Number of slots in the download queue. ``encryption`` 1 - Encryption mode, one of ``required``, ``preferred`` or ``tolerated``. ``idle_seeding_limit`` 10 - Seed inactivity limit in minutes. ``idle_seeding_limit_enabled`` 10 - True if the seed activity limit is enabled. ``incomplete_dir`` 7 - The path to the directory for incomplete torrent transfer data. ``incomplete_dir_enabled`` 7 - True if the incomplete dir is enabled. ``lpd_enabled`` 9 - True if local peer discovery is enabled. ``peer_limit`` 1 - 5 peer-limit-global Maximum number of peers. ``peer_limit_global`` 5 - Maximum number of peers. ``peer_limit_per_torrent`` 5 - Maximum number of peers per transfer. ``peer_port`` 5 - Peer port. ``peer_port_random_on_start`` 5 - Enables randomized peer port on start of Transmission. ``pex_allowed`` 1 - 5 pex-enabled True if PEX is allowed. ``pex_enabled`` 5 - True if PEX is enabled. ``port`` 1 - 5 peer-port Peer port. ``port_forwarding_enabled`` 1 - True if port forwarding is enabled. ``queue_stalled_enabled`` 14 - True if stalled tracking of transfers is enabled. ``queue_stalled_minutes`` 14 - Number of minutes of idle that marks a transfer as stalled. ``rename_partial_files`` 8 - True if ".part" is appended to incomplete files ``rpc_version`` 4 - Transmission RPC API Version. ``rpc_version_minimum`` 4 - Minimum accepted RPC API Version. ``script_torrent_done_enabled`` 9 - True if the done script is enabled. ``script_torrent_done_filename`` 9 - Filename of the script to run when the transfer is done. ``seed_queue_enabled`` 14 - True if upload queue is enabled. ``seed_queue_size`` 14 - Number of slots in the upload queue. ``seedRatioLimit`` 5 - Seed ratio limit. 1.0 means 1:1 download and upload ratio. ``seedRatioLimited`` 5 - True if seed ration limit is enabled. ``speed_limit_down`` 1 - Download speed limit (in Kib/s). ``speed_limit_down_enabled`` 1 - True if the download speed is limited. ``speed_limit_up`` 1 - Upload speed limit (in Kib/s). ``speed_limit_up_enabled`` 1 - True if the upload speed is limited. ``start_added_torrents`` 9 - When true uploaded torrents will start right away. ``trash_original_torrent_files`` 9 - When true added .torrent files will be deleted. ``units`` 10 - An object containing units for size and speed. ``utp_enabled`` 13 - True if Micro Transport Protocol (UTP) is enabled. ``version`` 3 - Transmission version. ================================ ===== ================= ====================================================================== Mutators -------- Some attributes can be changed, these are called mutators. These changes will be sent to the server when changed. To reload information from Transmission use ``update()``. Reference --------- .. autoclass:: Session :members: Client object ============= This class implements the JSON-RPC protocol to communicate with Transmission. Torrent ids ----------- Many functions in Client takes torrent id. A torrent id can either be id or hashString. When supplying multiple id's it is possible to use a list mixed with both id and hashString. Timeouts -------- In Python 2.6 it is possible to supply a timeout to a HTTP request. This is accessible through transmissionrpc by either changing the timeout property of a Client object or supply the named argument ``timeout`` in most methods of Client. The default timeout is 30 seconds. Reference --------- .. autoclass:: Client :members: blueluna-transmissionrpc-eb2a32720f8a/lintrc0000644000000000000000000001557312227066501017305 0ustar 00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Profiled execution. profile=no # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore=CVS # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [MESSAGES CONTROL] # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appears only once). #disable= [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html output-format=colorized # Include message's id in output include-ids=no # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (R0004). comment=no [FORMAT] # Maximum number of characters on a single line. max-line-length=240 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. generated-members=REQUEST,acl_users,aq_parent [BASIC] # Required attributes for module, separated by a comma required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=__.*__ [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching names used for dummy variables (i.e. not used). dummy-variables-rgx=_|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [DESIGN] # Maximum number of arguments for function / method max-args=10 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branchs=20 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=1 # Maximum number of public methods for a class (see R0904). max-public-methods=30 [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp blueluna-transmissionrpc-eb2a32720f8a/setup.py0000755000000000000000000000152012227066501017567 0ustar 00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. from setuptools import setup required = ['six>=1.1.0'] setup( name='transmissionrpc', version='0.11', description='Python module that implements the Transmission bittorent client RPC protocol.', author='Erik Svensson', author_email='erik.public@gmail.com', url='http://bitbucket.org/blueluna/transmissionrpc', keywords='transmission bittorent torrent', packages=['transmissionrpc'], install_requires = required, test_suite = "test", zip_safe=True, classifiers = [ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Topic :: Communications :: File Sharing', 'Topic :: Internet' ], ) blueluna-transmissionrpc-eb2a32720f8a/test/__init__.py0000755000000000000000000000064712227066501021156 0ustar 00000000000000# -*- coding: utf-8 -*- # 2008-12, Erik Svensson # Licensed under the MIT license. import unittest from . import top, client, torrent, utils def test(): tests = unittest.TestSuite([top.suite(), utils.suite(), torrent.suite(), client.suite()]) result = unittest.TestResult() tests.run(result) for e in result.errors: for m in e: print(m) test() blueluna-transmissionrpc-eb2a32720f8a/test/client.py0000755000000000000000000004507512227066501020701 0ustar 00000000000000# -*- coding: utf-8 -*- # 2008-12, Erik Svensson # Licensed under the MIT license. import os import unittest import base64 from six import iteritems, string_types, PY3 if PY3: from urllib.parse import urlparse else: from urlparse import urlparse import json import transmissionrpc.constants from transmissionrpc import TransmissionError, Client, HTTPHandler def tree_differences(a, b): return node_differences(a, b, '.') def node_differences(a, b, root): errors = [] if isinstance(a, dict) and isinstance(b, dict): for k, v in iteritems(a): node = root + '.' + k if k not in b: errors.append('Field %s missing from b at %s' % (k, node)) else: errors.extend(node_differences(a[k], b[k], node)) for k, v in iteritems(b): node = root + '.' + k if k not in a: errors.append('Field %s missing from a at %s' % (k, node)) elif isinstance(a, list) and isinstance(b, list): for v in a: if v not in b: errors.append('Value %s missing from b at %s' % (v[0:32], root)) for v in b: if v not in a: errors.append('Value %s missing from a at %s' % (v[0:32], root)) else: if a != b: errors.append('Value %s != %s at %s' % (a[0:32], b[0:32], root)) return errors class TestHTTPHandler(HTTPHandler): def __init__(self, test_name=None): self.url = None self.user = None self.password = None self.tests = None self.test_index = 0 if test_name: test_file = test_name + '.json' here = os.path.dirname(os.path.abspath(__file__)) test_path = os.path.join(here, 'data', test_file) fd = open(test_path, 'r') test_data = json.load(fd) fd.close() if 'test sequence' in test_data: self.tests = test_data['test sequence'] def set_authentication(self, url, user, password): urlo = urlparse(url) if urlo.scheme == '': raise ValueError('URL should have a scheme.') else: self.url = url if user and password: if isinstance(user, string_types): self.user = user else: raise TypeError('Invalid type for user.') if isinstance(password, string_types): self.password = password else: raise TypeError('Invalid type for password.') elif user or password: raise ValueError('User AND password or neither.') def request(self, url, query, headers, timeout): response = {} if self.url and self.url != url: raise ValueError('New URL?!') urlo = urlparse(url) if urlo.scheme == '': raise ValueError('URL should have a scheme.') else: self.url = url q = json.loads(query) if self.tests: test_data = self.tests[self.test_index] self.test_index += 1 errors = tree_differences(test_data['request'], q) if len(errors) > 0: errors = '\n\t'.join(errors) raise Exception('Invalid request\n%s\n%s\n. Errors: %s\n' % (json.dumps(q, indent=2), json.dumps(test_data['request'], indent=2), errors)) if 'response' in test_data: response = test_data['response'] else: response['tag'] = int(q['tag']) response['result'] = 'success' return json.dumps(response) def createClient(*args, **kwargs): test_name = None if 'test_name' in kwargs: test_name = kwargs['test_name'] del kwargs['test_name'] kwargs['http_handler'] = TestHTTPHandler(test_name) return Client(*args, **kwargs) class ClientTest(unittest.TestCase): def testConstruction(self): tc = createClient(test_name='construction') self.assertEqual(tc.url, 'http://localhost:%d/transmission/rpc' % (transmissionrpc.constants.DEFAULT_PORT)) tc = createClient('127.0.0.1', 7000, user='user', password='secret', test_name='construction') self.assertEqual(tc.url, 'http://127.0.0.1:7000/transmission/rpc') tc = createClient('127.0.0.1', 7000, user='user', password='secret', test_name='construction') self.assertEqual(tc.url, 'http://127.0.0.1:7000/transmission/rpc') tc = createClient('127.0.0.1', 7000, user='user', test_name='construction') self.assertEqual(tc.url, 'http://127.0.0.1:7000/transmission/rpc') tc = createClient('127.0.0.1', 7000, password='secret', test_name='construction') self.assertEqual(tc.url, 'http://127.0.0.1:7000/transmission/rpc') tc = createClient('127.0.0.1', 7000, password='secret', timeout=0.1, test_name='construction') self.assertEqual(tc.url, 'http://127.0.0.1:7000/transmission/rpc') self.assertEqual(tc.timeout, 0.1) tc = createClient('127.0.0.1', 7000, password='secret', timeout=10, test_name='construction') self.assertEqual(tc.timeout, 10.0) tc = createClient('127.0.0.1', 7000, password='secret', timeout=10, test_name='construction') self.assertEqual(tc.timeout, 10.0) def testTimeoutProperty(self): tc = createClient('127.0.0.1', 12345, timeout=10, test_name='construction') self.assertEqual(tc.timeout, 10.0) tc.timeout = 0.1 self.assertEqual(tc.timeout, 0.1) tc.timeout = 100 self.assertEqual(tc.timeout, 100.0) tc.timeout = 100 self.assertEqual(tc.timeout, 100.0) del tc.timeout self.assertEqual(tc.timeout, transmissionrpc.constants.DEFAULT_TIMEOUT) tc.timeout = '100.1' self.assertEqual(tc.timeout, 100.1) self.failUnlessRaises(ValueError, tc._set_timeout, '10 years') def testAddOld(self): tc = createClient(test_name='add') data = 'data' r = tc.add(data)[0] self.assertEqual(r.id, 0) self.assertEqual(r.hashString, 'A000') self.assertEqual(r.name, 'testtransfer0') r = tc.add(data, paused=True)[1] self.assertEqual(r.id, 1) self.assertEqual(r.hashString, 'A001') self.assertEqual(r.name, 'testtransfer1') r = tc.add(data, download_dir='/tmp')[2] self.assertEqual(r.id, 2) self.assertEqual(r.hashString, 'A002') self.assertEqual(r.name, 'testtransfer2') r = tc.add(data, peer_limit=10)[3] self.assertEqual(r.id, 3) self.assertEqual(r.hashString, 'A003') self.assertEqual(r.name, 'testtransfer3') r = tc.add(data, paused=True, download_dir='/tmp', peer_limit=10)[4] self.assertEqual(r.id, 4) self.assertEqual(r.hashString, 'A004') self.assertEqual(r.name, 'testtransfer4') self.failUnlessRaises(ValueError, tc.add, data, peer_limit='apa') def testAddUriOld(self): data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') tc = createClient(test_name='adduri') r = tc.add_uri('torrent.txt', paused=False, download_dir='/var/downloads', peer_limit=1)[0] self.assertEqual(r.id, 0) self.assertEqual(r.hashString, 'A000') self.assertEqual(r.name, 'testtransfer0') r = tc.add_uri('file://' + os.path.join(data_path, 'torrent.txt'), paused=True, download_dir='/tmp', peer_limit=200)[1] self.assertEqual(r.id, 1) self.assertEqual(r.hashString, 'A001') self.assertEqual(r.name, 'testtransfer1') def testAddTorrent(self): data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') tc = createClient(test_name='add_torrent_base64') torrent_path = os.path.join(data_path, 'ubuntu-12.04.2-alternate-amd64.iso.torrent') data = open(torrent_path, 'rb').read() data_b64 = base64.b64encode(data).decode('utf-8') r = tc.add_torrent(data_b64) self.assertEqual(r.id, 0) self.assertEqual(r.hashString, 'a21c45469c565f3fb9595e4e9707e6e9d45abca6') self.assertEqual(r.name, 'ubuntu-12.04.2-alternate-amd64.iso') tc = createClient(test_name='adduri') self.assertRaises(ValueError, tc.add_torrent, None) r = tc.add_torrent('torrent.txt', paused=False, download_dir='/var/downloads', peer_limit=1) self.assertEqual(r.id, 0) self.assertEqual(r.hashString, 'A000') self.assertEqual(r.name, 'testtransfer0') r = tc.add_torrent('file://' + os.path.join(data_path, 'torrent.txt'), paused=True, download_dir='/tmp', peer_limit=200) self.assertEqual(r.id, 1) self.assertEqual(r.hashString, 'A001') self.assertEqual(r.name, 'testtransfer1') def testRemoveOld(self): tc = createClient(test_name='remove') tc.remove(['b000', 2, 3]) tc.remove(1, delete_data=True) tc.remove('b002', delete_data=False) def testRemoveTorrent(self): tc = createClient(test_name='remove') tc.remove_torrent(['b000', 2, 3]) tc.remove_torrent(1, delete_data=True) tc.remove_torrent('b002', delete_data=False) def testStartOld(self): tc = createClient(test_name='start') tc.start(['abcdef', 20, 30]) tc.start(1) tc.start('a0123456789') def testStartTorrent(self): tc = createClient(test_name='start') tc.start_torrent(['abcdef', 20, 30]) tc.start_torrent(1) tc.start_torrent('a0123456789') def testStopOld(self): tc = createClient(test_name='stop') tc.stop(2) tc.stop('bad') tc.stop(['bad', 'ba5', '30', 20]) def testStopTorrent(self): tc = createClient(test_name='stop') tc.stop(2) tc.stop('bad') tc.stop(['bad', 'ba5', '30', 20]) def testVerifyOld(self): tc = createClient(test_name='verify') tc.verify(10000) tc.verify('d') tc.verify(['a', 'b', 'c']) def testVerifyTorrent(self): tc = createClient(test_name='verify') tc.verify_torrent(10000) tc.verify_torrent('d') tc.verify_torrent(['a', 'b', 'c']) def testInfo(self): tc = createClient(test_name='info') r = tc.info() self.assertTrue(2 in r) self.assertTrue(3 in r) t = r[2] self.assertEqual(t.id, 2) self.assertEqual(t.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(t.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') def testGetTorrent(self): tc = createClient(test_name='get_torrent') torrent = tc.get_torrent(2) self.assertEqual(torrent.id, 2) self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') tc = createClient(test_name='get_torrent_hash') torrent = tc.get_torrent('ab8ea951c022d4745a9b06ab8020b952a52b71ca') self.assertEqual(torrent.id, 2) self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') def testGetTorrents(self): tc = createClient(test_name='info') r = tc.get_torrents() for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") def testGetTorrentsRange(self): tc = createClient(test_name='get_torrents_2to3') r = tc.get_torrents([2,3]) for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") tc = createClient(test_name='get_torrents_2to3') r = tc.get_torrents("2:3") for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") tc = createClient(test_name='get_torrents_2to3') r = tc.get_torrents("2,3") for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") tc = createClient(test_name='get_torrents_2to3') r = tc.get_torrents("2 3") for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") def testGetTorrentsHashes(self): tc = createClient(test_name='get_torrents_hashes') r = tc.get_torrents(["ab8ea951c022d4745a9b06ab8020b952a52b71ca", "a33e98826003515e46ef5075fcbf4914b307abe2"]) for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") tc = createClient(test_name='get_torrents_hashes') r = tc.get_torrents("ab8ea951c022d4745a9b06ab8020b952a52b71ca,a33e98826003515e46ef5075fcbf4914b307abe2") for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") tc = createClient(test_name='get_torrents_hashes') r = tc.get_torrents("ab8ea951c022d4745a9b06ab8020b952a52b71ca a33e98826003515e46ef5075fcbf4914b307abe2") for torrent in r: if torrent.id == 2: self.assertEqual(torrent.name, 'ubuntu-10.04-server-amd64.iso') self.assertEqual(torrent.hashString, 'ab8ea951c022d4745a9b06ab8020b952a52b71ca') elif torrent.id == 3: self.assertEqual(torrent.name, 'ubuntu-10.04-alternate-i386.iso') self.assertEqual(torrent.hashString, 'a33e98826003515e46ef5075fcbf4914b307abe2') else: self.fail("Unknown torrent") def testParseId(self): from transmissionrpc.client import parse_torrent_id self.assertEqual(parse_torrent_id(None), None) self.assertEqual(parse_torrent_id(10), 10) self.assertEqual(parse_torrent_id(10.0), 10) self.assertEqual(parse_torrent_id(10.5), None) self.assertEqual(parse_torrent_id("10"), 10) self.assertEqual(parse_torrent_id("A"), "A") self.assertEqual(parse_torrent_id("a21c45469c565f3fb9595e4e9707e6e9d45abca6"), "a21c45469c565f3fb9595e4e9707e6e9d45abca6") self.assertEqual(parse_torrent_id("T"), None) self.assertEqual(parse_torrent_id([10]), None) self.assertEqual(parse_torrent_id((10, 11)), None) self.assertEqual(parse_torrent_id({10: 10}), None) def testParseIds(self): from transmissionrpc.client import parse_torrent_ids self.assertEqual(parse_torrent_ids(None), []) self.assertEqual(parse_torrent_ids(10), [10]) self.assertEqual(parse_torrent_ids(10.0), [10]) self.assertEqual(parse_torrent_ids("10"), [10]) self.assertEqual(parse_torrent_ids("A"), ["A"]) self.assertEqual(parse_torrent_ids("a21c45469c565f3fb9595e4e9707e6e9d45abca6"), ["a21c45469c565f3fb9595e4e9707e6e9d45abca6"]) self.assertEqual(parse_torrent_ids(",, "), []) self.assertEqual(parse_torrent_ids("1,2,3"), [1,2,3]) self.assertEqual(parse_torrent_ids("1:3"), [1,2,3]) self.assertRaises(ValueError, parse_torrent_ids, "A:3") self.assertRaises(ValueError, parse_torrent_ids, "T") self.assertEqual(parse_torrent_ids([10]), [10]) self.assertEqual(parse_torrent_ids((10, 11)), [10, 11]) self.assertRaises(ValueError, parse_torrent_ids, {10: 10}) def suite(): suite = unittest.TestLoader().loadTestsFromTestCase(ClientTest) return suite blueluna-transmissionrpc-eb2a32720f8a/test/data/add.json0000644000000000000000000001013512227066501021367 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-add", "arguments": {"metainfo": "data"}}, "response": { "tag": 1, "arguments": { "torrent-added": { "hashString": "A000", "id": 0, "name": "testtransfer0" } }, "result": "success" } }, { "request": {"tag": 2, "method": "torrent-add", "arguments": {"metainfo": "data", "paused": 1}}, "response": { "tag": 2, "arguments": { "torrent-added": { "hashString": "A001", "id": 1, "name": "testtransfer1" } }, "result": "success" } }, { "request": {"tag": 3, "method": "torrent-add", "arguments": {"metainfo": "data", "download-dir": "/tmp"}}, "response": { "tag": 3, "arguments": { "torrent-added": { "hashString": "A002", "id": 2, "name": "testtransfer2" } }, "result": "success" } }, { "request": {"tag": 4, "method": "torrent-add", "arguments": {"metainfo": "data", "peer-limit": 10}}, "response": { "tag": 4, "arguments": { "torrent-added": { "hashString": "A003", "id": 3, "name": "testtransfer3" } }, "result": "success" } }, { "request": {"tag": 5, "method": "torrent-add", "arguments": {"metainfo": "data", "paused": 1, "download-dir": "/tmp", "peer-limit": 10}}, "response": { "tag": 5, "arguments": { "torrent-added": { "hashString": "A004", "id": 4, "name": "testtransfer4" } }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/add_torrent_base64.json0000644000000000000000000011663412227066501024323 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-add", "arguments": {"metainfo": "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"}}, "response": { "tag": 1, "arguments": { "torrent-added": { "hashString": "a21c45469c565f3fb9595e4e9707e6e9d45abca6", "id": 0, "name": "ubuntu-12.04.2-alternate-amd64.iso" } }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/adduri.json0000644000000000000000000000543212227066501022113 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-add", "arguments": {"filename": "torrent.txt", "paused": 0, "download-dir": "/var/downloads", "peer-limit": 1}}, "response": { "tag": 1, "arguments": { "torrent-added": { "hashString": "A000", "id": 0, "name": "testtransfer0" } }, "result": "success" } }, { "request": {"tag": 2, "method": "torrent-add", "arguments": {"metainfo": "dGVzdHRyYW5zZmVyMA==", "paused": 1, "download-dir": "/tmp", "peer-limit": 200}}, "response": { "tag": 2, "arguments": { "torrent-added": { "hashString": "A001", "id": 1, "name": "testtransfer1" } }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/addurl.json0000644000000000000000000000534312227066501022117 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-add", "arguments": {"metainfo": "dGVzdHRyYW5zZmVyMA=="}}, "response": { "tag": 1, "arguments": { "torrent-added": { "hashString": "A000", "id": 0, "name": "testtransfer0" } }, "result": "success" } }, { "request": {"tag": 2, "method": "torrent-add", "arguments": {"metainfo": "dGVzdHRyYW5zZmVyMA==", "paused": 1, "download-dir": "/tmp", "peer-limit": 200}}, "response": { "tag": 2, "arguments": { "torrent-added": { "hashString": "A001", "id": 1, "name": "testtransfer1" } }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/construction.json0000644000000000000000000000246012227066501023373 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/get_torrent.json0000755000000000000000000003110412227066501023175 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": { "tag": 1, "method": "torrent-get", "arguments": { "fields": [ "comment", "priorities", "uploadRatio", "downloadedEver", "pieces", "fileStats", "activityDate", "peer-limit", "id", "seedRatioMode", "recheckProgress", "doneDate", "peersFrom", "downloadDir", "rateUpload", "totalSize", "creator", "downloadLimit", "uploadLimited", "honorsSessionLimits", "torrentFile", "maxConnectedPeers", "magnetLink", "manualAnnounceTime", "isPrivate", "downloadLimited", "addedDate", "files", "haveValid", "uploadLimit", "percentDone", "errorString", "pieceSize", "rateDownload", "peersKnown", "dateCreated", "seedRatioLimit", "webseeds", "wanted", "peersSendingToUs", "desiredAvailable", "metadataPercentComplete", "webseedsSendingToUs", "trackerStats", "peers", "trackers", "corruptEver", "name", "bandwidthPriority", "sizeWhenDone", "hashString", "peersGettingFromUs", "uploadedEver", "pieceCount", "eta", "status", "peersConnected", "error", "leftUntilDone", "startDate", "haveUnchecked" ], "ids": [2] } }, "response": { "tag": 1, "arguments": { "torrents": [ { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272813123, "creator": "", "uploadRatio": 0.064600000000000005, "downloadedEver": 714920851, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n//////////////////////////////////A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 710412288, "wanted": true } ], "activityDate": 1272815697, "peer-limit": 60, "id": 2, "sizeWhenDone": 710412288, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272788233, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 0, "totalSize": 710412288, "downloadLimit": 100, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:ab8ea951c022d4745a9b06ab8020b952a52b71ca&dn=ubuntu-10.04-server-amd64.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272787269, "peersConnected": 3, "files": [ { "bytesCompleted": 710412288, "length": 710412288, "name": "ubuntu-10.04-server-amd64.iso" } ], "haveValid": 710412288, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 100, "peersKnown": 199, "dateCreated": 1272558141, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-server-amd64.iso.ab8ea951c022d474.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272814924, "id": 0, "announceState": 1, "leecherCount": 89, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1302, "lastAnnounceTime": 1272814925, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272816725, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272814925, "tier": 0, "lastScrapeResult": "", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272816725, "downloadCount": -1, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272815148, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272817162, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814999, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272817024, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "74.0.163.137", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.98819999999999997, "port": 49905, "clientName": "KTorrent 3.3.4" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "95.32.241.9", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.13489999999999999, "port": 10001, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "201.0.124.127", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.81630000000000003, "port": 61508, "clientName": "\u00b5Torrent 2.0.1" } ], "corruptEver": 0, "name": "ubuntu-10.04-server-amd64.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "ab8ea951c022d4745a9b06ab8020b952a52b71ca", "peersGettingFromUs": 3, "uploadedEver": 46225560, "peersFrom": { "fromIncoming": 0, "fromPex": 3, "fromLtep": 0, "fromCache": 0, "fromDht": 0, "fromTracker": 0 }, "pieceCount": 1356, "eta": -2, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 } ] }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/get_torrent_hash.json0000755000000000000000000003116112227066501024203 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92 (00)", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": { "tag": 1, "method": "torrent-get", "arguments": { "fields": [ "comment", "priorities", "uploadRatio", "downloadedEver", "pieces", "fileStats", "activityDate", "peer-limit", "id", "seedRatioMode", "recheckProgress", "doneDate", "peersFrom", "downloadDir", "rateUpload", "totalSize", "creator", "downloadLimit", "uploadLimited", "honorsSessionLimits", "torrentFile", "maxConnectedPeers", "magnetLink", "manualAnnounceTime", "isPrivate", "downloadLimited", "addedDate", "files", "haveValid", "uploadLimit", "percentDone", "errorString", "pieceSize", "rateDownload", "peersKnown", "dateCreated", "seedRatioLimit", "webseeds", "wanted", "peersSendingToUs", "desiredAvailable", "metadataPercentComplete", "webseedsSendingToUs", "trackerStats", "peers", "trackers", "corruptEver", "name", "bandwidthPriority", "sizeWhenDone", "hashString", "peersGettingFromUs", "uploadedEver", "pieceCount", "eta", "status", "peersConnected", "error", "leftUntilDone", "startDate", "haveUnchecked" ], "ids": ["ab8ea951c022d4745a9b06ab8020b952a52b71ca"] } }, "response": { "tag": 1, "arguments": { "torrents": [ { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272813123, "creator": "", "uploadRatio": 0.064600000000000005, "downloadedEver": 714920851, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n//////////////////////////////////A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 710412288, "wanted": true } ], "activityDate": 1272815697, "peer-limit": 60, "id": 2, "sizeWhenDone": 710412288, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272788233, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 0, "totalSize": 710412288, "downloadLimit": 100, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:ab8ea951c022d4745a9b06ab8020b952a52b71ca&dn=ubuntu-10.04-server-amd64.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272787269, "peersConnected": 3, "files": [ { "bytesCompleted": 710412288, "length": 710412288, "name": "ubuntu-10.04-server-amd64.iso" } ], "haveValid": 710412288, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 100, "peersKnown": 199, "dateCreated": 1272558141, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-server-amd64.iso.ab8ea951c022d474.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272814924, "id": 0, "announceState": 1, "leecherCount": 89, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1302, "lastAnnounceTime": 1272814925, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272816725, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272814925, "tier": 0, "lastScrapeResult": "", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272816725, "downloadCount": -1, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272815148, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272817162, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814999, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272817024, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "74.0.163.137", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.98819999999999997, "port": 49905, "clientName": "KTorrent 3.3.4" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "95.32.241.9", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.13489999999999999, "port": 10001, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "201.0.124.127", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.81630000000000003, "port": 61508, "clientName": "\u00b5Torrent 2.0.1" } ], "corruptEver": 0, "name": "ubuntu-10.04-server-amd64.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "ab8ea951c022d4745a9b06ab8020b952a52b71ca", "peersGettingFromUs": 3, "uploadedEver": 46225560, "peersFrom": { "fromIncoming": 0, "fromPex": 3, "fromLtep": 0, "fromCache": 0, "fromDht": 0, "fromTracker": 0 }, "pieceCount": 1356, "eta": -2, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 } ] }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/get_torrents_2to3.json0000755000000000000000000007331112227066501024235 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": { "tag": 1, "method": "torrent-get", "arguments": { "fields": [ "comment", "priorities", "uploadRatio", "downloadedEver", "pieces", "fileStats", "activityDate", "peer-limit", "id", "seedRatioMode", "recheckProgress", "doneDate", "peersFrom", "downloadDir", "rateUpload", "totalSize", "creator", "downloadLimit", "uploadLimited", "honorsSessionLimits", "torrentFile", "maxConnectedPeers", "magnetLink", "manualAnnounceTime", "isPrivate", "downloadLimited", "addedDate", "files", "haveValid", "uploadLimit", "percentDone", "errorString", "pieceSize", "rateDownload", "peersKnown", "dateCreated", "seedRatioLimit", "webseeds", "wanted", "peersSendingToUs", "desiredAvailable", "metadataPercentComplete", "webseedsSendingToUs", "trackerStats", "peers", "trackers", "corruptEver", "name", "bandwidthPriority", "sizeWhenDone", "hashString", "peersGettingFromUs", "uploadedEver", "pieceCount", "eta", "status", "peersConnected", "error", "leftUntilDone", "startDate", "haveUnchecked" ], "ids": [2,3] } }, "response": { "tag": 1, "arguments": { "torrents": [ { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272813123, "creator": "", "uploadRatio": 0.064600000000000005, "downloadedEver": 714920851, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n//////////////////////////////////A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 710412288, "wanted": true } ], "activityDate": 1272815697, "peer-limit": 60, "id": 2, "sizeWhenDone": 710412288, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272788233, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 0, "totalSize": 710412288, "downloadLimit": 100, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:ab8ea951c022d4745a9b06ab8020b952a52b71ca&dn=ubuntu-10.04-server-amd64.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272787269, "peersConnected": 3, "files": [ { "bytesCompleted": 710412288, "length": 710412288, "name": "ubuntu-10.04-server-amd64.iso" } ], "haveValid": 710412288, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 100, "peersKnown": 199, "dateCreated": 1272558141, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-server-amd64.iso.ab8ea951c022d474.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272814924, "id": 0, "announceState": 1, "leecherCount": 89, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1302, "lastAnnounceTime": 1272814925, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272816725, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272814925, "tier": 0, "lastScrapeResult": "", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272816725, "downloadCount": -1, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272815148, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272817162, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814999, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272817024, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "74.0.163.137", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.98819999999999997, "port": 49905, "clientName": "KTorrent 3.3.4" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "95.32.241.9", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.13489999999999999, "port": 10001, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "201.0.124.127", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.81630000000000003, "port": 61508, "clientName": "\u00b5Torrent 2.0.1" } ], "corruptEver": 0, "name": "ubuntu-10.04-server-amd64.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "ab8ea951c022d4745a9b06ab8020b952a52b71ca", "peersGettingFromUs": 3, "uploadedEver": 46225560, "peersFrom": { "fromIncoming": 0, "fromPex": 3, "fromLtep": 0, "fromCache": 0, "fromDht": 0, "fromTracker": 0 }, "pieceCount": 1356, "eta": -2, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 }, { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272808923, "creator": "", "uploadRatio": 0.1173, "downloadedEver": 725912475, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n/////////////////////////////////////+A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 722683904, "wanted": true } ], "activityDate": 1272815699, "peer-limit": 60, "id": 3, "sizeWhenDone": 722683904, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272810355, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 7092, "totalSize": 722683904, "downloadLimit": 500, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:a33e98826003515e46ef5075fcbf4914b307abe2&dn=ubuntu-10.04-alternate-i386.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272808923, "peersConnected": 13, "files": [ { "bytesCompleted": 722683904, "length": 722683904, "name": "ubuntu-10.04-alternate-i386.iso" } ], "haveValid": 722683904, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 500, "peersKnown": 275, "dateCreated": 1272557451, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-alternate-i386.iso.a33e98826003515e.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272813957, "id": 0, "announceState": 1, "leecherCount": 153, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1062, "lastAnnounceTime": 1272813958, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 1272812157, "nextAnnounceTime": 1272815758, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272813958, "tier": 0, "lastScrapeResult": "Success", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272815758, "downloadCount": 80, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272814373, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 1272814887, "nextAnnounceTime": 1272816408, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814888, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272816915, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "24.215.220.104", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.224, "port": 32204, "clientName": "BitTorrent 6.1.2" }, { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "60.48.255.128", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.044900000000000002, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "EX", "rateToPeer": 0, "isUploadingTo": false, "peerIsChoked": true, "peerIsInterested": false, "address": "76.8.107.202", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 1.0, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "UEX", "rateToPeer": 2048, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "87.207.115.180", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.63739999999999997, "port": 18723, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UKEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "89.150.134.195", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": false, "progress": 0.81499999999999995, "port": 10002, "clientName": "BitTornado 0.3.18" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "92.196.118.210", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.99629999999999996, "port": 51413, "clientName": "Transmission 1.75" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "94.179.8.178", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.88170000000000004, "port": 41929, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "EX", "rateToPeer": 0, "isUploadingTo": false, "peerIsChoked": true, "peerIsInterested": false, "address": "96.233.96.35", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 1.0, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "UEH", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "115.108.13.28", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.77300000000000002, "port": 53981, "clientName": "Deluge 1.1.9.0" }, { "flagStr": "UH", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "151.48.184.171", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.40310000000000001, "port": 51413, "clientName": "Transmission 1.75" }, { "flagStr": "UEX", "rateToPeer": 724, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "189.54.193.254", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.73960000000000004, "port": 6881, "clientName": "libtorrent (Rasterbar) 0.14.0" }, { "flagStr": "UEX", "rateToPeer": 4320, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "190.173.104.49", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.41909999999999997, "port": 50298, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "217.85.171.222", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.86219999999999997, "port": 49209, "clientName": "BitTornado 0.3.18" } ], "corruptEver": 0, "name": "ubuntu-10.04-alternate-i386.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "a33e98826003515e46ef5075fcbf4914b307abe2", "peersGettingFromUs": 11, "uploadedEver": 85200997, "peersFrom": { "fromIncoming": 0, "fromPex": 11, "fromLtep": 0, "fromCache": 0, "fromDht": 2, "fromTracker": 0 }, "pieceCount": 1379, "eta": 90342, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 } ] }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/get_torrents_hashes.json0000755000000000000000000007343412227066501024727 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": { "tag": 1, "method": "torrent-get", "arguments": { "fields": [ "comment", "priorities", "uploadRatio", "downloadedEver", "pieces", "fileStats", "activityDate", "peer-limit", "id", "seedRatioMode", "recheckProgress", "doneDate", "peersFrom", "downloadDir", "rateUpload", "totalSize", "creator", "downloadLimit", "uploadLimited", "honorsSessionLimits", "torrentFile", "maxConnectedPeers", "magnetLink", "manualAnnounceTime", "isPrivate", "downloadLimited", "addedDate", "files", "haveValid", "uploadLimit", "percentDone", "errorString", "pieceSize", "rateDownload", "peersKnown", "dateCreated", "seedRatioLimit", "webseeds", "wanted", "peersSendingToUs", "desiredAvailable", "metadataPercentComplete", "webseedsSendingToUs", "trackerStats", "peers", "trackers", "corruptEver", "name", "bandwidthPriority", "sizeWhenDone", "hashString", "peersGettingFromUs", "uploadedEver", "pieceCount", "eta", "status", "peersConnected", "error", "leftUntilDone", "startDate", "haveUnchecked" ], "ids": ["ab8ea951c022d4745a9b06ab8020b952a52b71ca", "a33e98826003515e46ef5075fcbf4914b307abe2"] } }, "response": { "tag": 1, "arguments": { "torrents": [ { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272813123, "creator": "", "uploadRatio": 0.064600000000000005, "downloadedEver": 714920851, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n//////////////////////////////////A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 710412288, "wanted": true } ], "activityDate": 1272815697, "peer-limit": 60, "id": 2, "sizeWhenDone": 710412288, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272788233, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 0, "totalSize": 710412288, "downloadLimit": 100, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:ab8ea951c022d4745a9b06ab8020b952a52b71ca&dn=ubuntu-10.04-server-amd64.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272787269, "peersConnected": 3, "files": [ { "bytesCompleted": 710412288, "length": 710412288, "name": "ubuntu-10.04-server-amd64.iso" } ], "haveValid": 710412288, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 100, "peersKnown": 199, "dateCreated": 1272558141, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-server-amd64.iso.ab8ea951c022d474.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272814924, "id": 0, "announceState": 1, "leecherCount": 89, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1302, "lastAnnounceTime": 1272814925, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272816725, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272814925, "tier": 0, "lastScrapeResult": "", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272816725, "downloadCount": -1, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272815148, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272817162, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814999, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272817024, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "74.0.163.137", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.98819999999999997, "port": 49905, "clientName": "KTorrent 3.3.4" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "95.32.241.9", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.13489999999999999, "port": 10001, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "201.0.124.127", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.81630000000000003, "port": 61508, "clientName": "\u00b5Torrent 2.0.1" } ], "corruptEver": 0, "name": "ubuntu-10.04-server-amd64.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "ab8ea951c022d4745a9b06ab8020b952a52b71ca", "peersGettingFromUs": 3, "uploadedEver": 46225560, "peersFrom": { "fromIncoming": 0, "fromPex": 3, "fromLtep": 0, "fromCache": 0, "fromDht": 0, "fromTracker": 0 }, "pieceCount": 1356, "eta": -2, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 }, { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272808923, "creator": "", "uploadRatio": 0.1173, "downloadedEver": 725912475, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n/////////////////////////////////////+A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 722683904, "wanted": true } ], "activityDate": 1272815699, "peer-limit": 60, "id": 3, "sizeWhenDone": 722683904, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272810355, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 7092, "totalSize": 722683904, "downloadLimit": 500, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:a33e98826003515e46ef5075fcbf4914b307abe2&dn=ubuntu-10.04-alternate-i386.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272808923, "peersConnected": 13, "files": [ { "bytesCompleted": 722683904, "length": 722683904, "name": "ubuntu-10.04-alternate-i386.iso" } ], "haveValid": 722683904, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 500, "peersKnown": 275, "dateCreated": 1272557451, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-alternate-i386.iso.a33e98826003515e.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272813957, "id": 0, "announceState": 1, "leecherCount": 153, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1062, "lastAnnounceTime": 1272813958, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 1272812157, "nextAnnounceTime": 1272815758, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272813958, "tier": 0, "lastScrapeResult": "Success", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272815758, "downloadCount": 80, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272814373, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 1272814887, "nextAnnounceTime": 1272816408, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814888, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272816915, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "24.215.220.104", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.224, "port": 32204, "clientName": "BitTorrent 6.1.2" }, { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "60.48.255.128", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.044900000000000002, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "EX", "rateToPeer": 0, "isUploadingTo": false, "peerIsChoked": true, "peerIsInterested": false, "address": "76.8.107.202", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 1.0, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "UEX", "rateToPeer": 2048, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "87.207.115.180", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.63739999999999997, "port": 18723, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UKEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "89.150.134.195", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": false, "progress": 0.81499999999999995, "port": 10002, "clientName": "BitTornado 0.3.18" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "92.196.118.210", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.99629999999999996, "port": 51413, "clientName": "Transmission 1.75" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "94.179.8.178", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.88170000000000004, "port": 41929, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "EX", "rateToPeer": 0, "isUploadingTo": false, "peerIsChoked": true, "peerIsInterested": false, "address": "96.233.96.35", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 1.0, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "UEH", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "115.108.13.28", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.77300000000000002, "port": 53981, "clientName": "Deluge 1.1.9.0" }, { "flagStr": "UH", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "151.48.184.171", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.40310000000000001, "port": 51413, "clientName": "Transmission 1.75" }, { "flagStr": "UEX", "rateToPeer": 724, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "189.54.193.254", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.73960000000000004, "port": 6881, "clientName": "libtorrent (Rasterbar) 0.14.0" }, { "flagStr": "UEX", "rateToPeer": 4320, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "190.173.104.49", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.41909999999999997, "port": 50298, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "217.85.171.222", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.86219999999999997, "port": 49209, "clientName": "BitTornado 0.3.18" } ], "corruptEver": 0, "name": "ubuntu-10.04-alternate-i386.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "a33e98826003515e46ef5075fcbf4914b307abe2", "peersGettingFromUs": 11, "uploadedEver": 85200997, "peersFrom": { "fromIncoming": 0, "fromPex": 11, "fromLtep": 0, "fromCache": 0, "fromDht": 2, "fromTracker": 0 }, "pieceCount": 1379, "eta": 90342, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 } ] }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/info.json0000644000000000000000000007324712227066501021607 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": { "tag": 1, "method": "torrent-get", "arguments": { "fields": [ "comment", "priorities", "uploadRatio", "downloadedEver", "pieces", "fileStats", "activityDate", "peer-limit", "id", "seedRatioMode", "recheckProgress", "doneDate", "peersFrom", "downloadDir", "rateUpload", "totalSize", "creator", "downloadLimit", "uploadLimited", "honorsSessionLimits", "torrentFile", "maxConnectedPeers", "magnetLink", "manualAnnounceTime", "isPrivate", "downloadLimited", "addedDate", "files", "haveValid", "uploadLimit", "percentDone", "errorString", "pieceSize", "rateDownload", "peersKnown", "dateCreated", "seedRatioLimit", "webseeds", "wanted", "peersSendingToUs", "desiredAvailable", "metadataPercentComplete", "webseedsSendingToUs", "trackerStats", "peers", "trackers", "corruptEver", "name", "bandwidthPriority", "sizeWhenDone", "hashString", "peersGettingFromUs", "uploadedEver", "pieceCount", "eta", "status", "peersConnected", "error", "leftUntilDone", "startDate", "haveUnchecked" ] } }, "response": { "tag": 1, "arguments": { "torrents": [ { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272813123, "creator": "", "uploadRatio": 0.064600000000000005, "downloadedEver": 714920851, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n//////////////////////////////////A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 710412288, "wanted": true } ], "activityDate": 1272815697, "peer-limit": 60, "id": 2, "sizeWhenDone": 710412288, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272788233, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 0, "totalSize": 710412288, "downloadLimit": 100, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:ab8ea951c022d4745a9b06ab8020b952a52b71ca&dn=ubuntu-10.04-server-amd64.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272787269, "peersConnected": 3, "files": [ { "bytesCompleted": 710412288, "length": 710412288, "name": "ubuntu-10.04-server-amd64.iso" } ], "haveValid": 710412288, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 100, "peersKnown": 199, "dateCreated": 1272558141, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-server-amd64.iso.ab8ea951c022d474.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272814924, "id": 0, "announceState": 1, "leecherCount": 89, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1302, "lastAnnounceTime": 1272814925, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272816725, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272814925, "tier": 0, "lastScrapeResult": "", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272816725, "downloadCount": -1, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272815148, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 0, "nextAnnounceTime": 1272817162, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814999, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272817024, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "74.0.163.137", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.98819999999999997, "port": 49905, "clientName": "KTorrent 3.3.4" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "95.32.241.9", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.13489999999999999, "port": 10001, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "201.0.124.127", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.81630000000000003, "port": 61508, "clientName": "\u00b5Torrent 2.0.1" } ], "corruptEver": 0, "name": "ubuntu-10.04-server-amd64.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "ab8ea951c022d4745a9b06ab8020b952a52b71ca", "peersGettingFromUs": 3, "uploadedEver": 46225560, "peersFrom": { "fromIncoming": 0, "fromPex": 3, "fromLtep": 0, "fromCache": 0, "fromDht": 0, "fromTracker": 0 }, "pieceCount": 1356, "eta": -2, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 }, { "comment": "Ubuntu CD releases.ubuntu.com", "startDate": 1272808923, "creator": "", "uploadRatio": 0.1173, "downloadedEver": 725912475, "pieces": "////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n/////////////////////////////////////+A=\n", "priorities": [ 0 ], "fileStats": [ { "priority": 0, "bytesCompleted": 722683904, "wanted": true } ], "activityDate": 1272815699, "peer-limit": 60, "id": 3, "sizeWhenDone": 722683904, "seedRatioMode": 0, "recheckProgress": 0, "doneDate": 1272810355, "uploadLimited": false, "downloadDir": "/home/erik/downloads/torrents", "rateUpload": 7092, "totalSize": 722683904, "downloadLimit": 500, "honorsSessionLimits": true, "maxConnectedPeers": 60, "magnetLink": "magnet:?xt=urn:btih:a33e98826003515e46ef5075fcbf4914b307abe2&dn=ubuntu-10.04-alternate-i386.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce", "manualAnnounceTime": -1, "isPrivate": false, "downloadLimited": false, "webseedsSendingToUs": 0, "addedDate": 1272808923, "peersConnected": 13, "files": [ { "bytesCompleted": 722683904, "length": 722683904, "name": "ubuntu-10.04-alternate-i386.iso" } ], "haveValid": 722683904, "percentDone": 1.0, "errorString": "", "pieceSize": 524288, "rateDownload": 0, "uploadLimit": 500, "peersKnown": 275, "dateCreated": 1272557451, "seedRatioLimit": 1.0, "webseeds": [], "wanted": [ 1 ], "peersSendingToUs": 0, "desiredAvailable": 0, "metadataPercentComplete": 1.0, "torrentFile": "/home/erik/.config/transmission-daemon/torrents/ubuntu-10.04-alternate-i386.iso.a33e98826003515e.torrent", "trackerStats": [ { "scrapeState": 1, "lastAnnounceResult": "Success", "lastAnnounceStartTime": 1272813957, "id": 0, "announceState": 1, "leecherCount": 153, "hasScraped": true, "announce": "http://torrent.ubuntu.com:6969/announce", "seederCount": 1062, "lastAnnounceTime": 1272813958, "lastAnnouncePeerCount": 50, "lastScrapeStartTime": 1272812157, "nextAnnounceTime": 1272815758, "lastAnnounceTimedOut": false, "host": "torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": true, "lastScrapeTime": 1272813958, "tier": 0, "lastScrapeResult": "Success", "lastAnnounceSucceeded": true, "lastScrapeTimedOut": 0, "nextScrapeTime": 1272815758, "downloadCount": 80, "isBackup": false }, { "scrapeState": 1, "lastAnnounceResult": "tracker did not respond", "lastAnnounceStartTime": 0, "id": 1, "announceState": 1, "leecherCount": -1, "hasScraped": true, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "seederCount": -1, "lastAnnounceTime": 1272814373, "lastAnnouncePeerCount": 0, "lastScrapeStartTime": 1272814887, "nextAnnounceTime": 1272816408, "lastAnnounceTimedOut": true, "host": "ipv6.torrent.ubuntu.com:6969", "hasAnnounced": true, "lastScrapeSucceeded": false, "lastScrapeTime": 1272814888, "tier": 1, "lastScrapeResult": "tracker did not respond", "lastAnnounceSucceeded": false, "lastScrapeTimedOut": 1, "nextScrapeTime": 1272816915, "downloadCount": -1, "isBackup": false } ], "peers": [ { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "24.215.220.104", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.224, "port": 32204, "clientName": "BitTorrent 6.1.2" }, { "flagStr": "UX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "60.48.255.128", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.044900000000000002, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "EX", "rateToPeer": 0, "isUploadingTo": false, "peerIsChoked": true, "peerIsInterested": false, "address": "76.8.107.202", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 1.0, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "UEX", "rateToPeer": 2048, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "87.207.115.180", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.63739999999999997, "port": 18723, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UKEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "89.150.134.195", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": false, "progress": 0.81499999999999995, "port": 10002, "clientName": "BitTornado 0.3.18" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "92.196.118.210", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.99629999999999996, "port": 51413, "clientName": "Transmission 1.75" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "94.179.8.178", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.88170000000000004, "port": 41929, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "EX", "rateToPeer": 0, "isUploadingTo": false, "peerIsChoked": true, "peerIsInterested": false, "address": "96.233.96.35", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 1.0, "port": 51413, "clientName": "Transmission 1.92" }, { "flagStr": "UEH", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "115.108.13.28", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.77300000000000002, "port": 53981, "clientName": "Deluge 1.1.9.0" }, { "flagStr": "UH", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "151.48.184.171", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": false, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.40310000000000001, "port": 51413, "clientName": "Transmission 1.75" }, { "flagStr": "UEX", "rateToPeer": 724, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "189.54.193.254", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.73960000000000004, "port": 6881, "clientName": "libtorrent (Rasterbar) 0.14.0" }, { "flagStr": "UEX", "rateToPeer": 4320, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "190.173.104.49", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.41909999999999997, "port": 50298, "clientName": "\u00b5Torrent 2.0.1" }, { "flagStr": "UEX", "rateToPeer": 0, "isUploadingTo": true, "peerIsChoked": false, "peerIsInterested": true, "address": "217.85.171.222", "isIncoming": false, "isDownloadingFrom": false, "isEncrypted": true, "rateToClient": 0, "clientIsInterested": false, "clientIsChoked": true, "progress": 0.86219999999999997, "port": 49209, "clientName": "BitTornado 0.3.18" } ], "corruptEver": 0, "name": "ubuntu-10.04-alternate-i386.iso", "bandwidthPriority": 0, "trackers": [ { "tier": 0, "announce": "http://torrent.ubuntu.com:6969/announce", "id": 0, "scrape": "http://torrent.ubuntu.com:6969/scrape" }, { "tier": 1, "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", "id": 1, "scrape": "http://ipv6.torrent.ubuntu.com:6969/scrape" } ], "hashString": "a33e98826003515e46ef5075fcbf4914b307abe2", "peersGettingFromUs": 11, "uploadedEver": 85200997, "peersFrom": { "fromIncoming": 0, "fromPex": 11, "fromLtep": 0, "fromCache": 0, "fromDht": 2, "fromTracker": 0 }, "pieceCount": 1379, "eta": 90342, "status": 8, "error": 0, "leftUntilDone": 0, "haveUnchecked": 0 } ] }, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/remove.json0000644000000000000000000000506212227066501022137 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-remove", "arguments": {"ids": ["b000", 2, 3], "delete-local-data": 0}}, "response": { "tag": 1, "arguments": {}, "result": "success" } }, { "request": {"tag": 2, "method": "torrent-remove", "arguments": {"ids": [1], "delete-local-data": 1}}, "response": { "tag": 2, "arguments": {}, "result": "success" } }, { "request": {"tag": 3, "method": "torrent-remove", "arguments": {"ids": ["b002"], "delete-local-data": 0}}, "response": { "tag": 3, "arguments": {}, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/start.json0000644000000000000000000000476212227066501022005 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-start", "arguments": {"ids": ["abcdef", 20, 30]}}, "response": { "tag": 1, "arguments": {}, "result": "success" } }, { "request": {"tag": 2, "method": "torrent-start", "arguments": {"ids": [1]}}, "response": { "tag": 2, "arguments": {}, "result": "success" } }, { "request": {"tag": 3, "method": "torrent-start", "arguments": {"ids": ["a0123456789"]}}, "response": { "tag": 3, "arguments": {}, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/stop.json0000644000000000000000000000475312227066501021635 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-stop", "arguments": {"ids": [2]}}, "response": { "tag": 1, "arguments": {}, "result": "success" } }, { "request": {"tag": 2, "method": "torrent-stop", "arguments": {"ids": ["bad"]}}, "response": { "tag": 2, "arguments": {}, "result": "success" } }, { "request": {"tag": 3, "method": "torrent-stop", "arguments": {"ids": ["bad", "ba5", 30, 20]}}, "response": { "tag": 3, "arguments": {}, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/data/torrent.txt0000644000000000000000000000001512227066501022176 0ustar 00000000000000testtransfer0blueluna-transmissionrpc-eb2a32720f8a/test/data/ubuntu-12.04.2-alternate-amd64.iso.torrent0000644000000000000000000006757512227066501027212 0ustar 00000000000000d8:announce39:http://torrent.ubuntu.com:6969/announce13:announce-listll39:http://torrent.ubuntu.com:6969/announceel44:http://ipv6.torrent.ubuntu.com:6969/announceee7:comment29:Ubuntu CD releases.ubuntu.com13:creation datei1360861763e4:infod6:lengthi739246080e4:name34:ubuntu-12.04.2-alternate-amd64.iso12:piece lengthi524288e6:pieces28200:o["@°|SÙ(5Æ»® \ÃÑÆ3°¬C¡¿tAûœ&}ë¾]_é[˜ÑR×tn=cÚQbpM§#1–g³ôX™Qì!棅gEÓ?äÉEwºôl5JOK]Èüc%Òt IÓ"„«`ü/mÿz 8ÚûƒT73³YšÀÓnÆrµþè+Uà‹ø¢‰Ƭ%Ý—í…æÍèðÕ¼¼VîÔ2¥áñZ +À Çu} ("šªßfÄSÁ¼·’¼ì"3#2 tÞÆ–ïà:gý@L9²EÁäþpr±¨Jƒ'iI´ô³Æ`:1¬4b­óÔ “ôçûI{Kš·¾<ßêwÈîà²ÆØ€„Ò/(÷¤cHÀ„·¿7]+f³ÙÒ§$‹vVi.ÆSGÕÈï:®f[ÁÇqX05cžóÓ]Á˜ã/‘•âà?1ó­Ñµ¨“Qð´P)ÿTaJu›ß_UXèÜ,VÏ`'œm_]ºõ<³·‹Æ¨eP®ñ£ c/{'ÿ¶À\xþ/–øj_Tl÷'˜• :\(íä'wPºá÷ˆ¹«âɳ?š&–™gØø{¢+¨3®¾{‰òÎÄf¯x$V Òbÿiý”|Eé6^ÝÙG1³OÔ)+ºäÓ#HÚà½{Æc¾\D_M‘Zþ§æQ«’åž­ Sx_ø¥SªqC—É^´‹Ý âÞÊf&àwÆÁœJYü ¢Ê fŸúu„$¤ü®Å>ÅëS7žlÜvlÙPŸ4¦pL¯¨ÄЉžd<ÙÇKçL«ç)#†°™^·úCÄ t®«‡Ï”=„ëó©cJüñ¶6‹-1ðooî¡“Ò×lQa¹m9Ù:¾ sæàÂîh½xx»U¸ÄކÂø%l8SدU¯–¬ÒàÕ á‚ã­ T™ì6vo0­ôÄÓÓ+Þ·¾¹:“öÓ}S¿ÛŒ›ÐoIF@mWùâJ|,z3N ,ϰ~F£.ðX¬’NƒÂq“QgÕÄ6Y]ˆ—:–n9ÎNêÞr9P–a³™{úéö0¯ö¾÷v&»× å “'E¿¬Wz XÑFl!o!`…J­%(‰µöœ4#$éÄBh‹¿NUÌ»k’5A¡_²?µó7Ú“®:ùsï{;éPž´Zqlå·EóxöcÀê\@¹Ê/r»âÕ¦ð(&Hx"mÈ r¹9ÑN÷ZøÂ=)3͇¯fŒa#fhv“ƒÉÖ‚ýhhÿÞ°*æßj ;—ü¾ô?ö+õuGͨIÏKKb¢.É}—Z']Y¨ž«ËeSTôù?ä%~»#ÛlO—W°F#„ÆËQÅþ²‘ /ËAÂܧDKvP-o€ubl lÀ°`Ñé¢Ë‚I—FQ@·¿´>éÓò–ï-G¿îa4ƒV&6E‡°Ú©RÈ '´›y2à›øjÚPhÛ€c®+r°¦fÀ—kSÿU³Y0‹oÏ­å8ºˆ’žõXŽý­¶€êžh‚Í î&&¬N×Ñпc_?Ø!PLï=F©d¿žºôKü…V>-˜ÄŸ&{B ÑEû[?lúœ‚_ÌyjõTÈ}©M¸RÝ]Ô_“ªóýul%`´±¿1Î%—^/6\ÖæÛ¾aðÊâPWd6í¾£g|ytkT&•›âÜÁM1öq‘!•¶m<÷åÂø dþKq¼”µ táÐ@jsE _ô>fýE„Q*À» ÓõÁ2=@ŸŒ‘•¥hmc¯•p’~ÌÂΧx±#oMѬŒ{z;XÈ¿ý§¡–Ì¢„ëŒ-o5â¶c¼P¯û2'<ûnEÖUìaÑn¦ÍC¼MêÔ̓þ%V `¹¶^3ñë¯;¥<üÛ8G ò*g®ÉÁd ¨X­ÒZ¿,¼¢˜R¥n¬Ç›]ÿèþGâÑT1 f1˜ÀX_¢[T‡¯?~s!— ¼Ñ5’£jŠî_à^"rGÆs#Õ|3bïä·œmz904øšEûâ”Ûç'’›HÜ%`4¬C[ÐU Ϲ†e0z{‰A#e—·Uü™¢Ó‡ÿÕT[Ìû —Æx'n¥°ÚñøE€Còð™þQæ$#Smãf½ƒsš·¿})éÿ¿¨@À~j¸€ƒ­y­žYu2«ëT¤SÙ†”×#uoõ€kï½Áƒ+^ëóéP§HÊoŒÖÀ%MžEÆÀ}£ne±T{æÜgRãVdöº–˜¾¡.g9ªíDºí·?E’¸hGõ4§g™nÉaÞ]kCȪélù×ýt.hòïÍÏ/g?Ï’ýõ¢.&Ìé}:]w2¡“ç/ËÇ\Û”¡#§A¬}æ1v8¾K9frã¡/)*;–> ½²(†ÚâfÞÃõƒ¢ß B¿ º0ªægÿájŸ&(¢'š—u¹+y/U…Gž¢Ïú¼®%ôè–†qDH’âNS›|©FçškÏ…¼$GfC'S@F0È¡µâ¢Œ‰¼?’óüã~ =‰Ñ¼Ù\˜Óºw8ëõ?º/h;s‰¨XÕÂs¸Ÿqo¶l©Œ²Wä‰>¶„¥\?·cµ¦­¾}èôH‘´G"¥q/ã7¨Y\¥g22h§½ 4÷ÞšVyí F ÝE~ ôÜNØbòûÁ Dr’O5Ðõó1¡&¾×~œ¬ ûÿ©Ó¿e[©C1L;­¹?oM0õÅi´ùÆpeû*"Žt«¿¯¹S°q ñŸ<èçîç" 1*G<çy[7êç½éØ)W“ºÜ2ëeö»V¨ÜA$c>‰I8„¨#P†ÛK%ÿõ+œ n´ž~³ÜËyAs‹sìÌ]Ë÷G‰’ Zo;O _lóeظ³Œ™dœˆ¸áx!Ï’I›HOXN¹\M(dy €o k7ûÜgæ©ÁŠ-@¸œÙ(¨.¾u<%RÜ"2hÑæI=ìªϲm~,H¶"”®@zw`g‡üŸëCG^ºcO:êÅê½@Ѷ°QŽÓ~xV»1#`ñÃ7»ìÁ4a]w‡ÇÈIÉO¡~j:Ã>\ØåYª )L ð?D‡èøÑQi{6¬{è;Æ¿³t§Û¥â­+vû,î©:'v¸ òÃ赩æRqé…ˆëöCÓè+%é ªQöŒ ÷UwN ÃD8yá§8û›]‘™§´9#,9ÂÖƒþQ×ü<{"׿ÚOsNg‰Vƒ>4C’±Tô„X¶¯*ŸqAÛÔP!~êkì€Js®aT±HVʽºÊHï$I.°þp ú³tü”Á˜úbŸü#1Ec¢ŽãmuÝpõ›qE*ý’ÐÉ•Ÿ%˜†œýä8Ñ¢žY&qþØ›Üûàsö;è¸ÓØÛo—PRpiô­ÌKyÅ#RÖ4¸¢{á/¸Òv*÷Í5ë·Á±*g³á:Mr$NõT2"çl :³ïâc–VÃk”çlçxmF¼uF ¦uµ° ¨ÏåŸZ³æü^°57í[-ÔHÜ»´{"< Ú t—¦”!*ćW\"ä] šÏ7çúq–ÜÈ`è)Û§Æžl×B\ò”^å ÐýWÑT§Ú™Ã3>.§â¿6ëˆ& »OñÌòT¨bRäÈDYuN2ùX×I­6Æ#&É^Öˆ,Æ%Et0&ZÊj °"$€ð-  =[ô=E—ÏÇŸþlü~”ëvT’pa*«S¬#<ª?ŽØ`:°d)·¹µ‡òÆ8Á~º°8•ˆ+¼¶Í,*®ÄÀógé9½GïuÀ«/xmH]&mÝJšáÌ8Qõ»¯”Ñ÷¿5ý¡jÞà»z¯=Å÷ÏV~ž–µ€ææýP€1 a*±‘"ˆlñP”ë=‘< oÅ¡}ðÀHûûv£àÛ-—졚UÏôÆ„e|Œ0Éȳ†ã.+ÐÜ*ñcŠX¼!“`y¼‡šŒºbyWaŽ*´ècƒŒ=ç*#“~›ÝŽF¨…rÄ c.9 Î?PÔ4Ä* ï$BçãhˆÅ÷h_ÀdcàÁ°§CÌî³"òæ3ÑüˆÉQL )Ùø"#a3ÓD½ÜíUÒq ½n£f¥ÃSrwîIsž‚ýK´Ic±žˆý4ú㤛ôÈRÞ¹q*T "»Ü6“]N&JR‚Lá=I®¢:èéK4뀬Xx¶Á-œ_hÌHõ|ÀõÒ-%âE¢Õ··LÌu€ôŽ Ù):~9+UÇ1‡‹ít˜ËÇ-Ia0 ¯›² –‡YüœéïSS”,—¸;S¤$¼?ÿ7óg1±—ÜÚe\@ÜÞüÙ 2å¤(ÇG;È.Ö¼è7òê!p°² ÷ã$ÿÊø‚lêˆuZfK¢{Ùlýˆ·Š´ ‚~Fj¤–iE¸Ro‰Š2À)²ˆ…}@³„âp2Õˆ”ìÙ¢úëDž7ËñúýK>^ÙmÜ÷ׇïÐ%^¨F·Òù ¿ð§=A¬Lþ—ý_eödªÅKs]1ª q:ãªGý¯Âtz+uz’‚´—Ë;ŠB$‚÷è¹;)-ö ÐC°óÍ=…;äb4êé€òRúèl?ÝYf2u¨[Iªav- S»©ZÜÑv#.Aøl˜2%QkÀº\‚<¥„Œ%áNáÇý=?nÖQµêCôe¹ ÎŒ‘wVÃäÝ”Æ=´¿i'1êÞW¯,ž<T·FC…Çûîß:Œ—ëszëAí©É’BUƒžËu÷³5ߣðD¬|¬+ÝõKø§°X Œé ® ÙˆÇ#7.#ÇùxVŒ U¥£ÇÆy gêDÜþQCÖ?®2A¹ä`„wcðÍ’7—¤Çô…„AàrC>úõÞØgòçQ×5i3I\÷Õ`ÑüD¿´º-žúi 7âÖŸíbAPŠn@GÔÅ’¼¢±> Åó7S%'õìD¨Ó–óàÓd9±?l£ˆFÐÝòø¯5Â`'aU|%Zd!˜ Å®KµÝF¢“ V^\ô¬è~¬k)ëª7³ã€F½Û [btÊ]WL²›±µ3_‘Å©Sªò*Pzü¥t¬ì¤U À)«gƒïq®ò–:ò·zÅzz#±"3â‘£pÔåË6º•:iÛ±»>âÊ`êŸP)E,¶\¾P‹»Â 1¾fq!7;œJ HXšÏ–"^ †‰Nß5$´Û@\œp=ï²èRM>òO ‰!£%?þZ•4(²÷~ð3M)ãmÀA×!Ã&ZÌÒ-õŽ_ÓIArž @9Ñ•¥¨€+’Ÿ_Ù}`ÿÓã–[Ì rƒ¬?è‡*ÌèJ ær@ÿ—O>ø(¦Ê(ìR’;,êú°9¸ÂW¼±_Š„zVš#¢=>e1Ý…s<£†ÒÍ ™Ö¿­l²yõ‚ë©ZÉ8bdr|/B¤Tã»t{Ú5¤Æp&»HB6ûòþcƒQAÀfûÁ^ÞòAÊC£¸†D€•fÏ'à§à~éN: 1Kcí#.ùÃqJå4}qQhÊ5q‹ª.æ@‘ÖÒ‰5}-¯¨/-‰ÍqÊq£ ;ÉE™Y£ãÂ/ó˜ñQ®ÅÏÈVzf‡>Ǭ>#û€æjzÇ`ž}üXôe«ä00vÂ5Æö3‡/÷Ó›¦ñ$ÿqÔÏÀ~ÈÖòTdÔ¨–/F)Ÿ?$Û£/¿]¹®j£)E´²7ÃüŸ þ™%Ôà´÷q´BPg„ÞEL,r:c°Ñž2}[ž¯?M¿>Ã#p™ÂT]ÿ|ô]ø¼& Z ƒ$prž÷Ö鵋N íÙ5JŠmn,ntŠÖ(É¡öç4Ù‚ò°í|¡zÂ/üËÁAd=²Iòµ]PèdP†S×~¸\î­à‰'K›êrÑ ð0(€ bɶØñž¦<¯æQÜÁ3‚±4wØýOÜt'kÁÑÆïë äl¬ ,hÝüf§nl]Fa†/emÏ, cN®hB÷ üTð«ØmÅš˜Ðº8ÓÇ»·ÓpúT¥%묳?üdOTÚóŒ}—¾2˜nа@ci5ñ“Šª™§nÊ`8yG'ãŽW®ž4nÙ¡nž8 ð™,ÜÍi䇰e³²k`e„Ä-‰ò’ÇòNÅ+p†õŽM ÅNCn†°$þܶ›ú»Ô#…suýóÿ7Ÿ¦(_‘ñˆ° ?´K18OÙû‚¯º‚$§“¥›CPœ®R†‡¦o9^(ÂçóÕ\KÔgŽ®¹Ã6F˜S5_kΉ÷‡Ì&ÐÀgF³ çù-ÐPí?ZÁ(SY£ #ÍìÔjçÏ|LÙ9¯‹Ù~”¨˜§žy#q_ä`ƾÜìY—{7A¾HžE9:>,2XG'a6—ØÒ2€¾ì-‰‡¯4¼™Ãtá]0ZÏV­°jŽûh8-LvÛÆÔµØBn¶5ù z쯈,ÛG”geç!Ìyàõ~d\ všïûõfç¶…Äw'üygdz³;óÓ¤F”ýUJYšdóÉ«ˆÆZÝ®f{0s¾á€*;óÀ7œTãý+ºg|Yú£øZ¦"ÅS<™Åz¼F;\WÍ¥ê£Y.ÈçÆ`“(‚>BÖ;¶žý늄ϓ·X5Òa)Ѹj ¡)8Þ dBF/\õí&˘)2=¥ðg!ÕÙ>¸çÓç ì,§$⊹ÿ/iÓn¯ö'J«!šA( !žuºë± à%[¹íxóïلƧlJð¹UÛ<ì4À¨Ÿ~^Ú¤ê£g.øs8z–~N|™¢  è5"9³ï*rþË|aPyèîÊ×ÿÊ/~^ñÒe_qÚð¾i§¬‘T0ùâ¯M“oÒ\<ñâoîÒi›‚ÍËß-Ìù>pçó+·82_Æ2µjt²2¾#VŒ$ZÈu…™Êy*s÷Ñgi±kd‰é¤ØRR/Wµ~ób"g ã”#ºåÀææ¶~$,YlS9ä0¹ÙÐ6?5I¼³TSn¶‡íûP0Œœ¾‰‰ÉG‰”ûR}¸ÍE¶3,¦úéT^ýH‹äžùBwoÎøðÔœBAî…á9¡Û1G·OÀâž@_2ÚæóDÛѯŽeuÎsvˆ³¸1”#ŽrÜ\;,YÏ$…Ù .š¬³ô†uÚ/Y²w/‚G±KˆâaÞYÈUüýĈ’§ÑDôß—u¾‚úbåß1ÐRTv-C¤„¡ZqR¼¯Ò'ÈY:ø©9“7m'äÅÒ'‘G@1Ahe¼?¨ ñŠLžå¿×šúÁ`…ˆ(¨ä%(’KØÄ[̬Äq$Õ^TJá ¹j‡iÀ²ý¬ JIAþñEe¬;6¶z•Ô¦Áx8Œ2l²Ñ„Ëܶ,*Õíe#à–õ¸ëЇWŒ’2*¯Ïä6¯Âw¶Œ+9±õï«6ã(ˆÅ$¦€láFü¸Éâ”3æq4ò PŠÌ•«{9ÉÉfÚÙ«rt;ò ¯î>+_¬Ä'jõ»~ÿ(·sè¢8ˆ>¿ÉÃjS9V2²e½ÛàWØ•·Û2ÎĦìÇËÁšYäÿˆã’´ 91Ì3™=`|>³êÜjã6ÄÅÁEœälwÝ?¼„xd·¡ðÅñvNR‹W”ø­Ø‘!òµˆÎ¢eòXÄÊ4{¿É‘v!Ëó™;E7Ó0ÿÖ¿HuuF~̉‹`+A:ì'™î9i9™*œTì5óeÕò2­’¦"CD—'ÍeM³iÆœÊ/÷›‰¼ô¨hï ¦Û²Êð㛽GÉ?O‰èå(qÅcPØ1ò ¿ÃØoR±‹ FQ5"/G·w¬±iWz)ŠJFO7o¥iÞÀ€]¶ 8WÇŸcud «XI6ŸeãQÇCNÓi3døaÊE>Ÿ&+Íê°b}„òMýÆÄkg„ÌÊBûYËòײ@qêÁ wS×ÍTõ3rÐ~F6Vl^Sl®y­­V#–øÐÂÇØ ¸Û×@Åð§Í-Ýü¥/i÷dðf “Eëx^Þ·¦‰`¹J-*Ì}ᛸ`¿$Ú)Îs|¦`Š`èáØ“—¦±e0bVð}Ú§àÓ$ySð{ B;ä©}Òåê ô5pÜ´á²¼4dãú3ÀÕ'£äÀ÷´g¢H5mŒH58³îŽäðãòéÝØbDÏÀŒ§ãžÁÖÂÉ %Ä69¸:È7bô°åkýá —6`yhÿ(a_áÕîÂeˆû‰¿éÆÕð's~¨­’Vl tõ‡,7QMØRW]ä2—Fc)O_M“C MüP&&úo?0GöÜÏØ|ú¸¼,5É6’Íê¨&~‡ßŽYh[Jm½BÆ RJШ¬#4þ< åâ)fˆÿC~íBJ|VË1û1¨Ê§u –o«Qh°Üœ@vZ7N~¤aà2- »¤Ñó32%Õ)ýh©Ûˆç~ÛJt {Míš " ²þMü”1ïn”±_hRøàFúàîÀLUlRAÑH{†Ì”¨/[³72¯×­' ߘCMtùCa­C¶þ"|<hŪµØýi+Ûá?7g¡YS hÜÒçÔæúV¯Û·Òö8ÔbÛ,$UŽnâà¼é)Q.'Gç”Û3Û“²> EöžBf¿”i!”ÿ*4,Y[ ÿXõÆ|ðïm±ÿœÖf‡×ÎÓ"∺LÅÙÙOJ‘ì1—P†_C(x•-êVrüûRêSlìf»¯ Aro,ü&xEiñžf kØ–=ÙbtüÜ ‘ã»)AdMŽèòjÔ»ëܽ±ÿÓH–ªâþ&EÀgµDÓŸ÷n)‚c벤¯ÊT:ä*ÜŽ—í\ï>ï'ò}q•o¢W(tÚæŠªÆý2NFu‚VY³2îLdÎPI„êµM(Ã?KÊy^&y ;_|žä9^z;ÌðÅšH<%¶©fW?)›‡£ ÒJÑi±Î!DÃC@k¥S 9ºsÿœm$otò‰\7–yÌ9G6ŒÑoÌYÕ6£13çèà¿BŠ_›¦àö+ûyóÑXš™=ÊS=knå>ÒuÂpªÁyW?N)ÚÄ”y’sŸê#Ì\̤K-O®­e$0ö˜áò«e¯OãÂðº)xwòKz”š—U}ÀÀòŽZ\jLñùx$Rš¶7t¹À<ß«T Áì…'¿x==¨ÀDËD`ª»r½Ók%{XÂ÷0HE&!ÎñéF@Ûν‹#4óâXŽpnuQ_œÝ q¯ŒÚnzÌo­G ó‹0åf-Q«ãfvDêyß™™ƒ- æ!Þ||)<úòoÕ.Êä¿'1‡dL:‡œ3`¸³Se4,¾8/?µfÇPq O55ÉŠBbØa©U/©aÀ59é,}µ­Af[Id diϺ'¾¦K©Óý§â3›_[*]+Í£6­˜‹öœö&-¬‰wá …I^Ѝ¬%бä‘p½©Å\~õ஘hdyû7úÏ;-òÏW"£áÜ.þ£V§ \}—#@Ü€/„LñøÍþ Á00¤R. ¾QÁ>çr 5¿;cS¤aÒ˜† ú†y¢Î›b“3O!¿êP÷ ÄfÿjþfÞ+ýLâ=¶ª2_mа™‰Ž;q¼ÀžyÑ'ÐÊ@6€ÞŽ&ì-¡á{ßpíÏ· \}=‹SÛ9lcÌ~‡í€hLwñÔ_Ç(}…¢›¸®‡ÓؘHHâ)å¯L_â»ÝªLí«Àçª,jQÛ®ŒC,h‡Ŗܰ8ÄÿãÄ:«h±? 0,d¦qa{X„Û²¯C•Yì÷e-jg>ÎÙFÂí{’w×D;à©:ˆû²E,î$}Õõö¨½×Õ ®ôw6}÷ ó%3îÄïV¿Ä:õ[Œÿ¯D× AeZ Eóaùõö-´@©rËÔ»œÞàÚŽ‰öÉØ íd¯¤êÍ{L,Nš,¡«^=7úpì6FÝÆMâµãK—‘Q¯…:ë] n–ïƆ±k×NØ ’÷ÚßUV]œc;aµÝ¹r­jÅÀÛØ&®˜À¼Ñö¾‰—yñ®ó%B²—\W„[6Z1Èôž~rmo{^%ZÚ#róŒc}‹¡*dû·½§ ¶Ü@¾(¹Z( 2ÂF!¬No%<è C5/æÈÿ}œ0MWÂ×,DÈØƒ’ÜeØ¥4ÿð8è WMjêŠ*r½bÂæ@ã‚C8ý¿ÊfnÖjµúÆŠ˜0}Bœ<¯tÿSz¢ÛÇßoݨï“QÎö‰½R†}ÐÍ©¿‚SºN^Ÿ ¥\xZIŠÝ2·8— b°4/×< Û…ÕAÔl%üw#0ŠD÷.ïˇ&\·’Ü,ŸÆ#¡ŽoäT˜5yÑI)E´7šòÇÁá"ާcg: ‡vw )Eì÷TˆPÛÓÚItúpQÁö¤(gˆåˆJlÛTYtOˆNLvFðª—POƒ¾Â$5‹†¨¸ZKÆ3óRwî;Ò9åâgsS3OQÓX³-®PépáËNŒR~#¶åFj§ÔÌSV´úsp‚ïÃ0‰‹ýè”=R}Ä}¥¨r]Ýö ƒ’ž§žké§Ã—v9Ø5—\v'@ºÚ(.§;Á˜/(èN6œ‰ÏŸüÞòEVQÈ%6r§v"ìžL?™œ"öf§’6%[K¦f¬ulŠ…Ã±h‡ÓØDŠixÝÛL¤aÑ¡ æÉ¿[° ¼°7ŠëÚ‘¾½ ÖÄÏ}o¨ËF@åsß”Y¶!¼ú,?9m¨.h,¿ÓŠ}]Sl)f’G›/ß•A*¼]J$Ö§Å L.ÏÎáÜÂ]/a_c O|b{ý`»ŒÚ÷b<-&$„• Y$9†Ì-ÿ‘UcÀm‘å©4¢bF_´åh8䜿äK>êzÄÚáaL=bºéÏ+k'Ž&~ZFWp±þbäM}±k€’~m‡LdˆåÖCå(4L_Ö˜*ô²ªë#8ñ¾-zÛ¯¤R5(n2Õª‚™YØ.!{dø»(`¿ÝÈùg Âu߲蔀”3'f©)<ƒ@¶†ACPbŽ«éU–ñ).©—£:§¨b¬{µ-³à× ¡}‹ôÊ °{nüñhO4Ѳ8’bLìúϕսé™r3g‡ ‡M—$â¹G…Cpìµý.;nas/ž©M`T‰íCPíu-´0ì%HK¢®ñU²YGØžÃÐr¯3ïlè4lâä) „YBën—O5kõÂz8Óž¤ûÓOÅS~’ey¨qžî>±¬>?fË|zFÿ© ‚/'b0¦m|KXpà²C©Öøºv~ë_¬_ñ˜c½»× Ò\Œœ7„Ž’¾€”§ÝÊYcÚP€èà`ÿ᪚ÒpÂ*pqÄ(2¹Òú{à) œËjs¯!ì=ã J훆¢L'öá¦çÙ1¡ÝQ,±ãÏ:‰U¸! p\5ØÔ±o' Û˜Á_´¬¶&šìg¹iÝÍéUø $µRøÐ ¬Ñ‘1Uº¿?êìtðkÕSË>'÷î …¡™žzÉ‹0ÁSż'Çà wžç­ 6‚“`~®=fç¾ý'Ôd84êÈž¦ÚœõmÉ"<ï(¨Û-Zš 7LóT‹æUÝ€[¢àÆ#”‘4õ"• åÓÅqÅ`³ÎÅÓœëUÇ+LkàX-OPT•䘗Ö½| vánöšÆ c7òà¸Às¬ñǰ©&۷Ѱ)Þþ·¸+î…føåŠÐÖ,ÚE™›'W Ší±B<­#„K=cÔúvD‡¾ëXÖ½ã}¦›9‘e2Õdzgft®|W<`ON’r•z‡Ú¡‹ àáY¦†NħþDQøèT&ÂJ)E“7?”&8¦îµî"I%P]`¦#ŒúÚ(´w)ÿfIÉôàB¥¢Bgk3¯$>J¢M¥ð¯¨_(ÈþéŽ †ÉI'æÂãK¹mÕ{nFÖ± îä˜Þ6ÌD{³±(<ÔÀÙÄ„è3FþuÄÄÊ![7•ÜqŠþ‚/÷5¤˜Oe'+E²üt_& g™úÃÿ\7Â0šè•::J2N¸,R™úlfå¸efŸR†œ]©J—o{¨Bjƒ3ýÛµ8ä}à·c›4÷|%Þ–“ЇŽÓ|qVòwÍêXOçPÍßמq!UË+®ÊýRm± MVW÷8¢àý”Ym°²sÂ`-C\³”ȃ—²ã4¹$ÅAekª§²5öx\m|5½šjû›r“d釻;¯¦Y^,õ¬Ìà MY ÄöiºCº$àã²hvóãÄÇ š§>Ëe¤wRVˆlzvîÖqŒö­ë£X¢y²®½ßѬ: .ì úh‰.ùmÛêD¥¦HâÞˆ¾Â¡îÏôgòbÑŠœÁV²±šS,7‚!JÅ¿#ã>£ó5EsàƒMš<GÆjÉ×!ÖŽAÜøßeû®V±L_"ë„>V½T9lþÏ×g-÷æzQ¤³ˆ¬õus×sgù©«DkT£U‹‡¯Ó¿ÿXZfç'ë Ã\-Bô  d›©ñe;@$-¤tò–^oIqù‘áú/èDpÝÛ> $jvü}(o!ð=<óorÎtô¸ND'6ÂbÒݼ3•¢M±Òd—‘«q:.€kÉÿ’£ÀPâ$Š;ð½KÌÜ,ݧ2é.ÀêËë´ZE®ZF„±óÁ‘ÕrÆ1×(QKBg,É„’©ù âu´”a©ѫԔ†qCj>{Î !`±³ÃþW-Ýcãüt r‹x¢¾îz'è’ÆNTÖ’øÌ‚ØíßÊ Âô€k€·ƒúÝD0 nµgÂK]$C–+ôÔ¦CâvrŠYyèø@¥`,mWpf “oú®…ÊÒ:¥ÄÁÏË(‚ºU±ŸÞS/IŽB^yWüÙh„ÉÜLáˆEüø­[Üìõ‘¿£7Lÿ%½N}^2¶cðj÷ߺ6,¬’CY…x,’þú›·¤ùëwI¿«ð"? rÎÈhb|ƒ]^j‘ ìy×ÀíŸ7[0ìŒÙÃæEšIùÑypæy[à±[™PµKÝ`‹rdiì1⨼$•N-Ôƒ1¼(µ«Ë…isRŠN3ÒHÈc5çJ+wD8 âÀ™Zò¾×ESðÐujM¼Væ†éó´s´L3ãÐÛ†¾yMv—™™)—’8 ù"(ƒ&DTø|Ócù‰«ŒOÚ€eÞõFt }¹@X*-Z[ã Ù²Cæ–ŠÖ­†¤)˜Nú”¢¼|·û‰ûêgÏùºvm÷÷'*)àæð?y®4ʪgÓsåÙ–¾²­” V±ÇÐhZzAÄÎ ›:NòÚª_éÄ×ÝR–¡1V‚Ót ´ÂÕLïg÷°Þè3·à¥ ðòÁ5l°·9;îÙýˆª{t\AòúÁSK6žÙµ«½ynâðŽ•š0b³UsÅ1"ú ·\Gg*ÎgÓÄŽ\£qšò}ˆ¹\½€º]pœ8ào5&kª¦³ÌcÕƒÙ>(:'H öVþ 1}˨'*CøÜÎdœkdÓÜÀúw.u£o7Aù+zâˇm¯~éj©M¢$ѧ³Ì-\÷½¹@•ƒ–\Hƒ‹\{¸A`k„Ç\C ËZ¨Øt¶w!ëi©ï‰êçQÍÒžøèŠ˜(*–ßÿ#!jÄðÌ:q Ùò$:Ü&soè¹ÃU2ÇaO è,ˆ ×FÛœÜùâBïâëWÛ%  gêq'^mñÎð¹2/¼˜X’7‘ÇrÆ6oD eb1®@–¨%`Eš‹,Àj< ¶ùN0Ð8Çp†Afô¡¨¤R'yhе kÕµ5²@ç–å{n}V| Ðôž;YdÕï¥Ð˜‹~<Úã"~oÆÅÿ(¿š$¯Ç$ÜB#¬èîû‡1] ìZò5ZÜ-»ÕudEY…)—Úwy Oª¾R«ëÆ&œ•²WΖwg€¯vœ¥)ŸÅ Ò¤p¼>c8é´ÛoX-‚Ÿä\å¢U\™aŠ}^ÀeZÞHFTC¿èjõºk²Vž¶g¢$eæPþ;g6ÞHAóÍ"VjÈì¾ö ne£¦nù«s½÷c=­eêÇVdª†Ç´4*b$ÎÈÒý‡…u(ª”m¹*ïJ±ÉÁ‰NeuÍ_ÏO14– ÷ÞF«K-Ó Ô-'Ϭ¸õµ EAsOYNã8Š_*°}A³槇 ˜’iŒR®ËÁa9éMG¾@2ê¯ ¹Àð—]H,)òï"Uô™ye94Yâ›uЬ¡ƒý‰’Îk}‡Y]”/kBŸ€7^›âœŽ?„$b;p ~TfùRzuÆO0í7«¸£ëöíLÏ_MVÛ°±Lô™›¿‡³ûÿÆyéº,/)O޼âæ;þ­õ?ÝOd³ûKé †·¬ß¹²±&EæTÓÈ .гÅ=ÀµÌ‰ôFî•;_Ûò„]QÝ´¿³D›Ä‹ùL([íRÔýЙíJ~$6Àþb¼^ß2²-ƒX«Ûºì>Å©¼“*‚0C ‹o šüê_îgf;˜‘Ê{xѵÔR‡ÅšÐfÏ7ú1¿ ´ ½—Mêß ’¶¬Mäj£õGÑrc¡#¶²”´qVµ+ÜïÔº_n!£ßЬ¯ùû¦rW¦þ+‰u3qìkßõ£gi·Xã ®ËzøÚÃ<(†®h»;NéÓüz·96H•ª\Oò”‘MÛ<ªƒ†nÇxÈN ùðû*A_齋X=ý)‡Æöˆ5’ßõޱº’Nx°Ã W‚h ¤Ö:ÔB:LOTn!ïyºhÆ!Vø +³;Ö‰_X²€‚85úA͸‚)ðÅ×´ÁéfUŒqn”φÙKµÛr*zuÀ{Ve ÒªCU¯pÎÞïŒ0¦“úe˜:éd³öá7e3ÙX³A%†Ùám4*mGq³xuQêEkH„—Q¢*s‰´~ÕK»¥ÔL£‚vI@éCjõ@åÜjS‚#-žn”ûþç!S燮!¢D-Ú.êë3Ù£Ú(Gu`^6F¬…80Ø[‡ÿ@+?Í:B,F/Ö$‡É¢+­fß0ÏÖÅÔJÁÉå÷ýÊA˜W[vËŸX Õ -´˜óaWÄcô z¨ü–|¸d;./Bß“CClJe¥‡YGO sxA¼&AŸFS.å |„w@oW@QnúЪ&£Ený[‡©+ ;ΔÝÄÆJ#˜~b¾M]S85vWy  ŠJêÕE ØzjMo.Ä ©W|wÌJó\TâÈÇ+7ð?{#òÆéæŸG2÷ŒÞÊÅ³í µóÒÛ3Ì‹"ð§‹9eZG;!}p¼}òX¯Ì5â™°‹E±Q¾‹LÐÒE‰ûX= yÛbÄS+³Åî¶?`0…~^õ(À·Õ=y€ö’'ÚJcõGI2Øwpïs\L¦aôfæf§òÊÑaß\F¤ÖòFÆoî2Žvç¡WˆÜÏ‚…AÂg>q#0 ƒéôãëÉ+Ñc?„£Z(‡õØ*K:µ v@èuê¦ü“¬0F¢ ÚAû©”…6Ã_^,½â³ îüƒÿ{Oœn g1FÈCÍñyfd˜w1Añ:Æä&ßP™>ÕF-þ…­¨nÿ)ŠÜõ)4¡‚ ô³-œ˜¤\ ‹V4ô#V ‡L‡/³êšlÖôº:¸·éLJ·5Š>V²Ðƒ,‡ÝX`ľ^ODtêÎSˆ»¬òÞ‹:€'ËÌä°t”÷ÍöW$ÀvÉn…b"/¦f;xî„Tòûˆ"7œ¿ñÐË ,…Á¾YéÓ’…ŒbG-¦~kÀ×ÔU+O¬ŸMá&%÷£~U“žÆ$õ!L ØR.g+0Ð >å5W•B«(b|Ê[¯”Cõ~dð®ä’—ºR™ÿ!8üv=°ˆqÊí˹ø¥U–” 2.DtH鑦ÒGKþ>b!÷W$ñm™) i®ØyÈ¢Àõ›Ö5ˆ×KšQ~¦yy½UúÁ|ú×zˆ¹@—†È'ëÇ»ˆ/ãplé©Ò°ã}Ô|ĽgT—"ôù8°ç“ƒm…íÕ¨¦&Bƾ Ò0¾ˆ‹¥C ‰”À¡.Y-X˜|ž‡Éµê·Ë¿„;T x‹ãYS*O´Ðùg–¸Àb`¼áR †J^Òä~Ò,Þ›RÞÁøj~@d4–C­°j¹1ý@À¥È'ÔëRÞß6W¥üi‹<Ðõ{HçJŸ´%,º½3¦Ç`ŒHr†ùg$ˆùG‰‹R¹Tº2«ÜK¿ £¢ÉĨœ^´Í€Œvcä˜Ôö Ï}´ÎØN*Ž™ Cð]‘Tù7‘‘ÄwzE””!Jº”+²……ûdEÈÙ’;“¶¼Ø~fnRìœÇ7š#r«³iëwÒ(’Ðdo›'rL­Á‡Þ#M*Í_ ±ÓÕø ‘w»YW°¤ŠFæ¾ùE¥°Þ°ÿð€×_ _^>l¯‰õ,Õ^,—%ßVÇjžu6‚Ú^ã<ÍäG¿C5¿ç®È#É!¿h^4HëîÀ3JC3¡zò4§îx¯/+ù"2>´Éý7oñ0š²)5KÎUv‚yûžómTídÝýS|Ôòx½Zˆü=¹½Ξ¶ÌeW{9gšek!å XÖ^úDؤŠ âA•w8Û¿e'ïa#J,«ld(è~‰ =¯ÓöéúÒ\­Z÷êí((½ý?v¸Ðc'ĹxÙÕÃ9ŸÍ–Rùq$#qÌß™b€}Äá NAG³ßz ¤æ‘õù,P:ք׺œfY]”ħhêvUf¶5 ÝÄ÷`_¤PÊ3J¿¢`4™ÇLÖâ7·þ«þIróz÷aQ(Õ¦XüZ¼Ý1£oB^‡¶í*ƒ¡ï™?¹â¶Þ®,àPkÄ+ÂϺqÝNæl{¢V†Wñß¾, lïÄùëN+6‚# úþLƒ?¼µxúžNsc†ýŒƒŠª–ÏIbh1lw¨¢l•á¶®‡rƒ=ÐMìvTdS6A ÂÂË3æÁÀG×6 ZÈ$ˆ?Yl^”_–rÞRÃß›c¤¢ø^ZáDe͹•Wn-á®$Iìc@\À<ñ«AÝ›g.Äß8\ 5Ƭz€é›L¡V¹;¼|‡qUl{?ƒ$À©Ò¸ÐÅ]LL;×]ón*&ÛM˜ž§ƒVº†€ß÷lÐdUÀ‰QCó¿¿=|ÖìÇ®Í[oÊ•æëž¾+Õ³hTn[WfA#J~ù‹âMX€ñ!’>G½~j $ÛXÿŠNƒ’Hdƒ\94–§²ÃÒUÁy{ï*³ƒ“ÓÜ¿‹Cá>pqP‰T[jîMߨù†ÿàÓï<ßwp©/U1Î6tÄâ¶{Ùu}4&ËF{?Ãçh, ÌX°D1i[†YЫ-Þ“ãÌêTmá~l׃bå„8pµ Ö:ADæ¿—iŸÆ’éŠæ~ë³QàuuvŒ†¸0í]ü|ÕgmÁns5`èébaÐñÍIt)‹ù”ýY¬8ß>™Á¥\.¼ÏO<Éö­(ïÖ>Ëd>E8 !‘‘„1SÕdèp- ì6Ö*nþáƒÊoIÒ„±î1.ß®kÇ€îìÚƒWkx©…`7Ÿý¸y·òhŽëìA ¨½9ÊiôÅ–y LÕÁ­Æ¬-pËÛóå|ëçï÷hâ°æ‰ÇÏÌÜL»´u« ΋©RôN®ÑÉRì·ÒR[Yßå˜÷$Zb–ûWºRw+¾GÜÛOÎ{ªã€­ýôΊU[èN´m8hŠÄOw ׉VÆÒ’‘àº÷d'v3¢YÃ[{û"C• K+ÕR½ >=êì õ(›Eï Âp.±º …±55“B1µ·l÷€˜°#Mâ§3µä+íÑ»cTÑÿ Ý!­Óµd²Jܱ,YeéðdI™àUM´[ò¿¥hΜm„¯©“Kw{Òª8(Øy+ºwL¦nÔ¤àaÇq6òªÌuɰ{t'†æKàï­Àe|Û|Š-Œ—D´‘)ÿ{ÝG´V™Òw&ì°§ 2µ†Û‚±û¼Ê"´¶WiÝêj~ºmJytì˜nWô­îˆ„©NA+•õ–&(æU‘Ø]´~àãnšº™¥"4Ž5ÈNM3<õOÁ”ä[ÆÀ2»‹åEèé$âASáê)EE¥Cz ¿§‚d`WòjR:Ø£²ÎCéíéaéoOQm@Æßÿ‘ŽðÉ^FÐevÒˆvûQ4ĵÔV)²¿>j¹3Qú-ÄŸ¤Ɉ°ÿ‹ÄP–]×:×;îæÑoþ¡:÷ÓUÒ¡ÎÅþ'kœv? ÌlÔÉzñUø' 4)¿VÇ%5%÷jã0ÛØÎdúÞÜÚ6 ÚâÑ·Äk륡)T±†(€¨~ã–ÿǼsc@ò~¦5ǃ+’glE9œ”Z]uÈ»ÅÑGn°Cì›KfWHf†*±š<¨á2« häI)p‹$r®zå/2±j‘&:Ï©Rõ¾£-Êê²à£öF“H¡¿Xè×åo…Œ¿ï] C6Ü”.,¸z, Ë3¯2E*01™ô9mdHdT*È‘®¾£\};NΚ'â>~nWQIöƒÆÌO’yòÑrt\³þÀQÜ!Àª‹ ‘Bâ ¤bè¨Tô4”¾jLáÖþðŒb†ô ý9Þ/çŠBTq—GUÌ"˜EÓ¿˜¡Þˆ»1¿Å]®ò´·‘‡™ /m"î{p!×ý# ¬Ï (Hg§Ž©,¤êÒC¬Ý“"h0Ä;n¶“=n„›¼›Ÿ.Ãz,ñÛLÞç ³¯÷é"ÞN[iv‚’MJçlXŠ%—?j2ª>äxŸðý³Aö›Z€³Žº ójÁ¼nR<9Z|ÍC<ž”‚÷®yy9=ŸÊ--ªÄ´RÍ(ãÛÃIè/6Gް;¶ÁÕ~­`ÿkŸÒ4‰ÏÁ:WR\Ú`¹gA9 Ì^àH‰Ó|W}âhH c´®a›•µ¢ ô¡à`qJR]½´8O¦si§ØfѶ÷³PÞ~C‘—’݃òl|OH>Ì©sRí@©N=4Q>×#©&‹`L0/Â)„ò,ó3ÂÉBŠÙÝixòïà|ë²;°Êe —²'ÒÓ10HÂoòŽd„QlKŸ´Ù£R¶i+ž¤¤’M üu5îµ÷Q9õ’p)£÷n^É“Ÿe3b^Ç!™ ˆð=J<¹Ìy½bCöxÜk©EGãZaFr¬|Z®† '9N.w yEŠÓòØ—ÅN¶]TSrA—õ$ެBmî7ÀüVµj3]vÙÐÄ ºã{"h\+ôþ%5„±”Y‡ö:ŸÞ7Z75’± œ" ƒüu›âE¬œözÈÞ0~IXOÔµ[{¾Õ—k…‰W^n>ÁßfzLtYŒfÔˆAf1À„ÁŠbª™|û8y·G×üNêðÞ᩵ûš–AÁÎ £òÒ…bÉ/n]W/ˆemâ=CLÏ‚ÀŠqzâµaw4„ô\_†«x‹j0 7ö dj"ÊV ŠŒ§ÄÃJðyÜ4^˜u·±[Î3Ù¤6Œê¦³Á;…ñjÁ}_çYÒÆ—™N†]Ó†Âõl™¯ A¸ó[ ô6—–Ê~o}_¨½(âÍ2.Aw{ÒÅhøxJ\,J ¬†¦Çøyýê}‚XtYsÃTvD(?1Ê´”Ö ã•"fI[=–2>"E¬#¶<ˆ-Ý-Âg‰G]еU3tX꘭+ _ŒÉÿþ1<»(š€Ñµ,âêÓ›’óƒ;ä¼»ø –Aú›‘¥i ü7tßÑÏ^Ž…Í$yò{F7ÈeÇ…6®4h`“óéqv’ŒÛÎk\µ»¹š¤²:x­²T¯pX%w‰¤6">®ûÜdêëRù)Xâ‹pŒ5R£úè6M¡=0:ìT“”3m½¡‡Ãû½Y•‡Ȫ¸îçV0²_mwAbpŸž½»Ü­anR Ê8jCjÉBmõc¾'ˆ˜ûW)â&=7/Éx£7±:ºhã…B¤JÂÄ"Ñ¢Á Ë7,بÙßH ïc¶ËËQ«šq—zf(ˆˆv­´„ÛGÄ“˜ê®&XQ.%%À±«ô~˜ìÏÚ [ªªV —Ñû>ˆf¢ÂËÊÎû«zˆY2%ºõ€© …´ÌÖ•Žß!Ån|yL¡…›Ñs”“#NJ ûX,_¾èñ*m|·§Äß8¤}XRÞò,°O ×ÈþÕS¯†T¦V˜m1c(v6G‡þÌµŽ 5ßÅ~‡òêªÆ´æÙ;,ðg“–—*Ù[ûÌû;ÝÛSùÕÆ$Jéç©£3±6óE ˆ_?o"CO$/~€£œb z}±‰¥ ñ³ÏÃoY-^Jmyá¥+¬L(™Ò¤{êÁ 56¥ñ2 D ÿ‹»FMƒøGý4Þ´p€PpIwü}Pü ËB‹èU€úÚÑYái+ ™V¨ Gã5c-”‡fQ¢¬þ‹,ÐüL>”6‚L¿êPfâú–L1•ü82:†û®v(®EDæ²Ýæypœ‡ ¦²·`—{.ÑÀ?°| ÍW]v[3ÁªÕ$üX'= y0±cé2“©ÖKõ]Æf7Ð2‘ùÖ[]JÚ‚S4‘´3П{¹rº!ú)ÿ…4QôQ°H¡í#ÖG“0üÕŽ#m©}4 üll‘JRNÚ©Eë¦`R™Eù倳‡çP  ‚Û±ƒ4“þ3%‹¾‹4´¶0ßÃÉfìÒ' Ī’}%Èî2UÁ„#!—ô/ZóöíIvb¯—„ï«ÏómÍBÍ~“JÁ+Ý4Må_/¢­àjÆj»$5Û‡sñçìFc}¼Õn{ÈÍ+P äí·¡9ͬ%`qÑÁ.\Þ(ó¶ ;£o¿îu¼[îžå¬¶‹Ã6ÁCg4\žè~&ka··GT”Ê5C‚«ÊààC‚M x¹0ÊŽ°‚]‰¨!%D©NÙÿYÀA2·V5“Ù‰WÔØk9ºT™ ¨MDü„g0²fAZ ãI9ª\Ä Ú^d¯‡tƹ«‰;¦n’\ðÐî_µ>??×83a¹¨¬)rëYȤ¿ÚMÎâAç¡+c‡”£º.ú8¿™g¯Et°)ߨŒ/&ßÝî[›viFlpßITlM¢¬•Ëé)=”׬mh3ÈÔ¡©ßlª9Y>à¾>ñoÿŠeKu=R‡ÿ€˜# „¡‚Å­ˇ›öŠü*óg¹bØáû³úP:é|K$g6Z´ ÞI­‰s.R1€ *½WX*‹($`/‘8ô Y4«[ççcn0~1Û˜^ÆY˜›ë7ÇscX)5’@PöaAi÷b?¤Y9õÍ©Ñw[*%¡Ó! :Ú¬ÅÎylêy¼ÛúH¯Ön»dÖóõÀí{¨Á©ÀÁS,ÀM7²DG ¾îDú}‰öuÕV«–t>«I¾ ÎXÉ‹;ìÖy«¢’ *̈óº3²ÚªÝzú’T«ÎÊÙ)©r4*·_ -á.†Ü’fAj?û ?̓_À:?ð®;#ì ûlX’“KÏï‰RÁŽ|zÉ_k敲7øí2*‘áG«þw¶Io&ù…hŒ»›5‰);µäpÕ,4uÔÇãÈèœå>E³ÂS<ýkEge­Ú‡\>AÚ˜ÿîI$ÈE¸lü'{ð}úi·ºÀ6sxn’öõ»»_`û„Z³½e(*2PíJÍ­‰d†̩0PÌC°2„ =„œüMéˆÎŽ“ìªTAÇ[ôáب¯ÛÃì]Sf¯ Ó—Ý#ûË™\ þ ôÌÎÁÒlª“MˆNWÂØf>”;ˆŽlB{Pï}×Ó‰­è|â8*Ž'R œ¡œj o*…íÍ_Íßôzÿ ަD(wª/ÆÊ—^f®@&ò|¸êÚ#Wå+‚ÏŠ†*ÝÂ+oòZªÛÂÊ_QÕË'dbß|~ë:6ì¯Ç$?'$@÷1K{ ˆ‰ܦÁ³þq`_=Ø1è‘Ü]b)-Øê¥ÐlO™£[“ש÷Ã&BûF‚§—Û¯éäzöP°’jž ùZ¥Î'볜ZU™ɯ lE€?8¨F¯^Ç,¿{R8< ù·æ5RQ6®±¼²\ò·­•O%{Æ{™„7}bo®ÐEñö*•0Þrõ^Gî›Ô‰A€½çOZ½}EÔ`M“¿§.ób7ìRáY@®ì1æ:±U¶LŸO9Õ|!³Ú¨eTSg“·rî·Pa{ØNs\Õör?Ö›ã;<'e*ÁYK–ou·³I¬Zû«ÝÌ–êò•˜-ÜV£ÏwSL„lV†)vþ3jÈäþÁ]ÆIKðCXiøwÀ&½µa¼éð›(ßœò'áù7 ±/µ¾ÑføO êUÏKðþºy–’¢Ý9Íîÿy&q†¥\ö¿6,ÿPç|‡¬­D“–Š¿SàÚÖ®d‚µtû¸ñØÈ\Ã~êYðhU~8{¹aÇŠƒ 0’^WjІÂGÉœ‚x„"Aÿü=?0[ô½ÀÃ.ª’] H<|BPwz|mµsÃŽnD.’G†¯?KUèÇgR¡:häLìÓƒÈl¿)DQÝLr¼™>MŸÅP–1ÖF#Òuðµ½ FÝ:Ĉ©~™¿¶b À+þo?šî–êÙ´¢‰–$e±0wY3À΃7áö"ÕzHLÝ-‚Ψ̡•Ž.uåüŠwÄà#W`Û>Äq@ÊkûêœÉW'°ëƒP39• `_ž y{„‘éz+‘üôp^ëvü}ÛZèÿØëpn¤>k9¼óš)"R¹ÖˆîäŦNå4ÕÝ¢WyÝÐP×Þ óM4Ÿcï‰eŸÓ𯉡…\tŠ'rñ~OU"ß<ÇÓ¸ràž÷öÊópúr‹6޶Ma¤1¥aïüÿƒñ9ÌØBâL±‘hï­˜å Ûà W§¬ÙÄþIkdL Fò"ïx…Ø­ ¶{°]‰è…ÒþÝ=<x ‡SÆ/&{£Ý6IذÆ8š¤á ‰ÑƒËô´Ûúùù°=Š,‰ÂÃgÍ´1¼¦Ü$ß%c«O«ùe?öı)Aêsû­È‡ü_ÈØÙä]¾¹ÙáÎxým'uzÂE}Û0‰Å’ ÝU$Ø­ XZ jÉ:*øu63î³ýÿJ¢2ê¼’10¡N"+fn8:&ïAjª”P¡P…7²ãaQÙq4IV»Æ)™‡š/ ÔW‘’6ñYL.`øí¶å1œ0AIþ5!Þ?×…0ôåaC*—º¤òìCê€"j›…{J¶÷ḠŽáŸ-ÈÌ¡ù.?Ž’p2Fç •[v!µüe ÖÕDÆQáW&âHFEAœ ‡ëÁ*‡2Íà(¹mB[므MZ|ç²LãfŽ:õ<øìÃ@qÆÓ‘Þ„†¢wªü`;Fƒ¿äýʱ¡ÆÉ]V‚ùäZš~fÄ„£ À0g_–[жè9©2(Åàd‰"R™%ƒdÖ‘ €n¼‘¾gÚp@Jo›]Í’Š"3O›Ñ@4ïDÓ:žê•Á¼A¤X0 (1©¥T[ 3á©2IŠh#ÕßÄÞÚ8/è…£2ÈèìO»{^ãÊ7¨ÀÍ“¸H(î.§9‰ [ô+æc˜é9Ú|׃ móK>R‘s"Ï&,fûñãò ÒáêÝÑ¾éæ¬ &íýÉ3ÒMWjU‰õØz^ÐéÅ:€¸¨-˜‘cذFÙ»0Z¢”HðÑ"‚hÂ{¾Šî×·å%~_ïTͯ•K»Äg»jTÞTŒ;†•˜i—{vx÷±»"ö¦æ·›GeÎ0˜Œ¶(‚£õâðŽÏ)B2tªPµæLä•·©‚yð°’¿“†ó¶áöøCO8F<Á:8ý®à*ÂHÙ0~+¦uv+ "–šÙÊrzo/ëõúZå@Èq/{~Pik4TlÁuTø å—'–žv¸•€ÀЮàÌòø­â8³D©Ukå©]ƒZµ¸õ ngêÐŒ€!lŸ q2LKÝ ÆŸR`Vk¼zòÌ Tù.ÑËŸ‹…²šº»*‚—è_ŒÄ…MßC˜[ IMN^S{ó†›L•à:k Póöç¼TrN¼9†¶¼(»°Cé¸),lïd è²vw¬¸V[}55D ½¬Jè0‰ú4°ŠbV×+ß½\Ÿíð׋£w3U7‚ó§ð^Œ,r[p+Z–”KœiðÈÛòŽê=è/qÐBºTŠ=/zF¸®_¤—SÓ-=fM3ŠYÅbSót$ çF8™u¥ùg,­ÃæÔ‰z”g>9ʳoiÜYzÝôui ú­äŠŸ2#7 Àe¼àÉÄŸó‡xÆËI Eñº6Crqp²™„ûÒMô,œ4Ì9â/W™·0þaÓC òdò¼ÉRCi E*x«‹iT“sˆàîm×ìæÎmÓУƒ—1Û ·½™ƒ8RŠ<ÞNë…Sé g¡ì£_ÒUpã´µ+KÒÛêð¦:]ìÛåwß.‰Ø†êXL³…}Þâ•óKP\¡”++¹Õ ?š÷H?g“1QÎH\&0੟1Âæû®^^æ(‹…1´sé¬{[SØ ·¢Ï>­ƒ'¢âY¾fÈ“ýÔ0ÑD!±úÄãü“¸i"H–7õ ¾—É/<È¥§4 n"™mYÛ¹u¾„¹²máPYë ÉpzŒÊ™Ý3Ž*x~êèiŽÿU¸Ÿ¤®ÑEÜB†Z0º†»Ârºà Ój¤ ’¼î+b™½éá™C1Eü΀tê †¾ðlÃÙÞ(¡ œ¾ñ~á*3³1ɉÒìò]†˜—Œo_õ¼ùðe‹±®@û#,µ‘¦ò¿Ý~Ûå; ¬¯‚Öà–ný+Jâh¼áä÷«ÁÌÁ Ö*§íS7ñùËõIsÜSÔ‰$õÒMÕŽSÛ;ŠdL åt Cî1 lO¾RBWèŒZ-ôÓ\V3ùOILÈ tô! – !õ¤¤ŸåP3ötoùÑ@‚°…»? dzŸI8×öUõ-e)¶vfw!½$ÝÔØX¶'Ù£L‘·‡§lYí~CX·týá$îñÄ–‰ÅòaÅyO94TOÆ ,ã„~t}‚ga{…>FÅÅ—Õ1|qÉÝi·ûÖgøŽW? Ú¥ ‹ôŒ}ü ix¦Ýj²$M$‹†Rœv s¢Ø‚(‚Rqžf=ØÃÙ—ð˜&¢¯¤ý[sgtÁÕd‰rÚBZè,þÐJÂ<éœw;-TØ)cþ× `pÖŸhUwöT"'ªKæ8á8% ˜Q?ߺÁ•àoÎêc*³?g5Fnr½ÉÆ<ö~3Šh:5Ž©PƒÊ2ÁŸþÎ:Ö(øDœÓð™¢muëbIä…= Æ¥ÅôTæ:»o¶ˆ¥5ø² §ÝÁ:YMæºÒ7-‰$Ä½àž¡9*.ªmŽ/ ¸D¿»hAü1ÝÌØ…w…&µ’:´C6Ó”õß’ÐcT¬3¨%å=ºÕ)½šÚ’‡ÿ\c,䨱–úÛah¡…Éi6WÑÏrÏHÖ¦B¸ëƒf×Jeìb?£ú^»§ÒP]Ðf®U Ã+Z) ¥¾mÛAö³­\œ‘çIÆÁÏVß…š/¹Xežf…²ÅÔ—§=¡”ÅÐÔÒäBuî'Á5ólõÿîWªu®ßqó qÈW—A_L×ðL¥UyQÚçÖÌ/],3ØO%÷b4Ûép«#¨q0k¢.V ¬>™x\…K–§&øu2â¸v‚Ü gVº€2ÀUœƒ¢Åýš`©ô³s÷ï§kÞ äþx‡³– ~ý­osìætÒ«RIãt«‚âù™Ã£×PòX)ªé²ÓœÅv àþçC$ØãÎ ˆ^Béâ¬ËpVwÈMÁ_]r`&» 8·¬šì~×§j1;ìÔøí†dq#„a~rj¥¸äΦC3¹æƒ>U_kËèØ®™t*`§Að×CyT3+PŒ,X,ÑŒþNž, ˆNwh{Í"tM}•\ä3‰Ñ·ŽÊzÑO®Kíº ãTQèñ^¿óòû¼JrÕœn9T¹äðáw»Qö³ÒðzJ‘%yE²”ßjæ£ÌòZ3%.WæÐ¥/â…ù ¢ÖT–‚Vè]B¨Úxo¡Ö2ÔÊÐVŒæI­ZE•%Ç\¡ªYÄQDFÎl5’ÂÈ—ó÷é1 aI&mJ ¤¾¯¶·3-.èMÄ6s9JÑa…N—¨èüBþÊ)Zôí!û}è!èìO,\B?·©2=XWÀ –‹˜ÎC­†¼éaðc¨:ˆ.>&® OD¹D9ÍNE5‰Ÿ!«ä1“'p…®îamÛã¶ÄªÜØy'Ôëlê½`Ç®›úí›+ñÞ¾=; caƱÖbv}´a×üqÓøøPyŠ—,Ö¡PS%}½Q€Š·lÛ¯ØkôG–‰§Ik"™@;z>@Ýp4QõÑQé_ðmæ0mt3¶\bAfýw!Ü´`<ٵȫY5g*^^·iÓÜ<"{–6¸ÚÊå 峎…Yðé¾Ávª‚ªÖòŽ—Ý!bö‰qÑ‹ÎuNþ\fŸ]A€P¹ƒ¤QôHrÁé䯄‰v‰©Ñ³gdŧòû¯áéŽIeНÁé¦F¯æ[½ÑC±TÇáÛ}Ö DË(+ÕJeJ­•ǶÌìæ’\P½dz©}³ "P¼ÜT²,ԚÙG³im…ꊣ«¥ÒDn˜ýA=Å£i`'eH<ìùG@Eå$MÎQ»8nO2Rãæ­²ýÀô?X+Ô”0Ø <8É {N¸X†Ù½=,±ëxcšÄ¸Zóò“eÔeVð‰Åª­3T‚{‰½L6JçÌŒò[‚ˆñÐoYmû¢ÙÓ¼ã´Ò²¡!qjU«æÁ]î+²Jô¹1/à1©à~•Í€äÞ°<1÷^(ü¢¦‰÷K4W禟ácÿÅåGn«^ù\¡ÇŒ{;‰‡û !A[Àævä§ œã1õÆ×Ìœ+¡wL<à ™¢ÐÆ^a¡JëGuûäUFC¢&<¡:Ú!lXšˆÏ€ÉxK¹& Tª[Ð6<Š”ü?~¤ìGÒÓCmÌ£FlÅ7ኆJÓ<¸[6§ïOÕÀþ%‰½ðN¼%¼Î3ªÅý®ÖÖ©”Wï”5ÁÿX{¦òð2†ß±€¾­öÃÎ C/‹~Jë ÄS¬Aþúø¤®È„-ÐS=3‰Õ ŠGµó’ð0,\%=‚ºF¥†P¸Ç :^¿¸^£CÝ d7#ä€ý.‘5 1;)ÜÀ1ÿ‡Y¹§{Þïyá>÷jD.éÒ Áz§R ‹&ØLš]¡'l¢ø€{‹±H8‰¤jÊÛ¢o6N#NÛ-Šøc±ÇjR*c,&å;ƒÒÌKÞÏÁæŒjR*c,&å;ƒÒÌKÞÏÁ挿Õ芆µ"4k…0*µðh7—î6¦©Av’L:t•¹‡‰%}’Ϻá¾|X#a¥gMW#O?áF-u}³FPIÆåŽÙÑ‹ãyÉ~è!¦/˜?,cl.Ñ]G3²ãMúæ ‚mEý@º‹£¤&¸$ˆê]Ò9W, /DBõŸM{O¥Õ™eQNŒhP|Ãh©gì§E‡c¥æ!&Ý”}Ô¸ùa.Ð\;m£ [ܹBé\µgneeñ‡ÔÝdïxz£ šx.”Õâ’ìE¿}=[Q?J}¼dewÃêC½d¯® ð‡|ÒWŸÒ»J‘O#Ê‚¸°ß&ºkƘšZ‹ë}àkëR­|餷† yiÂÆ¸à@ÍóTHÐDÃ^r"X>\ßaYÏ>õò› ¢¦f2,•° ƒ#¾70ø¢Dk‰ Ž9;‹§î¸rß­H¦±Ùc¢Q‹õá/?×Iö‘°Úì1¤«ãDs + /Í'uŒš¯“T‚£'+óåîõöÒáÞR¾ôE *ÏŒ³’ÊÆ½¾>«™nåË]á»_D4·QP‚Ðëóêl1¸C A?£…x/}Õ×¾˜ß~A¡ñ bÿê§<ï4ü ÌÉç‚Þè„IÖ]޲ÿç=¬üs¯óø&øø—EpÈã÷EMê½õa÷Ø“†Þ’ÿ´Ö2oHýjßW…ǰb>Ø„»†w—ÓÜœõ]ÀÔ*ªD”©;vüì+âÊ”PRH ´ÕÑ*QÉÃl¿½*6* …t8!IÂ…‡h\çÊë%þ\•!ïbU •n¯8*Øw„¶ Ô!ß‹CÝ80Y²;r.6ŸXÌ, ¤…q/ó”,&]L·&+úª½ë @xÓ¶`å}…–6!+Y°·º·æ(í6ᜩ7ì&ˆ4Ëh‚]Ÿ.d)œ~Ùap¢lÓ€}—Â…‹™OѳŸò=µ}ݼÄß®+°~ñwÞq5ÃËÆörMSƒ'µ[¬“DR»ò ×ÑU–ç¢ùŒ"°ú¼6æ#ä7dJÖG$˜îv¸<²Z þûL¸Üª-Bפ†üXý8€HV@ÖvqòlK4`êÕ¦'èÝéä:Ÿ Q`ü@8O¯Æ¨¤¿—Èp®x”«™{üPª¶™ºQôË\&Ê"~R]êrˬ¬¨:/âc°¶å’;‘rNÁï3;k{‚z³ùSŠU]mI>Ç‘ˆ'jæÜÂAgÔªP|aú[>º]AOÖ³ïiåÅ—Z¿l '¥Ÿ^Õú ç©”Ú¶®ÞôìR8ß×Pb–>0;v¸%¥LunK“š.@\¦J¯=§ÑG¡Ôf}6€¬*»tÔælQG10BQ …ìD-ÂýÃv³ •¹‡´ó@0æv5'-‰‹zuòu”&º¥ºèéjá³eÉ©TÒ ©g¿ÉÓêÉÇ2ò¸Ò3w ˆ y÷P¥ç/á§¢ÍÍwý7¾ ®¥ÖñÃÆ¶•úþuI.Ú’I›vkž=Z©UrrÁšRÈIÜ]¯§(v'¶ Úi 2 A+{¥1ÔúŸU(á•pq &r ¹ ¡ˆ<äP<o°,`¡>ª}&^€©ƒÃ”®wh‚+YϬßfz4`wbõ#>%]_ólì\Ø×„~Í‘wàZtXÜmý*´ïå‹A½¡¨8§×C.÷¾J%ûxÙ'útqŠp9q0”ÖIc¼ÏR4Ĭ†"â›v²–ˆL·‰ßºcmQfQÎÃÙCâÇ-J»kf2ÜŽ’¿¶Ôì¸38žùR^¨:²(#¼›¨ó<¼sCœ¹º¢¼ÇœêŒIE¡ß0±à"’1ÆœÐç6#jŠýáXr#Äà¨[…¨™<†Õüä¹N–N(8]ú@(œº£¿Î"Ø\QÞ¶cŽˆÐÚÔ^¾îx5í£³‰+ö î"ôž'€²½¸ëšWP¿em|õ†¦¢Ë¦­ wü«o×ÄS"‘ Ü®ÔC2C—4DW=g¾¿ÿH67e›ô/²sçŒàé‘ûª{òÇ6¥jr)ebÉ›j]Ë Áp5QTAêÿÒÚ` JBySÉ’öÊýtv-;€èôU<ã<3†{»pköÇßûÅeeblueluna-transmissionrpc-eb2a32720f8a/test/data/verify.json0000644000000000000000000000475412227066501022155 0ustar 00000000000000{ "test sequence": [ { "request": {"tag": 0, "method": "session-get", "arguments": {}}, "response": { "tag": 0, "arguments": { "alt-speed-time-end": 1020, "alt-speed-time-begin": 540, "speed-limit-up-enabled": true, "rename-partial-files": true, "seedRatioLimited": true, "peer-limit-per-torrent": 60, "incomplete-dir": "/var/incomplete", "rpc-version": 8, "blocklist-enabled": false, "speed-limit-down-enabled": true, "seedRatioLimit": 1.0, "encryption": "preferred", "alt-speed-down": 50, "download-dir": "/var/torrents", "alt-speed-enabled": false, "version": "1.92", "blocklist-size": 0, "dht-enabled": true, "peer-limit-global": 240, "alt-speed-time-day": 127, "alt-speed-up": 50, "peer-port-random-on-start": 0, "rpc-version-minimum": 1, "peer-port": 51413, "port-forwarding-enabled": true, "speed-limit-up": 500, "speed-limit-down": 500, "alt-speed-time-enabled": false, "config-dir": "/etc/transmission-daemon", "incomplete-dir-enabled": false, "pex-enabled": true }, "result": "success" } }, { "request": {"tag": 1, "method": "torrent-verify", "arguments": {"ids": [10000]}}, "response": { "tag": 1, "arguments": {}, "result": "success" } }, { "request": {"tag": 2, "method": "torrent-verify", "arguments": {"ids": ["d"]}}, "response": { "tag": 2, "arguments": {}, "result": "success" } }, { "request": {"tag": 3, "method": "torrent-verify", "arguments": {"ids": ["a", "b", "c"]}}, "response": { "tag": 3, "arguments": {}, "result": "success" } } ] }blueluna-transmissionrpc-eb2a32720f8a/test/top.py0000755000000000000000000000161212227066501020212 0ustar 00000000000000# -*- coding: utf-8 -*- # 2013-03, Erik Svensson # Licensed under the MIT license. import unittest import transmissionrpc class TopTest(unittest.TestCase): def testConstants(self): self.assertTrue(isinstance(transmissionrpc.__author__, str)) self.assertTrue(isinstance(transmissionrpc.__version_major__, int)) self.assertTrue(isinstance(transmissionrpc.__version_minor__, int)) self.assertTrue(isinstance(transmissionrpc.__version__, str)) self.assertTrue(isinstance(transmissionrpc.__copyright__, str)) self.assertTrue(isinstance(transmissionrpc.__license__, str)) self.assertEqual('{0}.{1}'.format(transmissionrpc.__version_major__, transmissionrpc.__version_minor__), transmissionrpc.__version__) def suite(): suite = unittest.TestLoader().loadTestsFromTestCase(TopTest) return suite blueluna-transmissionrpc-eb2a32720f8a/test/torrent.py0000755000000000000000000000631412227066501021111 0ustar 00000000000000# -*- coding: utf-8 -*- # 2008-12, Erik Svensson # Licensed under the MIT license. import time, datetime import unittest import transmissionrpc import transmissionrpc.constants import transmissionrpc.utils class torrent(unittest.TestCase): def assertPropertyException(self, exception, object, property): try: getattr(object, property) except exception: pass else: self.fail() def testConstruction(self): self.failUnlessRaises(ValueError, transmissionrpc.Torrent, None, {}) torrent = transmissionrpc.Torrent(None, {'id': 42}) def testAttributes(self): torrent = transmissionrpc.Torrent(None, {'id': 42}) self.assertTrue(hasattr(torrent, 'id')) self.assertEqual(torrent.id, 42) self.assertPropertyException(KeyError, torrent, 'status') self.assertPropertyException(KeyError, torrent, 'progress') self.assertPropertyException(KeyError, torrent, 'ratio') self.assertPropertyException(KeyError, torrent, 'eta') self.assertPropertyException(KeyError, torrent, 'date_active') self.assertPropertyException(KeyError, torrent, 'date_added') self.assertPropertyException(KeyError, torrent, 'date_started') self.assertPropertyException(KeyError, torrent, 'date_done') self.failUnlessRaises(KeyError, torrent.format_eta) self.assertEqual(torrent.files(), {}) data = { 'id': 1, 'status': 4, 'sizeWhenDone': 1000, 'leftUntilDone': 500, 'uploadedEver': 1000, 'downloadedEver': 2000, 'uploadRatio': 0.5, 'eta': 3600, 'activityDate': time.mktime((2008,12,11,11,15,30,0,0,-1)), 'addedDate': time.mktime((2008,12,11,8,5,10,0,0,-1)), 'startDate': time.mktime((2008,12,11,9,10,5,0,0,-1)), 'doneDate': time.mktime((2008,12,11,10,0,15,0,0,-1)), } torrent = transmissionrpc.Torrent(None, data) self.assertEqual(torrent.id, 1) self.assertEqual(torrent.leftUntilDone, 500) self.assertEqual(torrent.status, 'downloading') self.assertEqual(torrent.progress, 50.0) self.assertEqual(torrent.ratio, 0.5) self.assertEqual(torrent.eta, datetime.timedelta(seconds=3600)) self.assertEqual(torrent.date_active, datetime.datetime(2008,12,11,11,15,30)) self.assertEqual(torrent.date_added, datetime.datetime(2008,12,11,8,5,10)) self.assertEqual(torrent.date_started, datetime.datetime(2008,12,11,9,10,5)) self.assertEqual(torrent.date_done, datetime.datetime(2008,12,11,10,0,15)) self.assertEqual(torrent.format_eta(), transmissionrpc.utils.format_timedelta(torrent.eta)) torrent = transmissionrpc.Torrent(None, {'id': 42, 'eta': -1}) self.assertPropertyException(ValueError, torrent, 'eta') def testUnicode(self): torrent = transmissionrpc.Torrent(None, {'id': 42, 'name': 'ã‚ã¿'}) self.assertEqual(torrent.id, 42) repr(torrent) str(torrent) def suite(): suite = unittest.TestLoader().loadTestsFromTestCase(torrent) return suite blueluna-transmissionrpc-eb2a32720f8a/test/utils.py0000644000000000000000000000667412227066501020562 0ustar 00000000000000# -*- coding: utf-8 -*- # 2008-12, Erik Svensson # Licensed under the MIT license. import datetime import unittest import transmissionrpc.utils as tu from six import iteritems class utils(unittest.TestCase): def testFormatSize(self): table = { 512: (512, 'B'), 1024: (1.0, 'KiB'), 1048575: (1023.999, 'KiB'), 1048576: (1.0, 'MiB'), 1073741824: (1.0, 'GiB'), 1099511627776: (1.0, 'TiB'), 1125899906842624: (1.0, 'PiB'), 1152921504606846976: (1.0, 'EiB'), } for size, expected in iteritems(table): result = tu.format_size(size) self.assertAlmostEqual(result[0], expected[0], 4) self.assertEqual(result[1], expected[1]) def testFormatSpeed(self): table = { 512: (512, 'B/s'), 1024: (1.0, 'KiB/s'), 1048575: (1023.999, 'KiB/s'), 1048576: (1.0, 'MiB/s'), 1073741824: (1.0, 'GiB/s'), 1099511627776: (1.0, 'TiB/s'), 1125899906842624: (1.0, 'PiB/s'), 1152921504606846976: (1.0, 'EiB/s'), } for size, expected in iteritems(table): result = tu.format_speed(size) self.assertAlmostEqual(result[0], expected[0], 4) self.assertEqual(result[1], expected[1]) def testFormatTimedelta(self): table = { datetime.timedelta(0, 0): '0 00:00:00', datetime.timedelta(0, 10): '0 00:00:10', datetime.timedelta(0, 60): '0 00:01:00', datetime.timedelta(0, 61): '0 00:01:01', datetime.timedelta(0, 3661): '0 01:01:01', datetime.timedelta(1, 3661): '1 01:01:01', datetime.timedelta(13, 65660): '13 18:14:20', } for delta, expected in iteritems(table): self.assertEqual(tu.format_timedelta(delta), expected) def testFormatTimestamp(self): table = { 0: '-', 1: '1970-01-01 00:00:01', 1129135532: '2005-10-12 16:45:32', } for timestamp, expected in iteritems(table): self.assertEqual(tu.format_timestamp(timestamp, utc=True), expected) def testInetAddress(self): table = { ('127.0.0.1:80', 2000): ('127.0.0.1', 80), ('127.0.0.1', 2000): ('127.0.0.1', 2000), (':80', 2000): ('localhost', 80), (':80', 2000, '127.0.0.1'): ('127.0.0.1', 80), ('0.0.0.0:443', 2000): ('0.0.0.0', 443), ('localhost:443', 2000): ('localhost', 443), } for args, expected in iteritems(table): self.assertEqual(tu.inet_address(*args), expected) self.failUnlessRaises(tu.INetAddressError, tu.inet_address, '256.256.256.256', 2000) def testRPCBool(self): table = { 0: 0, 1: 1, 1000: 1, 'true': 1, 'Yes': 1, 'truE': 1, 'baka': 0, 'false': 0, 'no': 0, True: 1, False: 0, } for value, expected in iteritems(table): self.assertEqual(tu.rpc_bool(value), expected) def suite(): suite = unittest.TestLoader().loadTestsFromTestCase(utils) return suite if __name__ == '__main__': unittest.main() blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/__init__.py0000755000000000000000000000151412227066501023427 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT, PRIORITY, RATIO_LIMIT, LOGGER from transmissionrpc.error import TransmissionError, HTTPHandlerError from transmissionrpc.httphandler import HTTPHandler, DefaultHTTPHandler from transmissionrpc.torrent import Torrent from transmissionrpc.session import Session from transmissionrpc.client import Client from transmissionrpc.utils import add_stdout_logger, add_file_logger __author__ = 'Erik Svensson ' __version_major__ = 0 __version_minor__ = 11 __version__ = '{0}.{1}'.format(__version_major__, __version_minor__) __copyright__ = 'Copyright (c) 2008-2013 Erik Svensson' __license__ = 'MIT' blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/client.py0000755000000000000000000012414712227066501023156 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. import re, time, operator, warnings, os import base64 import json from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT from transmissionrpc.error import TransmissionError, HTTPHandlerError from transmissionrpc.utils import LOGGER, get_arguments, make_rpc_name, argument_value_convert, rpc_bool from transmissionrpc.httphandler import DefaultHTTPHandler from transmissionrpc.torrent import Torrent from transmissionrpc.session import Session from six import PY3, integer_types, string_types, iteritems if PY3: from urllib.parse import urlparse from urllib.request import urlopen else: from urlparse import urlparse from urllib2 import urlopen def debug_httperror(error): """ Log the Transmission RPC HTTP error. """ try: data = json.loads(error.data) except ValueError: data = error.data LOGGER.debug( json.dumps( { 'response': { 'url': error.url, 'code': error.code, 'msg': error.message, 'headers': error.headers, 'data': data, } }, indent=2 ) ) def parse_torrent_id(arg): """Parse an torrent id or torrent hashString.""" torrent_id = None if isinstance(arg, integer_types): # handle index torrent_id = int(arg) elif isinstance(arg, float): torrent_id = int(arg) if torrent_id != arg: torrent_id = None elif isinstance(arg, string_types): try: torrent_id = int(arg) if torrent_id >= 2**31: torrent_id = None except (ValueError, TypeError): pass if torrent_id is None: # handle hashes try: int(arg, 16) torrent_id = arg except (ValueError, TypeError): pass return torrent_id def parse_torrent_ids(args): """ Take things and make them valid torrent identifiers """ ids = [] if args is None: pass elif isinstance(args, string_types): for item in re.split('[ ,]+', args): if len(item) == 0: continue addition = None torrent_id = parse_torrent_id(item) if torrent_id is not None: addition = [torrent_id] if not addition: # handle index ranges i.e. 5:10 match = re.match('^(\d+):(\d+)$', item) if match: try: idx_from = int(match.group(1)) idx_to = int(match.group(2)) addition = list(range(idx_from, idx_to + 1)) except ValueError: pass if not addition: raise ValueError('Invalid torrent id, \"%s\"' % item) ids.extend(addition) elif isinstance(args, (list, tuple)): for item in args: ids.extend(parse_torrent_ids(item)) else: torrent_id = parse_torrent_id(args) if torrent_id == None: raise ValueError('Invalid torrent id') else: ids = [torrent_id] return ids """ Torrent ids Many functions in Client takes torrent id. A torrent id can either be id or hashString. When supplying multiple id's it is possible to use a list mixed with both id and hashString. Timeouts Since most methods results in HTTP requests against Transmission, it is possible to provide a argument called ``timeout``. Timeout is only effective when using Python 2.6 or later and the default timeout is 30 seconds. """ class Client(object): """ Client is the class handling the Transmission JSON-RPC client protocol. """ def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None, http_handler=None, timeout=None): if isinstance(timeout, (integer_types, float)): self._query_timeout = float(timeout) else: self._query_timeout = DEFAULT_TIMEOUT urlo = urlparse(address) if urlo.scheme == '': base_url = 'http://' + address + ':' + str(port) self.url = base_url + '/transmission/rpc' else: if urlo.port: self.url = urlo.scheme + '://' + urlo.hostname + ':' + str(urlo.port) + urlo.path else: self.url = urlo.scheme + '://' + urlo.hostname + urlo.path LOGGER.info('Using custom URL "' + self.url + '".') if urlo.username and urlo.password: user = urlo.username password = urlo.password elif urlo.username or urlo.password: LOGGER.warning('Either user or password missing, not using authentication.') if http_handler is None: self.http_handler = DefaultHTTPHandler() else: if hasattr(http_handler, 'set_authentication') and hasattr(http_handler, 'request'): self.http_handler = http_handler else: raise ValueError('Invalid HTTP handler.') if user and password: self.http_handler.set_authentication(self.url, user, password) elif user or password: LOGGER.warning('Either user or password missing, not using authentication.') self._sequence = 0 self.session = None self.session_id = 0 self.server_version = None self.protocol_version = None self.get_session() self.torrent_get_arguments = get_arguments('torrent-get' , self.rpc_version) def _get_timeout(self): """ Get current timeout for HTTP queries. """ return self._query_timeout def _set_timeout(self, value): """ Set timeout for HTTP queries. """ self._query_timeout = float(value) def _del_timeout(self): """ Reset the HTTP query timeout to the default. """ self._query_timeout = DEFAULT_TIMEOUT timeout = property(_get_timeout, _set_timeout, _del_timeout, doc="HTTP query timeout.") def _http_query(self, query, timeout=None): """ Query Transmission through HTTP. """ headers = {'x-transmission-session-id': str(self.session_id)} result = {} request_count = 0 if timeout is None: timeout = self._query_timeout while True: LOGGER.debug(json.dumps({'url': self.url, 'headers': headers, 'query': query, 'timeout': timeout}, indent=2)) try: result = self.http_handler.request(self.url, query, headers, timeout) break except HTTPHandlerError as error: if error.code == 409: LOGGER.info('Server responded with 409, trying to set session-id.') if request_count > 1: raise TransmissionError('Session ID negotiation failed.', error) session_id = None for key in list(error.headers.keys()): if key.lower() == 'x-transmission-session-id': session_id = error.headers[key] self.session_id = session_id headers = {'x-transmission-session-id': str(self.session_id)} if session_id is None: debug_httperror(error) raise TransmissionError('Unknown conflict.', error) else: debug_httperror(error) raise TransmissionError('Request failed.', error) request_count += 1 return result def _request(self, method, arguments=None, ids=None, require_ids=False, timeout=None): """ Send json-rpc request to Transmission using http POST """ if not isinstance(method, string_types): raise ValueError('request takes method as string') if arguments is None: arguments = {} if not isinstance(arguments, dict): raise ValueError('request takes arguments as dict') ids = parse_torrent_ids(ids) if len(ids) > 0: arguments['ids'] = ids elif require_ids: raise ValueError('request require ids') query = json.dumps({'tag': self._sequence, 'method': method , 'arguments': arguments}) self._sequence += 1 start = time.time() http_data = self._http_query(query, timeout) elapsed = time.time() - start LOGGER.info('http request took %.3f s' % (elapsed)) try: data = json.loads(http_data) except ValueError as error: LOGGER.error('Error: ' + str(error)) LOGGER.error('Request: \"%s\"' % (query)) LOGGER.error('HTTP data: \"%s\"' % (http_data)) raise LOGGER.debug(json.dumps(data, indent=2)) if 'result' in data: if data['result'] != 'success': raise TransmissionError('Query failed with result \"%s\".' % (data['result'])) else: raise TransmissionError('Query failed without result.') results = {} if method == 'torrent-get': for item in data['arguments']['torrents']: results[item['id']] = Torrent(self, item) if self.protocol_version == 2 and 'peers' not in item: self.protocol_version = 1 elif method == 'torrent-add': item = None if 'torrent-added' in data['arguments']: item = data['arguments']['torrent-added'] elif 'torrent-duplicate' in data['arguments']: item = data['arguments']['torrent-duplicate'] if item: results[item['id']] = Torrent(self, item) else: raise TransmissionError('Invalid torrent-add response.') elif method == 'session-get': self._update_session(data['arguments']) elif method == 'session-stats': # older versions of T has the return data in "session-stats" if 'session-stats' in data['arguments']: self._update_session(data['arguments']['session-stats']) else: self._update_session(data['arguments']) elif method in ('port-test', 'blocklist-update', 'free-space', 'torrent-rename-path'): results = data['arguments'] else: return None return results def _update_session(self, data): """ Update session data. """ if self.session: self.session.from_request(data) else: self.session = Session(self, data) def _update_server_version(self): """Decode the Transmission version string, if available.""" if self.server_version is None: version_major = 1 version_minor = 30 version_changeset = 0 version_parser = re.compile('(\d).(\d+) \((\d+)\)') if hasattr(self.session, 'version'): match = version_parser.match(self.session.version) if match: version_major = int(match.group(1)) version_minor = int(match.group(2)) version_changeset = match.group(3) self.server_version = (version_major, version_minor, version_changeset) @property def rpc_version(self): """ Get the Transmission RPC version. Trying to deduct if the server don't have a version value. """ if self.protocol_version is None: # Ugly fix for 2.20 - 2.22 reporting rpc-version 11, but having new arguments if self.server_version and (self.server_version[0] == 2 and self.server_version[1] in [20, 21, 22]): self.protocol_version = 12 # Ugly fix for 2.12 reporting rpc-version 10, but having new arguments elif self.server_version and (self.server_version[0] == 2 and self.server_version[1] == 12): self.protocol_version = 11 elif hasattr(self.session, 'rpc_version'): self.protocol_version = self.session.rpc_version elif hasattr(self.session, 'version'): self.protocol_version = 3 else: self.protocol_version = 2 return self.protocol_version def _rpc_version_warning(self, version): """ Add a warning to the log if the Transmission RPC version is lower then the provided version. """ if self.rpc_version < version: LOGGER.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version)) def add_torrent(self, torrent, timeout=None, **kwargs): """ Add torrent to transfers list. Takes a uri to a torrent or base64 encoded torrent data in ``torrent``. Additional arguments are: ===================== ===== =========== ============================================================= Argument RPC Replaced by Description ===================== ===== =========== ============================================================= ``bandwidthPriority`` 8 - Priority for this transfer. ``cookies`` 13 - One or more HTTP cookie(s). ``download_dir`` 1 - The directory where the downloaded contents will be saved in. ``files_unwanted`` 1 - A list of file id's that shouldn't be downloaded. ``files_wanted`` 1 - A list of file id's that should be downloaded. ``paused`` 1 - If True, does not start the transfer when added. ``peer_limit`` 1 - Maximum number of peers allowed. ``priority_high`` 1 - A list of file id's that should have high priority. ``priority_low`` 1 - A list of file id's that should have low priority. ``priority_normal`` 1 - A list of file id's that should have normal priority. ===================== ===== =========== ============================================================= Returns a Torrent object with the fields. """ if torrent is None: raise ValueError('add_torrent requires data or a URI.') torrent_data = None parsed_uri = urlparse(torrent) if parsed_uri.scheme in ['ftp', 'ftps', 'http', 'https']: # there has been some problem with T's built in torrent fetcher, # use a python one instead torrent_file = urlopen(torrent) torrent_data = torrent_file.read() torrent_data = base64.b64encode(torrent_data).decode('utf-8') if parsed_uri.scheme in ['file']: filepath = torrent # uri decoded different on linux / windows ? if len(parsed_uri.path) > 0: filepath = parsed_uri.path elif len(parsed_uri.netloc) > 0: filepath = parsed_uri.netloc torrent_file = open(filepath, 'rb') torrent_data = torrent_file.read() torrent_data = base64.b64encode(torrent_data).decode('utf-8') if not torrent_data: if torrent.endswith('.torrent') or torrent.startswith('magnet:'): torrent_data = None else: might_be_base64 = False try: # check if this is base64 data if PY3: base64.b64decode(torrent.encode('utf-8')) else: base64.b64decode(torrent) might_be_base64 = True except Exception: pass if might_be_base64: torrent_data = torrent args = {} if torrent_data: args = {'metainfo': torrent_data} else: args = {'filename': torrent} for key, value in iteritems(kwargs): argument = make_rpc_name(key) (arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version) args[arg] = val return list(self._request('torrent-add', args, timeout=timeout).values())[0] def add(self, data, timeout=None, **kwargs): """ .. WARNING:: Deprecated, please use add_torrent. """ args = {} if data: args = {'metainfo': data} elif 'metainfo' not in kwargs and 'filename' not in kwargs: raise ValueError('No torrent data or torrent uri.') for key, value in iteritems(kwargs): argument = make_rpc_name(key) (arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version) args[arg] = val warnings.warn('add has been deprecated, please use add_torrent instead.', DeprecationWarning) return self._request('torrent-add', args, timeout=timeout) def add_uri(self, uri, **kwargs): """ .. WARNING:: Deprecated, please use add_torrent. """ if uri is None: raise ValueError('add_uri requires a URI.') # there has been some problem with T's built in torrent fetcher, # use a python one instead parsed_uri = urlparse(uri) torrent_data = None if parsed_uri.scheme in ['ftp', 'ftps', 'http', 'https']: torrent_file = urlopen(uri) torrent_data = torrent_file.read() torrent_data = base64.b64encode(torrent_data).decode('utf-8') if parsed_uri.scheme in ['file']: filepath = uri # uri decoded different on linux / windows ? if len(parsed_uri.path) > 0: filepath = parsed_uri.path elif len(parsed_uri.netloc) > 0: filepath = parsed_uri.netloc torrent_file = open(filepath, 'rb') torrent_data = torrent_file.read() torrent_data = base64.b64encode(torrent_data).decode('utf-8') warnings.warn('add_uri has been deprecated, please use add_torrent instead.', DeprecationWarning) if torrent_data: return self.add(torrent_data, **kwargs) else: return self.add(None, filename=uri, **kwargs) def remove_torrent(self, ids, delete_data=False, timeout=None): """ remove torrent(s) with provided id(s). Local data is removed if delete_data is True, otherwise not. """ self._rpc_version_warning(3) self._request('torrent-remove', {'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout) def remove(self, ids, delete_data=False, timeout=None): """ .. WARNING:: Deprecated, please use remove_torrent. """ warnings.warn('remove has been deprecated, please use remove_torrent instead.', DeprecationWarning) self.remove_torrent(ids, delete_data, timeout) def start_torrent(self, ids, bypass_queue=False, timeout=None): """Start torrent(s) with provided id(s)""" method = 'torrent-start' if bypass_queue and self.rpc_version >= 14: method = 'torrent-start-now' self._request(method, {}, ids, True, timeout=timeout) def start(self, ids, bypass_queue=False, timeout=None): """ .. WARNING:: Deprecated, please use start_torrent. """ warnings.warn('start has been deprecated, please use start_torrent instead.', DeprecationWarning) self.start_torrent(ids, bypass_queue, timeout) def start_all(self, bypass_queue=False, timeout=None): """Start all torrents respecting the queue order""" torrent_list = self.get_torrents() method = 'torrent-start' if self.rpc_version >= 14: if bypass_queue: method = 'torrent-start-now' torrent_list = sorted(torrent_list, key=operator.attrgetter('queuePosition')) ids = [x.id for x in torrent_list] self._request(method, {}, ids, True, timeout=timeout) def stop_torrent(self, ids, timeout=None): """stop torrent(s) with provided id(s)""" self._request('torrent-stop', {}, ids, True, timeout=timeout) def stop(self, ids, timeout=None): """ .. WARNING:: Deprecated, please use stop_torrent. """ warnings.warn('stop has been deprecated, please use stop_torrent instead.', DeprecationWarning) self.stop_torrent(ids, timeout) def verify_torrent(self, ids, timeout=None): """verify torrent(s) with provided id(s)""" self._request('torrent-verify', {}, ids, True, timeout=timeout) def verify(self, ids, timeout=None): """ .. WARNING:: Deprecated, please use verify_torrent. """ warnings.warn('verify has been deprecated, please use verify_torrent instead.', DeprecationWarning) self.verify_torrent(ids, timeout) def reannounce_torrent(self, ids, timeout=None): """Reannounce torrent(s) with provided id(s)""" self._rpc_version_warning(5) self._request('torrent-reannounce', {}, ids, True, timeout=timeout) def reannounce(self, ids, timeout=None): """ .. WARNING:: Deprecated, please use reannounce_torrent. """ warnings.warn('reannounce has been deprecated, please use reannounce_torrent instead.', DeprecationWarning) self.reannounce_torrent(ids, timeout) def get_torrent(self, torrent_id, arguments=None, timeout=None): """ Get information for torrent with provided id. ``arguments`` contains a list of field names to be returned, when None all fields are requested. See the Torrent class for more information. Returns a Torrent object with the requested fields. """ if not arguments: arguments = self.torrent_get_arguments torrent_id = parse_torrent_id(torrent_id) if torrent_id is None: raise ValueError("Invalid id") result = self._request('torrent-get', {'fields': arguments}, torrent_id, require_ids=True, timeout=timeout) if torrent_id in result: return result[torrent_id] else: for torrent in result.values(): if torrent.hashString == torrent_id: return torrent raise KeyError("Torrent not found in result") def get_torrents(self, ids=None, arguments=None, timeout=None): """ Get information for torrents with provided ids. For more information see get_torrent. Returns a list of Torrent object. """ if not arguments: arguments = self.torrent_get_arguments return list(self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout).values()) def info(self, ids=None, arguments=None, timeout=None): """ .. WARNING:: Deprecated, please use get_torrent or get_torrents. Please note that the return argument has changed in the new methods. info returns a dictionary indexed by torrent id. """ warnings.warn('info has been deprecated, please use get_torrent or get_torrents instead.', DeprecationWarning) if not arguments: arguments = self.torrent_get_arguments return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout) def list(self, timeout=None): """ .. WARNING:: Deprecated, please use get_torrent or get_torrents. Please note that the return argument has changed in the new methods. list returns a dictionary indexed by torrent id. """ warnings.warn('list has been deprecated, please use get_torrent or get_torrents instead.', DeprecationWarning) fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone' , 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver' , 'downloadedEver', 'uploadRatio', 'queuePosition'] return self._request('torrent-get', {'fields': fields}, timeout=timeout) def get_files(self, ids=None, timeout=None): """ Get list of files for provided torrent id(s). If ids is empty, information for all torrents are fetched. This function returns a dictionary for each requested torrent id holding the information about the files. :: { : { : { 'name': , 'size': , 'completed': , 'priority': , 'selected': } ... } ... } """ fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted'] request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout) result = {} for tid, torrent in iteritems(request_result): result[tid] = torrent.files() return result def set_files(self, items, timeout=None): """ Set file properties. Takes a dictionary with similar contents as the result of `get_files`. :: { : { : { 'priority': , 'selected': } ... } ... } """ if not isinstance(items, dict): raise ValueError('Invalid file description') for tid, files in iteritems(items): if not isinstance(files, dict): continue wanted = [] unwanted = [] high = [] normal = [] low = [] for fid, file_desc in iteritems(files): if not isinstance(file_desc, dict): continue if 'selected' in file_desc and file_desc['selected']: wanted.append(fid) else: unwanted.append(fid) if 'priority' in file_desc: if file_desc['priority'] == 'high': high.append(fid) elif file_desc['priority'] == 'normal': normal.append(fid) elif file_desc['priority'] == 'low': low.append(fid) args = { 'timeout': timeout } if len(high) > 0: args['priority_high'] = high if len(normal) > 0: args['priority_normal'] = normal if len(low) > 0: args['priority_low'] = low if len(wanted) > 0: args['files_wanted'] = wanted if len(unwanted) > 0: args['files_unwanted'] = unwanted self.change_torrent([tid], **args) def change_torrent(self, ids, timeout=None, **kwargs): """ Change torrent parameters for the torrent(s) with the supplied id's. The parameters are: ============================ ===== =============== ======================================================================================= Argument RPC Replaced by Description ============================ ===== =============== ======================================================================================= ``bandwidthPriority`` 5 - Priority for this transfer. ``downloadLimit`` 5 - Set the speed limit for download in Kib/s. ``downloadLimited`` 5 - Enable download speed limiter. ``files_unwanted`` 1 - A list of file id's that shouldn't be downloaded. ``files_wanted`` 1 - A list of file id's that should be downloaded. ``honorsSessionLimits`` 5 - Enables or disables the transfer to honour the upload limit set in the session. ``location`` 1 - Local download location. ``peer_limit`` 1 - The peer limit for the torrents. ``priority_high`` 1 - A list of file id's that should have high priority. ``priority_low`` 1 - A list of file id's that should have normal priority. ``priority_normal`` 1 - A list of file id's that should have low priority. ``queuePosition`` 14 - Position of this transfer in its queue. ``seedIdleLimit`` 10 - Seed inactivity limit in minutes. ``seedIdleMode`` 10 - Seed inactivity mode. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit. ``seedRatioLimit`` 5 - Seeding ratio. ``seedRatioMode`` 5 - Which ratio to use. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit. ``speed_limit_down`` 1 - 5 downloadLimit Set the speed limit for download in Kib/s. ``speed_limit_down_enabled`` 1 - 5 downloadLimited Enable download speed limiter. ``speed_limit_up`` 1 - 5 uploadLimit Set the speed limit for upload in Kib/s. ``speed_limit_up_enabled`` 1 - 5 uploadLimited Enable upload speed limiter. ``trackerAdd`` 10 - Array of string with announce URLs to add. ``trackerRemove`` 10 - Array of ids of trackers to remove. ``trackerReplace`` 10 - Array of (id, url) tuples where the announce URL should be replaced. ``uploadLimit`` 5 - Set the speed limit for upload in Kib/s. ``uploadLimited`` 5 - Enable upload speed limiter. ============================ ===== =============== ======================================================================================= .. NOTE:: transmissionrpc will try to automatically fix argument errors. """ args = {} for key, value in iteritems(kwargs): argument = make_rpc_name(key) (arg, val) = argument_value_convert('torrent-set' , argument, value, self.rpc_version) args[arg] = val if len(args) > 0: self._request('torrent-set', args, ids, True, timeout=timeout) else: ValueError("No arguments to set") def change(self, ids, timeout=None, **kwargs): """ .. WARNING:: Deprecated, please use change_torrent. """ warnings.warn('change has been deprecated, please use change_torrent instead.', DeprecationWarning) self.change_torrent(ids, timeout, **kwargs) def move_torrent_data(self, ids, location, timeout=None): """Move torrent data to the new location.""" self._rpc_version_warning(6) args = {'location': location, 'move': True} self._request('torrent-set-location', args, ids, True, timeout=timeout) def move(self, ids, location, timeout=None): """ .. WARNING:: Deprecated, please use move_torrent_data. """ warnings.warn('move has been deprecated, please use move_torrent_data instead.', DeprecationWarning) self.move_torrent_data(ids, location, timeout) def locate_torrent_data(self, ids, location, timeout=None): """Locate torrent data at the provided location.""" self._rpc_version_warning(6) args = {'location': location, 'move': False} self._request('torrent-set-location', args, ids, True, timeout=timeout) def locate(self, ids, location, timeout=None): """ .. WARNING:: Deprecated, please use locate_torrent_data. """ warnings.warn('locate has been deprecated, please use locate_torrent_data instead.', DeprecationWarning) self.locate_torrent_data(ids, location, timeout) def rename_torrent_path(self, torrent_id, location, name, timeout=None): """ Rename directory and/or files for torrent. Remember to use get_torrent or get_torrents to update your file information. """ self._rpc_version_warning(15) torrent_id = parse_torrent_id(torrent_id) if torrent_id is None: raise ValueError("Invalid id") dirname = os.path.dirname(name) if len(dirname) > 0: raise ValueError("Target name cannot contain a path delimiter") args = {'path': location, 'name': name} result = self._request('torrent-rename-path', args, torrent_id, True, timeout=timeout) return (result['path'], result['name']) def queue_top(self, ids, timeout=None): """Move transfer to the top of the queue.""" self._rpc_version_warning(14) self._request('queue-move-top', ids=ids, require_ids=True, timeout=timeout) def queue_bottom(self, ids, timeout=None): """Move transfer to the bottom of the queue.""" self._rpc_version_warning(14) self._request('queue-move-bottom', ids=ids, require_ids=True, timeout=timeout) def queue_up(self, ids, timeout=None): """Move transfer up in the queue.""" self._rpc_version_warning(14) self._request('queue-move-up', ids=ids, require_ids=True, timeout=timeout) def queue_down(self, ids, timeout=None): """Move transfer down in the queue.""" self._rpc_version_warning(14) self._request('queue-move-down', ids=ids, require_ids=True, timeout=timeout) def get_session(self, timeout=None): """ Get session parameters. See the Session class for more information. """ self._request('session-get', timeout=timeout) self._update_server_version() return self.session def set_session(self, timeout=None, **kwargs): """ Set session parameters. The parameters are: ================================ ===== ================= ========================================================================================================================== Argument RPC Replaced by Description ================================ ===== ================= ========================================================================================================================== ``alt_speed_down`` 5 - Alternate session download speed limit (in Kib/s). ``alt_speed_enabled`` 5 - Enables alternate global download speed limiter. ``alt_speed_time_begin`` 5 - Time when alternate speeds should be enabled. Minutes after midnight. ``alt_speed_time_day`` 5 - Enables alternate speeds scheduling these days. ``alt_speed_time_enabled`` 5 - Enables alternate speeds scheduling. ``alt_speed_time_end`` 5 - Time when alternate speeds should be disabled. Minutes after midnight. ``alt_speed_up`` 5 - Alternate session upload speed limit (in Kib/s). ``blocklist_enabled`` 5 - Enables the block list ``blocklist_url`` 11 - Location of the block list. Updated with blocklist-update. ``cache_size_mb`` 10 - The maximum size of the disk cache in MB ``dht_enabled`` 6 - Enables DHT. ``download_dir`` 1 - Set the session download directory. ``download_queue_enabled`` 14 - Enables download queue. ``download_queue_size`` 14 - Number of slots in the download queue. ``encryption`` 1 - Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``. ``idle_seeding_limit`` 10 - The default seed inactivity limit in minutes. ``idle_seeding_limit_enabled`` 10 - Enables the default seed inactivity limit ``incomplete_dir`` 7 - The path to the directory of incomplete transfer data. ``incomplete_dir_enabled`` 7 - Enables the incomplete transfer data directory. Otherwise data for incomplete transfers are stored in the download target. ``lpd_enabled`` 9 - Enables local peer discovery for public torrents. ``peer_limit`` 1 - 5 peer-limit-global Maximum number of peers. ``peer_limit_global`` 5 - Maximum number of peers. ``peer_limit_per_torrent`` 5 - Maximum number of peers per transfer. ``peer_port`` 5 - Peer port. ``peer_port_random_on_start`` 5 - Enables randomized peer port on start of Transmission. ``pex_allowed`` 1 - 5 pex-enabled Allowing PEX in public torrents. ``pex_enabled`` 5 - Allowing PEX in public torrents. ``port`` 1 - 5 peer-port Peer port. ``port_forwarding_enabled`` 1 - Enables port forwarding. ``queue_stalled_enabled`` 14 - Enable tracking of stalled transfers. ``queue_stalled_minutes`` 14 - Number of minutes of idle that marks a transfer as stalled. ``rename_partial_files`` 8 - Appends ".part" to incomplete files ``script_torrent_done_enabled`` 9 - Whether or not to call the "done" script. ``script_torrent_done_filename`` 9 - Filename of the script to run when the transfer is done. ``seed_queue_enabled`` 14 - Enables upload queue. ``seed_queue_size`` 14 - Number of slots in the upload queue. ``seedRatioLimit`` 5 - Seed ratio limit. 1.0 means 1:1 download and upload ratio. ``seedRatioLimited`` 5 - Enables seed ration limit. ``speed_limit_down`` 1 - Download speed limit (in Kib/s). ``speed_limit_down_enabled`` 1 - Enables download speed limiting. ``speed_limit_up`` 1 - Upload speed limit (in Kib/s). ``speed_limit_up_enabled`` 1 - Enables upload speed limiting. ``start_added_torrents`` 9 - Added torrents will be started right away. ``trash_original_torrent_files`` 9 - The .torrent file of added torrents will be deleted. ``utp_enabled`` 13 - Enables Micro Transport Protocol (UTP). ================================ ===== ================= ========================================================================================================================== .. NOTE:: transmissionrpc will try to automatically fix argument errors. """ args = {} for key, value in iteritems(kwargs): if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']: raise ValueError('Invalid encryption value') argument = make_rpc_name(key) (arg, val) = argument_value_convert('session-set' , argument, value, self.rpc_version) args[arg] = val if len(args) > 0: self._request('session-set', args, timeout=timeout) def blocklist_update(self, timeout=None): """Update block list. Returns the size of the block list.""" self._rpc_version_warning(5) result = self._request('blocklist-update', timeout=timeout) if 'blocklist-size' in result: return result['blocklist-size'] return None def port_test(self, timeout=None): """ Tests to see if your incoming peer port is accessible from the outside world. """ self._rpc_version_warning(5) result = self._request('port-test', timeout=timeout) if 'port-is-open' in result: return result['port-is-open'] return None def free_space(self, path, timeout=None): """ Get the ammount of free space (in bytes) at the provided location. """ self._rpc_version_warning(15) result = self._request('free-space', {'path': path}, timeout=timeout) if result['path'] == path: return result['size-bytes'] return None def session_stats(self, timeout=None): """Get session statistics""" self._request('session-stats', timeout=timeout) return self.session blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/constants.py0000755000000000000000000006722112227066501023713 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. import logging from six import iteritems LOGGER = logging.getLogger('transmissionrpc') LOGGER.setLevel(logging.ERROR) def mirror_dict(source): """ Creates a dictionary with all values as keys and all keys as values. """ source.update(dict((value, key) for key, value in iteritems(source))) return source DEFAULT_PORT = 9091 DEFAULT_TIMEOUT = 30.0 TR_PRI_LOW = -1 TR_PRI_NORMAL = 0 TR_PRI_HIGH = 1 PRIORITY = mirror_dict({ 'low' : TR_PRI_LOW, 'normal' : TR_PRI_NORMAL, 'high' : TR_PRI_HIGH }) TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio RATIO_LIMIT = mirror_dict({ 'global' : TR_RATIOLIMIT_GLOBAL, 'single' : TR_RATIOLIMIT_SINGLE, 'unlimited' : TR_RATIOLIMIT_UNLIMITED }) TR_IDLELIMIT_GLOBAL = 0 # follow the global settings TR_IDLELIMIT_SINGLE = 1 # override the global settings, seeding until a certain idle time TR_IDLELIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of activity IDLE_LIMIT = mirror_dict({ 'global' : TR_RATIOLIMIT_GLOBAL, 'single' : TR_RATIOLIMIT_SINGLE, 'unlimited' : TR_RATIOLIMIT_UNLIMITED }) # A note on argument maps # These maps are used to verify *-set methods. The information is structured in # a tree. # set +- - [, , , , , ] # | +- - [, , , , , ] # | # get +- - [, , , , , ] # +- - [, , , , , ] # Arguments for torrent methods TORRENT_ARGS = { 'get' : { 'activityDate': ('number', 1, None, None, None, 'Last time of upload or download activity.'), 'addedDate': ('number', 1, None, None, None, 'The date when this torrent was first added.'), 'announceResponse': ('string', 1, 7, None, None, 'The announce message from the tracker.'), 'announceURL': ('string', 1, 7, None, None, 'Current announce URL.'), 'bandwidthPriority': ('number', 5, None, None, None, 'Bandwidth priority. Low (-1), Normal (0) or High (1).'), 'comment': ('string', 1, None, None, None, 'Torrent comment.'), 'corruptEver': ('number', 1, None, None, None, 'Number of bytes of corrupt data downloaded.'), 'creator': ('string', 1, None, None, None, 'Torrent creator.'), 'dateCreated': ('number', 1, None, None, None, 'Torrent creation date.'), 'desiredAvailable': ('number', 1, None, None, None, 'Number of bytes avalable and left to be downloaded.'), 'doneDate': ('number', 1, None, None, None, 'The date when the torrent finished downloading.'), 'downloadDir': ('string', 4, None, None, None, 'The directory path where the torrent is downloaded to.'), 'downloadedEver': ('number', 1, None, None, None, 'Number of bytes of good data downloaded.'), 'downloaders': ('number', 4, 7, None, None, 'Number of downloaders.'), 'downloadLimit': ('number', 1, None, None, None, 'Download limit in Kbps.'), 'downloadLimited': ('boolean', 5, None, None, None, 'Download limit is enabled'), 'downloadLimitMode': ('number', 1, 5, None, None, 'Download limit mode. 0 means global, 1 means signle, 2 unlimited.'), 'error': ('number', 1, None, None, None, 'Kind of error. 0 means OK, 1 means tracker warning, 2 means tracker error, 3 means local error.'), 'errorString': ('number', 1, None, None, None, 'Error message.'), 'eta': ('number', 1, None, None, None, 'Estimated number of seconds left when downloading or seeding. -1 means not available and -2 means unknown.'), 'etaIdle': ('number', 15, None, None, None, 'Estimated number of seconds left until the idle time limit is reached. -1 means not available and -2 means unknown.'), 'files': ('array', 1, None, None, None, 'Array of file object containing key, bytesCompleted, length and name.'), 'fileStats': ('array', 5, None, None, None, 'Aray of file statistics containing bytesCompleted, wanted and priority.'), 'hashString': ('string', 1, None, None, None, 'Hashstring unique for the torrent even between sessions.'), 'haveUnchecked': ('number', 1, None, None, None, 'Number of bytes of partial pieces.'), 'haveValid': ('number', 1, None, None, None, 'Number of bytes of checksum verified data.'), 'honorsSessionLimits': ('boolean', 5, None, None, None, 'True if session upload limits are honored'), 'id': ('number', 1, None, None, None, 'Session unique torrent id.'), 'isFinished': ('boolean', 9, None, None, None, 'True if the torrent is finished. Downloaded and seeded.'), 'isPrivate': ('boolean', 1, None, None, None, 'True if the torrent is private.'), 'isStalled': ('boolean', 14, None, None, None, 'True if the torrent has stalled (been idle for a long time).'), 'lastAnnounceTime': ('number', 1, 7, None, None, 'The time of the last announcement.'), 'lastScrapeTime': ('number', 1, 7, None, None, 'The time af the last successful scrape.'), 'leechers': ('number', 1, 7, None, None, 'Number of leechers.'), 'leftUntilDone': ('number', 1, None, None, None, 'Number of bytes left until the download is done.'), 'magnetLink': ('string', 7, None, None, None, 'The magnet link for this torrent.'), 'manualAnnounceTime': ('number', 1, None, None, None, 'The time until you manually ask for more peers.'), 'maxConnectedPeers': ('number', 1, None, None, None, 'Maximum of connected peers.'), 'metadataPercentComplete': ('number', 7, None, None, None, 'Download progress of metadata. 0.0 to 1.0.'), 'name': ('string', 1, None, None, None, 'Torrent name.'), 'nextAnnounceTime': ('number', 1, 7, None, None, 'Next announce time.'), 'nextScrapeTime': ('number', 1, 7, None, None, 'Next scrape time.'), 'peer-limit': ('number', 5, None, None, None, 'Maximum number of peers.'), 'peers': ('array', 2, None, None, None, 'Array of peer objects.'), 'peersConnected': ('number', 1, None, None, None, 'Number of peers we are connected to.'), 'peersFrom': ('object', 1, None, None, None, 'Object containing download peers counts for different peer types.'), 'peersGettingFromUs': ('number', 1, None, None, None, 'Number of peers we are sending data to.'), 'peersKnown': ('number', 1, 13, None, None, 'Number of peers that the tracker knows.'), 'peersSendingToUs': ('number', 1, None, None, None, 'Number of peers sending to us'), 'percentDone': ('double', 5, None, None, None, 'Download progress of selected files. 0.0 to 1.0.'), 'pieces': ('string', 5, None, None, None, 'String with base64 encoded bitfield indicating finished pieces.'), 'pieceCount': ('number', 1, None, None, None, 'Number of pieces.'), 'pieceSize': ('number', 1, None, None, None, 'Number of bytes in a piece.'), 'priorities': ('array', 1, None, None, None, 'Array of file priorities.'), 'queuePosition': ('number', 14, None, None, None, 'The queue position.'), 'rateDownload': ('number', 1, None, None, None, 'Download rate in bps.'), 'rateUpload': ('number', 1, None, None, None, 'Upload rate in bps.'), 'recheckProgress': ('double', 1, None, None, None, 'Progress of recheck. 0.0 to 1.0.'), 'secondsDownloading': ('number', 15, None, None, None, ''), 'secondsSeeding': ('number', 15, None, None, None, ''), 'scrapeResponse': ('string', 1, 7, None, None, 'Scrape response message.'), 'scrapeURL': ('string', 1, 7, None, None, 'Current scrape URL'), 'seeders': ('number', 1, 7, None, None, 'Number of seeders reported by the tracker.'), 'seedIdleLimit': ('number', 10, None, None, None, 'Idle limit in minutes.'), 'seedIdleMode': ('number', 10, None, None, None, 'Use global (0), torrent (1), or unlimited (2) limit.'), 'seedRatioLimit': ('double', 5, None, None, None, 'Seed ratio limit.'), 'seedRatioMode': ('number', 5, None, None, None, 'Use global (0), torrent (1), or unlimited (2) limit.'), 'sizeWhenDone': ('number', 1, None, None, None, 'Size of the torrent download in bytes.'), 'startDate': ('number', 1, None, None, None, 'The date when the torrent was last started.'), 'status': ('number', 1, None, None, None, 'Current status, see source'), 'swarmSpeed': ('number', 1, 7, None, None, 'Estimated speed in Kbps in the swarm.'), 'timesCompleted': ('number', 1, 7, None, None, 'Number of successful downloads reported by the tracker.'), 'trackers': ('array', 1, None, None, None, 'Array of tracker objects.'), 'trackerStats': ('object', 7, None, None, None, 'Array of object containing tracker statistics.'), 'totalSize': ('number', 1, None, None, None, 'Total size of the torrent in bytes'), 'torrentFile': ('string', 5, None, None, None, 'Path to .torrent file.'), 'uploadedEver': ('number', 1, None, None, None, 'Number of bytes uploaded, ever.'), 'uploadLimit': ('number', 1, None, None, None, 'Upload limit in Kbps'), 'uploadLimitMode': ('number', 1, 5, None, None, 'Upload limit mode. 0 means global, 1 means signle, 2 unlimited.'), 'uploadLimited': ('boolean', 5, None, None, None, 'Upload limit enabled.'), 'uploadRatio': ('double', 1, None, None, None, 'Seed ratio.'), 'wanted': ('array', 1, None, None, None, 'Array of booleans indicated wanted files.'), 'webseeds': ('array', 1, None, None, None, 'Array of webseeds objects'), 'webseedsSendingToUs': ('number', 1, None, None, None, 'Number of webseeds seeding to us.'), }, 'set': { 'bandwidthPriority': ('number', 5, None, None, None, 'Priority for this transfer.'), 'downloadLimit': ('number', 5, None, 'speed-limit-down', None, 'Set the speed limit for download in Kib/s.'), 'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None, 'Enable download speed limiter.'), 'files-wanted': ('array', 1, None, None, None, "A list of file id's that should be downloaded."), 'files-unwanted': ('array', 1, None, None, None, "A list of file id's that shouldn't be downloaded."), 'honorsSessionLimits': ('boolean', 5, None, None, None, "Enables or disables the transfer to honour the upload limit set in the session."), 'location': ('array', 1, None, None, None, 'Local download location.'), 'peer-limit': ('number', 1, None, None, None, 'The peer limit for the torrents.'), 'priority-high': ('array', 1, None, None, None, "A list of file id's that should have high priority."), 'priority-low': ('array', 1, None, None, None, "A list of file id's that should have normal priority."), 'priority-normal': ('array', 1, None, None, None, "A list of file id's that should have low priority."), 'queuePosition': ('number', 14, None, None, None, 'Position of this transfer in its queue.'), 'seedIdleLimit': ('number', 10, None, None, None, 'Seed inactivity limit in minutes.'), 'seedIdleMode': ('number', 10, None, None, None, 'Seed inactivity mode. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.'), 'seedRatioLimit': ('double', 5, None, None, None, 'Seeding ratio.'), 'seedRatioMode': ('number', 5, None, None, None, 'Which ratio to use. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.'), 'speed-limit-down': ('number', 1, 5, None, 'downloadLimit', 'Set the speed limit for download in Kib/s.'), 'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited', 'Enable download speed limiter.'), 'speed-limit-up': ('number', 1, 5, None, 'uploadLimit', 'Set the speed limit for upload in Kib/s.'), 'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited', 'Enable upload speed limiter.'), 'trackerAdd': ('array', 10, None, None, None, 'Array of string with announce URLs to add.'), 'trackerRemove': ('array', 10, None, None, None, 'Array of ids of trackers to remove.'), 'trackerReplace': ('array', 10, None, None, None, 'Array of (id, url) tuples where the announce URL should be replaced.'), 'uploadLimit': ('number', 5, None, 'speed-limit-up', None, 'Set the speed limit for upload in Kib/s.'), 'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None, 'Enable upload speed limiter.'), }, 'add': { 'bandwidthPriority': ('number', 8, None, None, None, 'Priority for this transfer.'), 'download-dir': ('string', 1, None, None, None, 'The directory where the downloaded contents will be saved in.'), 'cookies': ('string', 13, None, None, None, 'One or more HTTP cookie(s).'), 'filename': ('string', 1, None, None, None, "A file path or URL to a torrent file or a magnet link."), 'files-wanted': ('array', 1, None, None, None, "A list of file id's that should be downloaded."), 'files-unwanted': ('array', 1, None, None, None, "A list of file id's that shouldn't be downloaded."), 'metainfo': ('string', 1, None, None, None, 'The content of a torrent file, base64 encoded.'), 'paused': ('boolean', 1, None, None, None, 'If True, does not start the transfer when added.'), 'peer-limit': ('number', 1, None, None, None, 'Maximum number of peers allowed.'), 'priority-high': ('array', 1, None, None, None, "A list of file id's that should have high priority."), 'priority-low': ('array', 1, None, None, None, "A list of file id's that should have low priority."), 'priority-normal': ('array', 1, None, None, None, "A list of file id's that should have normal priority."), } } # Arguments for session methods SESSION_ARGS = { 'get': { "alt-speed-down": ('number', 5, None, None, None, 'Alternate session download speed limit (in Kib/s).'), "alt-speed-enabled": ('boolean', 5, None, None, None, 'True if alternate global download speed limiter is ebabled.'), "alt-speed-time-begin": ('number', 5, None, None, None, 'Time when alternate speeds should be enabled. Minutes after midnight.'), "alt-speed-time-enabled": ('boolean', 5, None, None, None, 'True if alternate speeds scheduling is enabled.'), "alt-speed-time-end": ('number', 5, None, None, None, 'Time when alternate speeds should be disabled. Minutes after midnight.'), "alt-speed-time-day": ('number', 5, None, None, None, 'Days alternate speeds scheduling is enabled.'), "alt-speed-up": ('number', 5, None, None, None, 'Alternate session upload speed limit (in Kib/s)'), "blocklist-enabled": ('boolean', 5, None, None, None, 'True when blocklist is enabled.'), "blocklist-size": ('number', 5, None, None, None, 'Number of rules in the blocklist'), "blocklist-url": ('string', 11, None, None, None, 'Location of the block list. Updated with blocklist-update.'), "cache-size-mb": ('number', 10, None, None, None, 'The maximum size of the disk cache in MB'), "config-dir": ('string', 8, None, None, None, 'location of transmissions configuration directory'), "dht-enabled": ('boolean', 6, None, None, None, 'True if DHT enabled.'), "download-dir": ('string', 1, None, None, None, 'The download directory.'), "download-dir-free-space": ('number', 12, None, None, None, 'Free space in the download directory, in bytes'), "download-queue-size": ('number', 14, None, None, None, 'Number of slots in the download queue.'), "download-queue-enabled": ('boolean', 14, None, None, None, 'True if the download queue is enabled.'), "encryption": ('string', 1, None, None, None, 'Encryption mode, one of ``required``, ``preferred`` or ``tolerated``.'), "idle-seeding-limit": ('number', 10, None, None, None, 'Seed inactivity limit in minutes.'), "idle-seeding-limit-enabled": ('boolean', 10, None, None, None, 'True if the seed activity limit is enabled.'), "incomplete-dir": ('string', 7, None, None, None, 'The path to the directory for incomplete torrent transfer data.'), "incomplete-dir-enabled": ('boolean', 7, None, None, None, 'True if the incomplete dir is enabled.'), "lpd-enabled": ('boolean', 9, None, None, None, 'True if local peer discovery is enabled.'), "peer-limit": ('number', 1, 5, None, 'peer-limit-global', 'Maximum number of peers.'), "peer-limit-global": ('number', 5, None, 'peer-limit', None, 'Maximum number of peers.'), "peer-limit-per-torrent": ('number', 5, None, None, None, 'Maximum number of peers per transfer.'), "pex-allowed": ('boolean', 1, 5, None, 'pex-enabled', 'True if PEX is allowed.'), "pex-enabled": ('boolean', 5, None, 'pex-allowed', None, 'True if PEX is enabled.'), "port": ('number', 1, 5, None, 'peer-port', 'Peer port.'), "peer-port": ('number', 5, None, 'port', None, 'Peer port.'), "peer-port-random-on-start": ('boolean', 5, None, None, None, 'Enables randomized peer port on start of Transmission.'), "port-forwarding-enabled": ('boolean', 1, None, None, None, 'True if port forwarding is enabled.'), "queue-stalled-minutes": ('number', 14, None, None, None, 'Number of minutes of idle that marks a transfer as stalled.'), "queue-stalled-enabled": ('boolean', 14, None, None, None, 'True if stalled tracking of transfers is enabled.'), "rename-partial-files": ('boolean', 8, None, None, None, 'True if ".part" is appended to incomplete files'), "rpc-version": ('number', 4, None, None, None, 'Transmission RPC API Version.'), "rpc-version-minimum": ('number', 4, None, None, None, 'Minimum accepted RPC API Version.'), "script-torrent-done-enabled": ('boolean', 9, None, None, None, 'True if the done script is enabled.'), "script-torrent-done-filename": ('string', 9, None, None, None, 'Filename of the script to run when the transfer is done.'), "seedRatioLimit": ('double', 5, None, None, None, 'Seed ratio limit. 1.0 means 1:1 download and upload ratio.'), "seedRatioLimited": ('boolean', 5, None, None, None, 'True if seed ration limit is enabled.'), "seed-queue-size": ('number', 14, None, None, None, 'Number of slots in the upload queue.'), "seed-queue-enabled": ('boolean', 14, None, None, None, 'True if upload queue is enabled.'), "speed-limit-down": ('number', 1, None, None, None, 'Download speed limit (in Kib/s).'), "speed-limit-down-enabled": ('boolean', 1, None, None, None, 'True if the download speed is limited.'), "speed-limit-up": ('number', 1, None, None, None, 'Upload speed limit (in Kib/s).'), "speed-limit-up-enabled": ('boolean', 1, None, None, None, 'True if the upload speed is limited.'), "start-added-torrents": ('boolean', 9, None, None, None, 'When true uploaded torrents will start right away.'), "trash-original-torrent-files": ('boolean', 9, None, None, None, 'When true added .torrent files will be deleted.'), 'units': ('object', 10, None, None, None, 'An object containing units for size and speed.'), 'utp-enabled': ('boolean', 13, None, None, None, 'True if Micro Transport Protocol (UTP) is enabled.'), "version": ('string', 3, None, None, None, 'Transmission version.'), }, 'set': { "alt-speed-down": ('number', 5, None, None, None, 'Alternate session download speed limit (in Kib/s).'), "alt-speed-enabled": ('boolean', 5, None, None, None, 'Enables alternate global download speed limiter.'), "alt-speed-time-begin": ('number', 5, None, None, None, 'Time when alternate speeds should be enabled. Minutes after midnight.'), "alt-speed-time-enabled": ('boolean', 5, None, None, None, 'Enables alternate speeds scheduling.'), "alt-speed-time-end": ('number', 5, None, None, None, 'Time when alternate speeds should be disabled. Minutes after midnight.'), "alt-speed-time-day": ('number', 5, None, None, None, 'Enables alternate speeds scheduling these days.'), "alt-speed-up": ('number', 5, None, None, None, 'Alternate session upload speed limit (in Kib/s).'), "blocklist-enabled": ('boolean', 5, None, None, None, 'Enables the block list'), "blocklist-url": ('string', 11, None, None, None, 'Location of the block list. Updated with blocklist-update.'), "cache-size-mb": ('number', 10, None, None, None, 'The maximum size of the disk cache in MB'), "dht-enabled": ('boolean', 6, None, None, None, 'Enables DHT.'), "download-dir": ('string', 1, None, None, None, 'Set the session download directory.'), "download-queue-size": ('number', 14, None, None, None, 'Number of slots in the download queue.'), "download-queue-enabled": ('boolean', 14, None, None, None, 'Enables download queue.'), "encryption": ('string', 1, None, None, None, 'Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``.'), "idle-seeding-limit": ('number', 10, None, None, None, 'The default seed inactivity limit in minutes.'), "idle-seeding-limit-enabled": ('boolean', 10, None, None, None, 'Enables the default seed inactivity limit'), "incomplete-dir": ('string', 7, None, None, None, 'The path to the directory of incomplete transfer data.'), "incomplete-dir-enabled": ('boolean', 7, None, None, None, 'Enables the incomplete transfer data directory. Otherwise data for incomplete transfers are stored in the download target.'), "lpd-enabled": ('boolean', 9, None, None, None, 'Enables local peer discovery for public torrents.'), "peer-limit": ('number', 1, 5, None, 'peer-limit-global', 'Maximum number of peers.'), "peer-limit-global": ('number', 5, None, 'peer-limit', None, 'Maximum number of peers.'), "peer-limit-per-torrent": ('number', 5, None, None, None, 'Maximum number of peers per transfer.'), "pex-allowed": ('boolean', 1, 5, None, 'pex-enabled', 'Allowing PEX in public torrents.'), "pex-enabled": ('boolean', 5, None, 'pex-allowed', None, 'Allowing PEX in public torrents.'), "port": ('number', 1, 5, None, 'peer-port', 'Peer port.'), "peer-port": ('number', 5, None, 'port', None, 'Peer port.'), "peer-port-random-on-start": ('boolean', 5, None, None, None, 'Enables randomized peer port on start of Transmission.'), "port-forwarding-enabled": ('boolean', 1, None, None, None, 'Enables port forwarding.'), "rename-partial-files": ('boolean', 8, None, None, None, 'Appends ".part" to incomplete files'), "queue-stalled-minutes": ('number', 14, None, None, None, 'Number of minutes of idle that marks a transfer as stalled.'), "queue-stalled-enabled": ('boolean', 14, None, None, None, 'Enable tracking of stalled transfers.'), "script-torrent-done-enabled": ('boolean', 9, None, None, None, 'Whether or not to call the "done" script.'), "script-torrent-done-filename": ('string', 9, None, None, None, 'Filename of the script to run when the transfer is done.'), "seed-queue-size": ('number', 14, None, None, None, 'Number of slots in the upload queue.'), "seed-queue-enabled": ('boolean', 14, None, None, None, 'Enables upload queue.'), "seedRatioLimit": ('double', 5, None, None, None, 'Seed ratio limit. 1.0 means 1:1 download and upload ratio.'), "seedRatioLimited": ('boolean', 5, None, None, None, 'Enables seed ration limit.'), "speed-limit-down": ('number', 1, None, None, None, 'Download speed limit (in Kib/s).'), "speed-limit-down-enabled": ('boolean', 1, None, None, None, 'Enables download speed limiting.'), "speed-limit-up": ('number', 1, None, None, None, 'Upload speed limit (in Kib/s).'), "speed-limit-up-enabled": ('boolean', 1, None, None, None, 'Enables upload speed limiting.'), "start-added-torrents": ('boolean', 9, None, None, None, 'Added torrents will be started right away.'), "trash-original-torrent-files": ('boolean', 9, None, None, None, 'The .torrent file of added torrents will be deleted.'), 'utp-enabled': ('boolean', 13, None, None, None, 'Enables Micro Transport Protocol (UTP).'), }, } blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/error.py0000644000000000000000000000360712227066501023023 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. from six import string_types, integer_types class TransmissionError(Exception): """ This exception is raised when there has occurred an error related to communication with Transmission. It is a subclass of Exception. """ def __init__(self, message='', original=None): Exception.__init__(self) self.message = message self.original = original def __str__(self): if self.original: original_name = type(self.original).__name__ return '%s Original exception: %s, "%s"' % (self.message, original_name, str(self.original)) else: return self.message class HTTPHandlerError(Exception): """ This exception is raised when there has occurred an error related to the HTTP handler. It is a subclass of Exception. """ def __init__(self, httpurl=None, httpcode=None, httpmsg=None, httpheaders=None, httpdata=None): Exception.__init__(self) self.url = '' self.code = 600 self.message = '' self.headers = {} self.data = '' if isinstance(httpurl, string_types): self.url = httpurl if isinstance(httpcode, integer_types): self.code = httpcode if isinstance(httpmsg, string_types): self.message = httpmsg if isinstance(httpheaders, dict): self.headers = httpheaders if isinstance(httpdata, string_types): self.data = httpdata def __repr__(self): return '' % (self.code, self.message) def __str__(self): return 'HTTPHandlerError %d: %s' % (self.code, self.message) def __unicode__(self): return 'HTTPHandlerError %d: %s' % (self.code, self.message) blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/httphandler.py0000755000000000000000000000673612227066501024220 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2011-2013 Erik Svensson # Licensed under the MIT license. import sys from transmissionrpc.error import HTTPHandlerError from six import PY3 if PY3: from urllib.request import Request, build_opener, \ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, HTTPDigestAuthHandler from urllib.error import HTTPError, URLError from http.client import BadStatusLine else: from urllib2 import Request, build_opener, \ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, HTTPDigestAuthHandler from urllib2 import HTTPError, URLError from httplib import BadStatusLine class HTTPHandler(object): """ Prototype for HTTP handling. """ def set_authentication(self, uri, login, password): """ Transmission use basic authentication in earlier versions and digest authentication in later versions. * uri, the authentication realm URI. * login, the authentication login. * password, the authentication password. """ raise NotImplementedError("Bad HTTPHandler, failed to implement set_authentication.") def request(self, url, query, headers, timeout): """ Implement a HTTP POST request here. * url, The URL to request. * query, The query data to send. This is a JSON data string. * headers, a dictionary of headers to send. * timeout, requested request timeout in seconds. """ raise NotImplementedError("Bad HTTPHandler, failed to implement request.") class DefaultHTTPHandler(HTTPHandler): """ The default HTTP handler provided with transmissionrpc. """ def __init__(self): HTTPHandler.__init__(self) self.http_opener = build_opener() def set_authentication(self, uri, login, password): password_manager = HTTPPasswordMgrWithDefaultRealm() password_manager.add_password(realm=None, uri=uri, user=login, passwd=password) self.http_opener = build_opener(HTTPBasicAuthHandler(password_manager), HTTPDigestAuthHandler(password_manager)) def request(self, url, query, headers, timeout): request = Request(url, query.encode('utf-8'), headers) try: if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2: response = self.http_opener.open(request, timeout=timeout) else: response = self.http_opener.open(request) except HTTPError as error: if error.fp is None: raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs)) else: raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs), error.read()) except URLError as error: # urllib2.URLError documentation is horrendous! # Try to get the tuple arguments of URLError if hasattr(error.reason, 'args') and isinstance(error.reason.args, tuple) and len(error.reason.args) == 2: raise HTTPHandlerError(httpcode=error.reason.args[0], httpmsg=error.reason.args[1]) else: raise HTTPHandlerError(httpmsg='urllib2.URLError: %s' % (error.reason)) except BadStatusLine as error: raise HTTPHandlerError(httpmsg='httplib.BadStatusLine: %s' % (error.line)) return response.read().decode('utf-8') blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/session.py0000755000000000000000000000737512227066501023366 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. from transmissionrpc.utils import Field from six import iteritems, integer_types class Session(object): """ Session is a class holding the session data for a Transmission daemon. Access the session field can be done through attributes. The attributes available are the same as the session arguments in the Transmission RPC specification, but with underscore instead of hyphen. ``download-dir`` -> ``download_dir``. """ def __init__(self, client=None, fields=None): self._client = client self._fields = {} if fields is not None: self._update_fields(fields) def __getattr__(self, name): try: return self._fields[name].value except KeyError: raise AttributeError('No attribute %s' % name) def __str__(self): text = '' for key in sorted(self._fields.keys()): text += "% 32s: %s\n" % (key[-32:], self._fields[key].value) return text def _update_fields(self, other): """ Update the session data from a Transmission JSON-RPC arguments dictionary """ if isinstance(other, dict): for key, value in iteritems(other): self._fields[key.replace('-', '_')] = Field(value, False) elif isinstance(other, Session): for key in list(other._fields.keys()): self._fields[key] = Field(other._fields[key].value, False) else: raise ValueError('Cannot update with supplied data') def _dirty_fields(self): """Enumerate changed fields""" outgoing_keys = ['peer_port', 'pex_enabled'] fields = [] for key in outgoing_keys: if key in self._fields and self._fields[key].dirty: fields.append(key) return fields def _push(self): """Push changed fields to the server""" dirty = self._dirty_fields() args = {} for key in dirty: args[key] = self._fields[key].value self._fields[key] = self._fields[key]._replace(dirty=False) if len(args) > 0: self._client.set_session(**args) def update(self, timeout=None): """Update the session information.""" self._push() session = self._client.get_session(timeout=timeout) self._update_fields(session) session = self._client.session_stats(timeout=timeout) self._update_fields(session) def from_request(self, data): """Update the session information.""" self._update_fields(data) def _get_peer_port(self): """ Get the peer port. """ return self._fields['peer_port'].value def _set_peer_port(self, port): """ Set the peer port. """ if isinstance(port, integer_types): self._fields['peer_port'] = Field(port, True) self._push() else: raise ValueError("Not a valid limit") peer_port = property(_get_peer_port, _set_peer_port, None, "Peer port. This is a mutator.") def _get_pex_enabled(self): """Is peer exchange enabled?""" return self._fields['pex_enabled'].value def _set_pex_enabled(self, enabled): """Enable/disable peer exchange.""" if isinstance(enabled, bool): self._fields['pex_enabled'] = Field(enabled, True) self._push() else: raise TypeError("Not a valid type") pex_enabled = property(_get_pex_enabled, _set_pex_enabled, None, "Enable peer exchange. This is a mutator.") blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/torrent.py0000755000000000000000000004024012227066501023364 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. import sys, datetime from transmissionrpc.constants import PRIORITY, RATIO_LIMIT, IDLE_LIMIT from transmissionrpc.utils import Field, format_timedelta from six import integer_types, string_types, text_type, iteritems def get_status_old(code): """Get the torrent status using old status codes""" mapping = { (1<<0): 'check pending', (1<<1): 'checking', (1<<2): 'downloading', (1<<3): 'seeding', (1<<4): 'stopped', } return mapping[code] def get_status_new(code): """Get the torrent status using new status codes""" mapping = { 0: 'stopped', 1: 'check pending', 2: 'checking', 3: 'download pending', 4: 'downloading', 5: 'seed pending', 6: 'seeding', } return mapping[code] class Torrent(object): """ Torrent is a class holding the data received from Transmission regarding a bittorrent transfer. All fetched torrent fields are accessible through this class using attributes. This class has a few convenience properties using the torrent data. """ def __init__(self, client, fields): if 'id' not in fields: raise ValueError('Torrent requires an id') self._fields = {} self._update_fields(fields) self._incoming_pending = False self._outgoing_pending = False self._client = client def _get_name_string(self, codec=None): """Get the name""" if codec is None: codec = sys.getdefaultencoding() name = None # try to find name if 'name' in self._fields: name = self._fields['name'].value # if name is unicode, try to decode if isinstance(name, text_type): try: name = name.encode(codec) except UnicodeError: name = None return name def __repr__(self): tid = self._fields['id'].value name = self._get_name_string() if isinstance(name, str): return '' % (tid, name) else: return '' % (tid) def __str__(self): name = self._get_name_string() if isinstance(name, str): return 'Torrent \"%s\"' % (name) else: return 'Torrent' def __copy__(self): return Torrent(self._client, self._fields) def __getattr__(self, name): try: return self._fields[name].value except KeyError: raise AttributeError('No attribute %s' % name) def _rpc_version(self): """Get the Transmission RPC API version.""" if self._client: return self._client.rpc_version return 2 def _dirty_fields(self): """Enumerate changed fields""" outgoing_keys = ['bandwidthPriority', 'downloadLimit', 'downloadLimited', 'peer_limit', 'queuePosition' , 'seedIdleLimit', 'seedIdleMode', 'seedRatioLimit', 'seedRatioMode', 'uploadLimit', 'uploadLimited'] fields = [] for key in outgoing_keys: if key in self._fields and self._fields[key].dirty: fields.append(key) return fields def _push(self): """Push changed fields to the server""" dirty = self._dirty_fields() args = {} for key in dirty: args[key] = self._fields[key].value self._fields[key] = self._fields[key]._replace(dirty=False) if len(args) > 0: self._client.change_torrent(self.id, **args) def _update_fields(self, other): """ Update the torrent data from a Transmission JSON-RPC arguments dictionary """ fields = None if isinstance(other, dict): for key, value in iteritems(other): self._fields[key.replace('-', '_')] = Field(value, False) elif isinstance(other, Torrent): for key in list(other._fields.keys()): self._fields[key] = Field(other._fields[key].value, False) else: raise ValueError('Cannot update with supplied data') self._incoming_pending = False def _status(self): """Get the torrent status""" code = self._fields['status'].value if self._rpc_version() >= 14: return get_status_new(code) else: return get_status_old(code) def files(self): """ Get list of files for this torrent. This function returns a dictionary with file information for each file. The file information is has following fields: :: { : { 'name': , 'size': , 'completed': , 'priority': , 'selected': } ... } """ result = {} if 'files' in self._fields: files = self._fields['files'].value indices = range(len(files)) priorities = self._fields['priorities'].value wanted = self._fields['wanted'].value for item in zip(indices, files, priorities, wanted): selected = True if item[3] else False priority = PRIORITY[item[2]] result[item[0]] = { 'selected': selected, 'priority': priority, 'size': item[1]['length'], 'name': item[1]['name'], 'completed': item[1]['bytesCompleted']} return result @property def status(self): """ Returns the torrent status. Is either one of 'check pending', 'checking', 'downloading', 'seeding' or 'stopped'. The first two is related to verification. """ return self._status() @property def progress(self): """Get the download progress in percent.""" try: size = self._fields['sizeWhenDone'].value left = self._fields['leftUntilDone'].value return 100.0 * (size - left) / float(size) except ZeroDivisionError: return 0.0 @property def ratio(self): """Get the upload/download ratio.""" return float(self._fields['uploadRatio'].value) @property def eta(self): """Get the "eta" as datetime.timedelta.""" eta = self._fields['eta'].value if eta >= 0: return datetime.timedelta(seconds=eta) else: raise ValueError('eta not valid') @property def date_active(self): """Get the attribute "activityDate" as datetime.datetime.""" return datetime.datetime.fromtimestamp(self._fields['activityDate'].value) @property def date_added(self): """Get the attribute "addedDate" as datetime.datetime.""" return datetime.datetime.fromtimestamp(self._fields['addedDate'].value) @property def date_started(self): """Get the attribute "startDate" as datetime.datetime.""" return datetime.datetime.fromtimestamp(self._fields['startDate'].value) @property def date_done(self): """Get the attribute "doneDate" as datetime.datetime.""" return datetime.datetime.fromtimestamp(self._fields['doneDate'].value) def format_eta(self): """ Returns the attribute *eta* formatted as a string. * If eta is -1 the result is 'not available' * If eta is -2 the result is 'unknown' * Otherwise eta is formatted as ::. """ eta = self._fields['eta'].value if eta == -1: return 'not available' elif eta == -2: return 'unknown' else: return format_timedelta(self.eta) def _get_download_limit(self): """ Get the download limit. Can be a number or None. """ if self._fields['downloadLimited'].value: return self._fields['downloadLimit'].value else: return None def _set_download_limit(self, limit): """ Get the download limit. Can be a number, 'session' or None. """ if isinstance(limit, integer_types): self._fields['downloadLimited'] = Field(True, True) self._fields['downloadLimit'] = Field(limit, True) self._push() elif limit == None: self._fields['downloadLimited'] = Field(False, True) self._push() else: raise ValueError("Not a valid limit") download_limit = property(_get_download_limit, _set_download_limit, None, "Download limit in Kbps or None. This is a mutator.") def _get_peer_limit(self): """ Get the peer limit. """ return self._fields['peer_limit'].value def _set_peer_limit(self, limit): """ Set the peer limit. """ if isinstance(limit, integer_types): self._fields['peer_limit'] = Field(limit, True) self._push() else: raise ValueError("Not a valid limit") peer_limit = property(_get_peer_limit, _set_peer_limit, None, "Peer limit. This is a mutator.") def _get_priority(self): """ Get the priority as string. Can be one of 'low', 'normal', 'high'. """ return PRIORITY[self._fields['bandwidthPriority'].value] def _set_priority(self, priority): """ Set the priority as string. Can be one of 'low', 'normal', 'high'. """ if isinstance(priority, string_types): self._fields['bandwidthPriority'] = Field(PRIORITY[priority], True) self._push() priority = property(_get_priority, _set_priority, None , "Bandwidth priority as string. Can be one of 'low', 'normal', 'high'. This is a mutator.") def _get_seed_idle_limit(self): """ Get the seed idle limit in minutes. """ return self._fields['seedIdleLimit'].value def _set_seed_idle_limit(self, limit): """ Set the seed idle limit in minutes. """ if isinstance(limit, integer_types): self._fields['seedIdleLimit'] = Field(limit, True) self._push() else: raise ValueError("Not a valid limit") seed_idle_limit = property(_get_seed_idle_limit, _set_seed_idle_limit, None , "Torrent seed idle limit in minutes. Also see seed_idle_mode. This is a mutator.") def _get_seed_idle_mode(self): """ Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'. """ return IDLE_LIMIT[self._fields['seedIdleMode'].value] def _set_seed_idle_mode(self, mode): """ Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'. """ if isinstance(mode, str): self._fields['seedIdleMode'] = Field(IDLE_LIMIT[mode], True) self._push() else: raise ValueError("Not a valid limit") seed_idle_mode = property(_get_seed_idle_mode, _set_seed_idle_mode, None, """ Seed idle mode as string. Can be one of 'global', 'single' or 'unlimited'. * global, use session seed idle limit. * single, use torrent seed idle limit. See seed_idle_limit. * unlimited, no seed idle limit. This is a mutator. """ ) def _get_seed_ratio_limit(self): """ Get the seed ratio limit as float. """ return float(self._fields['seedRatioLimit'].value) def _set_seed_ratio_limit(self, limit): """ Set the seed ratio limit as float. """ if isinstance(limit, (integer_types, float)) and limit >= 0.0: self._fields['seedRatioLimit'] = Field(float(limit), True) self._push() else: raise ValueError("Not a valid limit") seed_ratio_limit = property(_get_seed_ratio_limit, _set_seed_ratio_limit, None , "Torrent seed ratio limit as float. Also see seed_ratio_mode. This is a mutator.") def _get_seed_ratio_mode(self): """ Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'. """ return RATIO_LIMIT[self._fields['seedRatioMode'].value] def _set_seed_ratio_mode(self, mode): """ Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'. """ if isinstance(mode, str): self._fields['seedRatioMode'] = Field(RATIO_LIMIT[mode], True) self._push() else: raise ValueError("Not a valid limit") seed_ratio_mode = property(_get_seed_ratio_mode, _set_seed_ratio_mode, None, """ Seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'. * global, use session seed ratio limit. * single, use torrent seed ratio limit. See seed_ratio_limit. * unlimited, no seed ratio limit. This is a mutator. """ ) def _get_upload_limit(self): """ Get the upload limit. Can be a number or None. """ if self._fields['uploadLimited'].value: return self._fields['uploadLimit'].value else: return None def _set_upload_limit(self, limit): """ Set the upload limit. Can be a number, 'session' or None. """ if isinstance(limit, integer_types): self._fields['uploadLimited'] = Field(True, True) self._fields['uploadLimit'] = Field(limit, True) self._push() elif limit == None: self._fields['uploadLimited'] = Field(False, True) self._push() else: raise ValueError("Not a valid limit") upload_limit = property(_get_upload_limit, _set_upload_limit, None, "Upload limit in Kbps or None. This is a mutator.") def _get_queue_position(self): """Get the queue position for this torrent.""" if self._rpc_version() >= 14: return self._fields['queuePosition'].value else: return 0 def _set_queue_position(self, position): """Set the queue position for this torrent.""" if self._rpc_version() >= 14: if isinstance(position, integer_types): self._fields['queuePosition'] = Field(position, True) self._push() else: raise ValueError("Not a valid position") else: pass queue_position = property(_get_queue_position, _set_queue_position, None, "Queue position") def update(self, timeout=None): """Update the torrent information.""" self._push() torrent = self._client.get_torrent(self.id, timeout=timeout) self._update_fields(torrent) def start(self, bypass_queue=False, timeout=None): """ Start the torrent. """ self._incoming_pending = True self._client.start_torrent(self.id, bypass_queue=bypass_queue, timeout=timeout) def stop(self, timeout=None): """Stop the torrent.""" self._incoming_pending = True self._client.stop_torrent(self.id, timeout=timeout) def move_data(self, location, timeout=None): """Move torrent data to location.""" self._incoming_pending = True self._client.move_torrent_data(self.id, location, timeout=timeout) def locate_data(self, location, timeout=None): """Locate torrent data at location.""" self._incoming_pending = True self._client.locate_torrent_data(self.id, location, timeout=timeout) blueluna-transmissionrpc-eb2a32720f8a/transmissionrpc/utils.py0000755000000000000000000001523312227066501023033 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2008-2013 Erik Svensson # Licensed under the MIT license. import socket, datetime, logging from collections import namedtuple import transmissionrpc.constants as constants from transmissionrpc.constants import LOGGER from six import string_types, iteritems UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'] def format_size(size): """ Format byte size into IEC prefixes, B, KiB, MiB ... """ size = float(size) i = 0 while size >= 1024.0 and i < len(UNITS): i += 1 size /= 1024.0 return (size, UNITS[i]) def format_speed(size): """ Format bytes per second speed into IEC prefixes, B/s, KiB/s, MiB/s ... """ (size, unit) = format_size(size) return (size, unit + '/s') def format_timedelta(delta): """ Format datetime.timedelta into ::. """ minutes, seconds = divmod(delta.seconds, 60) hours, minutes = divmod(minutes, 60) return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds) def format_timestamp(timestamp, utc=False): """ Format unix timestamp into ISO date format. """ if timestamp > 0: if utc: dt_timestamp = datetime.datetime.utcfromtimestamp(timestamp) else: dt_timestamp = datetime.datetime.fromtimestamp(timestamp) return dt_timestamp.isoformat(' ') else: return '-' class INetAddressError(Exception): """ Error parsing / generating a internet address. """ pass def inet_address(address, default_port, default_address='localhost'): """ Parse internet address. """ addr = address.split(':') if len(addr) == 1: try: port = int(addr[0]) addr = default_address except ValueError: addr = addr[0] port = default_port elif len(addr) == 2: try: port = int(addr[1]) except ValueError: raise INetAddressError('Invalid address "%s".' % address) if len(addr[0]) == 0: addr = default_address else: addr = addr[0] else: raise INetAddressError('Invalid address "%s".' % address) try: socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM) except socket.gaierror: raise INetAddressError('Cannot look up address "%s".' % address) return (addr, port) def rpc_bool(arg): """ Convert between Python boolean and Transmission RPC boolean. """ if isinstance(arg, string_types): try: arg = bool(int(arg)) except ValueError: arg = arg.lower() in ['true', 'yes'] return 1 if bool(arg) else 0 TR_TYPE_MAP = { 'number' : int, 'string' : str, 'double': float, 'boolean' : rpc_bool, 'array': list, 'object': dict } def make_python_name(name): """ Convert Transmission RPC name to python compatible name. """ return name.replace('-', '_') def make_rpc_name(name): """ Convert python compatible name to Transmission RPC name. """ return name.replace('_', '-') def argument_value_convert(method, argument, value, rpc_version): """ Check and fix Transmission RPC issues with regards to methods, arguments and values. """ if method in ('torrent-add', 'torrent-get', 'torrent-set'): args = constants.TORRENT_ARGS[method[-3:]] elif method in ('session-get', 'session-set'): args = constants.SESSION_ARGS[method[-3:]] else: return ValueError('Method "%s" not supported' % (method)) if argument in args: info = args[argument] invalid_version = True while invalid_version: invalid_version = False replacement = None if rpc_version < info[1]: invalid_version = True replacement = info[3] if info[2] and info[2] <= rpc_version: invalid_version = True replacement = info[4] if invalid_version: if replacement: LOGGER.warning( 'Replacing requested argument "%s" with "%s".' % (argument, replacement)) argument = replacement info = args[argument] else: raise ValueError( 'Method "%s" Argument "%s" does not exist in version %d.' % (method, argument, rpc_version)) return (argument, TR_TYPE_MAP[info[0]](value)) else: raise ValueError('Argument "%s" does not exists for method "%s".', (argument, method)) def get_arguments(method, rpc_version): """ Get arguments for method in specified Transmission RPC version. """ if method in ('torrent-add', 'torrent-get', 'torrent-set'): args = constants.TORRENT_ARGS[method[-3:]] elif method in ('session-get', 'session-set'): args = constants.SESSION_ARGS[method[-3:]] else: return ValueError('Method "%s" not supported' % (method)) accessible = [] for argument, info in iteritems(args): valid_version = True if rpc_version < info[1]: valid_version = False if info[2] and info[2] <= rpc_version: valid_version = False if valid_version: accessible.append(argument) return accessible def add_stdout_logger(level='debug'): """ Add a stdout target for the transmissionrpc logging. """ levels = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR} trpc_logger = logging.getLogger('transmissionrpc') loghandler = logging.StreamHandler() if level in list(levels.keys()): loglevel = levels[level] trpc_logger.setLevel(loglevel) loghandler.setLevel(loglevel) trpc_logger.addHandler(loghandler) def add_file_logger(filepath, level='debug'): """ Add a stdout target for the transmissionrpc logging. """ levels = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR} trpc_logger = logging.getLogger('transmissionrpc') loghandler = logging.FileHandler(filepath, encoding='utf-8') if level in list(levels.keys()): loglevel = levels[level] trpc_logger.setLevel(loglevel) loghandler.setLevel(loglevel) trpc_logger.addHandler(loghandler) Field = namedtuple('Field', ['value', 'dirty'])