diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 15a33f77..08ec3d7b 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -34,13 +34,11 @@ class PlexCompanion(Thread): """ def __init__(self, callback=None): LOG.info("----===## Starting PlexCompanion ##===----") - if callback is not None: - self.mgr = callback + self.mgr = callback # Start GDM for server/client discovery self.client = plexgdm.plexgdm() self.client.clientDetails() - LOG.debug("Registration string is:\n%s", - self.client.getClientDetails()) + LOG.debug("Registration string is:\n%s", self.client.getClientDetails()) # kodi player instance self.player = player.PKC_Player() self.httpd = False @@ -54,14 +52,13 @@ class PlexCompanion(Thread): try: xml[0].attrib except (AttributeError, IndexError, TypeError): - LOG.error('Could not download Plex metadata') + LOG.error('Could not download Plex metadata for: %s', data) return api = API(xml[0]) if api.getType() == v.PLEX_TYPE_ALBUM: LOG.debug('Plex music album detected') - queue = self.mgr.playqueue.init_playqueue_from_plex_children( - api.getRatingKey()) - queue.plex_transient_token = data.get('token') + self.mgr.playqueue.init_playqueue_from_plex_children( + api.getRatingKey(), transient_token=data.get('token')) else: state.PLEX_TRANSIENT_TOKEN = data.get('token') params = { @@ -92,13 +89,7 @@ class PlexCompanion(Thread): @LOCKER.lockthis def _process_playlist(self, data): # Get the playqueue ID - try: - _, container_key, query = ParseContainerKey(data['containerKey']) - except: - LOG.error('Exception while processing') - import traceback - LOG.error("Traceback:\n%s", traceback.format_exc()) - return + _, container_key, query = ParseContainerKey(data['containerKey']) try: playqueue = self.mgr.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) @@ -114,16 +105,12 @@ class PlexCompanion(Thread): api = API(xml[0]) playqueue = self.mgr.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - if playqueue.id == container_key: - # OK, really weird, this happens at least with Plex for Android - LOG.debug('Already know this Plex playQueue, ignoring this command') - else: - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - playqueue_id=container_key, - repeat=query.get('repeat'), - offset=data.get('offset'), - transient_token=data.get('token')) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + playqueue_id=container_key, + repeat=query.get('repeat'), + offset=data.get('offset'), + transient_token=data.get('token')) @LOCKER.lockthis def _process_streams(self, data): @@ -309,5 +296,5 @@ class PlexCompanion(Thread): # Don't sleep continue sleep(50) - self.subscription_manager.signal_stop() + subscription_manager.signal_stop() client.stop_all() diff --git a/resources/lib/companion.py b/resources/lib/companion.py index eccc60c2..7004ff06 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -9,6 +9,7 @@ from variables import ALEXA_TO_COMPANION from playqueue import Playqueue from PlexFunctions import GetPlexKeyNumber import json_rpc as js +import state ############################################################################### @@ -64,6 +65,7 @@ def process_command(request_path, params, queue=None): convert_alexa_to_companion(params) LOG.debug('Received request_path: %s, params: %s', request_path, params) if request_path == 'player/playback/playMedia': + state.PLAYBACK_INIT_DONE = False # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' queue.put({ @@ -71,6 +73,7 @@ def process_command(request_path, params, queue=None): 'data': params }) elif request_path == 'player/playback/refreshPlayQueue': + state.PLAYBACK_INIT_DONE = False queue.put({ 'action': 'refreshPlayQueue', 'data': params @@ -93,10 +96,13 @@ def process_command(request_path, params, queue=None): elif request_path == "player/playback/stepBack": js.smallbackward() elif request_path == "player/playback/skipNext": + state.PLAYBACK_INIT_DONE = False js.skipnext() elif request_path == "player/playback/skipPrevious": + state.PLAYBACK_INIT_DONE = False js.skipprevious() elif request_path == "player/playback/skipTo": + state.PLAYBACK_INIT_DONE = False skip_to(params) elif request_path == "player/navigation/moveUp": js.input_up() diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index c796f74a..51d18680 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -125,7 +125,9 @@ class KodiMonitor(Monitor): LOG.debug("Method: %s Data: %s", method, data) if method == "Player.OnPlay": + state.PLAYBACK_INIT_DONE = False self.PlayBackStart(data) + state.PLAYBACK_INIT_DONE = True elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') @@ -336,6 +338,17 @@ class KodiMonitor(Monitor): kodi_item={'id': kodi_id, 'type': kodi_type, 'file': path}) + # Set the Plex container key (e.g. using the Plex playqueue) + container_key = None + if info['playlistid'] != -1: + # -1 is Kodi's answer if there is no playlist + container_key = self.playqueue.playqueues[playerid].id + if container_key is not None: + container_key = '/playQueues/%s' % container_key + elif plex_id is not None: + container_key = '/library/metadata/%s' % plex_id + state.PLAYER_STATES[playerid]['container_key'] = container_key + LOG.debug('Set the Plex container_key to: %s', container_key) def StartDirectPath(self, plex_id, type, currentFile): """ diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index cdb23cf6..416078ca 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -77,7 +77,7 @@ class Playqueue(Thread): raise ValueError('Wrong playlist type passed in: %s' % typus) return playqueue - def init_playqueue_from_plex_children(self, plex_id): + def init_playqueue_from_plex_children(self, plex_id, transient_token=None): """ Init a new playqueue e.g. from an album. Alexa does this @@ -95,6 +95,7 @@ class Playqueue(Thread): for i, child in enumerate(xml): api = API(child) PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey()) + playqueue.plex_transient_token = transient_token LOG.debug('Firing up Kodi player') Player().play(playqueue.kodi_pl, None, False, 0) return playqueue @@ -114,6 +115,9 @@ class Playqueue(Thread): """ LOG.info('New playqueue %s received from Plex companion with offset ' '%s, repeat %s', playqueue_id, offset, repeat) + # Safe transient token from being deleted + if transient_token is None: + transient_token = playqueue.plex_transient_token with LOCK: xml = PL.get_PMS_playlist(playqueue, playqueue_id) playqueue.clear() @@ -123,7 +127,7 @@ class Playqueue(Thread): LOG.error('Could not get playqueue ID %s', playqueue_id) return playqueue.repeat = 0 if not repeat else int(repeat) - playqueue.token = transient_token + playqueue.plex_transient_token = transient_token PlaybackUtils(xml, playqueue).play_all() window('plex_customplaylist', value="true") if offset not in (None, "0"): diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index b3735a39..a39c9531 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -9,6 +9,7 @@ from urlparse import urlparse, parse_qs from xbmc import sleep from companion import process_command +from utils import window import json_rpc as js from clientinfo import getXArgsDeviceInfo import variables as v @@ -19,6 +20,21 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### +RESOURCES_XML = ('%s\n' + ' \n' + '\n') % (v.XML_HEADER, + v.ADDON_NAME, + v.PLATFORM, + v.ADDON_VERSION) class MyHandler(BaseHTTPRequestHandler): """ @@ -78,94 +94,68 @@ class MyHandler(BaseHTTPRequestHandler): self.serverlist = self.server.client.getServerList() sub_mgr = self.server.subscription_manager - try: - request_path = self.path[1:] - request_path = sub(r"\?.*", "", request_path) - url = urlparse(self.path) - paramarrays = parse_qs(url.query) - params = {} - for key in paramarrays: - params[key] = paramarrays[key][0] - LOG.debug("remote request_path: %s", request_path) - LOG.debug("params received from remote: %s", params) - sub_mgr.update_command_id(self.headers.get( - 'X-Plex-Client-Identifier', - self.client_address[0]), - params.get('commandID', False)) - if request_path == "version": - self.response( - "PlexKodiConnect Plex Companion: Running\nVersion: %s" - % v.ADDON_VERSION) - elif request_path == "verify": - self.response("XBMC JSON connection test:\n" + - js.ping()) - elif request_path == 'resources': - resp = ('%s' - '' - '' - '' - % (v.XML_HEADER, - v.DEVICENAME, - v.PKC_MACHINE_IDENTIFIER, - v.PLATFORM, - v.ADDON_VERSION)) - LOG.debug("crafted resources response: %s", resp) - self.response(resp, getXArgsDeviceInfo(include_token=False)) - elif "/poll" in request_path: - if params.get('wait', False) == '1': - sleep(950) - command_id = params.get('commandID', 0) - self.response( - sub(r"INSERTCOMMANDID", - str(command_id), - sub_mgr.msg(js.get_players())), - { - 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, - 'X-Plex-Protocol': '1.0', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Max-Age': '1209600', - 'Access-Control-Expose-Headers': - 'X-Plex-Client-Identifier', - 'Content-Type': 'text/xml;charset=utf-8' - }) - elif "/subscribe" in request_path: - self.response(v.COMPANION_OK_MESSAGE, - getXArgsDeviceInfo(include_token=False)) - protocol = params.get('protocol', False) - host = self.client_address[0] - port = params.get('port', False) - uuid = self.headers.get('X-Plex-Client-Identifier', "") - command_id = params.get('commandID', 0) - sub_mgr.add_subscriber(protocol, - host, - port, - uuid, - command_id) - elif "/unsubscribe" in request_path: - self.response(v.COMPANION_OK_MESSAGE, - getXArgsDeviceInfo(include_token=False)) - uuid = self.headers.get('X-Plex-Client-Identifier', False) \ - or self.client_address[0] - sub_mgr.remove_subscriber(uuid) - else: - # Throw it to companion.py - process_command(request_path, params, self.server.queue) - self.response('', getXArgsDeviceInfo(include_token=False)) - sub_mgr.notify() - except: - LOG.error('Error encountered. Traceback:') - import traceback - LOG.error(traceback.print_exc()) + request_path = self.path[1:] + request_path = sub(r"\?.*", "", request_path) + url = urlparse(self.path) + paramarrays = parse_qs(url.query) + params = {} + for key in paramarrays: + params[key] = paramarrays[key][0] + LOG.debug("remote request_path: %s", request_path) + LOG.debug("params received from remote: %s", params) + sub_mgr.update_command_id(self.headers.get( + 'X-Plex-Client-Identifier', self.client_address[0]), + params.get('commandID')) + if request_path == "version": + self.response( + "PlexKodiConnect Plex Companion: Running\nVersion: %s" + % v.ADDON_VERSION) + elif request_path == "verify": + self.response("XBMC JSON connection test:\n" + js.ping()) + elif request_path == 'resources': + self.response( + RESOURCES_XML.format( + title=v.DEVICENAME, + machineIdentifier=window('plex_machineIdentifier')), + getXArgsDeviceInfo(include_token=False)) + elif "/poll" in request_path: + if params.get('wait') == '1': + sleep(950) + self.response( + sub_mgr.msg(js.get_players()).format( + command_id=params.get('commandID', 0)), + { + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'X-Plex-Protocol': '1.0', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Max-Age': '1209600', + 'Access-Control-Expose-Headers': + 'X-Plex-Client-Identifier', + 'Content-Type': 'text/xml;charset=utf-8' + }) + elif "/subscribe" in request_path: + self.response(v.COMPANION_OK_MESSAGE, + getXArgsDeviceInfo(include_token=False)) + protocol = params.get('protocol') + host = self.client_address[0] + port = params.get('port') + uuid = self.headers.get('X-Plex-Client-Identifier') + command_id = params.get('commandID', 0) + sub_mgr.add_subscriber(protocol, + host, + port, + uuid, + command_id) + elif "/unsubscribe" in request_path: + self.response(v.COMPANION_OK_MESSAGE, + getXArgsDeviceInfo(include_token=False)) + uuid = self.headers.get('X-Plex-Client-Identifier') \ + or self.client_address[0] + sub_mgr.remove_subscriber(uuid) + else: + # Throw it to companion.py + process_command(request_path, params, self.server.queue) + self.response('', getXArgsDeviceInfo(include_token=False)) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 3876d73d..afab85c3 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -3,12 +3,10 @@ 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 threading import Thread, RLock 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 @@ -17,19 +15,19 @@ import json_rpc as js LOG = getLogger("PLEX." + __name__) # Need to lock all methods and functions messing with subscribers or state -LOCK = Lock() +LOCK = RLock() 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' + v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' + 'subtitleStream,seekTo,skipPrevious,skipNext,' + 'stepBack,stepForward', + v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' + 'skipPrevious,skipNext,stepBack,stepForward', + v.PLEX_PLAYLIST_TYPE_PHOTO: 'skipPrevious,skipNext,stop' } STREAM_DETAILS = { @@ -38,6 +36,24 @@ STREAM_DETAILS = { 'subtitle': 'currentsubtitle' } +XML = ('%s\n' + ' \n' + ' \n' + ' \n' + '\n') % (v.XML_HEADER, + v.PLEX_PLAYLIST_TYPE_VIDEO, + v.PLEX_PLAYLIST_TYPE_AUDIO, + v.PLEX_PLAYLIST_TYPE_PHOTO) + + +def update_player_info(playerid): + """ + Updates all player info for playerid [int] in state.py. + """ + state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) + state.PLAYER_STATES[playerid]['volume'] = js.get_volume() + state.PLAYER_STATES[playerid]['muted'] = js.get_muted() + class SubscriptionMgr(object): """ @@ -47,8 +63,6 @@ class SubscriptionMgr(object): self.serverlist = [] self.subscribers = {} self.info = {} - self.container_key = None - self.ratingkey = None self.server = "" self.protocol = "http" self.port = "" @@ -90,18 +104,126 @@ class SubscriptionMgr(object): 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): """ @@ -241,28 +256,27 @@ class SubscriptionMgr(object): if command_id and self.subscribers.get(uuid): self.subscribers[uuid].command_id = int(command_id) + @LOCKER.lockthis 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? + self._cleanup() + # Get all the active/playing Kodi players (video, audio, pictures) 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: + # Update the PKC info with what's playing on the Kodi side + for player in players.values(): + update_player_info(player['playerid']) + if self.subscribers and state.PLAYBACK_INIT_DONE is True: + msg = self.msg(players) 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 + self._notify_server(players) + self.lastplayers = players def _notify_server(self, players): for typus, player in players.iteritems(): @@ -273,7 +287,7 @@ class SubscriptionMgr(object): except KeyError: pass # Process the players we have left (to signal a stop) - for _, player in self.lastplayers.iteritems(): + for player in self.lastplayers.values(): self.last_params['state'] = 'stopped' self._send_pms_notification(player['playerid'], self.last_params) @@ -282,18 +296,17 @@ class SubscriptionMgr(object): status = 'paused' if info['speed'] == '0' else 'playing' params = { 'state': status, - 'ratingKey': self.ratingkey, - 'key': '/library/metadata/%s' % self.ratingkey, + 'ratingKey': info['plex_id'], + 'key': '/library/metadata/%s' % info['plex_id'], '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 + if info['container_key'] is not None: + params['containerKey'] = info['container_key'] + if info['container_key'].startswith('/playQueues/'): + playqueue = self.playqueue.playqueues[playerid] + params['playQueueVersion'] = playqueue.version + params['playQueueItemID'] = playqueue.id self.last_params = params return params @@ -384,11 +397,10 @@ class Subscriber(object): return True else: self.navlocationsent = True - msg = sub(r"INSERTCOMMANDID", str(self.command_id), msg) + msg = msg.format(command_id=self.command_id) 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" + url = '%s://%s:%s/:/timeline' % (self.protocol, self.host, self.port) thread = Thread(target=self._threaded_send, args=(url, msg)) thread.start() diff --git a/resources/lib/state.py b/resources/lib/state.py index a115f5bc..4c675e8d 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -106,6 +106,7 @@ PLAYER_STATES = { 'kodi_type': None, 'plex_id': None, 'plex_type': None, + 'container_key': None, 'volume': 100, 'muted': False }, @@ -116,6 +117,10 @@ PLAYER_STATES = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} +# Set to False after having received a Companion command to play something +# Set to True after Kodi monitor PlayBackStart is done +# This will prohibit "old" Plex Companion messages being sent +PLAYBACK_INIT_DONE = True # Kodi webserver details WEBSERVER_PORT = 8080 diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 2284f9c0..086a3ec0 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -129,6 +129,20 @@ PLEX_TYPE_MUSICVIDEO = 'musicvideo' PLEX_TYPE_PHOTO = 'photo' +# Used for /:/timeline XML messages +PLEX_PLAYLIST_TYPE_VIDEO = 'video' +PLEX_PLAYLIST_TYPE_AUDIO = 'music' +PLEX_PLAYLIST_TYPE_PHOTO = 'photo' + +KODI_PLAYLIST_TYPE_VIDEO = 'video' +KODI_PLAYLIST_TYPE_AUDIO = 'audio' +KODI_PLAYLIST_TYPE_PHOTO = 'picture' + +KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE = { + PLEX_PLAYLIST_TYPE_VIDEO: KODI_PLAYLIST_TYPE_VIDEO, + PLEX_PLAYLIST_TYPE_AUDIO: KODI_PLAYLIST_TYPE_AUDIO, + PLEX_PLAYLIST_TYPE_PHOTO: KODI_PLAYLIST_TYPE_PHOTO +} # All the Kodi types as e.g. used in the JSON API KODI_TYPE_VIDEO = 'video'