# -*- coding: utf-8 -*- ################################################################################################# import json import logging import threading import websocket import xbmc import clientinfo import downloadutils import librarysync import playlist import userclient import playbackutils from utils import window, settings, dialog, language as lang, JSONRPC from ga_client import log_error ################################################################################################## log = logging.getLogger("EMBY."+__name__) ################################################################################################## class WebSocketClient(threading.Thread): _shared_state = {} _client = None _stop_websocket = False def __init__(self): self.__dict__ = self._shared_state self.monitor = xbmc.Monitor() self.doutils = downloadutils.DownloadUtils() self.client_info = clientinfo.ClientInfo() self.device_id = self.client_info.get_device_id() self.library_sync = librarysync.LibrarySync() threading.Thread.__init__(self) def send_progress_update(self, data): log.debug("sendProgressUpdate") try: message = { 'MessageType': "ReportPlaybackProgress", 'Data': data } message_str = json.dumps(message) self._client.send(message_str) log.debug("Message data: %s", message_str) except Exception as error: log.exception(error) @log_error() def on_message(self, ws, message): result = json.loads(message) message_type = result['MessageType'] if message_type not in ('NotificationAdded', 'SessionEnded', 'RestartRequired', 'PackageInstalling'): # Mute certain events log.info("Message: %s", message) if message_type == 'Play': # A remote control play command has been sent from the server. data = result['Data'] self._play(data) elif message_type == 'Playstate': # A remote control update playstate command has been sent from the server. data = result['Data'] self._playstate(data) elif message_type == "UserDataChanged": # A user changed their personal rating for an item, or their playstate was updated data = result['Data'] userdata_list = data['UserDataList'] self.library_sync.triage_items("userdata", userdata_list) elif message_type == "LibraryChanged": data = result['Data'] self._library_changed(data) elif message_type == "GeneralCommand": data = result['Data'] self._general_commands(data) elif message_type == "ServerRestarting": self._server_restarting() elif message_type == "UserConfigurationUpdated": # Update user data set in userclient data = result['Data'] userclient.UserClient().get_user(data) self.library_sync.refresh_views = True elif message_type == "ServerShuttingDown": # Server went offline window('emby_online', value="false") @classmethod def _play(cls, data): ''' {"Id":"e2c106a953bfc0d9c191a49cda6561de", "ItemIds":["52f0c57e2133e1c11d36c59edcd835fc"], "PlayCommand":"PlayNow","ControllingUserId":"d40000000000000000000000000000000", "SubtitleStreamIndex":3,"AudioStreamIndex":1, "MediaSourceId":"ba83c549ac5c0e4180ae33ebdf813c51"}} ''' item_ids = data['ItemIds'] kwargs = { 'SubtitleStreamIndex': data.get('SubtitleStreamIndex'), 'AudioStreamIndex': data.get('AudioStreamIndex'), 'MediaSourceId': data.get('MediaSourceId') } command = data['PlayCommand'] playlist_ = playlist.Playlist() if command == 'PlayNow': if playbackutils.PlaybackUtils(None, item_ids[0]).play_all(item_ids, data.get('StartPositionTicks', 0), **kwargs): dialog(type_="notification", heading="{emby}", message="%s %s" % (len(item_ids), lang(33004)), icon="{emby}", sound=False) elif command == 'PlayNext': new_playlist = playlist_.modify_playlist(item_ids) dialog(type_="notification", heading="{emby}", message="%s %s" % (len(item_ids), lang(33005)), icon="{emby}", sound=False) player = xbmc.Player() if not player.isPlaying(): # Only start the playlist if nothing is playing player.play(new_playlist) @classmethod def _playstate(cls, data): command = data['Command'] player = xbmc.Player() actions = { 'Stop': player.stop, 'Unpause': player.pause, 'Pause': player.pause, 'PlayPause': player.pause, 'NextTrack': player.playnext, 'PreviousTrack': player.playprevious } if command == 'Seek': if player.isPlaying(): seek_to = data['SeekPositionTicks'] seek_time = seek_to / 10000000.0 player.seekTime(seek_time) log.info("Seek to %s", seek_time) elif command in actions: actions[command]() log.info("Command: %s completed", command) else: log.info("Unknown command: %s", command) return window('emby_command', value="true") def _library_changed(self, data): process_list = { 'added': data['ItemsAdded'], 'update': data['ItemsUpdated'], 'remove': data['ItemsRemoved'] } for action in process_list: self.library_sync.triage_items(action, process_list[action]) @classmethod def _general_commands(cls, data): command = data['Name'] arguments = data['Arguments'] if command in ('Mute', 'Unmute', 'SetVolume', 'SetSubtitleStreamIndex', 'SetAudioStreamIndex', 'SetRepeatMode'): player = xbmc.Player() # These commands need to be reported back if command == 'Mute': xbmc.executebuiltin('Mute') elif command == 'Unmute': xbmc.executebuiltin('Mute') elif command == 'SetVolume': volume = arguments['Volume'] xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume) elif command == 'SetAudioStreamIndex': index = int(arguments['Index']) player.setAudioStream(index - 1) elif command == 'SetRepeatMode': mode = arguments['RepeatMode'] xbmc.executebuiltin('xbmc.PlayerControl(%s)' % mode) elif command == 'SetSubtitleStreamIndex': emby_index = int(arguments['Index']) current_file = player.getPlayingFile() mapping = window('emby_%s.indexMapping.json' % current_file) if emby_index == -1: player.showSubtitles(False) elif mapping: external_index = json.loads(mapping) # If there's external subtitles added via playbackutils for index in external_index: if external_index[index] == emby_index: player.setSubtitleStream(int(index)) break else: # User selected internal subtitles external = len(external_index) audio_tracks = len(player.getAvailableAudioStreams()) player.setSubtitleStream(external + emby_index - audio_tracks - 1) else: # Emby merges audio and subtitle index together audio_tracks = len(player.getAvailableAudioStreams()) player.setSubtitleStream(emby_index - audio_tracks - 1) # Let service know window('emby_command', value="true") elif command == 'DisplayMessage': header = arguments['Header'] text = arguments['Text'] dialog(type_="notification", heading=header, message=text, icon="{emby}", time=int(settings('displayMessage'))*1000) elif command == 'SendString': params = { 'text': arguments['String'], 'done': False } JSONRPC('Input.SendText').execute(params) elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): # Commands that should wake up display actions = { 'MoveUp': "Input.Up", 'MoveDown': "Input.Down", 'MoveRight': "Input.Right", 'MoveLeft': "Input.Left" } JSONRPC(actions[command]).execute() elif command == 'GoHome': JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) elif command == "Guide": JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"}) else: builtin = { 'ToggleFullscreen': 'Action(FullScreen)', 'ToggleOsdMenu': 'Action(OSD)', 'ToggleContextMenu': 'Action(ContextMenu)', 'Select': 'Action(Select)', 'Back': 'Action(back)', 'PageUp': 'Action(PageUp)', 'NextLetter': 'Action(NextLetter)', 'GoToSearch': 'VideoLibrary.Search', 'GoToSettings': 'ActivateWindow(Settings)', 'PageDown': 'Action(PageDown)', 'PreviousLetter': 'Action(PrevLetter)', 'TakeScreenshot': 'TakeScreenshot', 'ToggleMute': 'Mute', 'VolumeUp': 'Action(VolumeUp)', 'VolumeDown': 'Action(VolumeDown)', } if command in builtin: xbmc.executebuiltin(builtin[command]) @classmethod def _server_restarting(cls): if settings('supressRestartMsg') == "true": dialog(type_="notification", heading="{emby}", message=lang(33006), icon="{emby}") window('emby_online', value="false") def on_close(self, ws): log.debug("closed") def on_open(self, ws): self.doutils.post_capabilities(self.device_id) def on_error(self, ws, error): if "10061" in str(error): # Server is offline pass else: log.debug("Error: %s", error) def run(self): # websocket.enableTrace(True) user_id = window('emby_currUser') server = window('emby_server%s' % user_id) token = window('emby_accessToken%s' % user_id) # Get the appropriate prefix for the websocket if "https" in server: server = server.replace('https', "wss") else: server = server.replace('http', "ws") websocket_url = "%s/embywebsocket?api_key=%s&deviceId=%s" % (server, token, self.device_id) log.info("websocket url: %s", websocket_url) self._client = websocket.WebSocketApp(websocket_url, on_message=self.on_message, on_error=self.on_error, on_close=self.on_close) self._client.on_open = self.on_open log.warn("----===## Starting WebSocketClient ##===----") while not self.monitor.abortRequested(): if window('emby_online') == "true": self._client.run_forever(ping_interval=10) if self._stop_websocket: break if self.monitor.waitForAbort(10): # Abort was requested, exit break log.warn("##===---- WebSocketClient Stopped ----===##") def stop_client(self): self._stop_websocket = True if self._client is not None: self._client.close() log.info("Stopping thread")