PlexKodiConnect/resources/lib/downloadutils.py

320 lines
11 KiB
Python
Raw Normal View History

2015-12-25 07:07:00 +11:00
# -*- coding: utf-8 -*-
2016-02-20 06:03:06 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
import requests
import xml.etree.ElementTree as etree
2015-12-25 07:07:00 +11:00
import xbmcgui
from utils import logging, settings, window
2015-12-25 07:07:00 +11:00
import clientinfo
2016-02-20 06:03:06 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
# Disable annoying requests warnings
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
2015-12-25 07:07:00 +11:00
2016-02-20 06:03:06 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
@logging
2015-12-25 07:07:00 +11:00
class DownloadUtils():
"""
Manages any up/downloads with PKC. Careful to initiate correctly
Use startSession() to initiate.
If not initiated, e.g. SSL check will fallback to False
"""
2016-02-20 06:03:06 +11:00
2015-12-25 07:07:00 +11:00
# Borg - multiple instances, shared state
2016-02-07 22:38:50 +11:00
_shared_state = {}
2015-12-25 07:07:00 +11:00
# Requests session
timeout = 30
2015-12-25 07:07:00 +11:00
def __init__(self):
2016-02-07 22:38:50 +11:00
self.__dict__ = self._shared_state
2015-12-25 07:07:00 +11:00
def setUsername(self, username):
"""
Reserved for userclient only
"""
2015-12-25 07:07:00 +11:00
self.username = username
self.logMsg("Set username: %s" % username, 0)
2015-12-25 07:07:00 +11:00
def setUserId(self, userId):
"""
Reserved for userclient only
"""
2015-12-25 07:07:00 +11:00
self.userId = userId
self.logMsg("Set userId: %s" % userId, 0)
2015-12-25 07:07:00 +11:00
def setServer(self, server):
"""
Reserved for userclient only
"""
2015-12-25 07:07:00 +11:00
self.server = server
self.logMsg("Set server: %s" % server, 0)
2015-12-25 07:07:00 +11:00
def setToken(self, token):
"""
Reserved for userclient only
"""
2015-12-25 07:07:00 +11:00
self.token = token
if token == '':
self.logMsg('Set token: empty token!', 0)
2015-12-25 07:07:00 +11:00
else:
self.logMsg("Set token: xxxxxxx", 0)
def setSSL(self, verifySSL=None, certificate=None):
"""
Reserved for userclient only
verifySSL must be 'true' to enable certificate validation
certificate must be path to certificate or 'None'
"""
if verifySSL is None:
verifySSL = settings('sslverify')
if certificate is None:
certificate = settings('sslcert')
self.logMsg("Verify SSL certificates set to: %s" % verifySSL, 0)
self.logMsg("SSL client side certificate set to: %s" % certificate, 0)
if verifySSL != 'true':
self.s.verify = False
if certificate != 'None':
self.s.cert = certificate
2015-12-25 07:07:00 +11:00
def startSession(self):
"""
User should be authenticated when this method is called (via
userclient)
"""
2015-12-25 07:07:00 +11:00
# Start session
self.s = requests.Session()
client = clientinfo.ClientInfo()
self.deviceId = client.getDeviceId()
# Attach authenticated header to the session
self.s.headers = client.getXArgsDeviceInfo()
self.s.encoding = 'utf-8'
# Set SSL settings
self.setSSL()
# Set other stuff
self.setServer(window('pms_server'))
self.setToken(window('pms_token'))
self.setUserId(window('currUserId'))
self.setUsername(window('plex_username'))
2015-12-25 07:07:00 +11:00
# Retry connections to the server
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
self.logMsg("Requests session started on: %s" % self.server, 0)
2015-12-25 07:07:00 +11:00
def stopSession(self):
try:
self.s.close()
except:
self.logMsg("Requests session could not be terminated.", 0)
try:
del self.s
except:
pass
self.logMsg('Request session stopped', 0)
def getHeader(self, options=None):
header = clientinfo.ClientInfo().getXArgsDeviceInfo()
if options is not None:
header.update(options)
2015-12-25 07:07:00 +11:00
return header
def __doDownload(self, s, type, **kwargs):
if type == "GET":
r = s.get(**kwargs)
elif type == "POST":
r = s.post(**kwargs)
elif type == "DELETE":
r = s.delete(**kwargs)
elif type == "OPTIONS":
r = s.options(**kwargs)
elif type == "PUT":
r = s.put(**kwargs)
return r
def downloadUrl(self, url, type="GET", postBody=None, parameters=None,
authenticate=True, headerOptions=None, verifySSL=True):
"""
Override SSL check with verifySSL=False
If authenticate=True, existing request session will be used/started
Otherwise, 'empty' request will be made
Returns:
False If an error occured
True If connection worked but no body was received
401, ... integer if PMS answered with HTTP error 401
(unauthorized) or other http error codes
xml xml etree root object, if applicable
JSON json() object, if applicable
"""
kwargs = {}
if authenticate:
# Get requests session
try:
s = self.s
except AttributeError:
self.logMsg("Request session does not exist: start one", 0)
self.startSession()
s = self.s
# Replace for the real values
url = url.replace("{server}", self.server)
else:
# User is not (yet) authenticated. Used to communicate with
# plex.tv and to check for PMS servers
s = requests
headerOptions = self.getHeader(options=headerOptions)
kwargs['timeout'] = self.timeout
if settings('sslcert') != 'None':
kwargs['cert'] = settings('sslcert')
# Set the variables we were passed (fallback to request session
# otherwise - faster)
kwargs['url'] = url
if verifySSL is False:
kwargs['verify'] = False
if headerOptions is not None:
kwargs['headers'] = headerOptions
if postBody is not None:
kwargs['data'] = postBody
if parameters is not None:
kwargs['params'] = parameters
# ACTUAL DOWNLOAD HAPPENING HERE
2015-12-25 07:07:00 +11:00
try:
r = self.__doDownload(s, type, **kwargs)
2015-12-25 07:07:00 +11:00
# THE EXCEPTIONS
2015-12-25 07:07:00 +11:00
except requests.exceptions.ConnectionError as e:
# Connection error
if authenticate is False:
self.logMsg("Server unreachable at: %s" % url, -1)
self.logMsg(e, 2)
# Make the addon aware of status
window('emby_online', value="false")
return False
2015-12-25 07:07:00 +11:00
except requests.exceptions.ConnectTimeout as e:
self.logMsg("Server timeout at: %s" % url, -1)
self.logMsg(e, 2)
return False
2015-12-25 07:07:00 +11:00
except requests.exceptions.HTTPError as e:
r = r.status_code
if r == 401:
2015-12-25 07:07:00 +11:00
# Unauthorized
self.logMsg('Error 401 contacting %s' % url, -1)
elif r in (301, 302):
2015-12-25 07:07:00 +11:00
# Redirects
self.logMsg('HTTP redirect error %s at %s' % (r, url), -1)
elif r == 400:
2015-12-25 07:07:00 +11:00
# Bad requests
self.logMsg('Bad request at %s' % url, -1)
else:
self.logMsg('HTTP Error %s at %s' % (r, url), -1)
self.logMsg(e, 2)
return r
2015-12-25 07:07:00 +11:00
except requests.exceptions.SSLError as e:
self.logMsg("Invalid SSL certificate for: %s" % url, -1)
self.logMsg(e, 2)
return False
2015-12-25 07:07:00 +11:00
except requests.exceptions.TooManyRedirects as e:
self.logMsg("Too many redirects connecting to: %s" % url, -1)
self.logMsg(e, 2)
return False
2015-12-25 07:07:00 +11:00
except requests.exceptions.RequestException as e:
self.logMsg("Unknown error connecting to: %s" % url, -1)
self.logMsg("Error message: %s" % e, 2)
return False
2015-12-25 07:07:00 +11:00
2016-04-10 00:57:45 +10:00
except SystemExit:
self.logMsg('SystemExit detected, aborting download', 0)
self.stopSession()
return False
except:
self.logMsg('Unknown requests error', -1)
import traceback
self.logMsg(traceback.format_exc(), 0)
return False
# THE RESPONSE #####
if r.status_code == 204:
# No body in the response
return True
elif r.status_code == 401:
if authenticate is False:
2016-04-08 00:46:12 +10:00
# Called when checking a connect - no need for rash action
return 401
r.encoding = 'utf-8'
self.logMsg('HTTP error 401 from PMS. Message received:', -1)
self.logMsg(r.text, -1)
if '401 Unauthorized' in r.text:
# Truly unauthorized
self.logMsg('We seem to be truely unauthorized', 0)
if window('emby_serverStatus') not in ('401', 'Auth'):
# Tell userclient token has been revoked.
self.logMsg('Setting emby_serverStatus to 401', 0)
window('emby_serverStatus', value="401")
xbmcgui.Dialog().notification(
self.addonName,
"Error connecting: Unauthorized.",
xbmcgui.NOTIFICATION_ERROR)
return 401
else:
# there might be other 401 where e.g. PMS is under strain (tbv)
self.logMsg('PMS might only be under strain', 0)
return 401
elif r.status_code in (200, 201):
# 200: OK
# 201: Created
try:
# xml response
r = etree.fromstring(r.content)
return r
except:
r.encoding = 'utf-8'
if r.text == '':
# Answer does not contain a body (even though it should)
return True
try:
# UNICODE - JSON object
r = r.json()
return r
except:
r.encoding = 'utf-8'
if '200 OK' in r.text:
# Received fucked up OK from PMS on playstate update
pass
else:
self.logMsg("Unable to convert the response for: %s"
% url, -1)
self.logMsg("Received headers were: %s"
% r.headers, -1)
self.logMsg('Received text:', -1)
self.logMsg(r.text, -1)
return True
else:
self.logMsg('Unknown answer from PMS %s with status code %s. '
'Message:' % (url, r.status_code), -1)
r.encoding = 'utf-8'
self.logMsg(r.text, -1)
return True