From 6aa3f62b79b4bd622b9a5d38e3bf5e8abbfe29f5 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 31 Jan 2016 16:13:40 +0100 Subject: [PATCH] Plexcompanion 1st version for entrypoint.py, playbackutils.py, PlexAPI.py --- resources/lib/PlexAPI.py | 40 +++++--- resources/lib/PlexFunctions.py | 31 +++--- resources/lib/entrypoint.py | 72 ++++++++------ resources/lib/playbackutils.py | 166 +++++++++++++++++---------------- resources/lib/playutils.py | 4 +- resources/lib/utils.py | 19 ++++ 6 files changed, 195 insertions(+), 137 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 1aae5053..021b68ad 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1425,7 +1425,13 @@ class API(): """ Returns the type of media, e.g. 'movie' or 'clip' for trailers """ - return self.item['type'] + # XML + try: + item = self.item.attrib + # JSON + except AttributeError: + item = self.item + return item.get('type', '') def getChecksum(self): """ @@ -1449,15 +1455,13 @@ class API(): Can be used on both XML and JSON Returns the Plex key such as '246922' as a string """ - item = self.item # XML try: - item = item[0].attrib + result = self.item.attrib # JSON - except (AttributeError, KeyError): - pass - key = item['ratingKey'] - return str(key) + except AttributeError: + item = self.item + return item['ratingKey'] def getKey(self): """ @@ -1887,7 +1891,13 @@ class API(): If not found, empty str is returned """ - return self.item.get('playQueueItemID') + # XML: + try: + item = self.item.attrib + # JSON + except AttributeError: + item = self.item + return item.get('playQueueItemID', '') def getDataFromPartOrMedia(self, key): """ @@ -1896,8 +1906,15 @@ class API(): If all fails, None is returned. """ - media = self.item['_children'][0] - part = media['_children'][self.part] + # JSON + try: + media = self.item['_children'][0] + part = media['_children'][self.part] + # XML + except TypeError: + media = self.item[0].attrib + part = self.item[0][self.part].attrib + try: try: value = part[key] @@ -2242,8 +2259,8 @@ class API(): } xargs = PlexAPI().getXArgsDeviceInfo(options=options) # For Direct Playing + path = self.getDataFromPartOrMedia('key') if action == "DirectPlay": - path = self.item['_children'][0]['_children'][self.partNumber]['key'] transcodePath = self.server + path # Be sure to have exactly ONE '?' in the path (might already have # been returned, e.g. trailers!) @@ -2257,7 +2274,6 @@ class API(): # For Direct Streaming or Transcoding transcodePath = self.server + \ '/video/:/transcode/universal/start.m3u8?' - path = self.getDataFromPartOrMedia('key') args = { 'path': path, 'mediaIndex': 0, # Probably refering to XML reply sheme diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index a053a114..3a83a5bd 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -21,6 +21,13 @@ def PlexToKodiTimefactor(): return 1.0 / 1000.0 +def ConvertPlexToKodiTime(plexTime): + """ + Converts Plextime to Koditime. Returns an int (in seconds). + """ + return int(float(plexTime) * PlexToKodiTimefactor()) + + def GetItemClassFromType(itemType): classes = { 'movie': 'Movies', @@ -94,26 +101,21 @@ def EmbyItemtypes(): def GetPlayQueue(playQueueID): """ - Fetches the PMS playqueue with the playQueueID as a JSON + Fetches the PMS playqueue with the playQueueID as an XML Returns False if something went wrong """ url = "{server}/playQueues/%s" % playQueueID - headerOptions = {'Accept': 'application/json'} - json = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + args = {'Accept': 'application/xml'} + xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=args) try: - json = json.json() - except: + xml.attrib['playQueueID'] + except (AttributeError, KeyError): return False - try: - json['_children'] - json['playQueueID'] - except KeyError: - return False - return json + return xml -def GetPlexMetadata(key): +def GetPlexMetadata(key, JSON=True): """ Returns raw API metadata for key as an etree XML. @@ -139,7 +141,10 @@ def GetPlexMetadata(key): 'includeConcerts': 1 } url = url + '?' + urlencode(arguments) - headerOptions = {'Accept': 'application/xml'} + if not JSON: + headerOptions = {'Accept': 'application/xml'} + else: + headerOptions = {} xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions) # Did we receive a valid XML? try: diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 21ff0a95..d651dde6 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -60,7 +60,8 @@ def plexCompanion(fullurl, params=None): utils.window('plex_machineIdentifier')), -1) return utils.window('plex_key', params.get('key')) - library, key, query = PlexFunctions(params.get('containerKey')) + library, key, query = PlexFunctions.ParseContainerKey( + params.get('containerKey')) # Construct a container key that works always (get rid of playlist args) utils.window('plex_containerKey', '/'+library+'/'+key) # Assume it's video when something goes wrong @@ -69,42 +70,57 @@ def plexCompanion(fullurl, params=None): if 'playQueues' in library: utils.logMsg(title, "Playing a playQueue. Query was: %s" % query, 1) # Playing a playlist that we need to fetch from PMS - playQueue = PlexFunctions.GetPlayQueue(key) - if not playQueue: + xml = PlexFunctions.GetPlayQueue(key) + if not xml: utils.logMsg( title, "Error getting PMS playlist for key %s" % key, -1) - return + else: + PassPlaylist(xml, resume=int(params.get('offset', '0'))) + else: + utils.logMsg( + title, "Not knowing what to do for now - no playQueue sent", -1) - # Set window properties to make them available for other threads - utils.window('plex_playQueueID', playQueue['playQueueID']) - utils.window('plex_playQueueVersion', playQueue['playQueueVersion']) - utils.window('plex_playQueueShuffled', playQueue['playQueueShuffled']) - utils.window( - 'plex_playQueueSelectedItemID', - playQueue['playQueueSelectedItemID']) - utils.window( - 'plex_playQueueSelectedItemOffset', - playQueue['playQueueSelectedItemOffset']) - pbutils.PlaybackUtils(playQueue['_children']).StartPlay( - resume=playQueue['playQueueSelectedItemOffset'], - resumeItem=playQueue['playQueueSelectedItemID']) +def PassPlaylist(xml, resume=0): + # Set window properties to make them available later for other threads + windowArgs = [ + 'playQueueID', + 'playQueueVersion', + 'playQueueShuffled'] + for arg in windowArgs: + utils.window(arg, utils.XMLAtt(xml, arg)) - # Start playing - item = PlexFunctions.GetPlexMetadata(itemid) - pbutils.PlaybackUtils(item).play(itemid, dbid, seektime=resume) + # Get resume point + resume1 = utils.IntFromStr( + utils.XMLAtt(xml, 'playQueueSelectedItemOffset')) + resume2 = resume + # Convert Plextime to Koditime + resume = PlexFunctions.ConvertPlexToKodiTime(max(resume1, resume2)) + + pbutils.PlaybackUtils(xml).StartPlay( + resume=resume, + resumeItem=utils.XMLAtt(xml, 'playQueueSelectedItemID')) def doPlayback(itemid, dbid): - # Get a first XML to get the librarySectionUUID - item = PlexFunctions.GetPlexMetadata(itemid) - # Use that to call the playlist - xmlPlaylist = PlexAPI.API(item).GetPlexPlaylist() - if xmlPlaylist: - pbutils.PlaybackUtils(xmlPlaylist).play(itemid, dbid) + utils.logMsg(title, "doPlayback called with %s %s" + % (itemid, dbid), 1) + item = PlexFunctions.GetPlexMetadata(itemid, JSON=True) + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + # If current playlist is NOT empty, we only need to update the item url + if playlist.size() != 0: + utils.logMsg(title, "Playlist is not empty, hence update url only", 1) + pbutils.PlaybackUtils(item).StartPlay() else: - # No playlist received e.g. when directly playing trailers - pbutils.PlaybackUtils(item).play(itemid, dbid) + # Get a first XML to get the librarySectionUUID + # Use librarySectionUUID to call the playlist + xmlPlaylist = PlexAPI.API(item).GetPlexPlaylist() + if xmlPlaylist: + PassPlaylist(xmlPlaylist) + else: + # No playlist received e.g. when directly playing trailers + pbutils.PlaybackUtils(item).StartPlay() + ##### DO RESET AUTH ##### def resetAuth(): diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 6e772b36..e99da361 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -39,41 +39,45 @@ class PlaybackUtils(): self.emby = embyserver.Read_EmbyServer() self.pl = playlist.Playlist() - def StartPlay(self, resume=None, resumeItem=None): + def StartPlay(self, resume=0, resumeItem=""): self.logMsg("StartPlay called with resume=%s, resumeItem=%s" % (resume, resumeItem), 1) - # Setup Kodi playlist (e.g. make new one or append or even update) # Why should we have different behaviour if user is on home screen?!? # self.homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - # Clear playlist since we're always using PMS playQueues - self.playlist.clear() - self.startPos = max(self.playlist.getposition(), 0) # Can return -1 - self.sizePlaylist = self.playlist.size() + sizePlaylist = self.playlist.size() self.currentPosition = self.startPos self.logMsg("Playlist start position: %s" % self.startPos, 1) self.logMsg("Playlist position we're starting with: %s" % self.currentPosition, 1) - self.logMsg("Playlist size: %s" % self.sizePlaylist, 1) + self.logMsg("Playlist size: %s" % sizePlaylist, 1) + + # Might have been called AFTER a playlist has been setup to only + # update the playitem's url + self.updateUrlOnly = True if sizePlaylist != 0 else False self.plexResumeItemId = resumeItem # Where should we ultimately start playback? - self.resumePost = self.startPos - - if resume: - if resume == '0': - resume = None - else: - resume = int(resume) + self.resumePos = self.startPos # Run through the passed PMS playlist and construct playlist + startitem = None for mediaItem in self.item: - self.AddMediaItemToPlaylist(mediaItem) - # Kick off playback + listitem = self.AddMediaItemToPlaylist(mediaItem) + if listitem: + startitem = listitem + + # Return the updated play Url if we've already setup the playlist + if self.updateUrlOnly: + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, startitem) + return + + # Kick off initial playback + self.logMsg("Starting playback", 1) Player = xbmc.Player() - Player.play(self.playlist, startpos=self.resumePost) - if resume: + Player.play(self.playlist, startpos=self.resumePos) + if resume != 0: try: Player.seekTime(resume) except: @@ -90,6 +94,11 @@ class PlaybackUtils(): API = PlexAPI.API(item) playutils = putils.PlayUtils(item) + # If we're only updating an url, we've been handed metadata for only + # one part - no need to run over all parts + if self.updateUrlOnly: + return playutils.getPlayUrl()[0] + # e.g. itemid='219155' itemid = API.getRatingKey() # Get DB id from Kodi by using plex id, if that works @@ -104,7 +113,8 @@ class PlaybackUtils(): embyconn.close() # Get playurls per part and process them - for playurl in playutils.getPlayUrl(): + returnListItem = None + for counter, playurl in enumerate(playutils.getPlayUrl()): # One new listitem per part listitem = xbmcgui.ListItem() # For items that are not (yet) synced to Kodi lib, e.g. trailers @@ -130,40 +140,39 @@ class PlaybackUtils(): playQueueItemID = API.GetPlayQueueItemID() # Is this the position where we should start playback? - if playQueueItemID == self.plexResumeItemId: - self.logMsg( - "Figure we should start playback at position %s " - "with playQueueItemID %s" - % (self.currentPosition, playQueueItemID), 2) - self.resumePost = self.currentPosition + if counter == 0: + if playQueueItemID == self.plexResumeItemId: + self.logMsg( + "Figure we should start playback at position %s " + "with playQueueItemID %s" + % (self.currentPosition, playQueueItemID), 2) + self.resumePost = self.currentPosition + returnListItem = listitem # We need to keep track of playQueueItemIDs for Plex Companion utils.window( - 'plex_%s.playQueueItemID' % playurl, API.GetPlayQueueItemID()) + 'plex_%s.playQueueItemID' % playurl, playQueueItemID) utils.window( - 'plex_%s.playlistPosition' % playurl, self.currentPosition) + 'plex_%s.playlistPosition' + % playurl, str(self.currentPosition)) # Log the playlist that we end up with self.pl.verifyPlaylist() - def play(self, item): + return returnListItem - API = PlexAPI.API(item) + def play(self, itemid, dbid=None): + """ + Original one + """ + + self.logMsg("Play called.", 1) + + doUtils = self.doUtils + item = self.item + API = self.API listitem = xbmcgui.ListItem() playutils = putils.PlayUtils(item) - # e.g. itemid='219155' - itemid = API.getRatingKey() - # Get DB id from Kodi by using plex id, if that works - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby = embydb_functions.Embydb_Functions(embycursor) - try: - dbid = emby.getItem_byId(itemid)[0] - except TypeError: - # Trailers and the like that are not in the kodi DB - dbid = None - embyconn.close() - playurl = playutils.getPlayUrl() if not playurl: return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) @@ -184,7 +193,6 @@ class PlaybackUtils(): propertiesPlayback = utils.window('emby_playbackProps') == "true" introsPlaylist = False - partsPlaylist = False dummyPlaylist = False self.logMsg("Playlist start position: %s" % startPos, 1) @@ -192,9 +200,9 @@ class PlaybackUtils(): self.logMsg("Playlist size: %s" % sizePlaylist, 1) ############### RESUME POINT ################ - + userdata = API.getUserData() - seektime = userdata['Resume'] + seektime = API.adjustResume(userdata['Resume']) # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. @@ -212,39 +220,41 @@ class PlaybackUtils(): # Remove the original item from playlist self.pl.removefromPlaylist(startPos+1) # Readd the original item to playlist - via jsonrpc so we have full metadata - self.pl.insertintoPlaylist(currentPosition+1, dbid, item[-1].attrib['type'].lower()) + self.pl.insertintoPlaylist(currentPosition+1, dbid, item['Type'].lower()) currentPosition += 1 ############### -- CHECK FOR INTROS ################ + if utils.settings('enableCinema') == "true" and not seektime: # if we have any play them when the movie/show is not being resumed - playListSize = int(item.attrib['size']) - if playListSize > 1: + url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid + intros = doUtils.downloadUrl(url) + + if intros['TotalRecordCount'] != 0: getTrailers = True + if utils.settings('askCinema') == "true": - resp = xbmcgui.Dialog().yesno(self.addonName, "Play trailers?") + resp = xbmcgui.Dialog().yesno("Emby Cinema Mode", "Play trailers?") if not resp: # User selected to not play trailers getTrailers = False self.logMsg("Skip trailers.", 1) + if getTrailers: - for i in range(0, playListSize - 1): - # The server randomly returns intros, process them - # Set the child in XML Plex response to a trailer - API.setChildNumber(i) + for intro in intros['Items']: + # The server randomly returns intros, process them. introListItem = xbmcgui.ListItem() - introPlayurl = playutils.getPlayUrl(child=i) - self.logMsg("Adding Trailer: %s" % introPlayurl, 1) + introPlayurl = putils.PlayUtils(intro).getPlayUrl() + self.logMsg("Adding Intro: %s" % introPlayurl, 1) + # Set listitem and properties for intros - self.setProperties(introPlayurl, introListItem) + pbutils = PlaybackUtils(intro) + pbutils.setProperties(introPlayurl, introListItem) self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) introsPlaylist = True currentPosition += 1 - self.logMsg("Key: %s" % API.getRatingKey(), 1) - self.logMsg("Successfally added trailer number %s" % i, 1) - # Set "working point" to the movie (last one in playlist) - API.setChildNumber(-1) + ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### @@ -252,37 +262,32 @@ class PlaybackUtils(): # Extend our current playlist with the actual item to play # only if there's no playlist first self.logMsg("Adding main item to playlist.", 1) - # self.pl.addtoPlaylist(dbid, item['Type'].lower()) - self.pl.addtoPlaylist(dbid, item[-1].attrib['type'].lower()) + self.pl.addtoPlaylist(dbid, item['Type'].lower()) # Ensure that additional parts are played after the main item currentPosition += 1 ############### -- CHECK FOR ADDITIONAL PARTS ################ - parts = API.GetParts() - partcount = len(parts) - if partcount > 1: + + if item.get('PartCount'): # Only add to the playlist after intros have played - partsPlaylist = True - i = 0 - for part in parts: - API.setPartNumber(i) + partcount = item['PartCount'] + url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid + parts = doUtils.downloadUrl(url) + for part in parts['Items']: + additionalListItem = xbmcgui.ListItem() - additionalPlayurl = playutils.getPlayUrl( - child=-1, - partIndex=i) - self.logMsg("Adding additional part: %s" % i, 1) + additionalPlayurl = putils.PlayUtils(part).getPlayUrl() + self.logMsg("Adding additional part: %s" % partcount, 1) # Set listitem and properties for each additional parts - pbutils = PlaybackUtils(item) + pbutils = PlaybackUtils(part) pbutils.setProperties(additionalPlayurl, additionalListItem) pbutils.setArtwork(additionalListItem) playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) self.pl.verifyPlaylist() currentPosition += 1 - i += 1 - API.setPartNumber(0) if dummyPlaylist: # Added a dummy file to the playlist, @@ -301,22 +306,21 @@ class PlaybackUtils(): # For transcoding only, ask for audio/subs pref if utils.window('emby_%s.playmethod' % playurl) == "Transcode": - playurl = playutils.audioSubsPref(playurl, listitem, child=self.API.getChildNumber()) + playurl = playutils.audioSubsPref(playurl, listitem) utils.window('emby_%s.playmethod' % playurl, value="Transcode") listitem.setPath(playurl) self.setProperties(playurl, listitem) ############### PLAYBACK ################ - customPlaylist = utils.window('emby_customPlaylist') + if homeScreen and seektime: self.logMsg("Play as a widget item.", 1) self.setListItem(listitem) xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - elif ((introsPlaylist and customPlaylist == "true") or - (homeScreen and not sizePlaylist) or - (partsPlaylist and customPlaylist == "true")): + elif ((introsPlaylist and utils.window('emby_customPlaylist') == "true") or + (homeScreen and not sizePlaylist)): # Playlist was created just now, play it. self.logMsg("Play playlist.", 1) xbmc.Player().play(playlist, startpos=startPos) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 4943e9fe..8cf11589 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -34,9 +34,7 @@ class PlayUtils(): Returns a list of playurls, one per part in item """ playurls = [] - # TODO: multiple media parts for e.g. trailers: replace [0] here - partCount = len(self.item['_children'][0]['_children']) - for partNumber in range(partCount): + for partNumber, part in enumerate(self.item[0]): playurl = None self.API.setPartNumber(partNumber) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index edb22bcc..8232aee8 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -140,6 +140,25 @@ def logging(cls): return cls +def XMLAtt(xml, key): + try: + result = xml.attrib['key'] + except KeyError: + result = '' + return result + + +def IntFromStr(string): + """ + Returns an int from string or the int 0 if something happened + """ + try: + result = int(string) + except: + result = 0 + return result + + def getUnixTimestamp(secondsIntoTheFuture=None): """ Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as