diff --git a/resources/lib/DownloadUtils.py b/resources/lib/DownloadUtils.py index caccd109..e8361e1c 100644 --- a/resources/lib/DownloadUtils.py +++ b/resources/lib/DownloadUtils.py @@ -1,341 +1,225 @@ import xbmc import xbmcgui import xbmcaddon -import urllib -import urllib2 -import httplib -import hashlib -import StringIO -import gzip -import sys -import inspect -import json as json -from random import randrange -from uuid import uuid4 as uuid4 + +import requests +import json +import logging + +import Utils as utils from ClientInformation import ClientInformation -import encodings -import time -import traceback + +# Disable requests logging +logging.getLogger("requests").setLevel(logging.WARNING) class DownloadUtils(): - - WINDOW = xbmcgui.Window(10000) - logLevel = 0 - addonSettings = None - getString = None - LogCalls = False - TrackLog = "" - TotalUrlCalls = 0 - - def __init__(self, *args): - addonId = ClientInformation().getAddonId() - self.addonSettings = xbmcaddon.Addon(id=addonId) - self.addon = xbmcaddon.Addon(id=addonId) - self.getString = self.addonSettings.getLocalizedString - level = self.addonSettings.getSetting('logLevel') - self.logLevel = 0 - if(level != None and level != ""): - self.logLevel = int(level) - if(self.logLevel == 2): - self.LogCalls = True - - def logMsg(self, msg, level = 1): - if(self.logLevel >= level): - try: - xbmc.log("emby DownloadUtils -> " + str(msg)) - except UnicodeEncodeError: - try: - xbmc.log("emby DownloadUtils -> " + str(msg.encode('utf-8'))) - except: pass - - def getServer(self, prefix=True): - - WINDOW = self.WINDOW - username = WINDOW.getProperty("currUser") - - if prefix: - server = WINDOW.getProperty("server%s" % username) - else: - server = WINDOW.getProperty("server_%s" % username) - - return server - - def getUserId(self, suppress=True): - - WINDOW = xbmcgui.Window( 10000 ) - self.addonSettings = xbmcaddon.Addon(id='plugin.video.emby') - port = self.addonSettings.getSetting('port') - host = self.addonSettings.getSetting('ipaddress') - userName = self.addonSettings.getSetting('username') - - userid = WINDOW.getProperty("userid" + userName) - - if(userid != None and userid != ""): - self.logMsg("DownloadUtils -> Returning saved (WINDOW) UserID : " + userid + "UserName: " + userName,2) - return userid - - userid = self.addonSettings.getSetting("userid" + userName) - if(userid != None and userid != ""): - WINDOW.setProperty("userid" + userName, userid) - self.logMsg("DownloadUtils -> Returning saved (SETTING) UserID : " + userid + "UserName: " + userName,2) - return userid - self.logMsg("Looking for user name: " + userName) + # Borg - multiple instances, shared state + _shared_state = {} + clientInfo = ClientInformation() - authOk = self.authenticate() - if(authOk == ""): - if(suppress == False): - xbmcgui.Dialog().ok(self.getString(30044), self.getString(30044)) - return "" + addonName = clientInfo.getAddonName() + addonId = clientInfo.getAddonId() + addon = xbmcaddon.Addon(id=addonId) + WINDOW = xbmcgui.Window(10000) - userid = WINDOW.getProperty("userid" + userName) - if(userid == "" and suppress == False): - xbmcgui.Dialog().ok(self.getString(30045),self.getString(30045)) + # Requests session + s = None + timeout = 30 - self.logMsg("userid : " + userid) - self.postcapabilities() - - return userid - - def postcapabilities(self): - self.logMsg("postcapabilities called") - - # Set Capabilities - server = self.getServer() - clientInfo = ClientInformation() - machineId = clientInfo.getMachineId() - - # get session id - url = "%s/mediabrowser/Sessions?DeviceId=%s&format=json" % (server, machineId) - self.logMsg("Session URL : " + url); - jsonData = self.downloadUrl(url) - self.logMsg("Session JsonData : " + jsonData) - result = json.loads(jsonData) - self.logMsg("Session JsonData : " + str(result)) - sessionId = result[0].get("Id") - self.logMsg("Session Id : " + str(sessionId)) - - # post capability data - #playableMediaTypes = "Audio,Video,Photo" + def __init__(self): + + self.__dict__ = self._shared_state + self.className = self.__class__.__name__ + + def logMsg(self, msg, lvl=1): + + utils.logMsg("%s %s" % (self.addonName, self.className), str(msg), int(lvl)) + + def setUsername(self, username): + # Reserved for UserClient only + self.username = username + self.logMsg("Set username: %s" % username, 1) + + def setUserId(self, userId): + # Reserved for UserClient only + self.userId = userId + self.logMsg("Set userId: %s" % userId, 2) + + def setServer(self, server): + # Reserved for UserClient only + self.server = server + self.logMsg("Set server: %s" % server, 2) + + def setToken(self, token): + # Reserved for UserClient only + self.token = token + self.logMsg("Set token: %s" % token, 2) + + def postCapabilities(self, deviceId): + + # Get sessionId + url = "{server}/mediabrowser/Sessions?DeviceId=%s&format=json" % deviceId + result = self.downloadUrl(url) + # sessionId result + self.logMsg("Session result: %s" % result, 1) + self.sessionId = result[0][u'Id'] + + # Settings for capabilities playableMediaTypes = "Audio,Video" - #supportedCommands = "Play,Playstate,DisplayContent,GoHome,SendString,GoToSettings,DisplayMessage,PlayNext" supportedCommands = "Play,Playstate,SendString,DisplayMessage,PlayNext" + + # Post settings to sessionId + url = "{server}/mediabrowser/Sessions/Capabilities?Id=%s&PlayableMediaTypes=%s&SupportedCommands=%s&SupportsMediaControl=True" % (self.sessionId, playableMediaTypes, supportedCommands) + data = {} + self.logMsg("Capabilities URL: %s" % url, 2) + self.logMsg("PostData: %s" % data, 2) + + self.downloadUrl(url, postBody=data, type="POST") + self.logMsg("Posted capabilities to sessionId: %s" % self.sessionId, 1) + + def startSession(self): + + self.deviceId = self.clientInfo.getMachineId() + + # User is identified from this point + # Attach authenticated header to the session + cert = None + header = self.getHeader() + + if self.addon.getSetting('sslcert') != "None": + # If user uses HTTPS and has a custom client certificate + cert = self.addon.getSetting('sslcert') - url = "%s/mediabrowser/Sessions/Capabilities?Id=%s&PlayableMediaTypes=%s&SupportedCommands=%s&SupportsMediaControl=True" % (server, sessionId, playableMediaTypes, supportedCommands) - postData = {} - #postData["Id"] = sessionId; - #postData["PlayableMediaTypes"] = "Video"; - #postData["SupportedCommands"] = "MoveUp"; - stringdata = json.dumps(postData) - self.logMsg("Capabilities URL : " + url); - self.logMsg("Capabilities Data : " + stringdata) - - self.downloadUrl(url, postBody=stringdata, type="POST") + # Start session + self.s = requests.Session() + self.s.headers.update(header) + self.s.cert = cert + # 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) + self.postCapabilities(self.deviceId) def imageUrl(self, id, type, index, width, height): - - server = self.getServer() - - return "%s/mediabrowser/Items/%s/Images/%s?MaxWidth=%s&MaxHeight=%s&Index=%s" % (server, id, type, width, height, index) - - def getAuthHeader(self, authenticate=True): - clientInfo = ClientInformation() - txt_mac = clientInfo.getMachineId() + # To move to API.py + return "%s/mediabrowser/Items/%s/Images/%s?MaxWidth=%s&MaxHeight=%s&Index=%s" % (self.server, id, type, width, height, index) + + def getHeader(self, authenticate=True): + + clientInfo = self.clientInfo + + deviceName = clientInfo.getDeviceName() + deviceId = clientInfo.getMachineId() version = clientInfo.getVersion() - - deviceName = self.addonSettings.getSetting('deviceName') - deviceName = deviceName.replace("\"", "_") - username = self.WINDOW.getProperty("currUser") - if(authenticate == False): - authString = "MediaBrowser Client=\"Kodi\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" - headers = {"Accept-encoding": "gzip", "Accept-Charset" : "UTF-8,*", "Authorization" : authString} - return headers + if not authenticate: + # If user is not authenticated + auth = 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (deviceName, deviceId, version) + header = {"Accept-encoding": "gzip", "Accept-Charset": "UTF-8,*", "Authorization": auth} + + self.logMsg("Header: %s" % header, 2) + return header + else: - userid = self.getUserId() - authString = "MediaBrowser UserId=\"" + userid + "\",Client=\"Kodi\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" - headers = {"Accept-encoding": "gzip", "Accept-Charset" : "UTF-8,*", "Authorization" : authString} - - authToken = self.WINDOW.getProperty("accessToken%s" % username) - if(authToken != ""): - headers["X-MediaBrowser-Token"] = authToken + userId = self.userId + token = self.token + # Attached to the requests session + auth = 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (userId, deviceName, deviceId, version) + header = {"Accept-encoding": "gzip", "Accept-Charset": "UTF-8,*", "Authorization": auth, "X-MediaBrowser-Token": token} - self.logMsg("Authentication Header : " + str(headers),2) - return headers - - def downloadUrl(self, url, suppress=False, postBody=None, type="GET", popup=0, authenticate=True ): - self.logMsg("== ENTER: getURL ==",2) + self.logMsg("Header: %s" % header, 2) + return header - if(authenticate == True and suppress == True): - token = self.authenticate(retreive=False) - if(token == ""): - self.logMsg("No auth info set and suppress is true so returning no data!") - return "" + def downloadUrl(self, url, postBody=None, type="GET", authenticate=True): - self.TotalUrlCalls = self.TotalUrlCalls + 1 - if(self.LogCalls): - stackString = "" - for f in inspect.stack(): - stackString = stackString + "\r - " + str(f) - self.TrackLog = self.TrackLog + "HTTP_API_CALL : " + url + stackString + "\r" + self.logMsg("=== ENTER downloadUrl ===", 2) + + WINDOW = self.WINDOW + timeout = self.timeout + default_link = "" + + # If user is authenticated + if (authenticate): + # Get requests session + s = self.s + # Replace for the real values and append api_key + url = url.replace("{server}", self.server, 1) + url = url.replace("{UserId}", self.userId, 1) + url = "%s&api_key=%s" % (url, self.token) - link = "" - https = None + self.logMsg("URL: %s" % url, 1) + # Prepare request + if type == "GET": + r = s.get(url, params=postBody, timeout=timeout) + elif type == "POST": + r = s.post(url, params=postBody, timeout=timeout) + elif type == "DELETE": + r = s.delete(url, params=postBody, timeout=timeout) + + # If user is not authenticated + elif not authenticate: + + self.logMsg("URL: %s" % url, 1) + header = self.getHeader(authenticate=False) + + # Prepare request + if type == "GET": + r = requests.get(url, params=postBody, headers=header, timeout=timeout, verify=False) + elif type == "POST": + r = requests.post(url, params=postBody, headers=header, timeout=timeout) + + # Process the response try: - if url[0:5] == "https": - serversplit = 2 - urlsplit = 3 - elif url[0:4] == "http": - serversplit = 2 - urlsplit = 3 - else: - serversplit = 0 - urlsplit = 1 + r.raise_for_status() - https = self.addonSettings.getSetting('https') - - server = url.split('/')[serversplit] - urlPath = "/"+"/".join(url.split('/')[urlsplit:]) - - self.logMsg("DOWNLOAD_URL = " + url,2) - self.logMsg("server = " + str(server),2) - self.logMsg("urlPath = " + str(urlPath),2) - - if(server[0:1] == ":" or server[-1:] == ":"): - self.logMsg("No server host or port set in url") - return "" - - head = self.getAuthHeader(authenticate) - self.logMsg("HEADERS : " + str(head), level=2) - - if (https == 'false'): - #xbmc.log("Https disabled.") - conn = httplib.HTTPConnection(server, timeout=30) - elif (https == 'true'): - #xbmc.log("Https enabled.") - conn = httplib.HTTPSConnection(server, timeout=30) - - # make the connection and send the request - if(postBody != None): - head["Content-Type"] = "application/x-www-form-urlencoded" - head["Content-Length"] = str(len(postBody)) - self.logMsg("POST DATA : " + postBody,2) - conn.request(method=type, url=urlPath, body=postBody, headers=head) - else: - conn.request(method=type, url=urlPath, headers=head) - - # get the response - tries = 0 - while tries <= 4: - try: - data = conn.getresponse() - break - except: - # TODO: we need to work out which errors we can just quit trying immediately - if(xbmc.abortRequested == True): - return "" - xbmc.sleep(100) - if(xbmc.abortRequested == True): - return "" - tries += 1 - if tries == 5: - data = conn.getresponse() - - self.logMsg("GET URL HEADERS : " + str(data.getheaders()), level=2) - - # process the response - contentType = "none" - if int(data.status) == 200: - retData = data.read() - contentType = data.getheader('content-encoding') - self.logMsg("Data Len Before : " + str(len(retData)), level=2) - if(contentType == "gzip"): - retData = StringIO.StringIO(retData) - gzipper = gzip.GzipFile(fileobj=retData) - link = gzipper.read() - else: - link = retData - self.logMsg("Data Len After : " + str(len(link)), level=2) - self.logMsg("====== 200 returned =======", level=2) - self.logMsg("Content-Type : " + str(contentType), level=2) - self.logMsg(link, level=2) - self.logMsg("====== 200 finished ======", level=2) - - elif ( int(data.status) == 301 ) or ( int(data.status) == 302 ): + if r.status_code == 204: + # No response in body + self.logMsg("====== 204 Success ======", 1) + return default_link + # Response code 200 + elif r.status_code == requests.codes.ok: try: - conn.close() - except: - pass - return data.getheader('Location') + # UTF-8 - JSON object + r = r.json() + self.logMsg("====== 200 Success ======", 1) + return r + except: + self.logMsg("Unable to convert the response for: %s" % url, 1) - elif int(data.status) == 401: - WINDOW = xbmcgui.Window(10000) + return default_link + + # TO REVIEW EXCEPTIONS + except requests.exceptions.ConnectionError as e: + self.logMsg("Server unreachable at: %s" % url, 0) + self.logMsg(e, 1) + + except requests.exceptions.ConnectTimeout as e: + self.logMsg("Server timeout at: %s" % url, 0) + self.logMsg(e, 1) + + except requests.exceptions.HTTPError as e: + + if r.status_code == 401: + # Unauthorized status = WINDOW.getProperty("Server_status") - # Prevent multiple re-auth if (status == "401") or (status == "Auth"): pass else: # Tell UserClient token has been revoked. WINDOW.setProperty("Server_status", "401") - error = "HTTP response error: " + str(data.status) + " " + str(data.reason) - xbmc.log(error) - #xbmcgui.Dialog().ok(self.getString(30135),"Reason: %s" % data.reason) #self.getString(30044), - - try: - conn.close() - except: - pass - return "" - - elif int(data.status) >= 400: - error = "HTTP response error: " + str(data.status) + " " + str(data.reason) - xbmc.log(error) - stack = self.FormatException() - self.logMsg(stack) - if suppress is False: - if popup == 0: - xbmc.executebuiltin("XBMC.Notification(URL error: "+ str(data.reason) +",)") - else: - xbmcgui.Dialog().ok(self.getString(30135),server) - try: - conn.close() - except: - pass - return "" - else: - link = "" - except Exception, msg: - error = "Unable to connect to " + str(server) + " : " + str(msg) - xbmc.log(error) - stack = self.FormatException() - self.logMsg(stack) - if suppress is False: - if popup == 0: - xbmc.executebuiltin("XBMC.Notification(: Connection Error: Error connecting to server,)") - else: - xbmcgui.Dialog().ok(self.getString(30204), str(msg)) - pass - else: - try: - conn.close() - except: + self.logMsg("HTTP Error: %s" % e, 0) + + elif (r.status_code == 301) or (r.status_code == 302): + # Redirects + pass + elif r.status_code == 400: + # Bad requests pass - return link - - def FormatException(self): - exception_list = traceback.format_stack() - exception_list = exception_list[:-2] - exception_list.extend(traceback.format_tb(sys.exc_info()[2])) - exception_list.extend(traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])) + except requests.exceptions.RequestException as e: + self.logMsg("Unknown error connecting to: %s" % url, 0) + self.logMsg(e, 1) - exception_str = "Traceback (most recent call last):\n" - exception_str += "".join(exception_list) - # Removing the last \n - exception_str = exception_str[:-1] - - return exception_str - - def __del__(self): - return - # xbmc.log("\rURL_REQUEST_REPORT : Total Calls : " + str(self.TotalUrlCalls) + "\r" + self.TrackLog) + return default_link