From 4d91652d6d188f8cac5b7d906e3d005f0ec811c5 Mon Sep 17 00:00:00 2001
From: Matt <matt@mattcarey.me>
Date: Sat, 21 Jan 2017 11:31:31 -0500
Subject: [PATCH] Added initial support for uTorrent

---
 influxdbSeedbox.py | 356 ++-------------------------
 requirements.txt   |   2 +-
 torrentclients.py  | 584 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 601 insertions(+), 341 deletions(-)
 create mode 100644 torrentclients.py

diff --git a/influxdbSeedbox.py b/influxdbSeedbox.py
index 2751c44..423d1b7 100644
--- a/influxdbSeedbox.py
+++ b/influxdbSeedbox.py
@@ -11,9 +11,15 @@ import json
 import gzip
 from urllib.request import Request, URLError, urlopen
 import socket
+from bs4 import BeautifulSoup
+import urllib.request
+from torrentclients import UTorrentClient, DelugeClient
+
 
 # TODO Move urlopen login in each method call to one central method
 # TODO Keep track of tracker overall ratios
+# TODO Add print to _send_log.  Will but down on logging and print code in methods
+
 __author__ = 'barry'
 class configManager():
 
@@ -21,7 +27,7 @@ class configManager():
 
     def __init__(self, config):
 
-        self.valid_torrent_clients = ['deluge']
+        self.valid_torrent_clients = ['deluge', 'utorrent']
 
         print('Loading Configuration File {}'.format(config))
         config_file = os.path.join(os.getcwd(), config)
@@ -63,7 +69,7 @@ class configManager():
         self.logging_censor = self.config['LOGGING'].getboolean('CensorLogs', fallback=True)
 
         # TorrentClient
-        self.tor_client = self.config['TORRENTCLIENT'].get('Client', fallback=None)
+        self.tor_client = self.config['TORRENTCLIENT'].get('Client', fallback=None).lower()
         self.tor_client_user = self.config['TORRENTCLIENT'].get('Username', fallback=None)
         self.tor_client_password = self.config['TORRENTCLIENT'].get('Password', fallback=None)
         self.tor_client_url = self.config['TORRENTCLIENT'].get('Url', fallback=None)
@@ -117,6 +123,13 @@ class influxdbSeedbox():
                                            password=self.config.tor_client_password,
                                            url=self.config.tor_client_url,
                                            hostname=self.config.hostname)
