jellyfin-kodi/resources/lib/downloadutils.py

381 lines
14 KiB
Python
Raw Normal View History

2015-12-24 20:07:00 +00:00
# -*- coding: utf-8 -*-
##################################################################################################
import json
import logging
import requests
2015-12-24 20:07:00 +00:00
import xbmcgui
import clientinfo
2016-09-27 02:50:58 +00:00
import connect.connectionmanager as connectionmanager
from utils import window, settings, language as lang
2015-12-24 20:07:00 +00:00
##################################################################################################
# Disable requests logging
2016-06-18 03:03:28 +00:00
from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning
2015-12-24 20:07:00 +00:00
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
2016-06-18 03:03:28 +00:00
requests.packages.urllib3.disable_warnings(InsecurePlatformWarning)
log = logging.getLogger("EMBY."+__name__)
2015-12-24 20:07:00 +00:00
##################################################################################################
2018-06-11 06:11:44 +00:00
class HTTPException(Exception):
# Emby HTTP exception
def __init__(self, status):
self.status = status
2015-12-24 20:07:00 +00:00
2016-09-27 02:50:58 +00:00
class DownloadUtils(object):
2016-03-31 03:37:37 +00:00
2015-12-24 20:07:00 +00:00
# Borg - multiple instances, shared state
_shared_state = {}
# Requests session
2016-09-27 02:50:58 +00:00
session = {}
session_requests = None
servers = {} # Multi server setup
default_timeout = 30
2015-12-24 20:07:00 +00:00
def __init__(self):
2016-06-18 03:03:28 +00:00
self.__dict__ = self._shared_state
2016-09-27 02:50:58 +00:00
self.client_info = clientinfo.ClientInfo()
2015-12-24 20:07:00 +00:00
2016-09-27 06:33:09 +00:00
def set_session(self, **kwargs):
2015-12-24 20:07:00 +00:00
# Reserved for userclient only
2016-09-27 02:50:58 +00:00
info = {}
for key in kwargs:
info[key] = kwargs[key]
2015-12-24 20:07:00 +00:00
2016-09-27 02:50:58 +00:00
self.session.update(info)
window('emby_server.json', value=self.session)
2016-09-27 06:33:09 +00:00
log.debug("Set info for server %s: %s", self.session['ServerId'], self.session)
2015-12-24 20:07:00 +00:00
2017-05-06 00:08:04 +00:00
def get_token(self):
2018-01-10 01:31:14 +00:00
return self._get_session_info()['Token']
2017-05-06 00:08:04 +00:00
def add_server(self, server, ssl):
2015-12-24 20:07:00 +00:00
# Reserved for userclient only
2016-09-27 02:50:58 +00:00
server_id = server['Id']
info = {
'UserId': server['UserId'],
'Server': connectionmanager.getServerAddress(server, server['LastConnectionMode']),
'Token': server['AccessToken'],
'SSL': ssl
2016-09-27 02:50:58 +00:00
}
for server_info in self.servers:
if server_info == server_id:
server_info.update(info)
2016-09-27 02:50:58 +00:00
# Set window prop
self._set_server_properties(server_id, server['Name'], info)
2016-09-27 02:50:58 +00:00
log.info("updating %s to available servers: %s", server_id, self.servers)
break
else:
self.servers[server_id] = info
self._set_server_properties(server_id, server['Name'], json.dumps(info))
log.info("adding %s to available servers: %s", server_id, self.servers)
2015-12-24 20:07:00 +00:00
2016-09-27 02:50:58 +00:00
def reset_server(self, server_id):
2015-12-24 20:07:00 +00:00
# Reserved for userclient only
2016-09-27 02:50:58 +00:00
for server in self.servers:
if server['ServerId'] == server_id:
self.servers.pop(server)
2016-09-27 02:50:58 +00:00
window('emby_server%s.json' % server_id, clear=True)
window('emby_server%s.name' % server_id, clear=True)
log.info("removing %s from available servers", server_id)
2015-12-24 20:07:00 +00:00
2016-09-27 02:50:58 +00:00
@staticmethod
def _set_server_properties(server_id, name, info):
window('emby_server%s.json' % server_id, value=info)
window('emby_server%s.name' % server_id, value=name)
2015-12-24 20:07:00 +00:00
def post_capabilities(self, device_id=clientinfo.ClientInfo.get_device_id()):
2015-12-24 20:07:00 +00:00
# Post settings to session
url = "{server}/emby/Sessions/Capabilities/Full?format=json"
data = {
2016-03-31 03:37:37 +00:00
2015-12-24 20:07:00 +00:00
'PlayableMediaTypes': "Audio,Video",
'SupportsMediaControl': True,
'SupportedCommands': (
2016-03-31 03:37:37 +00:00
2015-12-24 20:07:00 +00:00
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
"GoHome,PageUp,NextLetter,GoToSearch,"
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
2017-08-25 07:22:12 +00:00
"SetRepeatMode,"
2015-12-24 20:07:00 +00:00
"Mute,Unmute,SetVolume,"
"Play,Playstate,PlayNext,PlayMediaSource"
),
'IconUrl': "https://kodi.wiki/images/8/8e/Thumbnail-symbol-transparent.png",
2015-12-24 20:07:00 +00:00
}
try:
2018-06-11 06:11:44 +00:00
self.downloadUrl(url, postBody=data, action_type="POST")
log.debug("Posted capabilities to %s", self.session['Server'])
# Attempt at getting sessionId
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % device_id
result = self.downloadUrl(url)
session_id = result[0]['Id']
2016-03-31 03:37:37 +00:00
except Exception as error:
log.error("Failed to retrieve the session id: " + str(error))
2018-06-11 06:11:44 +00:00
return False
2016-03-31 03:37:37 +00:00
2015-12-24 20:07:00 +00:00
else:
log.info("SessionId: %s", session_id)
window('emby_sessionId', value=session_id)
2016-03-31 03:37:37 +00:00
2015-12-24 20:07:00 +00:00
# Post any permanent additional users
additional_users = settings('additionalUsers')
if additional_users:
2016-03-31 03:37:37 +00:00
additional_users = additional_users.split(',')
log.info("List of permanent users added to the session: %s", additional_users)
2015-12-24 20:07:00 +00:00
# Get the user list from server to get the userId
url = "{server}/emby/Users?format=json"
result = self.downloadUrl(url)
for additional in additional_users:
add_user = additional.decode('utf-8').lower()
2015-12-24 20:07:00 +00:00
# Compare to server users to list of permanent additional users
for user in result:
username = user['Name'].lower()
if username in add_user:
user_id = user['Id']
url = ("{server}/emby/Sessions/%s/Users/%s?format=json"
% (session_id, user_id))
2016-04-04 21:21:05 +00:00
self.downloadUrl(url, postBody={}, action_type="POST")
2015-12-24 20:07:00 +00:00
2018-06-11 06:11:44 +00:00
return True
2016-09-27 02:50:58 +00:00
def start_session(self):
2015-12-24 20:07:00 +00:00
# User is identified from this point
# Attach authenticated header to the session
2016-09-27 06:33:09 +00:00
session = requests.Session()
session.headers = self.get_header()
session.verify = self.session['SSL']
2015-12-24 20:07:00 +00:00
# Retry connections to the server
2018-06-11 06:11:44 +00:00
session.mount("http://", requests.adapters.HTTPAdapter(max_retries=3))
session.mount("https://", requests.adapters.HTTPAdapter(max_retries=3))
2016-09-27 06:33:09 +00:00
self.session_requests = session
2015-12-24 20:07:00 +00:00
2016-09-27 06:33:09 +00:00
log.info("requests session started on: %s", self.session['Server'])
2015-12-24 20:07:00 +00:00
2016-09-27 02:50:58 +00:00
def stop_session(self):
2015-12-24 20:07:00 +00:00
try:
2016-09-27 02:50:58 +00:00
self.session_requests.close()
except Exception as error:
log.error(error)
log.warn("requests session could not be terminated")
2015-12-24 20:07:00 +00:00
2016-09-27 02:50:58 +00:00
def get_header(self, server_id=None, authenticate=True):
2015-12-24 20:07:00 +00:00
2016-09-27 02:50:58 +00:00
device_name = self.client_info.get_device_name().encode('utf-8')
2017-06-25 03:04:05 +00:00
device_id = self.client_info.get_device_id().encode('utf-8')
version = self.client_info.get_version().encode('utf-8')
2015-12-24 20:07:00 +00:00
if authenticate:
2016-09-27 02:50:58 +00:00
2016-09-27 06:33:09 +00:00
user = self._get_session_info(server_id)
2017-06-25 03:04:05 +00:00
user_id = user['UserId'].encode('utf-8')
2016-09-27 02:50:58 +00:00
token = user['Token']
2015-12-24 20:07:00 +00:00
auth = (
'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
2016-09-27 02:50:58 +00:00
% (user_id, device_name, device_id, version)
)
2015-12-24 20:07:00 +00:00
header = {
'Authorization': auth,
2016-09-27 02:50:58 +00:00
'X-MediaBrowser-Token': token
2016-03-31 03:37:37 +00:00
}
2015-12-24 20:07:00 +00:00
else:
auth = (
'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
2016-09-27 02:50:58 +00:00
% (device_name, device_id, version)
)
header = {'Authorization': auth}
2016-09-06 22:43:15 +00:00
2016-09-27 02:50:58 +00:00
header.update({
'Content-type': 'application/json',
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
2018-05-13 21:42:53 +00:00
'User-Agent': 'Emby-Kodi (%s)' % version
2016-09-27 02:50:58 +00:00
})
return header
2015-12-24 20:07:00 +00:00
2017-06-25 03:04:05 +00:00
2016-06-18 03:03:28 +00:00
def downloadUrl(self, url, postBody=None, action_type="GET", parameters=None,
2016-09-27 02:50:58 +00:00
authenticate=True, server_id=None):
2016-03-31 03:37:37 +00:00
log.debug("===== ENTER downloadUrl =====")
2016-07-16 00:02:30 +00:00
kwargs = {}
2015-12-24 20:07:00 +00:00
try:
2016-09-27 06:33:09 +00:00
# Ensure server info is loaded
self._ensure_server(server_id)
server = self.session if server_id is None else self.servers[server_id]
requires_server = False
if url.find("{server}") > -1 or url.find("{UserId}") > -1:
requires_server = True
if requires_server and (not server or not server.get("Server") or not server.get("UserId")):
#xbmcgui.Dialog().ok('Emby for Kodi', "You are not connected to your emby server.")
2018-06-11 06:11:44 +00:00
raise Exception("Aborting download, Server Details Error: %s url=%s" % (server, url))
2016-09-27 06:33:09 +00:00
if server_id is None and self.session_requests is not None: # Main server
session = self.session_requests
else:
session = requests
2016-07-16 00:02:30 +00:00
kwargs.update({
2018-06-15 01:20:58 +00:00
'verify': server.get('SSL', False),
2016-09-27 02:50:58 +00:00
'headers': self.get_header(server_id, authenticate)
2016-07-16 00:02:30 +00:00
})
2016-09-27 06:33:09 +00:00
# Replace for the real values
if requires_server:
url = url.replace("{server}", server['Server'])
url = url.replace("{UserId}", server['UserId'])
# does the URL look ok
if url.startswith('/'):
2018-06-11 06:11:44 +00:00
raise Exception("URL Error: " + url)
2016-07-16 00:02:30 +00:00
##### PREPARE REQUEST #####
kwargs.update({
'url': url,
'timeout': self.default_timeout,
2016-07-16 00:02:30 +00:00
'json': postBody,
'params': parameters
})
2016-03-31 03:37:37 +00:00
2015-12-24 20:07:00 +00:00
##### THE RESPONSE #####
log.debug(kwargs)
response = self._requests(action_type, session, **kwargs)
#response = requests.get('http://httpbin.org/status/400')
if response.status_code == 204:
2015-12-24 20:07:00 +00:00
# No body in the response
log.debug("====== 204 Success ======")
# Read response to release connection
response.content
if action_type == "GET":
raise Exception("Response Code 204 for GET request")
else:
# this is probably valid for DELETE and PUT
return None
2015-12-24 20:07:00 +00:00
elif response.status_code == requests.codes.ok:
# UNICODE - JSON object
json_data = response.json()
log.debug("====== 200 Success ======")
log.debug("Response: %s", json_data)
return json_data
else: # Bad status code
log.error("=== Bad status response: %s ===", response.status_code)
response.raise_for_status()
2016-03-31 03:37:37 +00:00
2015-12-24 20:07:00 +00:00
##### EXCEPTIONS #####
except requests.exceptions.ConnectionError as error:
# Make the addon aware of status
if window('emby_online') != "false":
log.error("Server unreachable at: %s", url)
window('emby_online', value="false")
2018-06-11 06:11:44 +00:00
raise HTTPException(None)
except requests.exceptions.ReadTimeout as error:
log.error("ReadTimeout at: %s", url)
raise HTTPException(None)
2016-09-27 06:33:09 +00:00
except requests.exceptions.HTTPError as error:
2015-12-24 20:07:00 +00:00
if response.status_code == 401:
2015-12-24 20:07:00 +00:00
# Unauthorized
2016-06-18 03:03:28 +00:00
status = window('emby_serverStatus')
2015-12-24 20:07:00 +00:00
if 'X-Application-Error-Code' in response.headers:
2015-12-24 20:07:00 +00:00
# Emby server errors
if response.headers['X-Application-Error-Code'] == "ParentalControl":
2015-12-24 20:07:00 +00:00
# Parental control - access restricted
if status != "restricted":
xbmcgui.Dialog().notification(heading=lang(29999),
message="Access restricted.",
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
2016-06-18 03:03:28 +00:00
window('emby_serverStatus', value="restricted")
2015-12-24 20:07:00 +00:00
elif status not in ("401", "Auth"):
# Tell userclient token has been revoked.
2016-06-18 03:03:28 +00:00
window('emby_serverStatus', value="401")
2016-09-27 06:33:09 +00:00
log.error("HTTP Error: %s", error)
xbmcgui.Dialog().notification(heading="Error connecting",
message="Unauthorized.",
icon=xbmcgui.NOTIFICATION_ERROR)
2018-06-11 06:11:44 +00:00
raise HTTPException(response.status_code)
# if we got to here and did not process the download for some reason then that is bad
2018-06-11 06:11:44 +00:00
raise Exception("Unhandled Download : %s", url)
2016-09-27 02:50:58 +00:00
def _ensure_server(self, server_id=None):
if server_id is None and self.session_requests is None:
2016-09-27 06:33:09 +00:00
if not self.session:
server = self._get_session_info()
self.session = server
2016-09-27 02:50:58 +00:00
elif server_id and server_id not in self.servers:
2016-09-27 06:33:09 +00:00
if server_id not in self.servers:
server = self._get_session_info(server_id)
self.servers[server_id] = server
2016-09-27 02:50:58 +00:00
return True
@classmethod
def _get_session_info(cls, server_id=None):
2016-09-27 06:33:09 +00:00
info = {
'UserId': "",
'Server': "",
2018-06-15 01:20:58 +00:00
'Token': ""
2016-09-27 06:33:09 +00:00
}
2016-09-27 02:50:58 +00:00
if server_id is None: # Main server
2016-09-27 06:33:09 +00:00
server = window('emby_server.json')
2016-09-27 02:50:58 +00:00
else: # Other connect servers
server = window('emby_server%s.json' % server_id)
2016-09-27 06:33:09 +00:00
if server:
info.update(server)
2016-09-27 02:50:58 +00:00
return info
@classmethod
2016-09-27 06:33:09 +00:00
def _requests(cls, action, session, **kwargs):
if action == "GET":
2016-09-27 02:50:58 +00:00
response = session.get(**kwargs)
elif action == "POST":
2016-09-27 02:50:58 +00:00
response = session.post(**kwargs)
elif action == "DELETE":
2016-09-27 02:50:58 +00:00
response = session.delete(**kwargs)
2018-06-11 06:11:44 +00:00
return response