From 4547ec52af8c248cb93fdbd266a4e6294c29d987 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 21 Dec 2017 09:28:06 +0100 Subject: [PATCH] Major Plex Companion overhaul, part 4 --- resources/lib/PlexCompanion.py | 236 +++++++++++---------- resources/lib/companion.py | 2 +- resources/lib/json_rpc.py | 15 +- resources/lib/kodimonitor.py | 109 ++++++++-- resources/lib/playlist_func.py | 77 +++---- resources/lib/playqueue.py | 26 ++- resources/lib/plexbmchelper/httppersist.py | 32 +-- resources/lib/plexbmchelper/subscribers.py | 103 +++++---- resources/lib/plexdb_functions.py | 3 +- resources/lib/utils.py | 2 +- 10 files changed, 365 insertions(+), 240 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 89fd4b68..159ccc2d 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -11,6 +11,7 @@ from xbmc import sleep, executebuiltin from utils import settings, thread_methods from plexbmchelper import listener, plexgdm, subscribers, httppersist +from plexbmchelper.subscribers import LOCKER from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexAPI import API from playlist_func import get_pms_playqueue, get_plextype_from_xml @@ -44,8 +45,127 @@ class PlexCompanion(Thread): self.player = player.PKC_Player() self.httpd = False self.queue = None + self.subscription_manager = None Thread.__init__(self) + @LOCKER.lockthis + def _process_alexa(self, data): + xml = GetPlexMetadata(data['key']) + try: + xml[0].attrib + except (AttributeError, IndexError, TypeError): + LOG.error('Could not download Plex metadata') + 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') + else: + state.PLEX_TRANSIENT_TOKEN = data.get('token') + params = { + 'mode': 'plex_node', + 'key': '{server}%s' % data.get('key'), + 'view_offset': data.get('offset'), + 'play_directly': 'true', + 'node': 'false' + } + executebuiltin('RunPlugin(plugin://%s?%s)' + % (v.ADDON_ID, urlencode(params))) + + @staticmethod + def _process_node(data): + """ + E.g. watch later initiated by Companion. Basically navigating Plex + """ + state.PLEX_TRANSIENT_TOKEN = data.get('key') + params = { + 'mode': 'plex_node', + 'key': '{server}%s' % data.get('key'), + 'view_offset': data.get('offset'), + 'play_directly': 'true' + } + executebuiltin('RunPlugin(plugin://%s?%s)' + % (v.ADDON_ID, urlencode(params))) + + @LOCKER.lockthis + def _process_playlist(self, data): + # Get the playqueue ID + try: + _, plex_id, query = ParseContainerKey(data['containerKey']) + except: + LOG.error('Exception while processing') + import traceback + LOG.error("Traceback:\n%s", traceback.format_exc()) + return + try: + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) + except KeyError: + # E.g. Plex web does not supply the media type + # Still need to figure out the type (video vs. music vs. pix) + xml = GetPlexMetadata(data['key']) + try: + xml[0].attrib + except (AttributeError, IndexError, TypeError): + LOG.error('Could not download Plex metadata') + return + api = API(xml[0]) + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + plex_id, + repeat=query.get('repeat'), + offset=data.get('offset')) + playqueue.plex_transient_token = data.get('key') + + @LOCKER.lockthis + def _process_streams(self, data): + """ + Plex Companion client adjusted audio or subtitle stream + """ + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) + pos = js.get_position(playqueue.playlistid) + if 'audioStreamID' in data: + index = playqueue.items[pos].kodi_stream_index( + data['audioStreamID'], 'audio') + self.player.setAudioStream(index) + elif 'subtitleStreamID' in data: + if data['subtitleStreamID'] == '0': + self.player.showSubtitles(False) + else: + index = playqueue.items[pos].kodi_stream_index( + data['subtitleStreamID'], 'subtitle') + self.player.setSubtitleStream(index) + else: + LOG.error('Unknown setStreams command: %s', data) + + @LOCKER.lockthis + def _process_refresh(self, data): + """ + example data: {'playQueueID': '8475', 'commandID': '11'} + """ + xml = get_pms_playqueue(data['playQueueID']) + if xml is None: + return + if len(xml) == 0: + LOG.debug('Empty playqueue received - clearing playqueue') + plex_type = get_plextype_from_xml(xml) + if plex_type is None: + return + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) + playqueue.clear() + return + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + data['playQueueID']) + def _process_tasks(self, task): """ Processes tasks picked up e.g. by Companion listener, e.g. @@ -63,128 +183,25 @@ class PlexCompanion(Thread): """ LOG.debug('Processing: %s', task) data = task['data'] - - # Get the token of the user flinging media (might be different one) - token = data.get('token') if task['action'] == 'alexa': - # e.g. Alexa - xml = GetPlexMetadata(data['key']) - try: - xml[0].attrib - except (AttributeError, IndexError, TypeError): - LOG.error('Could not download Plex metadata') - 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 = token - else: - state.PLEX_TRANSIENT_TOKEN = token - params = { - 'mode': 'plex_node', - 'key': '{server}%s' % data.get('key'), - 'view_offset': data.get('offset'), - 'play_directly': 'true', - 'node': 'false' - } - executebuiltin('RunPlugin(plugin://%s?%s)' - % (v.ADDON_ID, urlencode(params))) - + self._process_alexa(data) elif (task['action'] == 'playlist' and data.get('address') == 'node.plexapp.com'): - # E.g. watch later initiated by Companion - state.PLEX_TRANSIENT_TOKEN = token - params = { - 'mode': 'plex_node', - 'key': '{server}%s' % data.get('key'), - 'view_offset': data.get('offset'), - 'play_directly': 'true' - } - executebuiltin('RunPlugin(plugin://%s?%s)' - % (v.ADDON_ID, urlencode(params))) - + self._process_node(data) elif task['action'] == 'playlist': - # Get the playqueue ID - try: - _, plex_id, query = ParseContainerKey(data['containerKey']) - except: - LOG.error('Exception while processing') - import traceback - LOG.error("Traceback:\n%s", traceback.format_exc()) - return - try: - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) - except KeyError: - # E.g. Plex web does not supply the media type - # Still need to figure out the type (video vs. music vs. pix) - xml = GetPlexMetadata(data['key']) - try: - xml[0].attrib - except (AttributeError, IndexError, TypeError): - LOG.error('Could not download Plex metadata') - return - api = API(xml[0]) - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - plex_id, - repeat=query.get('repeat'), - offset=data.get('offset')) - playqueue.plex_transient_token = token - + self._process_playlist(data) elif task['action'] == 'refreshPlayQueue': - # example data: {'playQueueID': '8475', 'commandID': '11'} - xml = get_pms_playqueue(data['playQueueID']) - if xml is None: - return - if len(xml) == 0: - LOG.debug('Empty playqueue received - clearing playqueue') - plex_type = get_plextype_from_xml(xml) - if plex_type is None: - return - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) - playqueue.clear() - return - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - data['playQueueID']) - + self._process_refresh(data) elif task['action'] == 'setStreams': - # Plex Companion client adjusted audio or subtitle stream - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) - pos = js.get_position(playqueue.playlistid) - if 'audioStreamID' in data: - index = playqueue.items[pos].kodi_stream_index( - data['audioStreamID'], 'audio') - self.player.setAudioStream(index) - elif 'subtitleStreamID' in data: - if data['subtitleStreamID'] == '0': - self.player.showSubtitles(False) - else: - index = playqueue.items[pos].kodi_stream_index( - data['subtitleStreamID'], 'subtitle') - self.player.setSubtitleStream(index) - else: - LOG.error('Unknown setStreams command: %s', task) + self._process_streams(data) def run(self): """ - Ensure that - - STOP sent to PMS - - sockets will be closed no matter what + Ensure that sockets will be closed no matter what """ try: self._run() finally: - self.subscription_manager.signal_stop() try: self.httpd.socket.shutdown(SHUT_RDWR) except AttributeError: @@ -288,4 +305,5 @@ class PlexCompanion(Thread): # Don't sleep continue sleep(50) + self.subscription_manager.signal_stop() client.stop_all() diff --git a/resources/lib/companion.py b/resources/lib/companion.py index 08db67c6..19f78fa9 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -58,7 +58,7 @@ def process_command(request_path, params, queue=None): if params.get('deviceName') == 'Alexa': convert_alexa_to_companion(params) LOG.debug('Received request_path: %s, params: %s', request_path, params) - if "/playMedia" in request_path: + if request_path == 'player/playback/playMedia': # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' queue.put({ diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index a8fa0f94..727ff9c5 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -247,20 +247,23 @@ def input_sendtext(text): return JsonRPC("Input.SendText").execute({'test': text, 'done': False}) -def playlist_get_items(playlistid, properties): +def playlist_get_items(playlistid): """ playlistid: [int] id of the Kodi playlist - properties: [list] of strings for the properties to return - e.g. 'title', 'file' Returns a list of Kodi playlist items as dicts with the keys specified in properties. Or an empty list if unsuccessful. Example: - [{u'title': u'3 Idiots', u'type': u'movie', u'id': 3, u'file': - u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label': u'3 Idiots'}] + [ + { + u'file':u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', + u'title': u'3 Idiots', + u'type': u'movie', # IF possible! Else key missing + u'id': 3, # IF possible! Else key missing + u'label': u'3 Idiots'}] """ reply = JsonRPC('Playlist.GetItems').execute({ 'playlistid': playlistid, - 'properties': properties + 'properties': ['title', 'file'] }) try: reply = reply['result']['items'] diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 555740f5..48974327 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -10,8 +10,10 @@ import plexdb_functions as plexdb from utils import window, settings, CatchExceptions, plex_command from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename +from plexbmchelper.subscribers import LOCKER from PlexAPI import API import json_rpc as js +import playlist_func as PL import state import variables as v @@ -124,17 +126,20 @@ class KodiMonitor(Monitor): if method == "Player.OnPlay": self.PlayBackStart(data) - elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') pass - + elif method == 'Playlist.OnAdd': + self._playlist_onadd(data) + elif method == 'Playlist.OnRemove': + self._playlist_onremove(data) + elif method == 'Playlist.OnClear': + self._playlist_onclear(data) elif method == "VideoLibrary.OnUpdate": # Manually marking as watched/unwatched playcount = data.get('playcount') item = data.get('item') - try: kodiid = item['id'] item_type = item['type'] @@ -161,30 +166,84 @@ class KodiMonitor(Monitor): scrobble(itemid, 'watched') else: scrobble(itemid, 'unwatched') - elif method == "VideoLibrary.OnRemove": pass - elif method == "System.OnSleep": # Connection is going to sleep LOG.info("Marking the server as offline. SystemOnSleep activated.") window('plex_online', value="sleep") - elif method == "System.OnWake": # Allow network to wake up sleep(10000) window('plex_onWake', value="true") window('plex_online', value="false") - elif method == "GUI.OnScreensaverDeactivated": if settings('dbSyncScreensaver') == "true": sleep(5000) plex_command('RUN_LIB_SCAN', 'full') - elif method == "System.OnQuit": LOG.info('Kodi OnQuit detected - shutting down') state.STOP_PKC = True + @LOCKER.lockthis + def _playlist_onadd(self, data): + """ + Called if an item is added to a Kodi playlist. Example data dict: + { + u'item': { + u'type': u'movie', + u'id': 2}, + u'playlistid': 1, + u'position': 0 + } + Will NOT be called if playback initiated by Kodi widgets + """ + playqueue = self.playqueue.playqueues[data['playlistid']] + # Check whether we even need to update our known playqueue + kodi_playqueue = js.playlist_get_items(data['playlistid']) + if playqueue.old_kodi_pl == kodi_playqueue: + # We already know the latest playqueue (e.g. because Plex + # initiated playback) + return + # Playlist has been updated; need to tell Plex about it + if playqueue.id is None: + PL.init_Plex_playlist(playqueue, kodi_item=data['item']) + else: + PL.add_item_to_PMS_playlist(playqueue, + data['position'], + kodi_item=data['item']) + # Make sure that we won't re-add this item + playqueue.old_kodi_pl = kodi_playqueue + + @LOCKER.lockthis + def _playlist_onremove(self, data): + """ + Called if an item is removed from a Kodi playlist. Example data dict: + { + u'playlistid': 1, + u'position': 0 + } + """ + playqueue = self.playqueue.playqueues[data['playlistid']] + # Check whether we even need to update our known playqueue + kodi_playqueue = js.playlist_get_items(data['playlistid']) + if playqueue.old_kodi_pl == kodi_playqueue: + # We already know the latest playqueue - nothing to do + return + PL.delete_playlist_item_from_PMS(playqueue, data['position']) + playqueue.old_kodi_pl = kodi_playqueue + + @LOCKER.lockthis + def _playlist_onclear(self, data): + """ + Called if a Kodi playlist is cleared. Example data dict: + { + u'playlistid': 1, + } + """ + self.playqueue.playqueues[data['playlistid']].clear() + + @LOCKER.lockthis def PlayBackStart(self, data): """ Called whenever playback is started. Example data: @@ -192,8 +251,7 @@ class KodiMonitor(Monitor): u'item': {u'type': u'movie', u'title': u''}, u'player': {u'playerid': 1, u'speed': 1} } - Unfortunately VERY random inputs! - E.g. when using Widgets, Kodi doesn't tell us shit + Unfortunately when using Widgets, Kodi doesn't tell us shit """ # Get the type of media we're playing try: @@ -237,16 +295,37 @@ class KodiMonitor(Monitor): except TypeError: # No plex id, hence item not in the library. E.g. clips pass - state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) - state.PLAYER_STATES[playerid]['file'] = json_data['file'] + info = js.get_player_props(playerid) + state.PLAYER_STATES[playerid].update(info) + state.PLAYER_STATES[playerid]['file'] = path state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type state.PLAYER_STATES[playerid]['plex_id'] = plex_id state.PLAYER_STATES[playerid]['plex_type'] = plex_type - # Set other stuff like volume - state.PLAYER_STATES[playerid]['volume'] = js.get_volume() - state.PLAYER_STATES[playerid]['muted'] = js.get_muted() LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) + # Check whether we need to init our playqueues (e.g. direct play) + init = False + playqueue = self.playqueue.playqueues[playerid] + try: + playqueue.items[info['position']] + except IndexError: + init = True + if init is False and plex_id is not None: + if plex_id != playqueue.items[ + state.PLAYER_STATES[playerid]['position']].id: + init = True + elif init is False and path != playqueue.items[ + state.PLAYER_STATES[playerid]['position']].file: + init = True + if init is True: + LOG.debug('Need to initialize Plex and PKC playqueue') + if plex_id: + PL.init_Plex_playlist(playqueue, plex_id=plex_id) + else: + PL.init_Plex_playlist(playqueue, + kodi_item={'id': kodi_id, + 'type': kodi_type, + 'file': path}) def StartDirectPath(self, plex_id, type, currentFile): """ diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index f5a39e96..a77b4796 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -25,22 +25,23 @@ REGEX = re_compile(r'''metadata%2F(\d+)''') # {u'type': u'movie', u'id': 3, 'file': path-to-file} -class Playlist_Object_Baseclase(object): +class PlaylistObjectBaseclase(object): """ Base class """ - playlistid = None - type = None - kodi_pl = None - items = [] - old_kodi_pl = [] - id = None - version = None - selectedItemID = None - selectedItemOffset = None - shuffled = 0 - repeat = 0 - plex_transient_token = None + def __init__(self): + self.playlistid = None + self.type = None + self.kodi_pl = None + self.items = [] + self.old_kodi_pl = [] + self.id = None + self.version = None + self.selectedItemID = None + self.selectedItemOffset = None + self.shuffled = 0 + self.repeat = 0 + self.plex_transient_token = None def __repr__(self): """ @@ -76,14 +77,14 @@ class Playlist_Object_Baseclase(object): LOG.debug('Playlist cleared: %s', self) -class Playlist_Object(Playlist_Object_Baseclase): +class Playlist_Object(PlaylistObjectBaseclase): """ To be done for synching Plex playlists to Kodi """ kind = 'playList' -class Playqueue_Object(Playlist_Object_Baseclase): +class Playqueue_Object(PlaylistObjectBaseclase): """ PKC object to represent PMS playQueues and Kodi playlist for queueing @@ -114,27 +115,27 @@ class Playlist_Item(object): id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID plex_id = None [str] Plex unique item id, "ratingKey" plex_type = None [str] Plex type, e.g. 'movie', 'clip' - plex_UUID = None [str] Plex librarySectionUUID + plex_uuid = None [str] Plex librarySectionUUID kodi_id = None Kodi unique kodi id (unique only within type!) kodi_type = None [str] Kodi type: 'movie' file = None [str] Path to the item's file. STRING!! - uri = None [str] Weird Plex uri path involving plex_UUID. STRING! + uri = None [str] Weird Plex uri path involving plex_uuid. STRING! guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below """ - id = None - plex_id = None - plex_type = None - plex_UUID = None - kodi_id = None - kodi_type = None - file = None - uri = None - guid = None - xml = None - - # Yet to be implemented: handling of a movie with several parts - part = 0 + def __init__(self): + self.id = None + self.plex_id = None + self.plex_type = None + self.plex_uuid = None + self.kodi_id = None + self.kodi_type = None + self.file = None + self.uri = None + self.guid = None + self.xml = None + # Yet to be implemented: handling of a movie with several parts + self.part = 0 def __repr__(self): """ @@ -201,7 +202,7 @@ def playlist_item_from_kodi(kodi_item): try: item.plex_id = plex_dbitem[0] item.plex_type = plex_dbitem[2] - item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-) + item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-) except TypeError: pass item.file = kodi_item.get('file') @@ -214,7 +215,7 @@ def playlist_item_from_kodi(kodi_item): else: # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % - (item.plex_UUID, item.plex_id)) + (item.plex_uuid, item.plex_id)) LOG.debug('Made playlist item from Kodi: %s', item) return item @@ -233,11 +234,11 @@ def playlist_item_from_plex(plex_id): item.plex_type = plex_dbitem[5] item.kodi_id = plex_dbitem[0] item.kodi_type = plex_dbitem[4] - except: + except (TypeError, IndexError): raise KeyError('Could not find plex_id %s in database' % plex_id) - item.plex_UUID = plex_id + item.plex_uuid = plex_id item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % - (item.plex_UUID, plex_id)) + (item.plex_uuid, plex_id)) LOG.debug('Made playlist item from plex: %s', item) return item @@ -335,6 +336,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): Returns True if successful, False otherwise """ LOG.debug('Initializing the playlist %s on the Plex side', playlist) + playlist.clear() try: if plex_id: item = playlist_item_from_plex(plex_id) @@ -349,13 +351,14 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): action_type="POST", parameters=params) get_playlist_details_from_xml(playlist, xml) - item.xml = xml[0] + # Need to get the details for the playlist item + item = playlist_item_from_xml(playlist, xml[0]) except (KeyError, IndexError, TypeError): LOG.error('Could not init Plex playlist with plex_id %s and ' 'kodi_item %s', plex_id, kodi_item) return False playlist.items.append(item) - LOG.debug('Initialized the playlist on the Plex side: %s' % playlist) + LOG.debug('Initialized the playlist on the Plex side: %s', playlist) return True diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index cf6a5250..8afdd3e2 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -206,8 +206,7 @@ class Playqueue(Thread): LOG.info("----===## Starting PlayQueue client ##===----") # Initialize the playqueues, if Kodi already got items in them for playqueue in self.playqueues: - for i, item in enumerate(js.playlist_get_items( - playqueue.id, ["title", "file"])): + for i, item in enumerate(js.playlist_get_items(playqueue.id)): if i == 0: PL.init_Plex_playlist(playqueue, kodi_item=item) else: @@ -217,17 +216,16 @@ class Playqueue(Thread): if thread_stopped(): break sleep(1000) - with LOCK: - for playqueue in self.playqueues: - kodi_playqueue = js.playlist_get_items(playqueue.id, - ["title", "file"]) - if playqueue.old_kodi_pl != kodi_playqueue: - # compare old and new playqueue - self._compare_playqueues(playqueue, kodi_playqueue) - playqueue.old_kodi_pl = list(kodi_playqueue) - # Still sleep a bit so Kodi does not become - # unresponsive - sleep(10) - continue + # with LOCK: + # for playqueue in self.playqueues: + # kodi_playqueue = js.playlist_get_items(playqueue.id) + # if playqueue.old_kodi_pl != kodi_playqueue: + # # compare old and new playqueue + # self._compare_playqueues(playqueue, kodi_playqueue) + # playqueue.old_kodi_pl = list(kodi_playqueue) + # # Still sleep a bit so Kodi does not become + # # unresponsive + # sleep(10) + # continue sleep(200) LOG.info("----===## PlayQueue client stopped ##===----") diff --git a/resources/lib/plexbmchelper/httppersist.py b/resources/lib/plexbmchelper/httppersist.py index e765ae9e..2d65bb60 100644 --- a/resources/lib/plexbmchelper/httppersist.py +++ b/resources/lib/plexbmchelper/httppersist.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger import httplib import traceback import string @@ -7,7 +7,7 @@ from socket import error as socket_error ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -17,20 +17,20 @@ class RequestMgr: self.conns = {} def getConnection(self, protocol, host, port): - conn = self.conns.get(protocol+host+str(port), False) + conn = self.conns.get(protocol + host + str(port), False) if not conn: if protocol == "https": conn = httplib.HTTPSConnection(host, port) else: conn = httplib.HTTPConnection(host, port) - self.conns[protocol+host+str(port)] = conn + self.conns[protocol + host + str(port)] = conn return conn def closeConnection(self, protocol, host, port): - conn = self.conns.get(protocol+host+str(port), False) + conn = self.conns.get(protocol + host + str(port), False) if conn: conn.close() - self.conns.pop(protocol+host+str(port), None) + self.conns.pop(protocol + host + str(port), None) def dumpConnections(self): for conn in self.conns.values(): @@ -45,7 +45,7 @@ class RequestMgr: conn.request("POST", path, body, header) data = conn.getresponse() if int(data.status) >= 400: - log.error("HTTP response error: %s" % str(data.status)) + LOG.error("HTTP response error: %s" % str(data.status)) # this should return false, but I'm hacking it since iOS # returns 404 no matter what return data.read() or True @@ -56,14 +56,14 @@ class RequestMgr: if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED): pass else: - log.error("Unable to connect to %s\nReason:" % host) - log.error(traceback.print_exc()) - self.conns.pop(protocol+host+str(port), None) + LOG.error("Unable to connect to %s\nReason:" % host) + LOG.error(traceback.print_exc()) + self.conns.pop(protocol + host + str(port), None) if conn: conn.close() return False except Exception as e: - log.error("Exception encountered: %s" % e) + LOG.error("Exception encountered: %s", e) # Close connection just in case try: conn.close() @@ -76,7 +76,7 @@ class RequestMgr: newpath = path + '?' pairs = [] for key in params: - pairs.append(str(key)+'='+str(params[key])) + pairs.append(str(key) + '=' + str(params[key])) newpath += string.join(pairs, '&') return self.get(host, port, newpath, header, protocol) @@ -87,7 +87,7 @@ class RequestMgr: conn.request("GET", path, headers=header) data = conn.getresponse() if int(data.status) >= 400: - log.error("HTTP response error: %s" % str(data.status)) + LOG.error("HTTP response error: %s", str(data.status)) return False else: return data.read() or True @@ -96,8 +96,8 @@ class RequestMgr: if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED): pass else: - log.error("Unable to connect to %s\nReason:" % host) - log.error(traceback.print_exc()) - self.conns.pop(protocol+host+str(port), None) + LOG.error("Unable to connect to %s\nReason:", host) + LOG.error(traceback.print_exc()) + self.conns.pop(protocol + host + str(port), None) conn.close() return False diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 2592f689..3876d73d 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -4,10 +4,11 @@ subscribed Plex Companion clients. """ from logging import getLogger from re import sub -from threading import Thread, RLock +from threading import Thread, Lock from downloadutils import DownloadUtils as DU -from utils import window, kodi_time_to_millis +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 @@ -15,6 +16,9 @@ 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) ############################################################################### @@ -48,6 +52,7 @@ class SubscriptionMgr(object): 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 = {} @@ -79,6 +84,7 @@ class SubscriptionMgr(object): return server return {} + @LOCKER.lockthis def msg(self, players): """ Returns a timeline xml as str @@ -94,7 +100,7 @@ class SubscriptionMgr(object): msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO), v.PLEX_TYPE_VIDEO) msg += "" - LOG.debug('msg is: %s', msg) + LOG.debug('Our PKC message is: %s', msg) return msg def signal_stop(self): @@ -125,9 +131,9 @@ class SubscriptionMgr(object): state.PLAYER_STATES[playerid]['plex_id'] return key - def _kodi_stream_index(self, playerid, stream_type): + def _plex_stream_index(self, playerid, stream_type): """ - Returns the current Kodi stream index [int] for the player playerid + Returns the current Plex stream index [str] for the player playerid stream_type: 'video', 'audio', 'subtitle' """ @@ -136,18 +142,34 @@ class SubscriptionMgr(object): return playqueue.items[info['position']].plex_stream_index( info[STREAM_DETAILS[stream_type]]['index'], stream_type) + @staticmethod + def _player_info(playerid): + """ + Grabs all player info again for playerid [int]. + Returns the dict state.PLAYER_STATES[playerid] + """ + # Update our PKC state of how the player actually looks like + 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() + return state.PLAYER_STATES[playerid] + def _timeline_xml(self, player, ptype): if player is None: return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) playerid = player['playerid'] - # Update our PKC state of how the player actually looks like - 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() - # Get the message together to send to Plex - info = state.PLAYER_STATES[playerid] - LOG.debug('timeline player state: %s', info) + 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 @@ -225,18 +246,22 @@ class SubscriptionMgr(object): Causes PKC to tell the PMS and Plex Companion players to receive a notification what's being played. """ - self._cleanup() + 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 + # fetch the message, subscribers or not, since the server will need the + # info anyway + self.isplaying = False msg = self.msg(players) - if self.subscribers: - with RLock(): + 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 + self._notify_server(players) + self.lastplayers = players return True def _notify_server(self, players): @@ -280,14 +305,16 @@ class SubscriptionMgr(object): 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) - # Save to be able to signal a stop at the end 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. @@ -299,28 +326,26 @@ class SubscriptionMgr(object): command_id, self, self.request_mgr) - with RLock(): - self.subscribers[subscriber.uuid] = subscriber + 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) """ - with RLock(): - for subscriber in self.subscribers.values(): - if subscriber.uuid == uuid or subscriber.host == uuid: - subscriber.cleanup() - del self.subscribers[subscriber.uuid] + for subscriber in self.subscribers.values(): + if subscriber.uuid == uuid or subscriber.host == uuid: + subscriber.cleanup() + del self.subscribers[subscriber.uuid] def _cleanup(self): - with RLock(): - for subscriber in self.subscribers.values(): - if subscriber.age > 30: - subscriber.cleanup() - del self.subscribers[subscriber.uuid] + for subscriber in self.subscribers.values(): + if subscriber.age > 30: + subscriber.cleanup() + del self.subscribers[subscriber.uuid] class Subscriber(object): diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index a08e0d40..239d25df 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -227,8 +227,7 @@ class Plex_DB_Functions(): ''' try: self.plexcursor.execute(query, (plex_id,)) - item = self.plexcursor.fetchone() - return item + return self.plexcursor.fetchone() except: return None diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 79ca110a..6dfdd941 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -1079,7 +1079,7 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): return cls -class Lock_Function: +class Lock_Function(object): """ Decorator for class methods and functions to lock them with lock.