From e81745f3d1e625eb8ec186e316027334af5468a7 Mon Sep 17 00:00:00 2001 From: TrueTechy Date: Mon, 23 Dec 2019 02:06:03 +0000 Subject: [PATCH] Reworking connection manager --- jellyfin_kodi/jellyfin/api.py | 64 ++++++ jellyfin_kodi/jellyfin/connection_manager.py | 211 ++++++++----------- jellyfin_kodi/jellyfin/credentials.py | 8 +- 3 files changed, 152 insertions(+), 131 deletions(-) diff --git a/jellyfin_kodi/jellyfin/api.py b/jellyfin_kodi/jellyfin/api.py index 4eab6872..aeda9671 100644 --- a/jellyfin_kodi/jellyfin/api.py +++ b/jellyfin_kodi/jellyfin/api.py @@ -1,3 +1,6 @@ +import requests +from helper.utils import settings + # -*- coding: utf-8 -*- def jellyfin_url(client, handler): return "%s/%s" % (client.config.data['auth.server'], handler) @@ -32,6 +35,9 @@ class API(object): ''' def __init__(self, client, *args, **kwargs): self.client = client + self.config = client.config + self.default_timeout = 5 + def _http(self, action, url, request={}): request.update({'type': action, 'handler': url}) @@ -341,3 +347,61 @@ class API(object): return self._delete("Videos/ActiveEncodings", params={ 'DeviceId': device_id }) + + + ################################################################################################# + + # New API calls + + ################################################################################################# + def get_default_headers(self): + auth = "MediaBrowser " + auth += "Client=%s, " % self.config.data['app.name'].encode('utf-8') + auth += "Device=%s, " % self.config.data['app.device_name'].encode('utf-8') + auth += "DeviceId=%s, " % self.config.data['app.device_id'].encode('utf-8') + auth += "Version=%s" % self.config.data['app.version'].encode('utf-8') + + return { + "Accept": "application/json", + "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "X-Application": "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']), + "Accept-Charset": "UTF-8,*", + "Accept-encoding": "gzip", + "User-Agent": self.config.data['http.user_agent'] or "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']), + "x-emby-authorization": auth + } + + def login(self, server_url, username, password): + url = server_url+"/Users/AuthenticateByName" + authData = { + 'Username': username, + 'Pw': password or "" + } + headers = self.get_default_headers() + + try: + response = requests.post(url, data=authData, timeout=self.default_timeout, headers=headers, verify=settings('sslverify')) + if response.status_code == 200: + return response.json() + else: + LOG.error("Failed to login to server with status code: "+str(response.status_code)) + LOG.debug(response.text) + + return {} + except Exception as e: #Find exceptions for likely cases i.e, server timeout, etc + LOG.error(e) + + return {} + + def validate_authentication_token(self, server): + url = server['address']+"/System/Info" + authTokenHeader = { + 'X-MediaBrowser-Token': server['AccessToken'] + } + headers = self.get_default_headers() + headers.update(authTokenHeader) + + response = requests.get(url, timeout=self.default_timeout, headers=headers, verify=settings('sslverify')) + + return response.json() if response.status_code == 200 else {} + diff --git a/jellyfin_kodi/jellyfin/connection_manager.py b/jellyfin_kodi/jellyfin/connection_manager.py index c3195e49..2750d998 100644 --- a/jellyfin_kodi/jellyfin/connection_manager.py +++ b/jellyfin_kodi/jellyfin/connection_manager.py @@ -13,6 +13,7 @@ import urllib3 from credentials import Credentials from http import HTTP # noqa: I201,I100 +from api import API ################################################################################################# @@ -28,8 +29,6 @@ CONNECTION_STATE = { class ConnectionManager(object): - min_server_version = "10.1.0" - server_version = min_server_version user = {} server_id = None timeout = 10 @@ -43,24 +42,25 @@ class ConnectionManager(object): self.credentials = Credentials() self.http = HTTP(client) + self.API = API(client) - def clear_data(self): + def clear_data(self): ## Never actually called LOG.info("connection manager clearing data") self.user = None - credentials = self.credentials.get_credentials() + credentials = self.credentials.get() credentials['Servers'] = list() - self.credentials.get_credentials(credentials) + self.credentials.set(credentials) self.config.auth(None, None) - def revoke_token(self): + def revoke_token(self): #Called once in http#L130 LOG.info("revoking token") self['server']['AccessToken'] = None - self.credentials.get_credentials(self.credentials.get_credentials()) + self.credentials.set_credentials(self.credentials.get()) self.config.data['auth.token'] = None @@ -69,7 +69,7 @@ class ConnectionManager(object): LOG.info("Begin getAvailableServers") # Clone the credentials - credentials = self.credentials.get_credentials() + credentials = self.credentials.get() found_servers = self._find_servers(self._server_discovery()) if not found_servers and not credentials['Servers']: # back out right away, no point in continuing @@ -77,7 +77,13 @@ class ConnectionManager(object): return list() servers = list(credentials['Servers']) - self._merge_servers(servers, found_servers) + + #Merges servers we already knew with newly found ones + for found_server in found_servers: + try: + self.credentials.add_update_server(servers, found_server) + except KeyError: + continue try: servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) @@ -85,36 +91,54 @@ class ConnectionManager(object): servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) credentials['Servers'] = servers - self.credentials.get_credentials(credentials) + self.credentials.set(credentials) return servers - def login(self, server, username, password=None, clear=True, options={}): + def login(self, server_url, username, password=None): if not username: raise AttributeError("username cannot be empty") - if not server: - raise AttributeError("server cannot be empty") + if not server_url: + raise AttributeError("server url cannot be empty") - try: - request = { - 'type': "POST", - 'url': self.get_jellyfin_url(server, "Users/AuthenticateByName"), - 'json': { - 'Username': username, - 'Pw': password or "" - } - } + data = self.API.login(server_url, username, password) # returns empty dict on failure - result = self._request_url(request, False) - except Exception as error: # Failed to login - LOG.exception(error) - return False + if not data: + LOG.info("Failed to login as `"+username+"`") + return {} + + + ## TODO Change when moving to database storage of server details + credentials = self.credentials.get() + + self.config.data['auth.user_id'] = data['User']['Id'] + self.config.data['auth.token'] = data['AccessToken'] + + for server in credentials['Servers']: + if server['Id'] == data['ServerId']: + found_server = server + break else: - self._on_authenticated(result, options) + return # No server found + + found_server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + found_server['UserId'] = data['User']['Id'] + found_server['AccessToken'] = data['AccessToken'] + + self.credentials.add_update_server(credentials['Servers'], found_server) + + info = { + 'Id': data['User']['Id'], + 'IsSignedInOffline': True + } + self.credentials.add_update_user(server, info) + + self.credentials.set_credentials(credentials) + + return data - return result def connect_to_address(self, address, options={}): @@ -145,12 +169,12 @@ class ConnectionManager(object): def connect_to_server(self, server, options={}): LOG.info("begin connectToServer") - timeout = self.timeout try: - result = self._try_connect(server['address'], timeout, options) + result = self._try_connect(server['address'], self.timeout, options) LOG.info("calling onSuccessfulConnection with server %s", server.get('Name')) - credentials = self.credentials.get_credentials() + + credentials = self.credentials.get() return self._after_connect_validated(server, credentials, result, True, options) except Exception as e: @@ -159,12 +183,24 @@ class ConnectionManager(object): def connect(self, options={}): LOG.info("Begin connect") - return self._connect_to_servers(self.get_available_servers(), options) - def jellyfin_user_id(self): + servers = self.get_available_servers() + LOG.info("connect has %s servers", len(servers)) + + if not (len(servers)): #No servers provided + return { + 'State': ['ServerSelection'] + } + + result = self.connect_to_server(servers[0], options) + LOG.debug("resolving connect with result: %s", result) + + return result + + def jellyfin_user_id(self): ## Never called return self.get_server_info(self.server_id)['UserId'] - def jellyfin_token(self): + def jellyfin_token(self): ## Called once monitor.py#163 return self.get_server_info(self.server_id)['AccessToken'] def get_server_info(self, server_id): @@ -173,13 +209,13 @@ class ConnectionManager(object): LOG.info("server_id is empty") return {} - servers = self.credentials.get_credentials()['Servers'] + servers = self.credentials.get()['Servers'] for server in servers: if server['Id'] == server_id: return server - def get_public_users(self): + def get_public_users(self): ## Required in connect.py#L213 return self.client.jellyfin.get_public_users() def get_jellyfin_url(self, base, handler): @@ -214,33 +250,6 @@ class ConnectionManager(object): 'application/x-www-form-urlencoded; charset=UTF-8' ) - def _connect_to_servers(self, servers, options): - - LOG.info("Begin connectToServers, with %s servers", len(servers)) - result = {} - - if len(servers) == 1: - result = self.connect_to_server(servers[0], options) - LOG.debug("resolving connectToServers with result['State']: %s", result) - - return result - - first_server = self._get_last_used_server() - # See if we have any saved credentials and can auto sign in - if first_server is not None and first_server['DateLastAccessed'] != "2001-01-01T00:00:00Z": - result = self.connect_to_server(first_server, options) - - if result['State'] in (CONNECTION_STATE['SignedIn'], CONNECTION_STATE['Unavailable']): - return result - - # Return loaded credentials if exists - credentials = self.credentials.get_credentials() - - return { - 'Servers': servers, - 'State': result.get('State') or CONNECTION_STATE['ServerSelection'], - } - def _try_connect(self, url, timeout=None, options={}): url = self.get_jellyfin_url(url, "system/info/public") @@ -292,9 +301,9 @@ class ConnectionManager(object): LOG.exception("Error trying to find servers: %s", e) return servers - def _get_last_used_server(self): + def _get_last_used_server(self): ## Never used - servers = self.credentials.get_credentials()['Servers'] + servers = self.credentials.get()['Servers'] if not len(servers): return @@ -306,16 +315,6 @@ class ConnectionManager(object): return servers[0] - def _merge_servers(self, list1, list2): - - for i in range(0, len(list2), 1): - try: - self.credentials.add_update_server(list1, list2[i]) - except KeyError: - continue - - return list1 - def _find_servers(self, found_servers): servers = [] @@ -372,14 +371,6 @@ class ConnectionManager(object): return url.url - def _save_user_info_into_credentials(self, server, user): - - info = { - 'Id': user['Id'], - 'IsSignedInOffline': True - } - self.credentials.add_update_user(server, info) - def _after_connect_validated(self, server, credentials, system_info, verify_authentication, options): if options.get('enableAutoLogin') is False: @@ -387,22 +378,20 @@ class ConnectionManager(object): self.config.data['auth.token'] = server.pop('AccessToken', None) elif verify_authentication and server.get('AccessToken'): - if self._validate_authentication(server, options) is not False: + if self._validate_authentication(server, options): self.config.data['auth.user_id'] = server['UserId'] self.config.data['auth.token'] = server['AccessToken'] + return self._after_connect_validated(server, credentials, system_info, False, options) return { 'State': CONNECTION_STATE['Unavailable'] } self._update_server_info(server, system_info) - self.server_version = system_info['Version'] - - if options.get('updateDateLastAccessed') is not False: - server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') self.credentials.add_update_server(credentials['Servers'], server) - self.credentials.get_credentials(credentials) + self.credentials.set(credentials) self.server_id = server['Id'] # Update configs @@ -421,20 +410,12 @@ class ConnectionManager(object): def _validate_authentication(self, server, options={}): - try: - system_info = self._request_url({ - 'type': "GET", - 'url': self.get_jellyfin_url(server['address'], "System/Info"), - 'verify': options.get('ssl'), - 'dataType': "json", - 'headers': { - 'X-MediaBrowser-Token': server['AccessToken'] - } - }) - self._update_server_info(server, system_info) - except Exception as error: - LOG.exception(error) + system_info = self.API.validate_authentication_token(server) + if system_info: + self._update_server_info(server, system_info) + return True + else: server['UserId'] = None server['AccessToken'] = None @@ -451,28 +432,4 @@ class ConnectionManager(object): if system_info.get('address'): server['address'] = system_info['address'] - ## Finish updating server info - - def _on_authenticated(self, result, options={}): - - credentials = self.credentials.get_credentials() - - self.config.data['auth.user_id'] = result['User']['Id'] - self.config.data['auth.token'] = result['AccessToken'] - - for server in credentials['Servers']: - if server['Id'] == result['ServerId']: - found_server = server - break - else: - return # No server found - - if options.get('updateDateLastAccessed') is not False: - found_server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') - - found_server['UserId'] = result['User']['Id'] - found_server['AccessToken'] = result['AccessToken'] - - self.credentials.add_update_server(credentials['Servers'], found_server) - self._save_user_info_into_credentials(found_server, result['User']) - self.credentials.get_credentials(credentials) + ## Finish updating server info \ No newline at end of file diff --git a/jellyfin_kodi/jellyfin/credentials.py b/jellyfin_kodi/jellyfin/credentials.py index 0b61f3dd..de93a07c 100644 --- a/jellyfin_kodi/jellyfin/credentials.py +++ b/jellyfin_kodi/jellyfin/credentials.py @@ -26,9 +26,9 @@ class Credentials(object): def get_credentials(self, data=None): if data is not None: - self._set(data) + self.set(data) - return self._get() + return self.get() def _ensure(self): @@ -45,12 +45,12 @@ class Credentials(object): LOG.debug("credentials initialized with: %s", self.credentials) self.credentials['Servers'] = self.credentials.setdefault('Servers', []) - def _get(self): + def get(self): self._ensure() return self.credentials - def _set(self, data): + def set(self, data): if data: self.credentials.update(data)