diff --git a/resources/lib/ConnectionManager.py b/resources/lib/ConnectionManager.py index 6de607ea..00826f15 100644 --- a/resources/lib/ConnectionManager.py +++ b/resources/lib/ConnectionManager.py @@ -7,83 +7,103 @@ import xbmcgui import xbmcaddon import json -import threading -from datetime import datetime -from DownloadUtils import DownloadUtils import urllib import sys import socket +import threading +from datetime import datetime + +from DownloadUtils import DownloadUtils +from UserClient import UserClient +from ClientInformation import ClientInformation + -#define our global download utils -logLevel = 1 -########################################################################### class ConnectionManager(): + + clientInfo = ClientInformation() + userClient = UserClient() + doUtils = DownloadUtils() + + logLevel = 0 + addon = None + WINDOW = xbmcgui.Window(10000) + + def __init__(self): + + clientInfo = self.clientInfo + + self.addonId = clientInfo.getAddonId() + self.addonName = clientInfo.getAddonName() + self.addon = xbmcaddon.Addon(id=self.addonId) + self.__language__ = self.addon.getLocalizedString - addonSettings = None - __addon__ = xbmcaddon.Addon(id='plugin.video.emby') - __addondir__ = xbmc.translatePath( __addon__.getAddonInfo('profile') ) - __language__ = __addon__.getLocalizedString - - def printDebug(self, msg, level = 1): - if(logLevel >= level): - if(logLevel == 2): + def logMsg(self, msg, level=1): + + addonName = self.addonName + className = self.__class__.__name__ + s_logLevel = self.userClient.getLogLevel() + + # Attempt to change logLevel live + if (self.logLevel != s_logLevel): + self.logLevel = s_logLevel + + if (self.logLevel >= level): + try: + xbmc.log("%s %s -> %s" % (addonName, className, str(msg))) + except UnicodeEncodeError: try: - xbmc.log("emby " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg)) - except UnicodeEncodeError: - xbmc.log("emby " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) - else: - try: - xbmc.log("emby " + str(level) + " -> " + str(msg)) - except UnicodeEncodeError: - xbmc.log("emby " + str(level) + " -> " + str(msg.encode('utf-8'))) + xbmc.log("%s %s -> %s" % (addonName, className, str(msg.encode('utf-8')))) + except: + pass def checkServer(self): - WINDOW = xbmcgui.Window( 10000 ) - WINDOW.setProperty("Server_Checked", "True") + self.WINDOW.setProperty("Server_Checked", "True") + self.logMsg("Connection Manager Called") - self.printDebug ("emby Connection Manager Called") - self.addonSettings = xbmcaddon.Addon(id='plugin.video.emby') - port = self.addonSettings.getSetting('port') - host = self.addonSettings.getSetting('ipaddress') - - if(len(host) != 0 and host != ""): - self.printDebug ("emby server already set") + addon = self.addon + server = self.userClient.getServer() + + if (server != ""): + self.logMsg("Server already set", 2) return serverInfo = self.getServerDetails() + prefix,ip,port = serverInfo.split(":") - if(serverInfo == None): - self.printDebug ("emby getServerDetails failed") - return - - index = serverInfo.find(":") - - if(index <= 0): - self.printDebug ("emby getServerDetails data not correct : " + serverInfo) + if (serverInfo == None): + self.logMsg("getServerDetails failed", 1) return - server_address = serverInfo[:index] - server_port = serverInfo[index+1:] - self.printDebug ("emby detected server info " + server_address + " : " + server_port) + setServer = xbmcgui.Dialog().yesno(self.__language__(30167), "Proceed with the following server?", self.__language__(30169) + serverInfo) - xbmcgui.Dialog().ok(self.__language__(30167), self.__language__(30168), self.__language__(30169) + server_address, self.__language__(30030) + server_port) - - # get a list of users - self.printDebug ("Getting user list") - jsonData = None - downloadUtils = DownloadUtils() + if setServer == 1: + self.logMsg("Server selected. Saving information.", 1) + addon.setSetting("ipaddress", ip.replace("/", "")) + addon.setSetting("port", port) + # If https is enabled + if (prefix == 'https'): + addon.setSetting('https', "true") + else: + self.logMsg("No server selected.", 1) + xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId) + return + + # Get List of public users + self.logMsg("Getting user list", 1) + server = ip.replace("/", "") + ":" + port + try: - jsonData = downloadUtils.downloadUrl(server_address + ":" + server_port + "/mediabrowser/Users/Public?format=json") + jsonData = self.doUtils.downloadUrl(serverInfo + "/mediabrowser/Users/Public?format=json", authenticate=False) except Exception, msg: - error = "Get User unable to connect to " + server_address + ":" + server_port + " : " + str(msg) + error = "Get User unable to connect to " + server + " : " + str(msg) xbmc.log (error) return "" - if(jsonData == False): + if (jsonData == False): return - self.printDebug("jsonData : " + str(jsonData), level=2) + self.logMsg("jsonData : " + str(jsonData), level=2) result = json.loads(jsonData) names = [] @@ -95,55 +115,48 @@ class ConnectionManager(): name = name + " (Secure)" names.append(name) - self.printDebug ("User List : " + str(names)) - self.printDebug ("User List : " + str(userList)) + self.logMsg("User List: " + str(names)) + self.logMsg("User List: " + str(userList)) return_value = xbmcgui.Dialog().select(self.__language__(30200), names) if(return_value > -1): selected_user = userList[return_value] - self.printDebug("Setting Selected User : " + selected_user) - self.addonSettings.setSetting("port", server_port) - self.addonSettings.setSetting("ipaddress", server_address) - self.addonSettings.setSetting("username", selected_user) - downloadUtils.authenticate() + self.logMsg("Setting Selected User: %s" % selected_user) + self.addon.setSetting("username", selected_user) + return else: xbmc.log("No user selected.") - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') + xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId) return def getServerDetails(self): - self.printDebug("Getting Server Details from Network") - - MESSAGE = "who is MediaBrowserServer?" - #MULTI_GROUP = ("224.3.29.71", 7359) - #MULTI_GROUP = ("127.0.0.1", 7359) + self.logMsg("Getting Server Details from Network") + MULTI_GROUP = ("", 7359) + MESSAGE = "who is EmbyServer?" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(6.0) - - #ttl = struct.pack('b', 20) - #sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) - xbmc.log("MutliGroup : " + str(MULTI_GROUP)); - xbmc.log("Sending UDP Data : " + MESSAGE); + self.logMsg("MutliGroup : %s" % str(MULTI_GROUP)); + self.logMsg("Sending UDP Data: %s" % MESSAGE); sock.sendto(MESSAGE, MULTI_GROUP) try: data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes - xbmc.log("Received Response : " + data) - if(data[0:18] == "MediaBrowserServer"): - xbmc.log("Found Server : " + data[19:]) - return data[19:] + self.logMsg("Received Response: %s" % data) + # Get the address + data = json.loads(data) + return data['Address'] except: - xbmc.log("No UDP Response") + self.logMsg("No UDP Response") pass - return None - + return None \ No newline at end of file diff --git a/resources/lib/DownloadUtils.py b/resources/lib/DownloadUtils.py index ad5c7bf8..2930f029 100644 --- a/resources/lib/DownloadUtils.py +++ b/resources/lib/DownloadUtils.py @@ -19,6 +19,7 @@ import traceback class DownloadUtils(): + WINDOW = xbmcgui.Window(10000) logLevel = 0 addonSettings = None getString = None @@ -49,30 +50,16 @@ class DownloadUtils(): def getServer(self, prefix=True): - # For https support - addon = self.addon - HTTPS = addon.getSetting('https') - host = addon.getSetting('ipaddress') - port = addon.getSetting('port') - server = host + ":" + port - return server - ''' - if len(server) < 2: - self.logMsg("No server information saved.") - return "" + WINDOW = self.WINDOW + username = WINDOW.getProperty("currUser") + + if prefix: + server = WINDOW.getProperty("server%s" % username) + else: + server = WINDOW.getProperty("server_%s" % username) + + return server - # If https is true - if prefix and (HTTPS == "true"): - server = "https://%s" % server - return server - # If https is false - elif prefix and (HTTPS == "false"): - server = "http://%s" % server - return server - # If only the host:port is required - elif (prefix == False): - return server - ''' def getUserId(self, suppress=True): WINDOW = xbmcgui.Window( 10000 ) @@ -146,7 +133,7 @@ class DownloadUtils(): self.downloadUrl(url, postBody=stringdata, type="POST") - def authenticate(self, retreive=True): + '''def authenticate(self, retreive=True): WINDOW = xbmcgui.Window(10000) self.addonSettings = xbmcaddon.Addon(id='plugin.video.emby') @@ -227,15 +214,13 @@ class DownloadUtils(): WINDOW.setProperty("userid" + username, "") self.addonSettings.setSetting("AccessToken" + username, "") self.addonSettings.setSetting("userid" + username, "") - return "" + return "" ''' def imageUrl(self, id, type, index, width, height): - port = self.addonSettings.getSetting('port') - host = self.addonSettings.getSetting('ipaddress') - server = host + ":" + port + server = self.getServer() - return "http://" + server + "/mediabrowser/Items/" + str(id) + "/Images/" + type + "/" + str(index) + "/e3ab56fe27d389446754d0fb04910a34/original/" + str(width) + "/" + str(height) + "/0" + return "%s/mediabrowser/Items/%s/Images/%s/%s//e3ab56fe27d389446754d0fb04910a34/original/%s/%s/0" % (server, id, type, index, width, height) def getAuthHeader(self, authenticate=True): clientInfo = ClientInformation() @@ -244,6 +229,7 @@ class DownloadUtils(): 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 + "\"" @@ -254,7 +240,7 @@ class DownloadUtils(): 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.authenticate() + authToken = self.WINDOW.getProperty("accessToken%s" % username) if(authToken != ""): headers["X-MediaBrowser-Token"] = authToken @@ -278,14 +264,20 @@ class DownloadUtils(): self.TrackLog = self.TrackLog + "HTTP_API_CALL : " + url + stackString + "\r" link = "" + https = None try: - if url[0:4] == "http": + if url[0:5] == "https": + serversplit = 2 + urlsplit = 3 + elif url[0:4] == "http": serversplit = 2 urlsplit = 3 else: serversplit = 0 urlsplit = 1 + https = self.addonSettings.getSetting('https') + server = url.split('/')[serversplit] urlPath = "/"+"/".join(url.split('/')[urlsplit:]) @@ -300,7 +292,12 @@ class DownloadUtils(): head = self.getAuthHeader(authenticate) self.logMsg("HEADERS : " + str(head), level=2) - conn = httplib.HTTPConnection(server, timeout=5) + if (https == 'false'): + #xbmc.log("Https disabled.") + conn = httplib.HTTPConnection(server, timeout=5) + elif (https == 'true'): + #xbmc.log("Https enabled.") + conn = httplib.HTTPSConnection(server, timeout=5) # make the connection and send the request if(postBody != None): @@ -356,22 +353,23 @@ class DownloadUtils(): return data.getheader('Location') elif int(data.status) == 401: - error = "HTTP response error: " + str(data.status) + " " + str(data.reason) - xbmc.log(error) - - username = self.addonSettings.getSetting("username") WINDOW = xbmcgui.Window(10000) - WINDOW.setProperty("AccessToken" + username, "") - WINDOW.setProperty("userid" + username, "") - self.addonSettings.setSetting("AccessToken" + username, "") - self.addonSettings.setSetting("userid" + username, "") - - xbmcgui.Dialog().ok(self.getString(30135), self.getString(30044), "Reason : " + str(data.reason)) - try: - conn.close() - except: + status = WINDOW.getProperty("Server_status") + # Prevent multiple re-auth + if (status == "401") or (status == "Auth"): pass - return "" + 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) diff --git a/resources/lib/UserClient.py b/resources/lib/UserClient.py new file mode 100644 index 00000000..4b99ddc4 --- /dev/null +++ b/resources/lib/UserClient.py @@ -0,0 +1,337 @@ +################################################################################################# +# UserClient thread +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon +import xbmcvfs + +import threading +import hashlib +import json as json + +import KodiMonitor +from ClientInformation import ClientInformation +from DownloadUtils import DownloadUtils + + +class UserClient(threading.Thread): + + + clientInfo = ClientInformation() + doUtils = DownloadUtils() + + stopClient = False + logLevel = 0 + addon = None + auth = True + retry = 0 + WINDOW = xbmcgui.Window(10000) + + currUser = None + currUserId = None + currServer = None + currToken = None + AdditionalUser = [] + + def __init__(self, *args): + + clientInfo = self.clientInfo + self.KodiMonitor = KodiMonitor.Kodi_Monitor() + + self.addonId = clientInfo.getAddonId() + self.addonName = clientInfo.getAddonName() + self.addon = xbmcaddon.Addon(id=self.addonId) + + self.logMsg("|---- Starting UserClient ----|", 0) + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level=1): + + addonName = self.addonName + className = self.__class__.__name__ + + if (self.logLevel != self.getLogLevel()): + xbmc.log("Adjusting logLevel to %i" % self.getLogLevel()) + self.logLevel = self.getLogLevel() + + if (self.logLevel >= level): + try: + xbmc.log("%s %s -> %s" % (addonName, className, str(msg))) + except UnicodeEncodeError: + try: + xbmc.log("%s %s -> %s" % (addonName, className, str(msg.encode('utf-8')))) + except: + pass + + def getUsername(self): + + username = self.addon.getSetting('username') + + if (username == ""): + self.logMsg("No username saved.", 2) + return "" + + return username + + def getLogLevel(self): + + logLevel = int(self.addon.getSetting('logLevel')) + return logLevel + + def getUserId(self): + + username = self.getUsername() + w_userId = self.WINDOW.getProperty('userId%s' % username) + s_userId = self.addon.getSetting('userId%s' % username) + + # Verify the window property + if (w_userId != ""): + self.logMsg("Returning userId from WINDOW for username: %s UserId: %s" % (username, w_userId), 2) + return w_userId + # Verify the settings + elif (s_userId != ""): + self.logMsg("Returning userId from SETTINGS for username: %s userId: %s" % (username, s_userId), 2) + return s_userId + # No userId found + else: + self.logMsg("No userId saved for username: %s." % username) + return + + def getServer(self, prefix=True): + + # For https support + addon = self.addon + HTTPS = addon.getSetting('https') + host = addon.getSetting('ipaddress') + port = addon.getSetting('port') + server = host + ":" + port + + if host == "": + self.logMsg("No server information saved.", 2) + return "" + + # If https is true + if prefix and (HTTPS == "true"): + server = "https://%s" % server + return server + # If https is false + elif prefix and (HTTPS == "false"): + server = "http://%s" % server + return server + # If only the host:port is required + elif (prefix == False): + return server + + def getToken(self): + + username = self.getUsername() + w_token = self.WINDOW.getProperty('accessToken%s' % username) + s_token = self.addon.getSetting('accessToken%s' % username) + + # Verify the window property + if (w_token != ""): + self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s" % (username, w_token), 2) + return w_token + # Verify the settings + elif (s_token != ""): + self.logMsg("Returning accessToken from SETTINGS for username: %s accessToken: %s" % (username, s_token), 2) + self.WINDOW.setProperty('accessToken%s' % username, s_token) + return s_token + else: + self.logMsg("No token found.") + return "" + + def getPublicUsers(self): + + server = self.getServer() + + # Get public Users + url = "%s/mediabrowser/Users/Public?format=json" % server + jsonData = self.doUtils.downloadUrl(url, authenticate=False) + + users = [] + + if (jsonData != ""): + users = json.loads(jsonData) + + return users + + def loadCurrUser(self): + + WINDOW = self.WINDOW + username = self.getUsername() + + # Only to be used if token exists + self.currUserId = self.getUserId() + self.currServer = self.getServer() + self.currToken = self.getToken() + + # Set to windows property + WINDOW.setProperty("currUser", username) + WINDOW.setProperty("accessToken%s" % username, self.currToken) + WINDOW.setProperty("server%s" % username, self.currServer) + WINDOW.setProperty("server_%s" % username, self.getServer(prefix=False)) + WINDOW.setProperty("userId%s" % username, self.currUserId) + + self.currUser = username + + def authenticate(self): + + WINDOW = self.WINDOW + addon = self.addon + + username = self.getUsername() + server = self.getServer() + addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')) + hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) + + # If there's no settings.xml + if (hasSettings == 0): + self.logMsg("No settings.xml found.") + self.auth = False + return + # If no user information + if (server == "") or (username == ""): + self.logMsg("Missing server information.") + self.auth = False + return + # If there's a token + if (self.getToken() != ""): + self.loadCurrUser() + self.logMsg("Current user: %s" % self.currUser, 0) + self.logMsg("Current userId: %s" % self.currUserId, 0) + self.logMsg("Current accessToken: %s" % self.currToken, 0) + return + + users = self.getPublicUsers() + password = "" + + # Find user in list + for user in users: + name = user.get("Name") + userHasPassword = False + if (username == name): + # Verify if user has a password + if (user.get("HasPassword") == True): + userHasPassword = True + # If user has password + if (userHasPassword): + password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) + # If password dialog is cancelled + if password == "": + self.logMsg("No password entered.", 0) + #addon.setSetting("username", "") + self.WINDOW.setProperty("Server_status", "Stop") + self.auth = False + #self.WINDOW.setProperty("Server_status", "") + return + break + else: + # Manual login, user is hidden + password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) + + sha1 = hashlib.sha1(password) + sha1 = sha1.hexdigest() + + # Authenticate username and password + url = "%s/mediabrowser/Users/AuthenticateByName?format=json" % server + messageData = "username=%s&password=%s" % (username, sha1) + + resp = self.doUtils.downloadUrl(url, postBody=messageData, type="POST", authenticate=False) + + result = None + accessToken = None + try: + self.logMsg("Auth_Reponse: %s" % resp, 1) + result = json.loads(resp) + accessToken = result.get("AccessToken") + except: + pass + + if (result != None and accessToken != None): + self.currUser = username + userId = result.get("User").get("Id") + addon.setSetting("accessToken%s" % username, accessToken) + addon.setSetting("userId%s" % username, userId) + self.logMsg("User Authenticated: %s" % accessToken) + self.loadCurrUser() + self.WINDOW.setProperty("Server_status", "") + self.retry = 0 + return + else: + self.logMsg("User authentication failed.") + addon.setSetting("accessToken%s" % username, "") + addon.setSetting("userId%s" % username, "") + xbmcgui.Dialog().ok("Error Connecting", "Wrong password.") + + # Give two attempts at entering password + self.retry += 1 + if self.retry == 2: + self.logMsg("Too many retries. Please restart Kodi.") + self.WINDOW.setProperty("Server_status", "Stop") + + self.auth = False + return + + def resetClient(self): + + if self.currToken != None: + # In case of 401, removed saved token + self.addon.setSetting("accessToken%s" % self.currUser, "") + self.WINDOW.setProperty("accessToken%s" % self.currUser, "") + self.currToken = None + self.logMsg("User token has been removed.", 1) + + self.auth = True + self.currUser = None + return + + + def run(self): + + while not self.KodiMonitor.abortRequested(): + + # Get the latest addon settings + self.addon = xbmcaddon.Addon(id=self.addonId) + + if (self.WINDOW.getProperty("Server_status") != ""): + status = self.WINDOW.getProperty("Server_status") + if status == "401": + self.WINDOW.setProperty("Server_status", "Auth") + # Revoked token + self.resetClient() + + if self.auth and (self.currUser == None): + status = self.WINDOW.getProperty("Server_status") + + if (status == "") or (status == "Auth"): + self.auth = False + self.authenticate() + + + if (self.auth == False) and (self.currUser == None): + # Only if there's information found to login + server = self.getServer() + username = self.getUsername() + status = self.WINDOW.getProperty("Server_status") + + # If user didn't enter a password when prompted + if status == "Stop": + pass + + elif (server != "") and (username != ""): + self.logMsg("Server found: %s" % server) + self.logMsg("Username found: %s" % username) + self.auth = True + + # If stopping the client didn't work + if self.stopClient == True: + break + + self.logMsg("|---- UserClient Stopped ----|", 0) + + def stopClient(self): + # As last resort + self.stopClient = True \ No newline at end of file diff --git a/service.py b/service.py index 248a6e46..d90f26da 100644 --- a/service.py +++ b/service.py @@ -16,27 +16,30 @@ from LibrarySync import LibrarySync from Player import Player from DownloadUtils import DownloadUtils from ConnectionManager import ConnectionManager +from ClientInformation import ClientInformation from WebSocketClient import WebSocketThread +from UserClient import UserClient librarySync = LibrarySync() class Service(): newWebSocketThread = None + newUserClient = None + + clientInfo = ClientInformation() def __init__(self, *args ): self.KodiMonitor = KodiMonitor.Kodi_Monitor() utils.logMsg("MB3 Sync Service", "starting Monitor",0) - - pass + xbmc.log("======== START %s ========" % self.clientInfo.getAddonName()) + pass def ServiceEntryPoint(self): ConnectionManager().checkServer() - DownloadUtils().authenticate(retreive=True) - player = Player() lastProgressUpdate = datetime.today() interval_FullSync = 600 @@ -45,6 +48,8 @@ class Service(): cur_seconds_fullsync = interval_FullSync cur_seconds_incrsync = interval_IncrementalSync + user = UserClient() + player = Player() ws = WebSocketThread() while not self.KodiMonitor.abortRequested(): @@ -74,8 +79,11 @@ class Service(): xbmc.log("MB3 Sync Service -> Exception in Playback Monitor Service : " + str(e)) pass else: + if (self.newUserClient == None): + self.newUserClient = "Started" + user.start() # background worker for database sync - if DownloadUtils().authenticate(retreive=False) != "": + if (user.currUser != None): # Correctly launch the websocket, if user manually launches the add-on if (self.newWebSocketThread == None): @@ -113,7 +121,10 @@ class Service(): utils.logMsg("MB3 Sync Service", "stopping Service",0) if (self.newWebSocketThread != None): - ws.stopClient() + ws.stopClient() + + if (self.newUserClient != None): + user.stopClient() #start the service