From 9e5385c1c6b2b9b7b2e0904109c1b83a570e3896 Mon Sep 17 00:00:00 2001 From: faush01 Date: Mon, 23 Mar 2015 15:54:16 +1100 Subject: [PATCH] add WebSocket for events and remote control For some reason playback stop is not being reported --- resources/lib/DownloadUtils.py | 5 +- resources/lib/PlaybackUtils.py | 125 +++++++++++++++ resources/lib/WebSocketClient.py | 262 +++++++++++++++++++++++++++++++ resources/settings.xml | 2 + service.py | 6 + 5 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 resources/lib/WebSocketClient.py diff --git a/resources/lib/DownloadUtils.py b/resources/lib/DownloadUtils.py index f29c148f..1a0ad5d0 100644 --- a/resources/lib/DownloadUtils.py +++ b/resources/lib/DownloadUtils.py @@ -55,7 +55,8 @@ class DownloadUtils(): host = addon.getSetting('ipaddress') port = addon.getSetting('port') server = host + ":" + port - + return server + ''' if len(server) < 2: self.logMsg("No server information saved.") return "" @@ -71,7 +72,7 @@ class DownloadUtils(): # If only the host:port is required elif (prefix == False): return server - + ''' def getUserId(self, suppress=True): WINDOW = xbmcgui.Window( 10000 ) diff --git a/resources/lib/PlaybackUtils.py b/resources/lib/PlaybackUtils.py index 746c24a5..b12d355a 100644 --- a/resources/lib/PlaybackUtils.py +++ b/resources/lib/PlaybackUtils.py @@ -203,3 +203,128 @@ class PlaybackUtils(): listItem.setInfo('video', {'writer' : people.get('Writer')}) listItem.setInfo('video', {'mpaa': result.get("OfficialRating")}) listItem.setInfo('video', {'genre': genre}) + + def seekToPosition(self, seekTo): + + #Set a loop to wait for positive confirmation of playback + count = 0 + while not xbmc.Player().isPlaying(): + self.logMsg( "Not playing yet...sleep for 1 sec") + count = count + 1 + if count >= 10: + return + else: + time.sleep(1) + + #Jump to resume point + jumpBackSec = int(self.settings.getSetting("resumeJumpBack")) + seekToTime = seekTo - jumpBackSec + count = 0 + while xbmc.Player().getTime() < (seekToTime - 5) and count < 11: # only try 10 times + count = count + 1 + xbmc.Player().pause + xbmc.sleep(100) + xbmc.Player().seekTime(seekToTime) + xbmc.sleep(100) + xbmc.Player().play() + + def PLAYAllItems(self, items, startPositionTicks): + xbmc.log("== ENTER: PLAYAllItems ==") + xbmc.log("Items : " + str(items)) + + du = DownloadUtils() + userid = du.getUserId() + server = du.getServer() + + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + playlist.clear() + started = False + + for itemID in items: + + xbmc.log("Adding Item to Playlist : " + itemID) + item_url = "http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + itemID + "?format=json" + jsonData = du.downloadUrl(item_url, suppress=False, popup=1 ) + + item_data = json.loads(jsonData) + added = self.addPlaylistItem(playlist, item_data, server, userid) + if(added and started == False): + started = True + xbmc.log("Starting Playback Pre") + xbmc.Player().play(playlist) + + if(started == False): + xbmc.log("Starting Playback Post") + xbmc.Player().play(playlist) + + #seek to position + seekTime = 0 + if(startPositionTicks != None): + seekTime = (startPositionTicks / 1000) / 10000 + + if seekTime > 0: + self.seekToPosition(seekTime) + + def AddToPlaylist(self, itemIds): + xbmc.log("== ENTER: PLAYAllItems ==") + + du = DownloadUtils() + userid = du.getUserId() + server = du.getServer() + + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + for itemID in itemIds: + + xbmc.log("Adding Item to Playlist : " + itemID) + item_url = "http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + itemID + "?format=json" + jsonData = du.downloadUrl(item_url, suppress=False, popup=1 ) + + item_data = json.loads(jsonData) + self.addPlaylistItem(playlist, item_data, server, userid) + + return playlist + + def addPlaylistItem(self, playlist, item, server, userid): + + id = item.get("Id") + + playurl = PlayUtils().getPlayUrl(server, id, item) + xbmc.log("Play URL: " + playurl) + api = API() + thumbPath = api.getArtwork(item, "Primary") + listItem = xbmcgui.ListItem(path=playurl, iconImage=thumbPath, thumbnailImage=thumbPath) + self.setListItemProps(server, id, listItem, item) + + # Can not play virtual items + if (item.get("LocationType") == "Virtual") or (item.get("IsPlaceHolder") == True): + + xbmcgui.Dialog().ok(self.language(30128), self.language(30129)) + return False + + else: + + watchedurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id + positionurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayingItems/' + id + + # set the current playing info + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty(playurl + "watchedurl", watchedurl) + WINDOW.setProperty(playurl + "positionurl", positionurl) + + WINDOW.setProperty(playurl + "runtimeticks", str(item.get("RunTimeTicks"))) + WINDOW.setProperty(playurl+"type", item.get("Type")) + WINDOW.setProperty(playurl + "item_id", id) + + if (item.get("Type") == "Episode"): + WINDOW.setProperty(playurl + "refresh_id", item.get("SeriesId")) + else: + WINDOW.setProperty(playurl + "refresh_id", id) + + xbmc.log( "PlayList Item Url : " + str(playurl)) + + playlist.add(playurl, listItem) + + return True + + \ No newline at end of file diff --git a/resources/lib/WebSocketClient.py b/resources/lib/WebSocketClient.py new file mode 100644 index 00000000..89ef2f99 --- /dev/null +++ b/resources/lib/WebSocketClient.py @@ -0,0 +1,262 @@ +################################################################################################# +# WebSocket Client thread +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +import urllib +import socket +import websocket +from ClientInformation import ClientInformation +from DownloadUtils import DownloadUtils +from PlaybackUtils import PlaybackUtils + +_MODE_BASICPLAY=12 + +class WebSocketThread(threading.Thread): + + logLevel = 0 + client = None + keepRunning = True + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("MB3SYNC WebSocketThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + try: + xbmc.log("MB3SYNC WebSocketThread -> " + str(msg)) + except UnicodeEncodeError: + try: + xbmc.log("MB3SYNC WebSocketThread -> " + str(msg.encode('utf-8'))) + except: pass + + ''' + def playbackStarted(self, itemId): + if(self.client != None): + try: + self.logMsg("Sending Playback Started") + messageData = {} + messageData["MessageType"] = "PlaybackStart" + messageData["Data"] = itemId + "|true|audio,video" + messageString = json.dumps(messageData) + self.logMsg("Message Data : " + messageString) + self.client.send(messageString) + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + else: + self.logMsg("Sending Playback Started NO Object ERROR") + ''' + + ''' + def playbackStopped(self, itemId, ticks): + if(self.client != None): + try: + self.logMsg("Sending Playback Stopped") + messageData = {} + messageData["MessageType"] = "PlaybackStopped" + messageData["Data"] = itemId + "|" + str(ticks) + messageString = json.dumps(messageData) + self.client.send(messageString) + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + else: + self.logMsg("Sending Playback Stopped NO Object ERROR") + ''' + + ''' + def sendProgressUpdate(self, itemId, ticks): + if(self.client != None): + try: + self.logMsg("Sending Progress Update") + messageData = {} + messageData["MessageType"] = "PlaybackProgress" + messageData["Data"] = itemId + "|" + str(ticks) + "|false|false" + messageString = json.dumps(messageData) + self.logMsg("Message Data : " + messageString) + self.client.send(messageString) + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + else: + self.logMsg("Sending Progress Update NO Object ERROR") + ''' + + def stopClient(self): + # stopping the client is tricky, first set keep_running to false and then trigger one + # more message by requesting one SessionsStart message, this causes the + # client to receive the message and then exit + if(self.client != None): + self.logMsg("Stopping Client") + self.keepRunning = False + self.client.keep_running = False + self.client.close() + self.logMsg("Stopping Client : KeepRunning set to False") + ''' + try: + self.keepRunning = False + self.client.keep_running = False + self.logMsg("Stopping Client") + self.logMsg("Calling Ping") + self.client.sock.ping() + + self.logMsg("Calling Socket Shutdown()") + self.client.sock.sock.shutdown(socket.SHUT_RDWR) + self.logMsg("Calling Socket Close()") + self.client.sock.sock.close() + self.logMsg("Stopping Client Done") + self.logMsg("Calling Ping") + self.client.sock.ping() + + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + ''' + else: + self.logMsg("Stopping Client NO Object ERROR") + + def on_message(self, ws, message): + self.logMsg("Message : " + str(message)) + result = json.loads(message) + + messageType = result.get("MessageType") + playCommand = result.get("PlayCommand") + data = result.get("Data") + + if(messageType != None and messageType == "Play" and data != None): + itemIds = data.get("ItemIds") + playCommand = data.get("PlayCommand") + + if(playCommand != None and playCommand == "PlayNow"): + + xbmc.executebuiltin("Dialog.Close(all,true)") + startPositionTicks = data.get("StartPositionTicks") + PlaybackUtils().PLAYAllItems(itemIds, startPositionTicks) + xbmc.executebuiltin("XBMC.Notification(Playlist: Added " + str(len(itemIds)) + " items to Playlist,)") + + elif(playCommand != None and playCommand == "PlayNext"): + + playlist = PlaybackUtils().AddToPlaylist(itemIds) + xbmc.executebuiltin("XBMC.Notification(Playlist: Added " + str(len(itemIds)) + " items to Playlist,)") + if(xbmc.Player().isPlaying() == False): + xbmc.Player().play(playlist) + + elif(messageType != None and messageType == "Playstate"): + command = data.get("Command") + if(command != None and command == "Stop"): + self.logMsg("Playback Stopped") + xbmc.executebuiltin('xbmc.activatewindow(10000)') + xbmc.Player().stop() + elif(command != None and command == "Pause"): + self.logMsg("Playback Paused") + xbmc.Player().pause() + elif(command != None and command == "Unpause"): + self.logMsg("Playback UnPaused") + xbmc.Player().pause() + elif(command != None and command == "NextTrack"): + self.logMsg("Playback NextTrack") + xbmc.Player().playnext() + elif(command != None and command == "PreviousTrack"): + self.logMsg("Playback PreviousTrack") + xbmc.Player().playprevious() + elif(command != None and command == "Seek"): + seekPositionTicks = data.get("SeekPositionTicks") + self.logMsg("Playback Seek : " + str(seekPositionTicks)) + seekTime = (seekPositionTicks / 1000) / 10000 + xbmc.Player().seekTime(seekTime) + + + def on_error(self, ws, error): + self.logMsg("Error : " + str(error)) + #raise + + def on_close(self, ws): + self.logMsg("Closed") + + def on_open(self, ws): + + clientInfo = ClientInformation() + machineId = clientInfo.getMachineId() + version = clientInfo.getVersion() + messageData = {} + messageData["MessageType"] = "Identity" + + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + deviceName = addonSettings.getSetting('deviceName') + deviceName = deviceName.replace("\"", "_") + + messageData["Data"] = "Kodi|" + machineId + "|" + version + "|" + deviceName + messageString = json.dumps(messageData) + self.logMsg("Opened : " + str(messageString)) + ws.send(messageString) + + # Set Capabilities + xbmc.log("postcapabilities_called") + downloadUtils = DownloadUtils() + downloadUtils.postcapabilities() + + + def getWebSocketPort(self, host, port): + + userUrl = "http://" + host + ":" + port + "/mediabrowser/System/Info?format=json" + + downloadUtils = DownloadUtils() + jsonData = downloadUtils.downloadUrl(userUrl, suppress=True, popup=1 ) + if(jsonData == ""): + return -1 + + result = json.loads(jsonData) + + wsPort = result.get("WebSocketPortNumber") + if(wsPort != None): + return wsPort + else: + return -1 + + def run(self): + + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + + if(self.logLevel >= 1): + websocket.enableTrace(True) + ''' + wsPort = self.getWebSocketPort(mb3Host, mb3Port); + self.logMsg("WebSocketPortNumber = " + str(wsPort)) + if(wsPort == -1): + self.logMsg("Could not retrieve WebSocket port, can not run WebScoket Client") + return + ''' + # Make a call to /System/Info. WebSocketPortNumber is the port hosting the web socket. + webSocketUrl = "ws://" + mb3Host + ":" + mb3Port + "/mediabrowser" + self.logMsg("WebSocket URL : " + webSocketUrl) + self.client = websocket.WebSocketApp(webSocketUrl, + on_message = self.on_message, + on_error = self.on_error, + on_close = self.on_close) + + self.client.on_open = self.on_open + + while(self.keepRunning): + self.logMsg("Client Starting") + self.client.run_forever() + if(self.keepRunning): + self.logMsg("Client Needs To Restart") + xbmc.sleep(10000) + + self.logMsg("Thread Exited") + + + + \ No newline at end of file diff --git a/resources/settings.xml b/resources/settings.xml index b19448ad..12807cce 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -20,6 +20,8 @@ + + diff --git a/service.py b/service.py index 746617ae..45bde93c 100644 --- a/service.py +++ b/service.py @@ -17,6 +17,7 @@ from LibrarySync import LibrarySync from Player import Player from DownloadUtils import DownloadUtils from ConnectionManager import ConnectionManager +from WebSocketClient import WebSocketThread librarySync = LibrarySync() class Service(): @@ -37,6 +38,8 @@ class Service(): player = Player() lastProgressUpdate = datetime.today() + newWebSocketThread = WebSocketThread() + newWebSocketThread.start() # check kodi library sources mayRun = utils.checkKodiSources() @@ -116,6 +119,9 @@ class Service(): xbmc.log("Not authenticated yet") utils.logMsg("MB3 Sync Service", "stopping Service",0) + + if(newWebSocketThread != None): + newWebSocketThread.stopClient() #start the service