├── .gitignore ├── Makefile ├── README.md ├── aria2clib ├── __init__.py ├── __main__.py ├── cmdline.py ├── command.py ├── commands │ ├── __init__.py │ ├── client.py │ └── event.py ├── legacy_api.py └── simple_api.py ├── init-scripts ├── README.md ├── aria2c ├── aria2c.conf └── install.sh ├── requirements-dev.txt └── tests ├── handlers ├── on-download-complete.py └── on-download-complete.sh └── test_simple.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | # ignore logs 30 | log 31 | 32 | # pid 33 | run 34 | 35 | # temp storage 36 | tests/store/* 37 | 38 | # service dirs 39 | data/* 40 | tests/json/* 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | aria2c-server-start: 2 | @ ./tests/bin/aria2c-server.sh start 3 | 4 | aria2c-server-stop: 5 | @ ./tests/bin/aria2c-server.sh stop 6 | 7 | aria2c-server-status: 8 | @ ./tests/bin/aria2c-server.sh status 9 | 10 | aria2c-server-restart: 11 | @ ./tests/bin/aria2c-server.sh restart 12 | 13 | 14 | dev-server-start: 15 | @ ./tests/bin/dev-server.sh start 16 | 17 | dev-server-stop: 18 | @ ./tests/bin/dev-server.sh stop 19 | 20 | dev-server-status: 21 | @ ./tests/bin/dev-server.sh status 22 | 23 | dev-server-restart: 24 | @ ./tests/bin/dev-server.sh restart 25 | 26 | 27 | test-all: aria2c-server-restart dev-server-restart 28 | @ rm -f tests/store/* 29 | @ sleep 2 30 | @ nosetests --cover-package=aria2clib --verbosity=1 --cover-erase 31 | 32 | test-all-with-coverage: aria2c-server-restart dev-server-restart 33 | @ rm -f tests/store/* 34 | @ sleep 2 35 | @ nosetests --with-coverage --cover-package=aria2clib --verbosity=1 --cover-erase 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aria2c-as-a-service 2 | =================== 3 | 4 | The collection of scripts and tools for running aria2c as a service for local install. 5 | 6 | [aria2](http://sourceforge.net/apps/trac/aria2/wiki) is a light-weight multi-protocol & multi-source download utility 7 | operated in command-line. The supported protocols are HTTP(S), FTP, BitTorrent (DHT, PEX, MSE/PE), and Metalink. 8 | 9 | ## Installation 10 | 11 | Installation of [init-scripts and configs](https://github.com/ownport/aria2c-as-a-service/tree/master/init-scripts) for 12 | running aria2c as service 13 | 14 | `aria2clib` is simple library, the main goal is simplify interface to control downloading of files. Just 4 methods are 15 | supported: 16 | 17 | - get() 18 | - put() 19 | - delete() 20 | - stats() 21 | 22 | 23 | ### for developers 24 | 25 | there's needed to install `nose` and `coverage` 26 | 27 | ``` 28 | $ pip install -r requirements-dev.txt 29 | ``` 30 | 31 | 32 | ## Links 33 | 34 | **Libraries and tools** 35 | 36 | - [Aria2JsonRpc](http://xyne.archlinux.ca/projects/python3-aria2jsonrpc/) is a Python 3 module that provides a wrapper 37 | class around Aria2's RPC interface. It can be used to build applications that use Aria2 for downloading data. 38 | - This [directory](https://github.com/tatsuhiro-t/aria2/tree/master/doc/xmlrpc) contains sample scripts to interact 39 | with aria2 via XML-RPC. For more information, see http://sourceforge.net/apps/trac/aria2/wiki/XmlrpcInterface 40 | - [Aria2 Tools](https://github.com/nmbooker/aria2-tools) Some tools and instructions to help supplement aria2 when 41 | used as a download server. 42 | 43 | 44 | **Web UI** 45 | 46 | - [webui-aria2](https://github.com/ziahamza/webui-aria2) Very simple to use, no build scripts, no installation scripts. 47 | First start aria2 in the background either in your local machine or in a remote one. 48 | - [Berserker](https://github.com/adityamukho/Berserker) Advanced web-based frontend for Aria2-JSONRPC. 49 | - [aria2remote](https://code.google.com/p/aria2remote/), Simple remote interface to aria2c. 50 | - [YAAW](https://github.com/binux/yaaw) Yet Another Aria2 Web Frontend in pure HTML/CSS/Javascirpt. No HTTP server, 51 | backend or server-side program. All you need is just a browser. 52 | 53 | 54 | -------------------------------------------------------------------------------- /aria2clib/__init__.py: -------------------------------------------------------------------------------- 1 | from simple_api import SimpleClient 2 | from legacy_api import LegacyClient 3 | from legacy_api import GLOBAL_OPTION_FIELDS 4 | from legacy_api import GLOBAL_STATS_FIELDS 5 | 6 | __all__ = [ 7 | 'GLOBAL_OPTION_FIELDS', 'GLOBAL_STATS_FIELDS', 8 | 'SimpleClient', 'LegacyClient' 9 | ] 10 | 11 | -------------------------------------------------------------------------------- /aria2clib/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from cmdline import execute 5 | 6 | if __name__ == '__main__': 7 | execute() 8 | 9 | -------------------------------------------------------------------------------- /aria2clib/cmdline.py: -------------------------------------------------------------------------------- 1 | # system modules 2 | import sys 3 | import inspect 4 | import argparse 5 | 6 | from pkgutil import iter_modules 7 | 8 | from command import Aria2cCommand 9 | 10 | 11 | def execute(argv=None): 12 | 13 | if argv is None: 14 | argv = sys.argv 15 | 16 | cmds = _get_commands_from_module('commands') 17 | argv, cmdname = _pop_command_name(argv) 18 | parser = argparse.ArgumentParser() 19 | 20 | if not cmdname: 21 | _print_commands(cmds) 22 | sys.exit(0) 23 | elif cmdname not in cmds: 24 | _print_unknown_command(cmdname) 25 | sys.exit(2) 26 | 27 | cmd = cmds[cmdname] 28 | parser.usage = "aria2clib %s %s" % (cmdname, cmd.syntax()) 29 | parser.description = cmd.long_desc() 30 | cmd.add_options(parser) 31 | 32 | if argv: 33 | args = parser.parse_args(args=argv) 34 | else: 35 | args = parser.parse_args(args=['--help']) 36 | 37 | _run_print_help(parser, cmd.process_options, args) 38 | _run_print_help(parser, cmd.run) 39 | sys.exit(cmd.exitcode) 40 | 41 | 42 | def _get_commands_from_module(module): 43 | ''' returns commands for module 44 | ''' 45 | return dict([(cmd.__module__.split('.')[-1], cmd()) \ 46 | for cmd in _iter_command_classes(module)]) 47 | 48 | 49 | def _iter_command_classes(module_name): 50 | 51 | for module in walk_modules(module_name): 52 | for obj in vars(module).itervalues(): 53 | if inspect.isclass(obj) and issubclass(obj, Aria2cCommand) and \ 54 | obj.__module__ == module.__name__: 55 | yield obj 56 | 57 | 58 | def walk_modules(path, load=False): 59 | """Loads a module and all its submodules from a the given module path and 60 | returns them. If *any* module throws an exception while importing, that 61 | exception is thrown back. 62 | 63 | For example: walk_modules('scrapy.utils') 64 | """ 65 | 66 | mods = [] 67 | mod = __import__(path, {}, {}, ['']) 68 | mods.append(mod) 69 | if hasattr(mod, '__path__'): 70 | for _, subpath, ispkg in iter_modules(mod.__path__): 71 | fullpath = path + '.' + subpath 72 | if ispkg: 73 | mods += walk_modules(fullpath) 74 | else: 75 | submod = __import__(fullpath, {}, {}, ['']) 76 | mods.append(submod) 77 | return mods 78 | 79 | 80 | def _pop_command_name(argv): 81 | 82 | if len(argv) > 1: 83 | argv = argv[1:] 84 | cmdname = argv.pop(0) if not argv[0].startswith('-') else None 85 | return argv, cmdname 86 | return (argv, None) 87 | 88 | 89 | def _print_commands(cmds): 90 | 91 | print "Usage:" 92 | print " aria2clib [options] [args]\n" 93 | print "Available commands:" 94 | for cmdname, cmdclass in sorted(cmds.iteritems()): 95 | print " %-13s %s" % (cmdname, cmdclass.short_desc()) 96 | print 97 | print 'Use "aria2clib -h" to see more info about a command' 98 | 99 | 100 | def _print_unknown_command(cmdname): 101 | print "Unknown command: %s\n" % cmdname 102 | print 'Use "aria2clib" to see available commands' 103 | 104 | 105 | def _run_print_help(parser, func, *a): 106 | 107 | try: 108 | func(*a) 109 | except RuntimeError as e: 110 | if str(e): 111 | parser.error(str(e)) 112 | if e.print_help: 113 | parser.print_help() 114 | sys.exit(2) 115 | 116 | -------------------------------------------------------------------------------- /aria2clib/command.py: -------------------------------------------------------------------------------- 1 | 2 | class UsageError(Exception): 3 | """To indicate a command-line usage error 4 | """ 5 | def __init__(self, *a, **kw): 6 | 7 | self.print_help = kw.pop('print_help', True) 8 | super(UsageError, self).__init__(*a, **kw) 9 | 10 | 11 | class Aria2cCommand(object): 12 | 13 | def __init__(self): 14 | 15 | self.name = 'Aria2cCommand' 16 | self.exitcode = 0 17 | self.settings = dict() 18 | 19 | self.logger = None 20 | self.settings['LOG_LEVEL'] = 'INFO' 21 | 22 | 23 | def syntax(self): 24 | """ Command syntax (preferably one-line). Do not include command name. 25 | """ 26 | return "" 27 | 28 | 29 | def short_desc(self): 30 | """ A short description of the command 31 | """ 32 | return "" 33 | 34 | 35 | def long_desc(self): 36 | """ A long description of the command. Return short description when not 37 | available. It cannot contain newlines, since contents will be formatted 38 | by argparse which removes newlines and wraps text. 39 | """ 40 | return self.short_desc() 41 | 42 | 43 | def help(self): 44 | """ An extensive help for the command. It will be shown when using the 45 | "help" command. It can contain newlines, since not post-formatting will 46 | be applied to its contents. 47 | """ 48 | return self.long_desc() 49 | 50 | 51 | def add_options(self, parser): 52 | """ Populate option parse with options available for this command 53 | """ 54 | parser.add_argument("--logfile", metavar="FILE", type=str, 55 | help="log file. if omitted stderr will be used") 56 | parser.add_argument("-L", "--loglevel", metavar="LEVEL", type=str, 57 | default=None, help="log level (default: INFO)") 58 | 59 | def process_options(self, args): 60 | 61 | if args.loglevel: 62 | self.settings['LOG_LEVEL'] = args.loglevel 63 | 64 | if args.logfile: 65 | self.settings['LOG_FILE'] = args.logfile 66 | self.logger = log.get_file_logger(self.name, self.settings['LOG_LEVEL'], args.logfile) 67 | 68 | 69 | def run(self): 70 | """ Entry point for running commands 71 | """ 72 | raise NotImplementedError 73 | 74 | -------------------------------------------------------------------------------- /aria2clib/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ownport/aria2c-as-a-service/2a3f806b73b63e0b75b5a2e6c4797436b3822712/aria2clib/commands/__init__.py -------------------------------------------------------------------------------- /aria2clib/commands/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pprint import pprint 3 | 4 | from simple_api import SimpleClient 5 | 6 | from command import Aria2cCommand, UsageError 7 | 8 | 9 | class Command(Aria2cCommand): 10 | 11 | name = 'client' 12 | 13 | def short_desc(self): 14 | 15 | return "operate with aria2c daemon via Simple Client API" 16 | 17 | 18 | def add_options(self, parser): 19 | 20 | Aria2cCommand.add_options(self, parser) 21 | parser.add_argument("--client_id", type=str, help="client id") 22 | parser.add_argument("--uri", type=str, help="URI to aria2c daemon") 23 | parser.add_argument("--username", type=str, help="aria2c daemon username") 24 | parser.add_argument("--password", type=str, help="aria2c daemon password") 25 | 26 | parser.add_argument("--put", type=str, 27 | help="put task specified by simple url " + \ 28 | "or parameters in JSON format " + \ 29 | "or reference to JSON file (file:)") 30 | parser.add_argument("--get", type=str, 31 | help="get details by one or several GIDs separated by ',' or 'all'") 32 | parser.add_argument("--delete", type=str, 33 | help="delete task(s) by one or several GIDs separated by ','") 34 | parser.add_argument("--stats", action="store_true", 35 | help="print aria2c info") 36 | 37 | 38 | def process_options(self, args): 39 | 40 | Aria2cCommand.process_options(self, args) 41 | 42 | if args.client_id: 43 | self.client_id = args.client_id 44 | else: 45 | raise UsageError('Missed --client_id parameter') 46 | 47 | if args.uri: 48 | self.uri = args.uri 49 | else: 50 | raise UsageError('Missed --uri parameter') 51 | 52 | self.username = args.username 53 | self.password = args.password 54 | self.command_put = args.put 55 | self.command_get = args.get 56 | self.command_delete = args.delete 57 | self.command_stats = args.stats 58 | 59 | 60 | def run(self): 61 | 62 | client = SimpleClient(client_id=self.client_id, uri=self.uri, username=self.username, password=self.password) 63 | 64 | if self.command_stats: 65 | pprint(client.stats()) 66 | 67 | elif self.command_get: 68 | if self.command_get == 'all': 69 | pprint(client.get()) 70 | else: 71 | pprint(client.get(self.command_get.split(','))) 72 | 73 | elif self.command_put: 74 | # URL 75 | if self.command_put.startswith('http://'): 76 | print client.put(self.command_put) 77 | 78 | # file 79 | elif self.command_put.startswith('file:'): 80 | with open(self.command_put.replace('file:', '')) as source: 81 | input_params = json.loads(source.read()) 82 | urls = input_params.get('urls', []) 83 | params = input_params.get('params', {}) 84 | pause = input_params.get('pause', False) 85 | pprint(client.put(urls, params, pause)) 86 | 87 | # parameters in JSON 88 | else: 89 | input_params = json.loads(self.command_put) 90 | urls = input_params.get('urls', []) 91 | params = input_params.get('params', {}) 92 | pause = input_params.get('pause', False) 93 | pprint(client.put(urls, params, pause)) 94 | 95 | elif self.command_delete: 96 | pprint(client.delete(self.command_delete.split(','))) 97 | 98 | else: 99 | raise RuntimeError('Unknown command or no command defined') 100 | 101 | 102 | -------------------------------------------------------------------------------- /aria2clib/commands/event.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import sys 3 | 4 | from simple_api import SimpleClient 5 | 6 | from command import Aria2cCommand, UsageError 7 | 8 | 9 | class Command(Aria2cCommand): 10 | 11 | name = 'event' 12 | 13 | def short_desc(self): 14 | 15 | return "aria2c event handler" 16 | 17 | 18 | def add_options(self, parser): 19 | 20 | Aria2cCommand.add_options(self, parser) 21 | parser.add_argument("--handler", type=str, help="handler module") 22 | parser.add_argument("--gid", type=str, help="GID") 23 | parser.add_argument("--files", type=str, help="the number of files") 24 | parser.add_argument("--path", type=str, help="path") 25 | 26 | 27 | def process_options(self, args): 28 | 29 | Aria2cCommand.process_options(self, args) 30 | 31 | if args.gid: 32 | self.gid = args.gid 33 | else: 34 | raise UsageError('Missed --gid parameter') 35 | 36 | if args.files: 37 | self.files = args.files 38 | else: 39 | raise UsageError('Missed --files parameter') 40 | 41 | if args.path: 42 | self.path = args.path 43 | else: 44 | raise UsageError('Missed --path parameter') 45 | 46 | if args.handler: 47 | self.handler = args.handler 48 | else: 49 | raise UsageError('Missed --handler parameter') 50 | 51 | def run(self): 52 | 53 | try: 54 | handler = imp.load_source('handler', self.handler) 55 | except IOError: 56 | print >> sys.stderr, 'Error! No handler module found, %s' % self.handler 57 | sys.exit(1) 58 | 59 | handler.main(SimpleClient, self.gid, self.files, self.path) 60 | 61 | -------------------------------------------------------------------------------- /aria2clib/legacy_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aria2c python library for JSON-RPC 5 | # 6 | # aria2 provides JSON-RPC over HTTP and XML-RPC over HTTP and they basically have the same functionality. 7 | # aria2 also provides JSON-RPC over WebSocket. JSON-RPC over WebSocket uses same method signatures and 8 | # response format with JSON-RPC over HTTP, but it additionally has server-initiated notifications. 9 | # 10 | # The request path of JSON-RPC interface (for both over HTTP and over WebSocket) is /jsonrpc. The request 11 | # path of XML-RPC interface is /rpc. 12 | # 13 | # The WebSocket URI for JSON-RPC over WebSocket is ws://HOST:PORT/jsonrpc. If you enabled SSL/TLS encryption, 14 | # use wss://HOST:PORT/jsonrpc instead. 15 | # 16 | # 17 | # Links 18 | # - [JSON-RPC 2.0 Specification](http://www.jsonrpc.org/specification) 19 | # 20 | 21 | import re 22 | import json 23 | import time 24 | import base64 25 | import urllib2 26 | import httplib 27 | 28 | from pprint import pprint 29 | 30 | JSON_RPC_VERSION = '2.0' 31 | ARIA2C_PREFIX = 'aria2' 32 | GLOBAL_STATS_FIELDS = [u'downloadSpeed', u'numActive', u'numStopped', u'numWaiting', u'uploadSpeed'] 33 | GLOBAL_OPTION_FIELDS = [ 34 | u'follow-torrent', u'help', u'uri-selector', u'enable-rpc', u'enable-http-keep-alive', 35 | u'max-overall-download-limit', u'no-conf', u'bt-min-crypto-level', u'max-concurrent-downloads', 36 | u'conf-path', u'max-download-result', u'no-netrc', u'peer-id-prefix', u'stop', u'max-resume-failure-tries', 37 | u'async-dns', u'force-save', u'daemon', u'max-overall-upload-limit', u'continue', u'enable-mmap', 38 | u'timeout', u'rpc-passwd', u'max-download-limit', u'no-file-allocation-limit', u'dir', u'enable-dht6', 39 | u'allow-piece-length-change', u'bt-tracker-connect-timeout', u'show-console-readout', u'human-readable', 40 | u'bt-stop-timeout', u'seed-ratio', u'use-head', u'follow-metalink', u'show-files', u'listen-port', 41 | u'dht-listen-port', u'retry-wait', u'file-allocation', u'connect-timeout', u'bt-request-peer-speed-limit', 42 | u'quiet', u'log-level', u'rpc-max-request-size', u'bt-enable-lpd', u'bt-max-peers', u'auto-file-renaming', 43 | u'ftp-type', u'rpc-secure', u'metalink-preferred-protocol', u'user-agent', u'disable-ipv6', u'http-no-cache', 44 | u'max-connection-per-server', u'remote-time', u'log', u'bt-metadata-only', u'piece-length', u'rpc-listen-port', 45 | u'disk-cache', u'conditional-get', u'console-log-level', u'ftp-reuse-connection', u'dry-run', 46 | u'rpc-listen-all', u'event-poll', u'allow-overwrite', u'remove-control-file', u'truncate-console-readout', 47 | u'max-file-not-found', u'rpc-user', u'deferred-input', u'rpc-allow-origin-all', u'dht-message-timeout', 48 | u'check-certificate', u'http-auth-challenge', u'realtime-chunk-checksum', u'always-resume', 49 | u'save-session-interval', u'reuse-uri', u'http-accept-gzip', u'bt-remove-unselected-file', u'max-upload-limit', 50 | u'rpc-save-upload-metadata', u'bt-require-crypto', u'check-integrity', u'proxy-method', u'ca-certificate', 51 | u'bt-tracker-timeout', u'max-tries', u'ftp-pasv', u'bt-tracker-interval', u'enable-dht', u'split', 52 | u'min-split-size', u'stream-piece-selector', u'bt-save-metadata', u'metalink-enable-unique-protocol', 53 | u'download-result', u'dht-file-path6', u'hash-check-only', u'server-stat-timeout', u'enable-peer-exchange', 54 | u'enable-http-pipelining', u'summary-interval', u'bt-max-open-files', u'dht-file-path', u'auto-save-interval', 55 | u'parameterized-uri', u'bt-hash-check-seed', u'bt-seed-unverified', u'lowest-speed-limit', 56 | u'on-download-complete', u'on-download-error', u'on-download-pause', u'on-download-start', u'on-download-stop', 57 | ] 58 | 59 | 60 | class LegacyClient(object): 61 | 62 | def __init__(self, client_id=None, uri=None, username=None, password=None): 63 | ''' 64 | - client_id - An identifier established by the Client that MUST contain a String, Number, or NULL value 65 | if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be 66 | Null and Numbers SHOULD NOT contain fractional parts 67 | 68 | - uri = host an port of aria2c server in the format `http://:` 69 | ''' 70 | self._json_rpc_version = JSON_RPC_VERSION 71 | self._aria2c_prefix = ARIA2C_PREFIX 72 | 73 | if not client_id: 74 | raise RuntimeError('client_id is not defined') 75 | self.client_id = client_id 76 | 77 | if not uri: 78 | raise RuntimeError('URI is not defined') 79 | 80 | if not isinstance(uri, (str, unicode)): 81 | raise RuntimeError('Incorrect URI type: %s' % type(uri)) 82 | 83 | elif not re.match(r'http://.+?:\d+', uri): 84 | raise RuntimeError('incorrect URI format: %s' % uri) 85 | self.uri = uri + '/jsonrpc' 86 | 87 | self.username = username 88 | self.password = password 89 | 90 | 91 | # IMPLEMENTED IN SIMPLE_CLIENT 92 | def send_request(self, command, parameters=[]): 93 | ''' send JSON-RPC request 94 | ''' 95 | 96 | jsonreq = json.dumps({ 97 | 'jsonrpc': self._json_rpc_version, 98 | 'id': self.client_id, 99 | 'method': '%s.%s' % (self._aria2c_prefix, command), 100 | 'params': parameters, 101 | }) 102 | 103 | request = urllib2.Request(self.uri, jsonreq) 104 | if self.username and self.password: 105 | base64string = base64.encodestring('%s:%s' % (self.username, self.password)).replace('\n', '') 106 | request.add_header('Authorization', 'Basic %s' % base64string) 107 | 108 | c = urllib2.urlopen(request) 109 | response = json.loads(c.read()) 110 | return response 111 | 112 | 113 | # IMPLEMENTED IN SIMPLE_CLIENT 114 | def add_uri(self, uris, options=[], position=None): 115 | ''' This method adds new HTTP(S)/FTP/BitTorrent Magnet URI. 116 | 117 | - uris is of type array and its element is URI which is of type string. For BitTorrent Magnet URI, uris must 118 | have only one element and it should be BitTorrent Magnet URI. URIs in uris must point to the same file. If 119 | you mix other URIs which point to another file, aria2 does not complain but download may fail. 120 | 121 | - options is of type struct and its members are a pair of option name and value. 122 | 123 | - If position is given as an integer starting from 0, the new download is inserted at position in the waiting 124 | queue. If position is not given or position is larger than the size of the queue, it is appended at the end of 125 | the queue. 126 | 127 | This method returns GID of registered download. GID calculates as md5 hash of first uri 128 | ''' 129 | # TODO support few uris to the same file 130 | 131 | if isinstance(uris, (unicode, str)): 132 | uris = [uris] 133 | 134 | return self.send_request('addUri', [uris, options]) 135 | 136 | 137 | def remove(self, gid): 138 | ''' This method removes the download denoted by gid. gid is of type string. If specified download is in 139 | progress, it is stopped at first. The status of removed download becomes removed. This method returns GID 140 | of removed download. 141 | ''' 142 | 143 | return self.send_request('remove', [gid,]) 144 | 145 | 146 | def force_remove(self, gid): 147 | '''This method removes the download denoted by gid. This method behaves just like aria2.remove() except 148 | that this method removes download without any action which takes time such as contacting BitTorrent 149 | tracker 150 | ''' 151 | return self.send_request('forceRemove', [gid,]) 152 | 153 | 154 | def pause(self, gid): 155 | ''' This method pauses the download denoted by gid. gid is of type string. The status of paused download 156 | becomes paused. If the download is active, the download is placed on the first position of waiting queue. 157 | As long as the status is paused, the download is not started. To change status to waiting, use aria2.unpause() 158 | method. This method returns GID of paused download. 159 | ''' 160 | return self.send_request('pause', [gid,]) 161 | 162 | 163 | def pause_all(self): 164 | ''' This method is equal to calling aria2.pause() for every active/waiting download. 165 | This methods returns OK for success. 166 | ''' 167 | return self.send_request('pauseAll') 168 | 169 | 170 | def force_pause(self, gid): 171 | ''' This method pauses the download denoted by gid. This method behaves just like aria2.pause() except that 172 | this method pauses download without any action which takes time such as contacting BitTorrent tracker. 173 | ''' 174 | return self.send_request('forcePause', [gid,]) 175 | 176 | 177 | def force_pause_all(self): 178 | ''' This method is equal to calling aria2.forcePause() for every active/waiting download. 179 | This methods returns OK for success. 180 | ''' 181 | return self.send_request('forcePauseAll') 182 | 183 | 184 | def unpause(self, gid): 185 | ''' This method changes the status of the download denoted by gid from paused to waiting. This makes 186 | the download eligible to restart. gid is of type string. This method returns GID of unpaused download. 187 | ''' 188 | return self.send_request('unpause', [gid,]) 189 | 190 | 191 | def unpause_all(self): 192 | ''' This method is equal to calling aria2.unpause() for every active/waiting download. 193 | This methods returns OK for success. 194 | ''' 195 | return self.send_request('unpauseAll') 196 | 197 | # IMPLEMENTED IN SIMPLE_CLIENT 198 | def tell_status(self, gid, keys=[]): 199 | ''' This method returns download progress of the download denoted by gid. 200 | 201 | - gid is of type string. 202 | 203 | - keys is array of string. If it is specified, the response contains only keys in keys array. If keys is 204 | empty or not specified, the response contains all keys. This is useful when you just want specific keys and 205 | avoid unnecessary transfers. 206 | 207 | the list of keys is available by http://aria2.sourceforge.net/manual/en/html/aria2c.html#aria2.tellStatus 208 | ''' 209 | 210 | return self.send_request('tellStatus', [gid, keys]) 211 | 212 | 213 | def get_uris(self, gid): 214 | ''' This method returns URIs used in the download denoted by gid. gid is of type string. The response is of 215 | type array and its element is of type struct and it contains following keys. The value type is string. 216 | 217 | uri: URI 218 | status: 'used' if the URI is already used. 'waiting' if the URI is waiting in the queue. 219 | ''' 220 | return self.send_request('getUris', [gid,]) 221 | 222 | 223 | def get_files(self, gid): 224 | ''' This method returns file list of the download denoted by gid. gid is of type string. The response is of 225 | type array and its element is of type struct and it contains following keys. The value type is string. 226 | 227 | - index: Index of file. Starting with 1. This is the same order with the files in multi-file torrent. 228 | - path: File path. 229 | - length: File size in bytes. 230 | - completedLength: Completed length of this file in bytes. Please note that it is possible that sum of 231 | completedLength is less than completedLength in aria2.tellStatus() method. This is because completedLength 232 | in aria2.getFiles() only calculates completed pieces. On the other hand, completedLength in aria2.tellStatus() 233 | takes into account of partially completed piece. 234 | - selected: true if this file is selected by --select-file option. If --select-file is not specified or this is 235 | single torrent or no torrent download, this value is always true. Otherwise false. 236 | - uris: Returns the list of URI for this file. The element of list is the same struct used in aria2.getUris() method. 237 | ''' 238 | return self.send_request('getFiles', [gid,]) 239 | 240 | 241 | def get_servers(self, gid): 242 | '''This method returns currently connected HTTP(S)/FTP servers of the download denoted by gid. gid is of type 243 | string. The response is of type array and its element is of type struct and it contains following keys. 244 | The value type is string. 245 | 246 | - index: Index of file. Starting with 1. This is the same order with the files in multi-file torrent. 247 | - servers: The list of struct which contains following keys. 248 | - uri: URI originally added. 249 | - currentUri: This is the URI currently used for downloading. If redirection is involved, currentUri and uri may differ. 250 | - downloadSpeed: Download speed (byte/sec) 251 | ''' 252 | return self.send_request('getServers', [gid,]) 253 | 254 | 255 | # IMPLEMENTED IN SIMPLE_CLIENT 256 | def tell_active(self, keys=[]): 257 | ''' This method returns the list of active downloads. The response is of type array and its element is the 258 | same struct returned by aria2.tellStatus() method. For keys parameter, 259 | please refer to aria2.tellStatus() method. 260 | ''' 261 | return self.send_request('tellActive', [keys]) 262 | 263 | 264 | # IMPLEMENTED IN SIMPLE_CLIENT 265 | def tell_waiting(self, offset=0, num=100, keys=[]): 266 | ''' This method returns the list of waiting download, including paused downloads. 267 | 268 | - offset is of type integer and specifies the offset from the download waiting at the front. 269 | - num is of type integer and specifies the number of downloads to be returned. 270 | - For keys parameter, please refer to aria2.tellStatus() method. 271 | 272 | If offset is a positive integer, this method returns downloads in the range of [offset, offset + num). 273 | 274 | offset can be a negative integer. offset == -1 points last download in the waiting queue and 275 | offset == -2 points the download before the last download, and so on. The downloads in the response are 276 | in reversed order. 277 | 278 | For example, imagine that three downloads "A","B" and "C" are waiting in this order. aria2.tellWaiting(0, 1) 279 | returns ["A"]. aria2.tellWaiting(1, 2) returns ["B", "C"]. aria2.tellWaiting(-1, 2) returns ["C", "B"]. 280 | 281 | The response is of type array and its element is the same struct returned by aria2.tellStatus() method. 282 | ''' 283 | return self.send_request('tellWaiting', [offset, num, [keys]]) 284 | 285 | 286 | # IMPLEMENTED IN SIMPLE_CLIENT 287 | def tell_stopped(self, offset=0, num=100, keys=[]): 288 | ''' This method returns the list of stopped download. offset is of type integer and specifies the offset 289 | from the oldest download. num is of type integer and specifies the number of downloads to be returned. 290 | For keys parameter, please refer to aria2.tellStatus() method. 291 | 292 | offset and num have the same semantics as aria2.tellWaiting() method. 293 | 294 | The response is of type array and its element is the same struct returned by aria2.tellStatus() method. 295 | ''' 296 | return self.send_request('tellStopped', [offset, num, [keys]]) 297 | 298 | 299 | # IMPLEMENTED IN SIMPLE_CLIENT 300 | def get_option(self, gid): 301 | ''' This method returns options of the download denoted by gid. The response is of type struct. Its key is 302 | the name of option. The value type is string. Note that this method does not return options which have no 303 | default value and have not been set by the command-line options, configuration files or RPC methods. 304 | ''' 305 | return self.send_request('getOption', [gid,]) 306 | 307 | 308 | # IMPLEMENTED IN SIMPLE_CLIENT 309 | def get_global_stats(self): 310 | ''' This method returns global statistics such as overall download and upload speed. The response is of 311 | type struct and contains following keys. The value type is string. 312 | 313 | downloadSpeed: Overall download speed (byte/sec). 314 | uploadSpeed: Overall upload speed(byte/sec). 315 | numActive: The number of active downloads. 316 | numWaiting: The number of waiting downloads. 317 | numStopped: The number of stopped downloads. 318 | ''' 319 | return self.send_request('getGlobalStat') 320 | 321 | 322 | # IMPLEMENTED IN SIMPLE_CLIENT 323 | def get_global_option(self): 324 | ''' This method returns global options. The response is of type struct. Its key is the name of option. 325 | The value type is string. Note that this method does not return options which have no default value and 326 | have not been set by the command-line options, configuration files or RPC methods. Because global options 327 | are used as a template for the options of newly added download, the response contains keys returned by 328 | self.getOption() method. 329 | ''' 330 | return self.send_request('getGlobalOption') 331 | 332 | 333 | def purge_download_result(self): 334 | ''' This method purges completed/error/removed downloads to free memory. This method returns OK. 335 | ''' 336 | return self.send_request('purgeDownloadResult') 337 | 338 | 339 | def remove_download_result(self, gid): 340 | ''' This method removes completed/error/removed download denoted by gid from memory. 341 | This method returns OK for success. 342 | ''' 343 | return self.send_request('removeDownloadResult', [gid,]) 344 | 345 | 346 | # IMPLEMENTED IN SIMPLE_CLIENT 347 | def get_version(self): 348 | ''' This method returns version of the program and the list of enabled features. 349 | The response is of type struct and contains following keys. 350 | 351 | - version: Version number of the program in string. 352 | - enabledFeatures: List of enabled features. Each feature name is of type string. 353 | ''' 354 | return self.send_request('getVersion') 355 | 356 | 357 | # IMPLEMENTED IN SIMPLE_CLIENT 358 | def get_session_info(self): 359 | ''' This method returns session information. 360 | The response is of type struct and contains following key. 361 | 362 | - sessionId: Session ID, which is generated each time when aria2 is invoked. 363 | ''' 364 | return self.send_request('getSessionInfo') 365 | 366 | 367 | def shutdown(self): 368 | ''' This method shutdowns aria2. This method returns OK. 369 | ''' 370 | return self.send_request('shutdown') 371 | 372 | 373 | def forceShutdown(self): 374 | ''' This method shutdowns aria2. This method behaves like aria2.shutdown() except that any actions 375 | which takes time such as contacting BitTorrent tracker are skipped. This method returns OK. 376 | ''' 377 | return self.send_request('forceShutdown') 378 | 379 | 380 | -------------------------------------------------------------------------------- /aria2clib/simple_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # simplified aria2c python library for JSON-RPC 5 | # 6 | # aria2 provides JSON-RPC over HTTP and XML-RPC over HTTP and they basically have the same functionality. 7 | # aria2 also provides JSON-RPC over WebSocket. JSON-RPC over WebSocket uses same method signatures and 8 | # response format with JSON-RPC over HTTP, but it additionally has server-initiated notifications. 9 | # 10 | # The request path of JSON-RPC interface (for both over HTTP and over WebSocket) is /jsonrpc. The request 11 | # path of XML-RPC interface is /rpc. 12 | # 13 | # The WebSocket URI for JSON-RPC over WebSocket is ws://HOST:PORT/jsonrpc. If you enabled SSL/TLS encryption, 14 | # use wss://HOST:PORT/jsonrpc instead. 15 | # 16 | # 17 | # Links 18 | # - [JSON-RPC 2.0 Specification](http://www.jsonrpc.org/specification) 19 | # 20 | 21 | from hashlib import md5 22 | from urllib2 import HTTPError 23 | 24 | from legacy_api import LegacyClient 25 | 26 | 27 | class SimpleClient(object): 28 | ''' SimpleClient 29 | ''' 30 | def __init__(self, client_id=None, uri=None, username=None, password=None): 31 | ''' __init__ 32 | ''' 33 | self._client = LegacyClient(client_id=client_id, uri=uri, username=username, password=password) 34 | 35 | 36 | def _calc_gid(self, url): 37 | ''' return gid as hash of url 38 | 39 | Hash calculated based on md5 algorithm with length limitation up to 16 characters 40 | ''' 41 | return md5(url).hexdigest()[:16] 42 | 43 | 44 | def _queues_files(self): 45 | ''' return the list of active, waiting, stopped queues 46 | 47 | flatting `uris` fields 48 | ''' 49 | result = list() 50 | 51 | queues = ( 52 | (u'active', self._client.tell_active()), 53 | (u'waiting', self._client.tell_waiting()), 54 | (u'stopped', self._client.tell_stopped()), 55 | ) 56 | 57 | for (status, queue) in queues: 58 | queue = queue.get('result', []) 59 | for f in queue: 60 | for file_info in f.get('files', []): 61 | file_info[u'status'] = status 62 | file_info = self._flatting_file_info(file_info) 63 | file_info[u'gids'] = [self._calc_gid(u) for u in file_info[u'uris']] 64 | result.append(file_info) 65 | 66 | return result 67 | 68 | 69 | def _flatting_file_info(self, info): 70 | ''' return flatted file info 71 | ''' 72 | uris = dict() 73 | for uri in info['uris']: 74 | if uri[u'uri'] not in uris: 75 | uris[uri[u'uri']] = [uri['status'],] 76 | else: 77 | uris[uri[u'uri']].append(uri['status']) 78 | uris[uri[u'uri']] = list(set(uris[uri[u'uri']])) 79 | info[u'uris'] = uris 80 | return info 81 | 82 | 83 | def _update_urls(self, url): 84 | ''' return the list of pairs: url and GID (md5 hash of url) 85 | 86 | The GID must be hex string of 16 characters, thus [0-9a-zA-Z] are allowed and leading zeros must not be 87 | stripped. The GID all 0 is reserved and must not be used. The GID must be unique, otherwise error is 88 | reported and the download is not added. 89 | ''' 90 | urls = list() 91 | 92 | if isinstance(url, (unicode, str)): 93 | urls.append(url) 94 | elif isinstance(url, (list, tuple)): 95 | urls.extend(url) 96 | else: 97 | raise RuntimeError('Incorrect url type: %s' % type(url)) 98 | 99 | urls = [(u, self._calc_gid(u)) for u in urls] 100 | return urls 101 | 102 | 103 | def get(self, gid=[]): 104 | ''' get status of files 105 | ''' 106 | files = self._queues_files() 107 | if isinstance(gid, (unicode, str)): 108 | return [f for f in files if gid in f[u'gids']] 109 | 110 | elif isinstance(gid, (list, tuple)) and gid: 111 | result = list() 112 | for f in files: 113 | result.extend([f for g in gid if g in f[u'gids']]) 114 | return result 115 | else: 116 | return files 117 | 118 | 119 | def put(self, urls, params={}, pause=False): 120 | ''' put url(s) to queue for downloading 121 | 122 | if pause=True, If the download is active, the download is placed on the first position of waiting queue. 123 | As long as the status is paused, the download is not started. To continue download, call put() with pause=False 124 | for those urls 125 | ''' 126 | 127 | result = list() 128 | for u in self._update_urls(urls): 129 | params['gid'] = u[1] 130 | response = self._client.add_uri(u[0], params) 131 | result.append(response.get('result', None)) 132 | return result 133 | 134 | 135 | def delete(self, gid, force=False): 136 | ''' delete file(s) by gid(s) 137 | 138 | - gid can be as single value or a list of gids 139 | 140 | - force=True, this method removes download without any action which takes time such as contacting 141 | BitTorrent tracker 142 | - force=Falce (default), it is stopped download at first than download becomes removed. 143 | ''' 144 | gids = list() 145 | 146 | # handle list or single url(s) 147 | if isinstance(gid, (str, unicode)): 148 | gids.append(gid) 149 | else: 150 | gids.extend(gid) 151 | 152 | result = list() 153 | for gid in gids: 154 | if force: 155 | try: 156 | response = self._client.force_remove(gid).get('result', None) 157 | except HTTPError: 158 | pass 159 | else: 160 | try: 161 | response = self._client.remove(gid).get('result', None) 162 | except HTTPError: 163 | pass 164 | result.append((gid, self._client.remove_download_result(gid).get('result', None))) 165 | return result 166 | 167 | 168 | def stats(self): 169 | ''' return aria2c stats 170 | ''' 171 | response = {} 172 | response['version'] = self._client.get_version().get('result', None) 173 | response['session_info'] = self._client.get_session_info().get('result', None) 174 | response['global_stats'] = self._client.get_global_stats().get('result', None) 175 | response['global_option'] = self._client.get_global_option().get('result', None) 176 | return response 177 | 178 | 179 | -------------------------------------------------------------------------------- /init-scripts/README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | **Warning!** The configation below tested only in Ubuntu environment (13.10) 4 | 5 | - download [installation package](https://github.com/ownport/aria2c-as-a-service/archive/master.zip) 6 | 7 | ## Fast installation 8 | 9 | ``` 10 | $ sudo ./install.sh 11 | ``` 12 | 13 | ## Manual install 14 | 15 | - install aria2c 16 | ```shell 17 | $ sudo apt-get install aria2 18 | ``` 19 | 20 | - create user _aria2c_ and directories 21 | ```shell 22 | $ sudo useradd --system --home-dir /var/local/aria2c aria2c 23 | 24 | $ sudo mkdir -p /var/local/aria2c/store 25 | $ sudo touch /var/local/aria2c/session 26 | 27 | $ sudo chown -R aria2c: /var/local/aria2c 28 | $ sudo chmod -R ug=rwx,o=rx /var/local/aria2c 29 | 30 | $ sudo mkdir -p /var/log/aria2c 31 | $ sudo chown -R aria2c: /var/log/aria2c 32 | ``` 33 | 34 | - copy in _aria2c_ script to /etc/init.d/aria2c 35 | ```shell 36 | $ sudo cp aria2c /etc/init.d/aria2c 37 | ``` 38 | 39 | - put the following in /etc/aria2.conf 40 | ```shell 41 | $ sudo cp etc/aria2c.conf /etc/aria2c.conf 42 | ``` 43 | 44 | ## Manage aria2c service 45 | 46 | ``` 47 | $ sudo service aria2c 48 | * Usage: /etc/init.d/aria2c {start|stop|restart|reload|force-reload|status} 49 | $ 50 | $ sudo service aria2c start 51 | * Starting aria2c aria2c 52 | $ 53 | $ sudo service aria2c status 54 | * aria2c is running 55 | $ 56 | $ sudo service aria2c restart 57 | * Restarting aria2c aria2c 58 | $ sudo service aria2c stop 59 | * Stopping aria2c aria2c [ OK ] 60 | $ 61 | $ sudo service aria2c status 62 | * aria2c is not running 63 | $ 64 | ``` 65 | 66 | ## to run aria2c at startup: 67 | ``` 68 | $ sudo update-rc.d aria2c defaults 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /init-scripts/aria2c: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: aria2c 4 | # Required-Start: $network $local_fs $remote_fs 5 | # Required-Stop:: $network $local_fs $remote_fs 6 | # Should-Start: $all 7 | # Should-Stop: $all 8 | # Default-Start: 2 3 4 5 9 | # Default-Stop: 0 1 6 10 | # Short-Description: aria2c - Download Manager 11 | ### END INIT INFO 12 | 13 | NAME=aria2c 14 | ARIA2C=/usr/bin/$NAME 15 | PIDFILE=/var/run/$NAME.pid 16 | CONF=/etc/aria2c.conf 17 | ARGS="--conf-path=${CONF}" 18 | USER=aria2c 19 | 20 | test -f $ARIA2C || exit 0 21 | 22 | . /lib/lsb/init-functions 23 | 24 | case "$1" in 25 | 26 | start) 27 | log_daemon_msg "Starting aria2c" "aria2c" 28 | start-stop-daemon --start --quiet -b -m --pidfile $PIDFILE --chuid $USER --startas $ARIA2C -- $ARGS 29 | log_end_msg $? 30 | ;; 31 | 32 | stop) 33 | log_daemon_msg "Stopping aria2c" "aria2c" 34 | start-stop-daemon --stop --quiet --pidfile $PIDFILE 35 | log_end_msg $? 36 | ;; 37 | 38 | restart|reload|force-reload) 39 | log_daemon_msg "Restarting aria2c" "aria2c" 40 | start-stop-daemon --stop --retry 5 --quiet --pidfile $PIDFILE 41 | start-stop-daemon --start --quiet -b -m --pidfile $PIDFILE --chuid $USER --startas $ARIA2C -- $ARGS 42 | log_end_msg $? 43 | ;; 44 | 45 | status) 46 | status_of_proc -p $PIDFILE $ARIA2C aria2c && exit 0 || exit $? 47 | ;; 48 | 49 | *) 50 | log_action_msg "Usage: /etc/init.d/aria2c {start|stop|restart|reload|force-reload|status}" 51 | exit 2 52 | ;; 53 | esac 54 | exit 0 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /init-scripts/aria2c.conf: -------------------------------------------------------------------------------- 1 | log-level=error 2 | log=/var/log/aria2c/aria2c.log 3 | 4 | continue=true 5 | user-agent=Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) 6 | 7 | max-connection-per-server=1 8 | max-concurrent-downloads=1 9 | 10 | disk-cache=25M 11 | 12 | enable-rpc=true 13 | rpc-listen-port=6800 14 | rpc-listen-all=true 15 | rpc-user=aria2c 16 | rpc-passwd=aria2c 17 | 18 | dir=/var/local/aria2c/store 19 | 20 | save-session-interval=30 21 | input-file=/var/local/aria2c/session 22 | save-session=/var/local/aria2c/session 23 | 24 | 25 | -------------------------------------------------------------------------------- /init-scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # aria2c installation script 4 | # 5 | 6 | echo '- installing aria2 ...' 7 | apt-get install aria2 8 | 9 | echo '- creation user `aria2c`' 10 | useradd --system --home-dir /var/local/aria2c aria2c 11 | 12 | echo '- creation directories and assigning rights' 13 | mkdir -p /var/local/aria2c/store 14 | chown -R aria2c: /var/local/aria2c 15 | chmod -R ug=rwx,o=rx /var/local/aria2c 16 | touch /var/local/aria2c/session 17 | 18 | mkdir -p /var/log/aria2c 19 | chown -R aria2c: /var/log/aria2c 20 | 21 | echo '- coping script and conf files' 22 | cp aria2c /etc/init.d/aria2c 23 | cp aria2c.conf /etc/aria2c.conf 24 | 25 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | nose 2 | coverage 3 | bottle 4 | 5 | -------------------------------------------------------------------------------- /tests/handlers/on-download-complete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # aria2c event handler `on-download-comleted` 4 | # 5 | 6 | import os 7 | import sys 8 | import json 9 | 10 | from hashlib import md5, sha1 11 | #from aria2clib import SimpleClient 12 | 13 | 14 | def calc_hash(hash_func, filename): 15 | ''' return file checksum calculated by hash_func 16 | ''' 17 | with open(filename, 'rb') as fh: 18 | while True: 19 | data = fh.read(8192) 20 | if not data: 21 | break 22 | hash_func.update(data) 23 | return hash_func.hexdigest() 24 | 25 | 26 | def update_meta(gid, path): 27 | ''' update file metadata 28 | ''' 29 | metafile = path + '.meta' 30 | if os.path.exists(metafile): 31 | metadata = json.load(open(metafile)) 32 | else: 33 | metadata = dict() 34 | 35 | metadata[u'gid'] = unicode(gid) 36 | try: 37 | metadata[u'md5'] = unicode(calc_hash(md5(), path)) 38 | metadata[u'sha1'] = unicode(calc_hash(sha1(), path)) 39 | except IOError: 40 | print >> sys.stderr, 'Error! File not found, %s' % path 41 | sys.exit(1) 42 | 43 | json.dump(metadata, open(metafile, 'wb')) 44 | 45 | 46 | def main(client, gid, files, path): 47 | 48 | update_meta(gid, path) 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/handlers/on-download-complete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # on-download-complete.sh 4 | # 5 | # $ export ARIA2CLIB_HOME=".../aria2c-as-a-service" 6 | GID=$1 7 | FILES=$2 8 | FILEPATH=${@:3} 9 | 10 | python ${ARIA2CLIB_HOME}/aria2clib event \ 11 | --handler=${ARIA2CLIB_HOME}/tests/handlers/on-download-complete.py \ 12 | --gid=$GID --files=$FILES --path="$FILEPATH" 13 | 14 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | from pprint import pprint 5 | from urllib2 import HTTPError 6 | from aria2clib import SimpleClient 7 | from aria2clib import GLOBAL_STATS_FIELDS 8 | from aria2clib import GLOBAL_OPTION_FIELDS 9 | 10 | 11 | class SimpleClientTest(unittest.TestCase): 12 | 13 | def test_client_misconfiguration(self): 14 | ''' test_client_misconfiguration 15 | ''' 16 | self.assertRaises(RuntimeError, SimpleClient, ) 17 | self.assertRaises(RuntimeError, SimpleClient, 'aria2clib-client') 18 | self.assertRaises(RuntimeError, SimpleClient, 'aria2clib-client', 'localhost') 19 | self.assertRaises(RuntimeError, SimpleClient, 'aria2clib-client', 'localhost:6800') 20 | self.assertRaises(RuntimeError, SimpleClient, 'aria2clib-client', 'http:localhost:6800') 21 | self.assertRaises(RuntimeError, SimpleClient, 'aria2clib-client', ['http:localhost:6800']) 22 | 23 | 24 | def test_create_client_without_auth(self): 25 | ''' test_create_client_without_auth 26 | ''' 27 | client = SimpleClient('aria2clib-client', 'http://localhost:6800') 28 | self.assertRaises(HTTPError, client.put, ('http://localhost:8080/static/sc.test_create_client_without_auth')) 29 | 30 | 31 | def test_incorrect_uri_type(self): 32 | ''' test_incorrect_uri_type 33 | ''' 34 | client = SimpleClient('aria2clib-client', 'http://localhost:6800') 35 | self.assertRaises(RuntimeError, client.put, ({})) 36 | 37 | 38 | def test_create_client_with_auth(self): 39 | ''' test_create_client_without_auth 40 | ''' 41 | client = SimpleClient('aria2clib-client', 'http://localhost:6800', 'aria2c', 'aria2c') 42 | response = client.put('http://localhost:8080/static/sc.test_create_client_with_auth') 43 | self.assertEqual(response, [u'233600615c1e6308',]) 44 | 45 | 46 | def test_get_put_delete_one_url(self): 47 | ''' test_get_put_delete_one_url 48 | ''' 49 | client = SimpleClient('aria2clib-client', 'http://localhost:6800', 'aria2c', 'aria2c') 50 | response = client.put('http://localhost:8080/static/sc.test_get_put_delete_one_url') 51 | self.assertEqual(response, [u'e99400b47fd00b83']) 52 | 53 | response = client.get(u'e99400b47fd00b83') 54 | if len(response) < 1: 55 | raise RuntimeError('len(response) < 1, len(): %d' % len(response)) 56 | 57 | response = client.delete(u'e99400b47fd00b83') 58 | self.assertEqual(response, [(u'e99400b47fd00b83', u'OK')]) 59 | 60 | 61 | def test_get_put_delete_many_urls(self): 62 | ''' test_get_put_delete_many_urls 63 | ''' 64 | gids = [u'851ee2c323baaa8b', u'150ba9ab5d31ff46', u'340d520c4ea83c91'] 65 | client = SimpleClient('aria2clib-client', 'http://localhost:6800', 'aria2c', 'aria2c') 66 | response = client.put([ 67 | 'http://localhost:8080/static/sc.test_get_put_delete_many_urls.1', 68 | 'http://localhost:8080/static/sc.test_get_put_delete_many_urls.2', 69 | 'http://localhost:8080/static/sc.test_get_put_delete_many_urls.3', 70 | ]) 71 | self.assertEqual(response, gids) 72 | 73 | response = client.get(gids) 74 | if len(response) < 3: 75 | raise RuntimeError('len(response) < 3, len(): %d' % len(response)) 76 | 77 | response = client.get() 78 | if len(response) < 3: 79 | raise RuntimeError('len(response) < 3, len(): %d' % len(response)) 80 | 81 | # TODO delete many 82 | response = client.delete([u'851ee2c323baaa8b', u'150ba9ab5d31ff46']) 83 | self.assertEqual(response, [(u'851ee2c323baaa8b', u'OK'), (u'150ba9ab5d31ff46', u'OK')]) 84 | 85 | response = client.delete(u'340d520c4ea83c91', force=True) 86 | self.assertEqual(response, [(u'340d520c4ea83c91', u'OK'),]) 87 | 88 | 89 | def test_stats(self): 90 | ''' test stats 91 | ''' 92 | client = SimpleClient('aria2clib-client', 'http://localhost:6800', 'aria2c', 'aria2c') 93 | stats = client.stats() 94 | self.assertEqual(set(stats['version'].keys()), set([u'version', u'enabledFeatures'])) 95 | self.assertEqual(set(stats['session_info'].keys()), set([u'sessionId'])) 96 | self.assertEqual(set(stats['global_stats'].keys()).issubset(set(GLOBAL_STATS_FIELDS)), True) 97 | self.assertEqual(set(stats['global_option'].keys()).issubset(set(GLOBAL_OPTION_FIELDS)), True) 98 | 99 | 100 | --------------------------------------------------------------------------------