From 9d2902baa5caec9d1c7e7150687767cd29f93e02 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 13:14:21 +0100 Subject: [PATCH] Playqueues overhaul continued --- resources/lib/PlexAPI.py | 2 +- resources/lib/PlexCompanion.py | 59 +++----- resources/lib/PlexFunctions.py | 16 -- resources/lib/kodimonitor.py | 4 +- resources/lib/playbackutils.py | 3 +- resources/lib/playlist_func.py | 167 ++++++++++++++------- resources/lib/playqueue.py | 76 +++++++++- resources/lib/plexbmchelper/subscribers.py | 28 ++-- 8 files changed, 216 insertions(+), 139 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b63bf71a..06aa9484 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1646,7 +1646,7 @@ class API(): If not found, empty str is returned """ - return self.item.attrib.get('playQueueItemID', '') + return self.item.attrib.get('playQueueItemID') def getDataFromPartOrMedia(self, key): """ diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index ddff3abf..002a6f91 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -9,8 +9,7 @@ from xbmc import sleep from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods from plexbmchelper import listener, plexgdm, subscribers, functions, \ httppersist, plexsettings -from PlexFunctions import ParseContainerKey, GetPlayQueue, \ - ConvertPlexToKodiTime +from PlexFunctions import ParseContainerKey import player ############################################################################### @@ -29,7 +28,6 @@ class PlexCompanion(Thread): log.info("----===## Starting PlexCompanion ##===----") if callback is not None: self.mgr = callback - self.playqueue = self.mgr.playqueue self.settings = plexsettings.getSettings() # Start GDM for server/client discovery self.client = plexgdm.plexgdm() @@ -60,12 +58,18 @@ class PlexCompanion(Thread): def processTasks(self, task): """ - Processes tasks picked up e.g. by Companion listener - - task = { - 'action': 'playlist' - 'data': as received from Plex companion - } + Processes tasks picked up e.g. by Companion listener, e.g. + {'action': 'playlist', + 'data': {'address': 'xyz.plex.direct', + 'commandID': '7', + 'containerKey': '/playQueues/6669?own=1&repeat=0&window=200', + 'key': '/library/metadata/220493', + 'machineIdentifier': 'xyz', + 'offset': '0', + 'port': '32400', + 'protocol': 'https', + 'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd', + 'type': 'video'}} """ log.debug('Processing: %s' % task) data = task['data'] @@ -79,36 +83,11 @@ class PlexCompanion(Thread): import traceback log.error("Traceback:\n%s" % traceback.format_exc()) return - self.mgr.playqueue.update_playqueue_with_companion(data) - - self.playqueue = self.mgr.playqueue.get_playqueue_from_plextype( - data.get('type')) - if queueId != self.playqueue.ID: - log.info('New playlist received, updating!') - xml = GetPlayQueue(queueId) - if xml in (None, 401): - log.error('Could not download Plex playlist.') - return - # Clear existing playlist on the Kodi side - self.playqueue.clear() - # Set new values - self.playqueue.QueueId(queueId) - self.playqueue.PlayQueueVersion(int( - xml.attrib.get('playQueueVersion'))) - self.playqueue.Guid(xml.attrib.get('guid')) - items = [] - for item in xml: - items.append({ - 'playQueueItemID': item.get('playQueueItemID'), - 'plexId': item.get('ratingKey'), - 'kodiId': None}) - self.playqueue.playAll( - items, - startitem=self._getStartItem(data.get('key', '')), - offset=ConvertPlexToKodiTime(data.get('offset', 0))) - log.info('Initiated playlist no %s with version %s' - % (self.playqueue.QueueId(), - self.playqueue.PlayQueueVersion())) + playqueue = self.mgr.playqueue.get_playqueue_from_type( + data['type']) + if ID != playqueue.ID: + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, ID, int(query['repeat'])) else: log.error('This has never happened before!') @@ -123,7 +102,7 @@ class PlexCompanion(Thread): requestMgr = httppersist.RequestMgr() jsonClass = functions.jsonClass(requestMgr, self.settings) subscriptionManager = subscribers.SubscriptionManager( - jsonClass, requestMgr, self.player, self.playqueue) + jsonClass, requestMgr, self.player, self.mgr) queue = Queue.Queue(maxsize=100) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 2705e0a6..621aae90 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -171,22 +171,6 @@ def SelectStreams(url, args): url + '?' + urlencode(args), action_type='PUT') -def GetPlayQueue(playQueueID): - """ - Fetches the PMS playqueue with the playQueueID as an XML - - Returns None if something went wrong - """ - url = "{server}/playQueues/%s" % playQueueID - args = {'Accept': 'application/xml'} - xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=args) - try: - xml.attrib['playQueueID'] - except (AttributeError, KeyError): - return None - return xml - - def GetPlexMetadata(key): """ Returns raw API metadata for key as an etree XML. diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 09c42445..09d797fd 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -168,10 +168,10 @@ class KodiMonitor(xbmc.Monitor): pass elif method == "Playlist.OnAdd": - # User manipulated Kodi playlist + # User (or PKC) manipulated Kodi playlist # Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1, # u'position': 0} - self.playlist.kodi_onadd(data) + self.playqueue.kodi_onadd(data) def PlayBackStart(self, data): """ diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 40a984f9..17342df7 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -37,7 +37,8 @@ class PlaybackUtils(): self.userid = window('currUserId') self.server = window('pms_server') - self.pl = Playqueue().get_playqueue_from_plextype(self.API.getType()) + self.pl = Playqueue().get_playqueue_from_type( + PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.API.getType()]) def play(self, itemid, dbid=None): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 6dc6a02c..2b243899 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -4,6 +4,7 @@ from urllib import quote import embydb_functions as embydb from downloadutils import DownloadUtils as DU from utils import JSONRPC, tryEncode +from PlexAPI import API ############################################################################### @@ -24,13 +25,35 @@ class Playlist_Object_Baseclase(object): selectedItemOffset = None shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ??? repeat = 0 # [int], 0: not repeated, 1: ??? 2: ??? + # Hack to later ignore all Kodi playlist adds that PKC did (Kodimonitor) + PKC_playlist_edits = [] def __repr__(self): - answ = "<%s object: " % (self.__class__.__name__) + answ = "<%s: " % (self.__class__.__name__) for key in self.__dict__: answ += '%s: %s, ' % (key, getattr(self, key)) return answ[:-2] + ">" + def clear(self): + """ + Resets the playlist object to an empty playlist + """ + # Clear Kodi playlist object + self.kodi_pl.clear() + 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.PKC_playlist_edits = [] + log.debug('Playlist cleared: %s' % self) + + def log_Kodi_playlist(self): + log.debug('Current Kodi playlist: %s' % get_kodi_playlist_items(self)) + class Playlist_Object(Playlist_Object_Baseclase): kind = 'playList' @@ -48,6 +71,13 @@ class Playlist_Item(object): kodi_type = None # Kodi type: 'movie' file = None # Path to the item's file uri = None # Weird Plex uri path involving plex_UUID + guid = None # Weird Plex guid + + def __repr__(self): + answ = "<%s: " % (self.__class__.__name__) + for key in self.__dict__: + answ += '%s: %s, ' % (key, getattr(self, key)) + return answ[:-2] + ">" def playlist_item_from_kodi_item(kodi_item): @@ -127,8 +157,10 @@ def _get_playlist_details_from_xml(playlist, xml): try: playlist.ID = xml.attrib['%sID' % playlist.kind] playlist.version = xml.attrib['%sVersion' % playlist.kind] - playlist.selectedItemID = xml.attrib['%sSelectedItemID' % playlist.kind] - playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' % playlist.kind] + playlist.selectedItemID = xml.attrib['%sSelectedItemID' + % playlist.kind] + playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' + % playlist.kind] playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] except: log.error('Could not parse xml answer from PMS for playlist %s' @@ -141,9 +173,9 @@ def _get_playlist_details_from_xml(playlist, xml): def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): """ - Supply either plex_id or the data supplied by Kodi JSON-RPC + Supply either with a plex_id OR the data supplied by Kodi JSON-RPC """ - if plex_id is not None: + if plex_id: item = playlist_item_from_plex(plex_id) else: item = playlist_item_from_kodi_item(kodi_item) @@ -155,7 +187,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, action_type="POST", parameters=params) - _get_playlist_details_from_xml(xml) + _get_playlist_details_from_xml(playlist, xml) playlist.items.append(item) log.debug('Initialized the playlist: %s' % playlist) @@ -176,6 +208,10 @@ def add_playlist_item(playlist, kodi_item, after_pos): % (kodi_item, playlist)) _log_xml(xml) return + # Get the guid for this item + for plex_item in xml: + if plex_item.attrib['%sItemID' % playlist.kind] == item.ID: + item.guid = plex_item.attrib['guid'] playlist.items.append(item) if after_pos == len(playlist.items) - 1: # Item was added at the end @@ -259,6 +295,39 @@ def get_kodi_playqueues(): # Functions operating on the Kodi playlist objects ########## +def add_to_Kodi_playlist(playlist, xml_video_element): + """ + Adds a new item to the Kodi playlist via JSON (at the end of the playlist). + Pass in the PMS xml's video element (one level underneath MediaContainer). + + Will return a Playlist_Item + """ + item = Playlist_Item() + api = API(xml_video_element) + params = { + 'playlistid': playlist.playlistid + } + item.plex_id = api.getRatingKey() + item.ID = xml_video_element.attrib['%sItemID' % playlist.kind] + item.guid = xml_video_element.attrib.get('guid') + if item.plex_id: + with embydb.GetEmbyDB() as emby_db: + db_element = emby_db.getItem_byId(item.plex_id) + try: + item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4] + except TypeError: + pass + if item.kodi_id: + params['item'] = {'%sid' % item.kodi_type: item.kodi_id} + else: + item.file = api.getFilePath() + params['item'] = {'file': tryEncode(item.file)} + log.debug(JSONRPC('Playlist.Add').execute(params)) + playlist.PKC_playlist_edits.append( + item.kodi_id if item.kodi_id else item.file) + return item + + def insertintoPlaylist(self, position, dbid=None, @@ -275,57 +344,51 @@ def insertintoPlaylist(self, JSONRPC('Playlist.Insert').execute(params) -def addtoPlaylist(self, dbid=None, mediatype=None, url=None): - params = { - 'playlistid': self.playlistId - } - if dbid is not None: - params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} - else: - params['item'] = {'file': url} - JSONRPC('Playlist.Add').execute(params) - - def removefromPlaylist(self, position): params = { 'playlistid': self.playlistId, 'position': position } - JSONRPC('Playlist.Remove').execute(params) + log.debug(JSONRPC('Playlist.Remove').execute(params)) -def playAll(self, items, startitem, offset): +def get_PMS_playlist(playlist, playlist_id=None): """ - items: list of dicts of the form - { - 'playQueueItemID': Plex playQueueItemID, e.g. '29175' - 'plexId': Plex ratingKey, e.g. '125' - 'kodiId': Kodi's db id of the same item - } + Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we + need to fetch a new playlist - startitem: tuple (typus, id), where typus is either - 'playQueueItemID' or 'plexId' and id is the corresponding - id as a string - offset: First item's time offset to play in Kodi time (an int) + Returns None if something went wrong """ - log.info("---*** PLAY ALL ***---") - log.debug('Startitem: %s, offset: %s, items: %s' - % (startitem, offset, items)) - self.items = items - if self.playlist is None: - self._initiatePlaylist() - if self.playlist is None: - log.error('Could not create playlist, abort') + playlist_id = playlist_id if playlist_id else playlist.ID + xml = DU().downloadUrl( + "{server}/%ss/%s" % (playlist.kind, playlist_id), + headerOptions={'Accept': 'application/xml'}) + try: + xml.attrib['%sID' % playlist.kind] + except (AttributeError, KeyError): + xml = None + return xml + + +def update_playlist_from_PMS(playlist, playlist_id=None, repeat=None): + """ + Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we + need to fetch a new playqueue + """ + xml = get_PMS_playlist(playlist, playlist_id) + try: + xml.attrib['%sVersion' % playlist.kind] + except: + log.error('Could not download Plex playlist.') return - - window('plex_customplaylist', value="true") - if offset != 0: - # Seek to the starting position - window('plex_customplaylist.seektime', str(offset)) - self._processItems(startitem, startPlayer=True) - # Log playlist - self._verifyPlaylist() - log.debug('Internal playlist: %s' % self.items) + # Clear our existing playlist and the associated Kodi playlist + playlist.clear() + # Set new values + _get_playlist_details_from_xml(playlist, xml) + if repeat: + playlist.repeat = repeat + for plex_item in xml: + playlist.items.append(add_to_Kodi_playlist(playlist, plex_item)) def _processItems(self, startitem, startPlayer=False): @@ -380,15 +443,3 @@ def _addtoPlaylist_xbmc(self, item): playbackutils.PlaybackUtils(item).setArtwork(listitem) self.playlist.add(playurl, listitem) - - -def clear(self): - """ - Empties current Kodi playlist and associated variables - """ - self.playlist.clear() - self.items = [] - self.queueId = None - self.playQueueVersion = None - self.guid = None - log.info('Playlist cleared') \ No newline at end of file diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 997c6bef..507fc6b9 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,10 +5,10 @@ from threading import Lock, Thread import xbmc -from utils import ThreadMethods, ThreadMethodsAdditionalSuspend, Lock_Function +from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend, \ + Lock_Function import playlist_func as PL -from PlexFunctions import KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, GetPlayQueue, \ - ParseContainerKey +from PlexFunctions import ConvertPlexToKodiTime ############################################################################### log = logging.getLogger("PLEX."+__name__) @@ -36,6 +36,7 @@ class Playqueue(Thread): if self.playqueues is not None: return self.mgr = callback + self.player = xbmc.Player() # Initialize Kodi playqueues self.playqueues = [] @@ -52,8 +53,64 @@ class Playqueue(Thread): # Currently, only video or audio playqueues available playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) self.playqueues.append(playqueue) + # sort the list by their playlistid, just in case + self.playqueues = sorted( + self.playqueues, key=lambda i: i.playlistid) log.debug('Initialized the Kodi play queues: %s' % self.playqueues) + def get_playqueue_from_type(self, typus): + """ + Returns the playqueue according to the typus ('video', 'audio', + 'picture') passed in + """ + for playqueue in self.playqueues: + if playqueue.type == typus: + break + else: + raise ValueError('Wrong type was passed in: %s' % typus) + return playqueue + + def get_playqueue_from_playerid(self, kodi_player_id): + for playqueue in self.playqueues: + if playqueue.playlistid == kodi_player_id: + break + else: + raise ValueError('Wrong kodi_player_id passed was passed in: %s' + % kodi_player_id) + return playqueue + + @lockmethod.lockthis + def update_playqueue_from_PMS(self, + playqueue, + playqueue_id=None, + repeat=None): + """ + Completely updates the Kodi playqueue with the new Plex playqueue. Pass + in playqueue_id if we need to fetch a new playqueue + + repeat = 0, 1, 2 + """ + log.info('New playqueue received, updating!') + PL.update_playlist_from_PMS(playqueue, playqueue_id, repeat) + log.debug('Updated playqueue: %s' % playqueue) + + window('plex_customplaylist', value="true") + if playqueue.selectedItemOffset not in (None, "0"): + window('plex_customplaylist.seektime', + str(ConvertPlexToKodiTime(playqueue.selectedItemOffset))) + for startpos, item in enumerate(playqueue.items): + if item.ID == playqueue.selectedItemID: + break + else: + startpos = None + # Start playback + if startpos: + self.player.play(playqueue.kodi_pl, startpos=startpos) + else: + self.player.play(playqueue.kodi_pl) + log.debug('Playqueue at the end: %s' % playqueue) + playqueue.log_Kodi_playlist() + @lockmethod.lockthis def update_playqueue_with_companion(self, data): """ @@ -61,10 +118,6 @@ class Playqueue(Thread): """ # Get the correct queue - for playqueue in self.playqueues: - if playqueue.type == KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[ - data['type']]: - break @lockmethod.lockthis def kodi_onadd(self, data): @@ -80,11 +133,20 @@ class Playqueue(Thread): for playqueue in self.playqueues: if playqueue.playlistid == data['playlistid']: break + if playqueue.PKC_playlist_edits: + old = (data['item'].get('id') if data['item'].get('id') + else data['item'].get('file')) + for i, item in enumerate(playqueue.PKC_playlist_edits): + if old == item: + log.debug('kodimonitor told us of a PKC edit - ignore.') + del playqueue.PKC_playlist_edits[i] + return if playqueue.ID is None: # Need to initialize the queue for the first time PL.init_Plex_playlist(playqueue, kodi_item=data['item']) else: PL.add_playlist_item(playqueue, data['item'], data['position']) + log.debug('Added a new item to the playqueue: %s' % playqueue) @lockmethod.lockthis def _compare_playqueues(self, playqueue, new): diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index bf6941a9..b88ba0db 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -15,7 +15,7 @@ log = logging.getLogger("PLEX."+__name__) class SubscriptionManager: - def __init__(self, jsonClass, RequestMgr, player, playlist): + def __init__(self, jsonClass, RequestMgr, player, mgr): self.serverlist = [] self.subscribers = {} self.info = {} @@ -36,7 +36,7 @@ class SubscriptionManager: self.playerprops = {} self.doUtils = downloadutils.DownloadUtils().downloadUrl self.xbmcplayer = player - self.playlist = playlist + self.playqueue = mgr.playqueue self.js = jsonClass self.RequestMgr = RequestMgr @@ -231,6 +231,8 @@ class SubscriptionManager: def getPlayerProperties(self, playerid): try: + # Get the playqueue + playqueue = self.playqueue.playqueues[playerid] # get info from the player props = self.js.jsonrpc( "Player.GetProperties", @@ -248,18 +250,16 @@ class SubscriptionManager: 'shuffle': ("0", "1")[props.get('shuffled', False)], 'repeat': pf.getPlexRepeat(props.get('repeat')), } - if self.playlist is not None: - if self.playlist.QueueId() is not None: - info['playQueueID'] = self.playlist.QueueId() - info['playQueueVersion'] = self.playlist.PlayQueueVersion() - info['guid'] = self.playlist.Guid() - # Get the playlist position - pos = self.js.jsonrpc( - "Player.GetProperties", - {"playerid": playerid, - "properties": ["position"]}) - info['playQueueItemID'] = \ - self.playlist.getQueueIdFromPosition(pos['position']) + if playqueue.ID is not None: + info['playQueueID'] = playqueue.ID + info['playQueueVersion'] = playqueue.version + # Get the playlist position + pos = self.js.jsonrpc( + "Player.GetProperties", + {"playerid": playerid, + "properties": ["position"]})['position'] + info['playQueueItemID'] = playqueue.items[pos].ID + info['guid'] = playqueue.items[pos].guid except: import traceback log.error("Traceback:\n%s" % traceback.format_exc())