Merge pull request #221 from TrueTechy/cleanup-connection-manager

Cleanup connection manager
This commit is contained in:
mcarlton00 2020-03-16 09:05:26 -04:00 committed by GitHub
commit a7ef2f6243
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 184 additions and 224 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ machine_guid
.idea/ .idea/
.DS_Store .DS_Store
.vscode/ .vscode/
pyinstrument/
pyinstrument_cext.so

View file

@ -1,5 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals from __future__ import division, absolute_import, print_function, unicode_literals
import requests
import json
import logging
from helper.utils import settings
LOG = logging.getLogger('JELLYFIN.' + __name__)
def jellyfin_url(client, handler): def jellyfin_url(client, handler):
@ -35,6 +41,8 @@ class API(object):
''' '''
def __init__(self, client, *args, **kwargs): def __init__(self, client, *args, **kwargs):
self.client = client self.client = client
self.config = client.config
self.default_timeout = 5
def _http(self, action, url, request={}): def _http(self, action, url, request={}):
request.update({'type': action, 'handler': url}) request.update({'type': action, 'handler': url})
@ -344,3 +352,82 @@ class API(object):
return self._delete("Videos/ActiveEncodings", params={ return self._delete("Videos/ActiveEncodings", params={
'DeviceId': device_id 'DeviceId': device_id
}) })
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 send_request(self, url, path, method="get", timeout=None, headers=None, data=None):
request_method = getattr(requests, method.lower())
url = "%s/%s" % (url, path)
request_settings = {
"timeout": timeout or self.default_timeout,
"headers": headers or self.get_default_headers(),
"data": data
}
if not settings('sslverify'):
request_settings["verify"] = False
LOG.info("Sending %s request to %s" % (method, path))
LOG.debug(request_settings['timeout'])
LOG.debug(request_settings['headers'])
return request_method(url, **request_settings)
def login(self, server_url, username, password=""):
path = "Users/AuthenticateByName"
authData = {
"username": username,
"Pw": password
}
headers = self.get_default_headers()
headers.update({'Content-type': "application/json"})
try:
LOG.info("Trying to login to %s/%s as %s" % (server_url, path, username))
response = self.send_request(server_url, path, method="post", headers=headers, data=json.dumps(authData))
if response.status_code == 200:
return response.json()
else:
LOG.error("Failed to login to server with status code: " + str(response.status_code))
LOG.error("Server Response:\n" + str(response.content))
LOG.debug(headers)
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 = "%s/%s" % (server['address'], "system/info")
authTokenHeader = {
'X-MediaBrowser-Token': server['AccessToken']
}
headers = self.get_default_headers()
headers.update(authTokenHeader)
response = self.send_request(server['address'], "system/info", headers=headers)
return response.json() if response.status_code == 200 else {}
def get_public_info(self, server_address):
response = self.send_request(server_address, "system/info/public")
return response.json() if response.status_code == 200 else {}

View file

@ -8,12 +8,14 @@ import logging
import socket import socket
import time import time
from datetime import datetime from datetime import datetime
from distutils.version import LooseVersion from operator import itemgetter
import urllib3 import urllib3
from .credentials import Credentials from .credentials import Credentials
from .http import HTTP # noqa: I201,I100 from .http import HTTP # noqa: I201,I100
from .api import API
import traceback
################################################################################################# #################################################################################################
@ -29,11 +31,8 @@ CONNECTION_STATE = {
class ConnectionManager(object): class ConnectionManager(object):
min_server_version = "10.1.0"
server_version = min_server_version
user = {} user = {}
server_id = None server_id = None
timeout = 10
def __init__(self, client): def __init__(self, client):
@ -43,25 +42,14 @@ class ConnectionManager(object):
self.config = client.config self.config = client.config
self.credentials = Credentials() self.credentials = Credentials()
self.http = HTTP(client) self.API = API(client)
def clear_data(self):
LOG.info("connection manager clearing data")
self.user = None
credentials = self.credentials.get_credentials()
credentials['Servers'] = list()
self.credentials.get_credentials(credentials)
self.config.auth(None, None)
def revoke_token(self): def revoke_token(self):
LOG.info("revoking token") LOG.info("revoking token")
self['server']['AccessToken'] = None 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 self.config.data['auth.token'] = None
@ -70,52 +58,72 @@ class ConnectionManager(object):
LOG.info("Begin getAvailableServers") LOG.info("Begin getAvailableServers")
# Clone the credentials # Clone the credentials
credentials = self.credentials.get_credentials() credentials = self.credentials.get()
found_servers = self._find_servers(self._server_discovery()) found_servers = self.process_found_servers(self._server_discovery())
if not found_servers and not credentials['Servers']: # back out right away, no point in continuing if not found_servers and not credentials['Servers']: # back out right away, no point in continuing
LOG.info("Found no servers") LOG.info("Found no servers")
return list() return list()
servers = list(credentials['Servers']) 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: try:
servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) self.credentials.add_update_server(servers, found_server)
except TypeError: except KeyError:
servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) continue
servers.sort(key=itemgetter('DateLastAccessed'), reverse=True)
credentials['Servers'] = servers credentials['Servers'] = servers
self.credentials.get_credentials(credentials) self.credentials.set(credentials)
return servers return servers
def login(self, server, username, password=None, clear=True, options={}): def login(self, server_url, username, password=None):
if not username: if not username:
raise AttributeError("username cannot be empty") raise AttributeError("username cannot be empty")
if not server: if not server_url:
raise AttributeError("server cannot be empty") raise AttributeError("server url cannot be empty")
try: data = self.API.login(server_url, username, password) # returns empty dict on failure
request = {
'type': "POST",
'url': self.get_jellyfin_url(server, "Users/AuthenticateByName"),
'json': {
'Username': username,
'Pw': password or ""
}
}
result = self._request_url(request, False) if not data:
except Exception as error: # Failed to login LOG.info("Failed to login as `"+username+"`")
LOG.exception(error) return {}
return False
LOG.info("Succesfully logged in as %s" % (username))
# 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: 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={}): def connect_to_address(self, address, options={}):
@ -125,7 +133,7 @@ class ConnectionManager(object):
address = self._normalize_address(address) address = self._normalize_address(address)
try: try:
public_info = self._try_connect(address, options=options) public_info = self.API.get_public_info(address)
LOG.info("connectToAddress %s succeeded", address) LOG.info("connectToAddress %s succeeded", address)
server = { server = {
'address': address, 'address': address,
@ -146,26 +154,42 @@ class ConnectionManager(object):
def connect_to_server(self, server, options={}): def connect_to_server(self, server, options={}):
LOG.info("begin connectToServer") LOG.info("begin connectToServer")
timeout = self.timeout
try: try:
result = self._try_connect(server['address'], timeout, options) result = self.API.get_public_info(server.get('address'))
if not result:
LOG.error("Failed to connect to server: %s" % server.get('address'))
return { 'State': CONNECTION_STATE['Unavailable'] }
LOG.info("calling onSuccessfulConnection with server %s", server.get('Name')) 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) return self._after_connect_validated(server, credentials, result, True, options)
except Exception as e: except Exception as e:
LOG.info("Failing server connection. ERROR msg: {}".format(e)) LOG.error(traceback.format_exc())
LOG.error("Failing server connection. ERROR msg: {}".format(e))
return { 'State': CONNECTION_STATE['Unavailable'] } return { 'State': CONNECTION_STATE['Unavailable'] }
def connect(self, options={}): def connect(self, options={}):
LOG.info("Begin connect") LOG.info("Begin connect")
return self._connect_to_servers(self.get_available_servers(), options)
def jellyfin_user_id(self): servers = self.get_available_servers()
return self.get_server_info(self.server_id)['UserId'] LOG.info("connect has %s servers", len(servers))
def jellyfin_token(self): 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_token(self): # Called once monitor.py#163
return self.get_server_info(self.server_id)['AccessToken'] return self.get_server_info(self.server_id)['AccessToken']
def get_server_info(self, server_id): def get_server_info(self, server_id):
@ -174,7 +198,7 @@ class ConnectionManager(object):
LOG.info("server_id is empty") LOG.info("server_id is empty")
return {} return {}
servers = self.credentials.get_credentials()['Servers'] servers = self.credentials.get()['Servers']
for server in servers: for server in servers:
if server['Id'] == server_id: if server['Id'] == server_id:
@ -183,81 +207,7 @@ class ConnectionManager(object):
def get_public_users(self): def get_public_users(self):
return self.client.jellyfin.get_public_users() return self.client.jellyfin.get_public_users()
def get_jellyfin_url(self, base, handler):
return "%s/%s" % (base, handler)
def _request_url(self, request, headers=True):
request['timeout'] = request.get('timeout') or self.timeout
if headers:
self._get_headers(request)
try:
return self.http.request(request)
except Exception as error:
LOG.exception(error)
raise
def _add_app_info(self):
return "%s/%s" % (self.config.data['app.name'], self.config.data['app.version'])
def _get_headers(self, request):
headers = request.setdefault('headers', {})
if request.get('dataType') == "json":
headers['Accept'] = "application/json"
request.pop('dataType')
headers['X-Application'] = self._add_app_info()
headers['Content-type'] = request.get(
'contentType',
'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")
LOG.info("tryConnect url: %s", url)
return self._request_url({
'type': "GET",
'url': url,
'dataType': "json",
'timeout': timeout,
'verify': options.get('ssl'),
'retry': False
})
def _server_discovery(self): def _server_discovery(self):
MULTI_GROUP = ("<broadcast>", 7359) MULTI_GROUP = ("<broadcast>", 7359)
MESSAGE = b"who is JellyfinServer?" MESSAGE = b"who is JellyfinServer?"
@ -277,6 +227,7 @@ class ConnectionManager(object):
try: try:
sock.sendto(MESSAGE, MULTI_GROUP) sock.sendto(MESSAGE, MULTI_GROUP)
except Exception as error: except Exception as error:
LOG.exception(traceback.format_exc())
LOG.exception(error) LOG.exception(error)
return servers return servers
@ -290,34 +241,11 @@ class ConnectionManager(object):
return servers return servers
except Exception as e: except Exception as e:
LOG.error(traceback.format_exc())
LOG.exception("Error trying to find servers: %s", e) LOG.exception("Error trying to find servers: %s", e)
return servers return servers
def _get_last_used_server(self): def process_found_servers(self, found_servers):
servers = self.credentials.get_credentials()['Servers']
if not len(servers):
return
try:
servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True)
except TypeError:
servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True)
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 = [] servers = []
@ -373,14 +301,6 @@ class ConnectionManager(object):
return url.url 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): def _after_connect_validated(self, server, credentials, system_info, verify_authentication, options):
if options.get('enableAutoLogin') is False: if options.get('enableAutoLogin') is False:
@ -388,22 +308,24 @@ class ConnectionManager(object):
self.config.data['auth.token'] = server.pop('AccessToken', None) self.config.data['auth.token'] = server.pop('AccessToken', None)
elif verify_authentication and server.get('AccessToken'): elif verify_authentication and server.get('AccessToken'):
if self._validate_authentication(server, options) is not False: system_info = self.API.validate_authentication_token(server)
if system_info:
self._update_server_info(server, system_info)
self.config.data['auth.user_id'] = server['UserId'] self.config.data['auth.user_id'] = server['UserId']
self.config.data['auth.token'] = server['AccessToken'] self.config.data['auth.token'] = server['AccessToken']
return self._after_connect_validated(server, credentials, system_info, False, options) return self._after_connect_validated(server, credentials, system_info, False, options)
server['UserId'] = None
server['AccessToken'] = None
return { 'State': CONNECTION_STATE['Unavailable'] } return { 'State': CONNECTION_STATE['Unavailable'] }
self._update_server_info(server, system_info) 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.add_update_server(credentials['Servers'], server)
self.credentials.get_credentials(credentials) self.credentials.set(credentials)
self.server_id = server['Id'] self.server_id = server['Id']
# Update configs # Update configs
@ -420,27 +342,6 @@ class ConnectionManager(object):
# Connected # Connected
return result return result
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)
server['UserId'] = None
server['AccessToken'] = None
return False
def _update_server_info(self, server, system_info): def _update_server_info(self, server, system_info):
if server is None or system_info is None: if server is None or system_info is None:
@ -451,29 +352,3 @@ class ConnectionManager(object):
if system_info.get('address'): if system_info.get('address'):
server['address'] = system_info['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)

View file

@ -24,12 +24,8 @@ class Credentials(object):
def set_credentials(self, credentials): def set_credentials(self, credentials):
self.credentials = credentials self.credentials = credentials
def get_credentials(self, data=None): def get_credentials(self):
return self.get()
if data is not None:
self._set(data)
return self._get()
def _ensure(self): def _ensure(self):
@ -46,12 +42,12 @@ class Credentials(object):
LOG.debug("credentials initialized with: %s", self.credentials) LOG.debug("credentials initialized with: %s", self.credentials)
self.credentials['Servers'] = self.credentials.setdefault('Servers', []) self.credentials['Servers'] = self.credentials.setdefault('Servers', [])
def _get(self): def get(self):
self._ensure() self._ensure()
return self.credentials return self.credentials
def _set(self, data): def set(self, data):
if data: if data:
self.credentials.update(data) self.credentials.update(data)