Repository: Flexget/Flexget

Issue:
aria2.py  is not compatible to new version of aira2
<!---
Before opening an issue, verify:

- Is this a feature request? Post it on https://feathub.com/Flexget/Flexget
- Is this an issue with webui? Make an issue over on https://github.com/Flexget/webui
- Did you recently upgrade? Look at the Change Log and Upgrade Actions to make sure that you don't need to make any changes to your config https://flexget.com/ChangeLog https://flexget.com/UpgradeActions
- Are you running FlexGet as a daemon? Stop it completely and then start it again https://flexget.com/CLI/daemon
- Did you search to see if the issue already exists? https://github.com/Flexget/Flexget/issues
- Did you fill out the issue template as completely as possible?

The issue template is here because it helps to ensure you submitted all the necessary information the first time, and allows us to more quickly review issues. Please fill it out correctly and do not ignore it, no matter how irrelevant you think it may be. Thanks in advance for your help with this!
--->

### Expected behaviour:

<!---
Please don't just say "it doesn't crash" or "it works". Explain what the expected result is.
--->

### Actual behaviour:

### Steps to reproduce:
- Step 1: ...

#### Config:
```yaml
Paste FULL config and remove any personal info if config is too long, attach the file to the ticket.
If issue is with a single task, you can get get resulting configuration by running:
  flexget execute --task <NAME> --dump-config
Make sure to redact any personal information (passwords, api keys, etc) !
```
  
#### Log:
<details>
 <summary>(click to expand)</summary>

```
paste log output here
```
</details>

### Additional information:

- FlexGet version:
- Python version:
- Installation method:
- Using daemon (yes/no):
- OS and version:
- Link to crash log:

<!---
In config and debug/crash logs, remember to redact any personal or sensitive information such as passwords, API keys, private URLs and so on.

Please verify that the following data is present before submitting your issue:

- Link to a paste service or paste above the relevant config (preferably full config, including templates if present). Please make sure the paste does not expire, if possible.
- Link to a paste service or paste above debug-level logs of the relevant task/s (use `flexget -L debug execute --tasks <Task_name>`).
- FlexGet version (use `flexget -V` to get it).
- Full Python version, for example `2.7.11` (use `python -V` to get it). 
- Installation method (pip, git install, etc).
- Whether or not you're running FlexGet as a daemon.
- OS and version.
- Attach crash log if one was generated, in addition to the debug-level log. It can be found in the directory with your config file.
--->
hi, good gays, thanks for your good tools.
now i have a problem, the new  aria2  RPC  could not support http,  only support https/wss,  i connect  https://xxx:jsonrpc well in aria2ng.  but it give the error message below in flexget.  thanks a lot!
INFO     aria2         soft            Connecting to daemon at **### http://xxxx:2083/rpc**
CRITICAL task          soft            BUG: Unhandled error in plugin aria2: <ProtocolError for **### xxx:2083/rpc: 400 Bad Request**> 


