Recreated Branch

This commit is contained in:
Abby Gourlay 2020-02-21 00:42:50 +00:00
parent 2fbdd191d5
commit 417b9fb938
3 changed files with 196 additions and 217 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,10 @@
# -*- 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 +40,9 @@ 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,97 @@ class API(object):
return self._delete("Videos/ActiveEncodings", params={ return self._delete("Videos/ActiveEncodings", params={
'DeviceId': device_id '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 send_request(self, url, path, method=None, timeout=None, headers=None, data=None):
if not timeout:
LOG.debug("No timeout set, using default")
timeout = self.default_timeout
if not headers:
LOG.debug("No headers set, using default")
headers = self.get_default_headers()
if not method:
LOG.debug("No method set, using default")
method = "get" #Defaults to get request if none specified
request_method = getattr(requests, method.lower())
url = "%s/%s" % (url, path)
request_settings = {
"timeout": timeout,
"headers": headers,
"data": data
}
if not settings('sslverify'):
request_settings["verify"] = False
LOG.info("Sending %s request to %s" % (method, url))
LOG.debug(request_settings)
return request_method(url, **request_settings)
def login(self, server_url, username, password):
path = "Users/AuthenticateByName"
authData = {
"username": username,
"Pw": password or ""
}
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(response.text)
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): def revoke_token(self): #Called once in http#L130
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):
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,7 +58,7 @@ 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._find_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
@ -78,44 +66,64 @@ class ConnectionManager(object):
return list() return list()
servers = list(credentials['Servers']) servers = list(credentials['Servers'])
self._merge_servers(servers, found_servers)
try: #Merges servers we already knew with newly found ones
servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) for found_server in found_servers:
except TypeError: try:
servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) self.credentials.add_update_server(servers, found_server)
except KeyError:
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,41 @@ 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,88 +197,15 @@ 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:
return server return server
def get_public_users(self): def get_public_users(self): ## Required in connect.py#L213
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)
@ -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,33 +241,10 @@ 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):
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): def _find_servers(self, found_servers):
servers = [] servers = []
@ -336,7 +264,7 @@ class ConnectionManager(object):
return servers return servers
# TODO: Make IPv6 compatable # TODO: Make IPv6 compatable
def _convert_endpoint_address_to_manual_address(self, info): def _convert_endpoint_address_to_manual_address(self, info): # Called once ^^ right there
if info.get('Address') and info.get('EndpointAddress'): if info.get('Address') and info.get('EndpointAddress'):
address = info['EndpointAddress'].split(':')[0] address = info['EndpointAddress'].split(':')[0]
@ -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
@ -419,28 +341,7 @@ class ConnectionManager(object):
result['State'] = CONNECTION_STATE['SignedIn'] if server.get('AccessToken') else CONNECTION_STATE['ServerSignIn'] result['State'] = CONNECTION_STATE['SignedIn'] if server.get('AccessToken') else CONNECTION_STATE['ServerSignIn']
# 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:
@ -450,30 +351,4 @@ class ConnectionManager(object):
server['Id'] = system_info['Id'] server['Id'] = system_info['Id']
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)