+        elif self.config.tor_client == 'utorrent':
+            print('Generating uTorrent Client')
+            self.tor_client = UTorrentClient(self.send_log,
+                                           username=self.config.tor_client_user,
+                                           password=self.config.tor_client_password,
+                                           url=self.config.tor_client_url,
+                                           hostname=self.config.hostname)
 
     def _set_logging(self):
         """
@@ -224,351 +237,14 @@ class influxdbSeedbox():
             torrent_json = self.tor_client.process_torrents()
             if torrent_json:
                 self.write_influx_data(torrent_json)
-            self.tor_client.get_active_plugins()
+            #self.tor_client.get_active_plugins()
             tracker_json = self.tor_client.process_tracker_list()
             if tracker_json:
                 self.write_influx_data(tracker_json)
             time.sleep(self.delay)
 
 
-class TorrentClient:
-    """
-    Stub class to base individual torrent client classes on
-    """
-    def __init__(self, logger, username=None, password=None, url=None, hostname=None):
-
-        self.send_log = logger
-        self.hostname = hostname
-
-        # TODO Validate we're not getting None
-
-        # API Data
-        self.username = username
-        self.password = password
-        self.url = url
-
-        # Torrent Data
-        self.torrent_client = None
-        self.torrent_list = {}
-        self.trackers = []
-        self.active_plugins = []
-
-    def _create_request(self):
-        raise NotImplementedError
-
-    def _process_response(self, res):
-        raise NotImplementedError
-
-    def _authenticate(self):
-        raise NotImplementedError
-
-    def get_all_torrents(self):
-        raise NotImplementedError
-
-    def get_active_plugins(self):
-        raise NotImplementedError
-
-    def process_tracker_list(self):
-        raise NotImplementedError
-
-    def process_torrents(self):
-        raise NotImplementedError
-
-
-class DelugeClient(TorrentClient):
-
-    def __init__(self, logger, username=None, password=None, url=None, hostname=None):
-        TorrentClient.__init__(self, logger, username=username, password=password, url=url, hostname=hostname)
-
-        self.session_id = None
-        self.request_id = 0
-        self.torrent_client = 'Deluge'
-
-        self._authenticate()
-
-    def _add_common_headers(self, req):
-        """
-        Add common headers needed to make the API requests
-        :return: request
-        """
-
-        self.send_log('Adding headers to request', 'info')
-
-        headers = {
-            'Content-Type': 'application/json',
-            'Accept': 'application/json'
-        }
 
-        for k, v in headers.items():
-            req.add_header(k, v)
-
-        if self.session_id:
-            req.add_header('Cookie', self.session_id)
-
-        return req
-
-    def _check_session(self):
-        """
-        Make sure we still have an active session. If not, authenticate again
-        :return:
-        """
-
-        self.send_log('Checking Session State', 'debug')
-
-        req = self._create_request(method='auth.check_session', params=[''])
-
-        try:
-            res = urlopen(req)
-        except URLError as e:
-            msg = 'Failed To check session state.  HTTP Error'
-            self.send_log(msg, 'error')
-            print(msg)
-            print(e)
-            return None
-
-        result = self._process_response(res)
-
-        if not result:
-            self.send_log('No active session. Attempting to re-authenticate', 'error')
-            self._authenticate()
-            return
-
-        self.send_log('Session is still active', 'debug')
-
-    def _create_request(self, method=None, params=None):
-        """
-        Creates and returns a Request object, Allowing us to track request IDs in one spot.
-        We also add the common headers here
-        :return:
-        """
-
-        # TODO Validate method and params
-        data = json.dumps({
-            'id': self.request_id,
-            'method': method,
-            'params': params
-        }).encode('utf-8')
-
-        req = self._add_common_headers(Request(self.url, data=data))
-        self.request_id += 1
-
-        return req
-
-    def _process_response(self, res):
-        """
-        Take the response object and return JSON
-        :param res:
-        :return:
-        """
-        # TODO Figure out exceptions here
-        if res.headers['Content-Encoding'] == 'gzip':
-            self.send_log('Detected gzipped response', 'debug')
-            raw_output = gzip.decompress(res.read()).decode('utf-8')
-        else:
-            self.send_log('Detected other type of response encoding: {}'.format(res.headers['Content-Encoding']), 'debug')
-            raw_output = res.read().decode('utf-8')
-
-        json_output = json.loads(raw_output)
-
-        return json_output if json_output['result'] else None
-
-    def _authenticate(self):
-        """
-        Authenticate against torrent client so we can make future requests
-        If we return from this method we assume we are authenticated for all future requests
-        :return: None
-        """
-        msg = 'Attempting to authenticate against {} API'.format(self.torrent_client)
-        self.send_log(msg, 'info')
-        print(msg)
-
-        req = self._create_request(method='auth.login', params=[self.password])
-
-        try:
-            res = urlopen(req)
-        except URLError as e:
-            msg = 'Failed To Authenticate with torrent client.  HTTP Error'
-            self.send_log(msg, 'critical')
-            print(msg)
-            print(e)
-            sys.exit(1)
-
-        # We need the session ID to send with future requests
-        self.session_id = res.headers['Set-Cookie'].split(';')[0]
-
-        output = self._process_response(res)
-
-        if output and not output['result']:
-            msg = 'Failed to authenticate to {} API. Aborting'.format(self.torrent_client)
-            self.send_log(msg, 'error')
-            print(msg)
-            sys.exit(1)
-
-        msg = 'Successfully Authenticated With {} API'.format(self.torrent_client)
-        self.send_log(msg, 'info')
-        print(msg)
-
-    def get_all_torrents(self):
-        """
-        Return a list of all torrents from the API
-        :return:
-        """
-
-        req = self._create_request(method='core.get_torrents_status', params=['',''])
-        try:
-            self._check_session() # Make sure we still have an active session
-            res = urlopen(req)
-        except URLError as e:
-            msg = 'Failed to get list of torrents.  HTTP Error'
-            self.send_log(msg, 'error')
-            print(msg)
-            print(e)
-            self.torrent_list = []
-            return
-
-        output = self._process_response(res)
-        if output['error']:
-            msg = 'Problem getting torrent list from {}. Error: {}'.format(self.torrent_client, output['error'])
-            print(msg)
-            self.send_log(msg, 'error')
-            self.torrent_list = []
-            return
-
-        self.torrent_list = output['result']
-
-        # Temp trap to find weird characters that won't decode
-        """
-        for k, v in output['result'].items():
-            print(k)
-            print(v.keys())
-            for k2, v2 in v.items():
-                try:
-                    print('Key: ' + k2)
-                    print('Value: ' + str(v2))
-                except Exception as e:
-                    print('test')
-                    print(e)
-        """
-
-
-    def get_active_plugins(self):
-        """
-        Return all active plugins
-        :return:
-        """
-
-        req = self._create_request(method='core.get_enabled_plugins', params=[])
-        try:
-            self._check_session() # Make sure we still have an active session
-            res = urlopen(req)
-        except URLError as e:
-            msg = 'Failed to get list of plugins.  HTTP Error'
-            self.send_log(msg, 'error')
-            print(msg)
-            print(e)
-            self.active_plugins = []
-            return
-
-        output = self._process_response(res)
-        if output['error']:
-            msg = 'Problem getting plugin list from {}. Error: {}'.format(self.torrent_client, output['error'])
-            print(msg)
-            self.send_log(msg, 'error')
-            self.active_plugins = []
-            return
-
-        self.active_plugins = output['result']
-
-
-    def process_tracker_list(self):
-        """
-        Loop through each torrent and pull the tracker data.  This will allow us to track how many torrents we are
-        downloading from each tracker
-        :return:
-        """
-
-        if len(self.torrent_list) == 0:
-            return None
-
-        trackers = {}
-        json_list = []
-
-        # The tracker list is a dict of torrent hashes.  The value for each hash is another dict with data about the
-        # torrent
-        for hash, data in self.torrent_list.items():
-            if data['tracker_host'] in trackers:
-                trackers[data['tracker_host']]['total_torrents'] += 1
-                trackers[data['tracker_host']]['total_upload'] += data['total_uploaded']
-                trackers[data['tracker_host']]['total_download'] += data['all_time_download']
-            else:
-                trackers[data['tracker_host']] = {}
-                trackers[data['tracker_host']]['total_torrents'] = 1
-                trackers[data['tracker_host']]['total_upload'] = data['total_uploaded']
-                trackers[data['tracker_host']]['total_download'] = data['all_time_download']
-
-        for k, v in trackers.items():
-
-            total_ratio = round(v['total_upload'] / v['total_download'], 3)
-            tracker_json = [
-                {
-                    'measurement': 'trackers',
-                    'fields': {
-                        'total_torrents': v['total_torrents'],
-                        'total_upload': v['total_upload'],
-                        'total_download': v['total_download'],
-                        'total_ratio': total_ratio
-                    },
-                    'tags': {
-                        'host': self.hostname,
-                        'tracker': k
-                    }
-                }
-            ]
-            #return tracker_json
-            json_list.append(tracker_json)
-        return json_list
-
-
-    def process_torrents(self):
-        """
-        Go through the list of torrents, format them in JSON and send to influx
-        :return:
-        """
-        if len(self.torrent_list) == 0:
-            return None
-
-        json_list = []
-
-        for hash, data in self.torrent_list.items():
-
-            torrent_json = [
-                {
-                    'measurement': 'torrents',
-                    'fields': {
-                        'hash': hash,
-                        'tracker': data['tracker_host'],
-                        'name': data['name'],
-                        'state': data['state'],
-                        'uploaded': data['total_uploaded'],
-                        'downloaded': data['all_time_download'],
-                        'ratio': round(data['ratio'], 2),
-                        'progress': round(data['progress'], 2),
-                        'seeds': data['total_seeds'],
-                        'size': data['total_size'],
-                        'total_files': data['num_files'],
-                    },
-                    'tags': {
-
-                        'host': self.hostname,
-                        'hash': hash,
-                        'tracker': data['tracker_host'],
-
-                    }
-                }
-            ]
-            #return torrent_json
-            json_list.append(torrent_json)
-        return json_list
 
 def main():
 
diff --git a/requirements.txt b/requirements.txt
index 69dde6a..7738dea 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
 influxdb
-
+beautifulsoup4
\ No newline at end of file
diff --git a/torrentclients.py b/torrentclients.py
new file mode 100644
index 0000000..7db31ca
--- /dev/null
+++ b/torrentclients.py
@@ -0,0 +1,584 @@
+__author__ = 'barry'
+import urllib.request
+from urllib.request import Request, urlopen, URLError
+from urllib.parse import urlsplit
+from bs4 import BeautifulSoup
+import json
+import sys
+import re
+import gzip
+
+# TODO Deal with slashes in client URL
+# TODO Unify the final torrent list so it can be built into json structure by parent instead of each child
+
+class TorrentClient:
+    """
+    Stub class to base individual torrent client classes on
+    """
+    def __init__(self, logger, username=None, password=None, url=None, hostname=None):
+
+        self.send_log = logger
+        self.hostname = hostname
+
+        # TODO Validate we're not getting None
+
+        # API Data
+        self.username = username
+        self.password = password
+        self.url = url
+
+        # Torrent Data
+        self.torrent_client = None
+        self.torrent_list = {}
+        self.trackers = []
+        self.active_plugins = []
+
+    def _add_common_headers(self, req, headers=None):
+        """
+        Add common headers to request
+        :param req: Request object to add headers to
+        :param headers: Dict of headers to add
+        :return:
+        """
+
+        if not headers:
+            return req
+
+        self.send_log('Adding headers to request', 'info')
+        for k, v in headers.items():
+            req.add_header(k, v)
+
+        return req
+
+    def _create_request(self, method=None, params=None):
+        """
+        Needs to be implemented in the child to deal with unique API requirements
+        :param method: Used in Deluge request
+        :param params: Extra data unique to the request
+        :return: Request
+        """
+        raise NotImplementedError
+
+    def _process_response(self, res):
+        # TODO May only be needed for deluge.  Remove from parent if that's the case
+        raise NotImplementedError
+
+    def _authenticate(self):
+        """
+        Needs to be implemented in the child to deal with unique API requirements
+        :return: None
+        """
+        raise NotImplementedError
+
+    def _build_torrent_list(self, torrents):
+        """
+        Take the raw list of torrents from the API and build a unified structure shared by all clients
+        Expected to be implemented in each child to deal with unique returns from each API
+        :param torrents:
+        :return:
+        """
+        raise NotImplementedError
+
+    def get_all_torrents(self):
+        """
+        Needs to be implemented in the child to deal with unique API requirements
+
+        Retrieve a list of all torrents from the client.  Send them on to _build_torrent_list() to put the output
+        into a consistent format that can be used in the parent
+
+        :return: None
+        """
+        raise NotImplementedError
+
+    def get_active_plugins(self):
+        # TODO probably only needed in Deluge.
+        raise NotImplementedError
+
+    def process_tracker_list(self):
+        """
+        Go through the list of torrents and build the list of trackers
+        :return: list of JSON objects for each tracker
+        """
+        if len(self.torrent_list) == 0:
+            return None
+
+        trackers = {}
+        json_list = []
+
+        # The tracker list is a dict of torrent hashes.  The value for each hash is another dict with data about the
+        # torrent
+        for hash, data in self.torrent_list.items():
+            if data['tracker'] in trackers:
+                trackers[data['tracker']]['total_torrents'] += 1
+                trackers[data['tracker']]['total_uploaded'] += data['total_uploaded']
+                trackers[data['tracker']]['total_downloaded'] += data['total_downloaded']
+                trackers[data['tracker']]['total_size'] += data['total_size']
+            else:
+                trackers[data['tracker']] = {}
+                trackers[data['tracker']]['total_torrents'] = 1
+                trackers[data['tracker']]['total_uploaded'] = data['total_uploaded']
+                trackers[data['tracker']]['total_downloaded'] = data['total_downloaded']
+                trackers[data['tracker']]['total_size'] = data['total_size']
+
+        for k, v in trackers.items():
+            print(v)
+            total_ratio = round(v['total_uploaded'] / v['total_size'], 3)
+            tracker_json = [
+                {
+                    'measurement': 'trackers',
+                    'fields': {
+                        'total_torrents': v['total_torrents'],
+                        'total_upload': v['total_uploaded'],
+                        'total_download': v['total_downloaded'],
+                        'total_ratio': total_ratio
+                    },
+                    'tags': {
+                        'host': self.hostname,
+                        'tracker': k,
+                        'client': self.torrent_client
+                    }
+                }
+            ]
+
+            json_list.append(tracker_json)
+
+        return json_list
+
+    def process_torrents(self):
+        """
+        Go through the list of torrents, format them in JSON and send to influx
+        :return:
+        """
+        if len(self.torrent_list) == 0:
+            return None
+
+        json_list = []
+
+        for hash, data in self.torrent_list.items():
+
+            torrent_json = [
+                {
+                    'measurement': 'torrents',
+                    'fields': {
+                        'hash': hash,
+                        'tracker': data['tracker'],
+                        'name': data['name'],
+                        'state': data['state'],
+                        'uploaded': data['total_uploaded'],
+                        'downloaded': data['total_downloaded'],
+                        'ratio': round(data['ratio'], 2),
+                        'progress': round(data['progress'], 2),
+                        'seeds': data['total_seeds'],
+                        'size': data['total_size'],
+                        'total_files': data['total_files'],
+                    },
+                    'tags': {
+
+                        'host': self.hostname,
+                        'hash': hash,
+                        'tracker': data['tracker'],
+                        'client': self.torrent_client
+
+                    }
+                }
+            ]
+
+            json_list.append(torrent_json)
+
+        return json_list
+
+
+class UTorrentClient(TorrentClient):
+
+    def __init__(self, logger, username=None, password=None, url=None, hostname=None):
+        TorrentClient.__init__(self, logger, username=username, password=password, url=url, hostname=hostname)
+
+        self.token = None
+        self.cookie = None
+        self.torrent_client = 'uTorrent'
+
+        self._authenticate()
+
+    def _authenticate(self):
+
+        # TODO Clean this whole mess up.  It's barely functional
+        pwd_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
+        pwd_mgr.add_password(None, self.url, self.username, self.password)
+        handler = urllib.request.HTTPBasicAuthHandler(pwd_mgr)
+        opener = urllib.request.build_opener(handler)
+        token_url = self.url + '/token.html'
+        print('Attempting To Get Token From URL {}'.format(token_url))
+        self.send_log('Attempting To Get Token From URL {}'.format(token_url), 'info')
+        opener.open(token_url)
+        urllib.request.install_opener(opener)
+        req = Request(self.url + '/token.html')
+        res = urlopen(req)
+        self.cookie = res.headers['Set-Cookie'].split(';')[0]
+        soup = BeautifulSoup(res, 'html.parser')
+        token = soup.find("div", {"id": "token"}).text
+        print('Got Token: ' + token)
+        self.token = token
+
+    def _add_common_headers(self, req, headers=None):
+        """
+        Add common headers to the request
+        :param req:
+        :return:
+        """
+
+        headers = {
+            'cache-control': 'no-cache',
+            'Cookie': self.cookie
+        }
+
+        return TorrentClient._add_common_headers(self, req, headers=headers)
+
+    def _create_request(self, method=None, params=None):
+        # TODO Validate that we get params
+        url = self.url + '/?token={}&{}'.format(self.token, params)
+        print('Creating request with url: ' + url)
+
+        req = self._add_common_headers(Request(url))
+
+        return req
+
+    def _build_torrent_list(self, torrents):
+        """
+        Take the resulting torrent list and create a consistent structure shared through all clients
+        :return:
+        """
+
+        msg = 'Structuring list of torrents'
+        self.send_log(msg, 'debug')
+
+        for torrent in torrents:
+            self.torrent_list[torrent[0]] = {}
+            self.torrent_list[torrent[0]]['name'] = torrent[2]
+            self.torrent_list[torrent[0]]['total_size'] = torrent[3]
+            self.torrent_list[torrent[0]]['progress'] = torrent[4] / 1000 * 100
+            self.torrent_list[torrent[0]]['total_downloaded'] = torrent[5]
+            self.torrent_list[torrent[0]]['total_uploaded'] = torrent[6]
+            self.torrent_list[torrent[0]]['ratio'] = torrent[7] / 1000
+            self.torrent_list[torrent[0]]['total_seeds'] = torrent[15]
+            self.torrent_list[torrent[0]]['state'] = torrent[22]
+            self.torrent_list[torrent[0]]['tracker'] = self._get_tracker(torrent[0])
+            self.torrent_list[torrent[0]]['total_files'] = self._get_file_count(torrent[0])
+
+
+    def _get_tracker(self, hash):
+        """
+        Get the tracker for a specific torrent for uTorrent
+        :param hash:
+        :return:
+        """
+
+        msg = 'Attempting to get tracker for hash {}'.format(hash)
+        print(msg)
+        self.send_log(msg, 'debug')
+        req = self._create_request(params='action=getprops&hash={}'.format(hash))
+
+        try:
+            res = urlopen(req).read().decode('utf-8')
+        except URLError as e:
+            msg = 'Failed to get trackers from URL for hash {}'.format(hash)
+            print(msg)
+            self.send_log(msg, 'error')
+            return 'N/A'
+
+        res_json = json.loads(res)
+
+        tracker = res_json['props'][0]['trackers'].split()[0]
+
+        tracker_url = urlsplit(tracker).netloc
+
+        # TODO Exception here.  Deal with it or just return URL with port
+        # Remove port from URL
+        for match in re.findall(r":\d{4,4}", tracker_url):
+            tracker_url = tracker_url.replace(match, '')
+
+        return tracker_url
+
+    def _get_file_count(self, hash):
+        """
+        Method for uTorrent to get total file since it requires another API call
+        :param hash:
+        :return:
+        """
+
+        msg = 'Attempting to get file list for hash {}'.format(hash)
+        print(msg)
+        self.send_log(msg, 'debug')
+
+        req = self._create_request(params='action=getfiles&hash={}'.format(hash))
+
+        try:
+            res = urlopen(req).read().decode('utf-8')
+        except URLError as e:
+            msg = 'Failed to get file list from URL for hash {}'.format(hash)
+            print(msg)
+            self.send_log(msg, 'error')
+            return 'N/A'
+
+        res_json = json.loads(res)
+
+        return len(res_json['files'][1])
+
+    def get_all_torrents(self):
+        """
+        Get all torrents that are currently active
+        :return:
+        """
+
+        msg = 'Attempting to get all torrents from {}'.format(self.url)
+        print(msg)
+        self.send_log(msg, 'info')
+
+        req = self._create_request(params='list=1')
+
+        try:
+            res = urlopen(req)
+            final = res.read().decode('utf-8')
+        except URLError as e:
+            msg = 'Failed to get list of all torrents'
+            print(msg)
+            print(e)
+            self.send_log(msg, 'error')
+            self.torrent_list = {}
+            return
+
+        final_json = json.loads(final)
+
+        self._build_torrent_list(final_json['torrents'])
+
+
+
+
+class DelugeClient(TorrentClient):
+
+    def __init__(self, logger, username=None, password=None, url=None, hostname=None):
+        TorrentClient.__init__(self, logger, username=username, password=password, url=url, hostname=hostname)
+
+        self.session_id = None
+        self.request_id = 0
+        self.torrent_client = 'Deluge'
+
+        self._authenticate()
+
+    def _add_common_headers(self, req, headers=None):
+        """
+        Add common headers needed to make the API requests
+        :return: request
+        """
+
+        self.send_log('Adding headers to request', 'info')
+
+        headers = {
+            'Content-Type': 'application/json',
+            'Accept': 'application/json'
+        }
+
+        for k, v in headers.items():
+            req.add_header(k, v)
+
+        if self.session_id:
+            req.add_header('Cookie', self.session_id)
+
+        return req
+
+    def _check_session(self):
+        """
+        Make sure we still have an active session. If not, authenticate again
+        :return:
+        """
+
+        self.send_log('Checking Session State', 'debug')
+
+        req = self._create_request(method='auth.check_session', params=[''])
+
+        try:
+            res = urlopen(req)
+        except URLError as e:
+            msg = 'Failed To check session state.  HTTP Error'
+            self.send_log(msg, 'error')
+            print(msg)
+            print(e)
+            return None
+
+        result = self._process_response(res)
+
+        if not result:
+            self.send_log('No active session. Attempting to re-authenticate', 'error')
+            self._authenticate()
+            return
+
+        self.send_log('Session is still active', 'debug')
+
+    def _create_request(self, method=None, params=None):
+        """
+        Creates and returns a Request object, Allowing us to track request IDs in one spot.
+        We also add the common headers here
+        :return:
+        """
+
+        # TODO Validate method and params
+        data = json.dumps({
+            'id': self.request_id,
+            'method': method,
+            'params': params
+        }).encode('utf-8')
+
+        req = self._add_common_headers(Request(self.url, data=data))
+        self.request_id += 1
+
+        return req
+
+    def _process_response(self, res):
+        """
+        Take the response object and return JSON
+        :param res:
+        :return:
+        """
+        # TODO Figure out exceptions here
+        if res.headers['Content-Encoding'] == 'gzip':
+            self.send_log('Detected gzipped response', 'debug')
+            raw_output = gzip.decompress(res.read()).decode('utf-8')
+        else:
+            self.send_log('Detected other type of response encoding: {}'.format(res.headers['Content-Encoding']), 'debug')
+            raw_output = res.read().decode('utf-8')
+
+        json_output = json.loads(raw_output)
+
+        return json_output if json_output['result'] else None
+
+    def _authenticate(self):
+        """
+        Authenticate against torrent client so we can make future requests
+        If we return from this method we assume we are authenticated for all future requests
+        :return: None
+        """
+        msg = 'Attempting to authenticate against {} API'.format(self.torrent_client)
+        self.send_log(msg, 'info')
+        print(msg)
+
+        req = self._create_request(method='auth.login', params=[self.password])
+
+        try:
+            res = urlopen(req)
+        except URLError as e:
+            msg = 'Failed To Authenticate with torrent client.  HTTP Error'
+            self.send_log(msg, 'critical')
+            print(msg)
+            print(e)
+            sys.exit(1)
+
+        # We need the session ID to send with future requests
+        self.session_id = res.headers['Set-Cookie'].split(';')[0]
+
+        output = self._process_response(res)
+
+        if output and not output['result']:
+            msg = 'Failed to authenticate to {} API. Aborting'.format(self.torrent_client)
+            self.send_log(msg, 'error')
+            print(msg)
+            sys.exit(1)
+
+        msg = 'Successfully Authenticated With {} API'.format(self.torrent_client)
+        self.send_log(msg, 'info')
+        print(msg)
+
+    def _build_torrent_list(self, torrents):
+        """
+        Take the resulting torrent list and create a consistent structure shared through all clients
+        :return:
+        """
+        msg = 'Structuring list of torrents'
+        self.send_log(msg, 'debug')
+
+        for hash, data in torrents.items():
+            self.torrent_list[hash] = {}
+            self.torrent_list[hash]['name'] = data['name']
+            self.torrent_list[hash]['total_size'] = data['total_size']
+            self.torrent_list[hash]['progress'] = round(data['progress'], 2)
+            self.torrent_list[hash]['total_downloaded'] = data['all_time_download']
+            self.torrent_list[hash]['total_uploaded'] = data['total_uploaded']
+            self.torrent_list[hash]['ratio'] = round(data['ratio'], 2)
+            self.torrent_list[hash]['total_seeds'] = data['total_seeds']
+            self.torrent_list[hash]['state'] = data['state']
+            self.torrent_list[hash]['tracker'] = data['tracker_host']
+            self.torrent_list[hash]['total_files'] = data['num_files']
+
+
+    def get_all_torrents(self):
+        """
+        Return a list of all torrents from the API
+        :return:
+        """
+
+        req = self._create_request(method='core.get_torrents_status', params=['',''])
+        try:
+            self._check_session() # Make sure we still have an active session
+            res = urlopen(req)
+        except URLError as e:
+            msg = 'Failed to get list of torrents.  HTTP Error'
+            self.send_log(msg, 'error')
+            print(msg)
+            print(e)
+            self.torrent_list = {}
+            return
+
+        output = self._process_response(res)
+        if output['error']:
+            msg = 'Problem getting torrent list from {}. Error: {}'.format(self.torrent_client, output['error'])
+            print(msg)
+            self.send_log(msg, 'error')
+            self.torrent_list = {}
+            return
+
+        self._build_torrent_list(output['result'])
+
+        # Temp trap to find weird characters that won't decode
+        """
+        for k, v in output['result'].items():
+            print(k)
+            print(v.keys())
+            for k2, v2 in v.items():
+                try:
+                    print('Key: ' + k2)
+                    print('Value: ' + str(v2))
+                except Exception as e:
+                    print('test')
+                    print(e)
+        """
+
+
+    def get_active_plugins(self):
+        """
+        Return all active plugins
+        :return:
+        """
+
+        req = self._create_request(method='core.get_enabled_plugins', params=[])
+        try:
+            self._check_session() # Make sure we still have an active session
+            res = urlopen(req)
+        except URLError as e:
+            msg = 'Failed to get list of plugins.  HTTP Error'
+            self.send_log(msg, 'error')
+            print(msg)
+            print(e)
+            self.active_plugins = []
+            return
+
+        output = self._process_response(res)
+        if output['error']:
+            msg = 'Problem getting plugin list from {}. Error: {}'.format(self.torrent_client, output['error'])
+            print(msg)
+            self.send_log(msg, 'error')
+            self.active_plugins = []
+            return
+
+        self.active_plugins = output['result']
+
+
+
+
-- 
GitLab