Discussion:
It's hard to know exactly what you're facing without seeing your configuration and some log output, but I think the issue is that you are trying to connect Flexget with [`AiraNg`](https://github.com/mayswind/AriaNg) which isn't supported.  The Flexget aria2 plugin connects *directly* to the aria2 process' XML-RPC running in daemon mode.
thanks for your reply.   i make a mistake,  it is that AiraNg do not support  http  but https/wss  ,  and   aira rpc support  http,https,ws,wss, all. 
so i have to enable  tls in aria2.conf.  
because  flexget connect aria2-rpc by ' http://ip:port/rpc'  default  ,so  it get error.
then i modify the aria2.py  (/root/env/lib/python3.9/site-packages/flexget/plugins/clients/aria2.py) , it works well now.
"url = 'http://%s%s:%s/rpc' % (userpass, server, port)"----->"url = 'https://%s%s:%s/rpc' % (userpass, server, port)"
@wx69wx You might be interested in following #3473 as it looks like it will address your issue.
yes，you are right.  my problem is same to #3473 ，thanks for your advice. let me try use_ssl. haha

Resolving patch:
diff --git a/flexget/plugins/clients/aria2.py b/flexget/plugins/clients/aria2.py
--- a/flexget/plugins/clients/aria2.py
+++ b/flexget/plugins/clients/aria2.py
@@ -1,8 +1,11 @@
+import json
 import os
 import re
+import ssl
 import xmlrpc.client
 from socket import error as socket_error
 
+import requests
 from loguru import logger
 
 from flexget import plugin
@@ -12,6 +15,137 @@
 logger = logger.bind(name='aria2')
 
 
+class RpcClient:
+    def __init__(self, server, port, scheme, rpc_path, username, password, secret):
+        if secret:
+            self.token = 'token:' + secret
+        else:
+            self.token = ''
+        if username and password:
+            userpass = '%s:%s@' % (username, password)
+        else:
+            userpass = ''
+        self.url = '%s://%s%s:%s/%s' % (scheme, userpass, server, port, rpc_path)
+        logger.debug('aria2 url: {}', self.url)
+
+    def add_uri(self, uris, options):
+        raise plugin.PluginError('Unsupported Operation')
+
+    def add_torrent(self, torrent, options):
+        raise plugin.PluginError('Unsupported Operation')
+
+
+class JsonRpcClient(RpcClient):
+    RPC_ID = 'FLEXGET'
+    RPC_VERSION = '2.0'
+
+    ADDURI_METHOD = 'aria2.addUri'
+    GETGLOBALSTAT_METHOD = 'aria2.getGlobalStat'
+    ADDTORRENT_METHOD = 'aria2.addTorrent'
+    ADDMETALINK_METHOD = 'aria2.addMetalink'
+
+    def __init__(self, server, port, scheme, rpc_path, username=None, password=None, secret=None):
+        super(JsonRpcClient, self).__init__(
+            server, port, scheme, rpc_path, username, password, secret
+        )
+        # trigger _default_error_handle on failure
+        self.get_global_stat()
+
+    def _get_req_params(self, method, params=[]):
+        req_params = {
+            'jsonrpc': JsonRpcClient.RPC_VERSION,
+            'id': JsonRpcClient.RPC_ID,
+            'method': method,
+            'params': params,
+        }
+        if self.token:
+            req_params['params'].insert(0, self.token)
+        if not req_params['params']:
+            del req_params['params']
+        return req_params
+
+    def _default_error_handle(code, message):
+        logger.critical('Fault code {} message {}', code, message)
+        raise plugin.PluginError('Fault code %s message %s' % (code, message), logger)
+
+    def _default_success_handle(response):
+        return response.text
+
+    def _post(
+        self, method, params, on_success=_default_success_handle, on_fail=_default_error_handle
+    ):
+        resp = requests.post(self.url, data=json.dumps(self._get_req_params(method, params)))
+        result = resp.json()
+        if "error" in result:
+            return on_fail(result["error"]["code"], result["error"]["message"])
+        else:
+            return on_success(resp)
+
+    def add_uri(self, uris, options):
+        # https://aria2.github.io/manual/en/html/aria2c.html#aria2.addUri
+        return self._post(JsonRpcClient.ADDURI_METHOD, params=[[uris], options])
+
+    def add_torrent(self, torrent, options):
+        # https://aria2.github.io/manual/en/html/aria2c.html#aria2.addTorrent
+        params = [torrent]
+        return self._post(JsonRpcClient.ADDTORRENT_METHOD, params=[torrent, options])
+
+    def get_global_stat(self):
+        # https://aria2.github.io/manual/en/html/aria2c.html#aria2.getGlobalStat
+        return self._post(JsonRpcClient.GETGLOBALSTAT_METHOD, params=[])
+
+
+class XmlRpcClient(RpcClient):
+    def __init__(self, server, port, scheme, rpc_path, username=None, password=None, secret=None):
+        schemes = {'http': None, 'https': ssl.SSLContext()}
+        if scheme not in schemes:
+            raise plugin.PluginError('Unknown scheme: %s' % (scheme), logger)
+        super(XmlRpcClient, self).__init__(
+            server, port, scheme, rpc_path, username, password, secret
+        )
+        try:
+            self._aria2 = xmlrpc.client.ServerProxy(self.url, context=schemes[scheme]).aria2
+        except xmlrpc.client.ProtocolError as err:
+            raise plugin.PluginError(
+                'Could not connect to aria2 at %s. Protocol error %s: %s'
+                % (self.url, err.errcode, err.errmsg),
+                logger,
+            )
+        except xmlrpc.client.Fault as err:
+            raise plugin.PluginError(
+                'XML-RPC fault: Unable to connect to aria2 daemon at %s: %s'
+                % (self.url, err.faultString),
+                logger,
+            )
+        except socket_error as e:
+            raise plugin.PluginError(
+                'Socket connection issue with aria2 daemon at %s: %s' % (self.url, e), logger
+            )
+        except:
+            logger.opt(exception=True).debug('Unexpected error during aria2 connection')
+            raise plugin.PluginError(
+                'Unidentified error during connection to aria2 daemon', logger
+            )
+
+    def add_uri(self, uris, options):
+        # https://aria2.github.io/manual/en/html/aria2c.html#aria2.addUri
+        params = [[uris]]
+        if options:
+            params.append(options)
+        if self.token:
+            params.insert(0, self.token)
+        return self._aria2.addUri(*params)
+
+    def add_torrent(self, torrent, options):
+        # https://aria2.github.io/manual/en/html/aria2c.html#aria2.addTorrent
+        params = [torrent]
+        if options:
+            params.append(options)
+        if self.token:
+            params.insert(0, self.token)
+        return self._aria2.addTorrent(*params)
+
+
 class OutputAria2:
     """
     Simple Aria2 output
@@ -29,8 +163,13 @@ class OutputAria2:
             'server': {'type': 'string', 'default': 'localhost'},
             'port': {'type': 'integer', 'default': 6800},
             'secret': {'type': 'string', 'default': ''},
-            'username': {'type': 'string', 'default': ''},  # NOTE: To be deprecated by aria2
+            # NOTE: To be deprecated by aria2
+            'username': {'type': 'string', 'default': ''},
             'password': {'type': 'string', 'default': ''},
+            'scheme': {'type': 'string', 'default': 'http'},
+            # NOTE: xml/json
+            'rpc_mode': {'type': 'string', 'default': 'xml'},
+            'rpc_path': {'type': 'string', 'default': 'rpc'},
             'path': {'type': 'string'},
             'filename': {'type': 'string'},
             'add_extension': {
@@ -49,43 +188,14 @@ class OutputAria2:
         'additionalProperties': False,
     }
 
-    def aria2_connection(self, server, port, username=None, password=None):
-        if username and password:
-            userpass = '%s:%s@' % (username, password)
-        else:
-            userpass = ''
-        url = 'http://%s%s:%s/rpc' % (userpass, server, port)
-        logger.debug('aria2 url: {}', url)
-        logger.info('Connecting to daemon at {}', url)
-        try:
-            return xmlrpc.client.ServerProxy(url).aria2
-        except xmlrpc.client.ProtocolError as err:
-            raise plugin.PluginError(
-                'Could not connect to aria2 at %s. Protocol error %s: %s'
-                % (url, err.errcode, err.errmsg),
-                logger,
-            )
-        except xmlrpc.client.Fault as err:
-            raise plugin.PluginError(
-                'XML-RPC fault: Unable to connect to aria2 daemon at %s: %s'
-                % (url, err.faultString),
-                logger,
-            )
-        except socket_error as e:
-            raise plugin.PluginError(
-                'Socket connection issue with aria2 daemon at %s: %s' % (url, e), logger
-            )
-        except:
-            logger.opt(exception=True).debug('Unexpected error during aria2 connection')
-            raise plugin.PluginError(
-                'Unidentified error during connection to aria2 daemon', logger
-            )
-
     def prepare_config(self, config):
         config.setdefault('server', 'localhost')
         config.setdefault('port', 6800)
         config.setdefault('username', '')
         config.setdefault('password', '')
+        config.setdefault('scheme', 'http')
+        config.setdefault('rpc_mode', 'xml')
+        config.setdefault('rpc_path', 'rpc')
         config.setdefault('secret', '')
         config.setdefault('options', {})
         config.setdefault('add_extension', False)
@@ -96,9 +206,19 @@ def on_task_output(self, task, config):
         if task.options.learn:
             return
         config = self.prepare_config(config)
-        aria2 = self.aria2_connection(
-            config['server'], config['port'], config['username'], config['password']
+        rpc_clients = {'xml': XmlRpcClient, 'json': JsonRpcClient}
+        if config['rpc_mode'] not in rpc_clients:
+            entry.fail('Unknown rpc_mode: %s' % config['rpc_mode'])
+        aria2 = rpc_clients[config['rpc_mode']](
+            config['server'],
+            config['port'],
+            config['scheme'],
+            config['rpc_path'],
+            config['username'],
+            config['password'],
+            config['secret'],
         )
+
         for entry in task.accepted:
             if task.options.test:
                 logger.verbose('Would add `{}` to aria2.', entry['title'])
@@ -114,7 +234,7 @@ def on_task_output(self, task, config):
                 logger.opt(exception=True).debug('Exception type {}', type(e))
                 raise
 
-    def add_entry(self, aria2, entry, config, task):
+    def add_entry(self, aria2: RpcClient, entry, config, task):
         """
         Add entry to Aria2
         """
@@ -179,9 +299,6 @@ def add_entry(self, aria2, entry, config, task):
                 entry.fail('failed to render \'filename\': %s' % e)
                 return
 
-        secret = None
-        if config['secret']:
-            secret = 'token:%s' % config['secret']
         # handle torrent files
         if 'torrent' in entry:
             if 'file' in entry:
@@ -192,18 +309,12 @@ def add_entry(self, aria2, entry, config, task):
             else:
                 entry.fail('Cannot find torrent file')
                 return
-            if secret:
-                return aria2.addTorrent(
-                    secret, xmlrpc.client.Binary(open(torrent_file, mode='rb').read()), [], options
-                )
-            return aria2.addTorrent(
-                xmlrpc.client.Binary(open(torrent_file, mode='rb').read()), [], options
+            return aria2.add_torrent(
+                xmlrpc.client.Binary(open(torrent_file, mode='rb').read()), options
             )
         # handle everything else (except metalink -- which is unsupported)
         # so magnets, https, http, ftp .. etc
-        if secret:
-            return aria2.addUri(secret, [entry['url']], options)
-        return aria2.addUri([entry['url']], options)
+        return aria2.add_uri(entry['url'], options)
 
 
 @event('plugin.register')


Test patch: