""" Manages getting playstate from Kodi and sending it to the PMS as well as subscribed Plex Companion clients. """ from logging import getLogger from re import sub from threading import Thread, Lock from downloadutils import DownloadUtils as DU from utils import window, kodi_time_to_millis, Lock_Function from playlist_func import init_Plex_playlist import state import variables as v import json_rpc as js ############################################################################### LOG = getLogger("PLEX." + __name__) # Need to lock all methods and functions messing with subscribers or state LOCK = Lock() LOCKER = Lock_Function(LOCK) ############################################################################### # What is Companion controllable? CONTROLLABLE = { v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' 'skipPrevious,skipNext,stepBack,stepForward', v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' 'subtitleStream,seekTo,skipPrevious,skipNext,' 'stepBack,stepForward' } STREAM_DETAILS = { 'video': 'currentvideostream', 'audio': 'currentaudiostream', 'subtitle': 'currentsubtitle' } class SubscriptionMgr(object): """ Manages Plex companion subscriptions """ def __init__(self, request_mgr, player, mgr): self.serverlist = [] self.subscribers = {} self.info = {} self.container_key = None self.ratingkey = None self.server = "" self.protocol = "http" self.port = "" self.isplaying = False # In order to be able to signal a stop at the end self.last_params = {} self.lastplayers = {} self.xbmcplayer = player self.playqueue = mgr.playqueue self.request_mgr = request_mgr @staticmethod def _headers(): """ Headers are different for Plex Companion! """ return { 'Content-type': 'text/plain', 'Connection': 'Keep-Alive', 'Keep-Alive': 'timeout=20', 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', 'X-Plex-Protocol': "1.0" } def _server_by_host(self, host): if len(self.serverlist) == 1: return self.serverlist[0] for server in self.serverlist: if (server.get('serverName') in host or server.get('server') in host): return server return {} @LOCKER.lockthis def msg(self, players): """ Returns a timeline xml as str (xml containing video, audio, photo player state) """ msg = v.XML_HEADER msg += '\n' % (CONTROLLABLE[ptype], ptype, ptype) playerid = player['playerid'] info = self._player_info(playerid) playqueue = self.playqueue.playqueues[playerid] pos = info['position'] try: playqueue.items[pos] except IndexError: # E.g. for direct path playback for single item return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) LOG.debug('INFO: %s', info) LOG.debug('playqueue: %s', playqueue) status = 'paused' if info['speed'] == '0' else 'playing' ret = ' \n' @LOCKER.lockthis def update_command_id(self, uuid, command_id): """ Updates the Plex Companien client with the machine identifier uuid with command_id """ if command_id and self.subscribers.get(uuid): self.subscribers[uuid].command_id = int(command_id) def notify(self): """ Causes PKC to tell the PMS and Plex Companion players to receive a notification what's being played. """ with LOCK: self._cleanup() # Do we need a check to NOT tell about e.g. PVR/TV and Addon playback? players = js.get_players() # fetch the message, subscribers or not, since the server will need the # info anyway self.isplaying = False msg = self.msg(players) with LOCK: if self.isplaying is True: # If we don't check here, Plex Companion devices will simply # drop out of the Plex Companion playback screen for subscriber in self.subscribers.values(): subscriber.send_update(msg, not players) self._notify_server(players) self.lastplayers = players return True def _notify_server(self, players): for typus, player in players.iteritems(): self._send_pms_notification( player['playerid'], self._get_pms_params(player['playerid'])) try: del self.lastplayers[typus] except KeyError: pass # Process the players we have left (to signal a stop) for _, player in self.lastplayers.iteritems(): self.last_params['state'] = 'stopped' self._send_pms_notification(player['playerid'], self.last_params) def _get_pms_params(self, playerid): info = state.PLAYER_STATES[playerid] status = 'paused' if info['speed'] == '0' else 'playing' params = { 'state': status, 'ratingKey': self.ratingkey, 'key': '/library/metadata/%s' % self.ratingkey, 'time': kodi_time_to_millis(info['time']), 'duration': kodi_time_to_millis(info['totaltime']) } if self.container_key: params['containerKey'] = self.container_key if self.container_key is not None and \ self.container_key.startswith('/playQueues/'): playqueue = self.playqueue.playqueues[playerid] params['playQueueVersion'] = playqueue.version params['playQueueItemID'] = playqueue.id self.last_params = params return params def _send_pms_notification(self, playerid, params): serv = self._server_by_host(self.server) xargs = self._headers() playqueue = self.playqueue.playqueues[playerid] if state.PLEX_TRANSIENT_TOKEN: xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN elif playqueue.plex_transient_token: xargs['X-Plex-Token'] = playqueue.plex_transient_token elif state.PLEX_TOKEN: xargs['X-Plex-Token'] = state.PLEX_TOKEN url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), serv.get('server', 'localhost'), serv.get('port', '32400')) DU().downloadUrl(url, parameters=params, headerOptions=xargs) LOG.debug("Sent server notification with parameters: %s to %s", params, url) @LOCKER.lockthis def add_subscriber(self, protocol, host, port, uuid, command_id): """ Adds a new Plex Companion subscriber to PKC. """ subscriber = Subscriber(protocol, host, port, uuid, command_id, self, self.request_mgr) self.subscribers[subscriber.uuid] = subscriber return subscriber @LOCKER.lockthis def remove_subscriber(self, uuid): """ Removes a connected Plex Companion subscriber with machine identifier uuid from PKC notifications. (Calls the cleanup() method of the subscriber) """ for subscriber in self.subscribers.values(): if subscriber.uuid == uuid or subscriber.host == uuid: subscriber.cleanup() del self.subscribers[subscriber.uuid] def _cleanup(self): for subscriber in self.subscribers.values(): if subscriber.age > 30: subscriber.cleanup() del self.subscribers[subscriber.uuid] class Subscriber(object): """ Plex Companion subscribing device """ def __init__(self, protocol, host, port, uuid, command_id, sub_mgr, request_mgr): self.protocol = protocol or "http" self.host = host self.port = port or 32400 self.uuid = uuid or host self.command_id = int(command_id) or 0 self.navlocationsent = False self.age = 0 self.sub_mgr = sub_mgr self.request_mgr = request_mgr def __eq__(self, other): return self.uuid == other.uuid def cleanup(self): """ Closes the connection to the Plex Companion client """ self.request_mgr.closeConnection(self.protocol, self.host, self.port) def send_update(self, msg, is_nav): """ Sends msg to the Plex Companion client (via .../:/timeline) """ self.age += 1 if not is_nav: self.navlocationsent = False elif self.navlocationsent: return True else: self.navlocationsent = True msg = sub(r"INSERTCOMMANDID", str(self.command_id), msg) LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", self.uuid, self.command_id, msg) url = self.protocol + '://' + self.host + ':' + self.port \ + "/:/timeline" thread = Thread(target=self._threaded_send, args=(url, msg)) thread.start() def _threaded_send(self, url, msg): """ Threaded POST request, because they stall due to PMS response missing the Content-Length header :-( """ response = DU().downloadUrl(url, postBody=msg, action_type="POST") if response in (False, None, 401): self.sub_mgr.remove_subscriber(self.uuid)