From 7f674acbac2f5424c27d98021686c62a6eefe43f Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 16 Mar 2016 17:02:22 +0100 Subject: [PATCH] Redesign Kodi monitor and player Allows now to have playback initiated by Kodi - especially when using direct paths --- resources/lib/PlexAPI.py | 4 +- resources/lib/itemtypes.py | 1 + resources/lib/kodimonitor.py | 156 ++++++--- resources/lib/playbackutils.py | 10 +- resources/lib/player.py | 384 ++++++++++----------- resources/lib/plexbmchelper/subscribers.py | 4 +- service.py | 7 +- 7 files changed, 302 insertions(+), 264 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index e1ac6719..1d685874 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1981,7 +1981,7 @@ class API(): def getMediaStreams(self): """ - Returns the media streams + Returns the media streams for metadata purposes Output: each track contains a dictionaries { @@ -2220,7 +2220,7 @@ class API(): kodiindex += 1 mapping = json.dumps(mapping) utils.window('emby_%s.indexMapping' % playurl, value=mapping) - + self.logMsg('Found external subs: %s' % externalsubs) return externalsubs def CreateListItemFromPlexItem(self, listItem=None): diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index a4ebdddf..d10903fe 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -82,6 +82,7 @@ class Items(object): Returns True if sync should stop, else False """ + self.logMsg('Cannot access file: %s' % url, -1) import xbmcaddon string = xbmcaddon.Addon().getLocalizedString resp = xbmcgui.Dialog().yesno( diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 2adae92d..70b3bed8 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -3,6 +3,7 @@ ############################################################################### import json +from unicodedata import normalize import xbmc import xbmcgui @@ -21,7 +22,8 @@ class KodiMonitor(xbmc.Monitor): def __init__(self): - self.doUtils = downloadutils.DownloadUtils() + self.doUtils = downloadutils.DownloadUtils().downloadUrl + self.xbmcplayer = xbmc.Player() self.logMsg("Kodi monitor started.", 1) @@ -60,68 +62,25 @@ class KodiMonitor(xbmc.Monitor): utils.window('emby_logLevel', value=currentLog) def onNotification(self, sender, method, data): + window = utils.window - doUtils = self.doUtils if method not in ("Playlist.OnAdd"): self.logMsg("Method: %s Data: %s" % (method, data), 1) - + if data: - data = json.loads(data,'utf-8') + data = json.loads(data, 'utf-8') if method == "Player.OnPlay": - # Set up report progress for emby playback - item = data.get('item') - try: - kodiid = item['id'] - type = item['type'] - except (KeyError, TypeError): - self.logMsg("Item is invalid for playstate update.", 1) - else: - if ((utils.settings('useDirectPaths') == "1" and not type == "song") or - (type == "song" and utils.settings('enableMusic') == "true")): - # Set up properties for player - with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) - try: - itemid = emby_dbitem[0] - except TypeError: - self.logMsg("No kodiid returned.", 1) - else: - # Tell everyone else what's going on - utils.window('Plex_currently_playing_itemid', - value=itemid) - url = "{server}/library/metadata/%s" % itemid - result = doUtils.downloadUrl(url) - try: - result.attrib - except AttributeError: - self.logMsg('Could not retrieve PMS xml for %s' - % itemid, -1) - return - playurl = None - count = 0 - while not playurl and count < 2: - try: - playurl = xbmc.Player().getPlayingFile() - except RuntimeError: - count += 1 - xbmc.sleep(200) - else: - listItem = xbmcgui.ListItem() - playback = pbutils.PlaybackUtils(result) - - if type == "song" and utils.settings('streamMusic') == "true": - utils.window('emby_%s.playmethod' % playurl, - value="DirectStream") - else: - utils.window('emby_%s.playmethod' % playurl, - value="DirectPlay") - # Set properties for player.py - playback.setProperties(playurl, listItem) + self.PlayBackStart(data) elif method == "Player.OnStop": # Get rid of some values - utils.window('Plex_currently_playing_itemid', clear=True) + window('Plex_currently_playing_itemid', clear=True) + window('emby_customPlaylist', clear=True) + window('emby_customPlaylist.seektime', clear=True) + window('emby_playbackProps', clear=True) + window('suspend_LibraryThread', clear=True) + window('emby_customPlaylist.seektime', clear=True) elif method == "VideoLibrary.OnUpdate": # Manually marking as watched/unwatched @@ -188,11 +147,96 @@ class KodiMonitor(xbmc.Monitor): finally: embycursor.close()''' - elif method == "System.OnWake": # Allow network to wake up xbmc.sleep(10000) utils.window('emby_onWake', value="true") elif method == "Playlist.OnClear": - pass \ No newline at end of file + pass + + def PlayBackStart(self, data): + """ + Called whenever a playback is started + """ + log = self.logMsg + window = utils.window + + # Try to get a Kodi ID + item = data.get('item') + try: + kodiid = item['id'] + type = item['type'] + except (KeyError, TypeError): + log("Item is invalid for Plex playstate update.", 0) + return + + # Get Plex' item id + with embydb.GetEmbyDB() as emby_db: + emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) + try: + plexid = emby_dbitem[0] + except TypeError: + log("No Plex id returned for kodiid %s" % kodiid, 0) + return + log("Found Plex id %s for Kodi id %s" % (plexid, kodiid), 1) + + # Get currently playing file - can take a while + try: + currentFile = self.xbmcplayer.getPlayingFile() + xbmc.sleep(300) + except: + currentFile = "" + count = 0 + while not currentFile: + xbmc.sleep(100) + try: + currentFile = self.xbmcplayer.getPlayingFile() + except: + pass + if count == 20: + log("No current File - Cancelling OnPlayBackStart...", -1) + return + else: + count += 1 + currentFile = currentFile.decode('utf-8') + log("Currently playing file is: %s" % currentFile, 1) + # Normalize to string, because we need to use this in WINDOW(key), + # where key can only be string + currentFile = normalize('NFKD', currentFile).encode('ascii', 'ignore') + log('Normalized filename: %s' % currentFile, 1) + + # Set some stuff if Kodi initiated playback + if ((utils.settings('useDirectPaths') == "1" and not type == "song") or + (type == "song" and utils.settings('enableMusic') == "true")): + if self.StartDirectPath(plexid, type, currentFile) is False: + log('Could not initiate monitoring; aborting', -1) + return + + # Save currentFile for cleanup later and to be able to access refs + window('plex_lastPlayedFiled', value=currentFile) + window('Plex_currently_playing_itemid', value=plexid) + window("emby_%s.itemid" % currentFile, value=plexid) + log('Finish playback startup', 1) + + def StartDirectPath(self, plexid, type, currentFile): + """ + Set some additional stuff if playback was initiated by Kodi, not PKC + """ + result = self.doUtils('{server}/library/metadata/%s' % plexid) + try: + result[0].attrib + except: + self.logMsg('Did not receive a valid XML for plexid %s.' + % plexid, -1) + return False + # Setup stuff, because playback was started by Kodi, not PKC + pbutils.PlaybackUtils(result[0]).setProperties( + currentFile, xbmcgui.ListItem()) + if type == "song" and utils.settings('streamMusic') == "true": + utils.window('emby_%s.playmethod' % currentFile, + value="DirectStream") + else: + utils.window('emby_%s.playmethod' % currentFile, + value="DirectPlay") + self.logMsg('Window properties set for direct paths!', 0) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index b5ed9174..1a1b1539 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -278,7 +278,7 @@ class PlaybackUtils(): # Only for direct stream if playmethod in ("DirectStream"): # Direct play automatically appends external - subtitles = self.externalSubs(playurl) + subtitles = self.API.externalSubs(playurl) listitem.setSubtitles(subtitles) self.setArtwork(listitem) @@ -288,12 +288,8 @@ class PlaybackUtils(): externalsubs = [] mapping = {} - item = self.item - itemid = item['Id'] - try: - mediastreams = item['MediaSources'][0]['MediaStreams'] - except (TypeError, KeyError, IndexError): - return + itemid = self.API.getRatingKey() + mediastreams = self.API.getMediaStreams() kodiindex = 0 for stream in mediastreams: diff --git a/resources/lib/player.py b/resources/lib/player.py index a29ab512..fc379baf 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -################################################################################################# +############################################################################### import json +from unicodedata import normalize import xbmc import xbmcgui @@ -13,7 +14,7 @@ import downloadutils from urllib import urlencode -################################################################################################# +############################################################################### @utils.logging @@ -48,7 +49,9 @@ class Player(xbmc.Player): return self.playStats def onPlayBackStarted(self): - + """ + Window values need to have been set in Kodimonitor.py + """ log = self.logMsg window = utils.window # Will be called when xbmc starts playing a file @@ -66,205 +69,203 @@ class Player(xbmc.Player): xbmc.sleep(100) try: currentFile = xbmcplayer.getPlayingFile() - except: pass - - if count == 5: # try 5 times + except: + pass + if count == 20: log("Cancelling playback report...", 1) break - else: count += 1 - - if currentFile: - - self.currentFile = currentFile - # Save currentFile for cleanup later - window('plex_lastPlayedFiled', value=currentFile) - # We may need to wait for info to be set in kodi monitor - itemId = window("emby_%s.itemid" % currentFile) - tryCount = 0 - while not itemId: - - xbmc.sleep(200) - itemId = window("emby_%s.itemid" % currentFile) - if tryCount == 20: # try 20 times or about 10 seconds - log("Could not find itemId, cancelling playback report...", 1) - break - else: tryCount += 1 - - else: - window('Plex_currently_playing_itemid', value=itemId) - log("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0) - - # Only proceed if an itemId was found. - embyitem = "emby_%s" % currentFile - runtime = window("%s.runtime" % embyitem) - refresh_id = window("%s.refreshid" % embyitem) - playMethod = window("%s.playmethod" % embyitem) - itemType = window("%s.type" % embyitem) - window('emby_skipWatched%s' % itemId, value="true") - - log("Playing itemtype is: %s" % itemType, 1) - # Suspend library sync thread while movie is playing - if itemType in ('movie', 'episode'): - log("Suspending library sync while playing", 1) - window('suspend_LibraryThread', value='true') - - customseek = window('emby_customPlaylist.seektime') - if (window('emby_customPlaylist') == "true" and customseek): - # Start at, when using custom playlist (play to Kodi from webclient) - log("Seeking to: %s" % customseek, 1) - xbmcplayer.seekTime(int(customseek)) - window('emby_customPlaylist.seektime', clear=True) - - seekTime = xbmcplayer.getTime() - - # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) - result = result.get('result') - - volume = result.get('volume') - muted = result.get('muted') - - # Postdata structure to send to Emby server - url = "{server}/:/timeline?" - postdata = { - - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PlayMethod': playMethod, - 'VolumeLevel': volume, - 'PositionTicks': int(seekTime * 10000000), - 'IsMuted': muted - } - - # Get the current audio track and subtitles - if playMethod == "Transcode": - # property set in PlayUtils.py - postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) - postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) else: - # Get the current kodi audio and subtitles and convert to Emby equivalent - tracks_query = { + count += 1 + if not currentFile: + log('Error getting a currently playing file; abort reporting', -1) + return + currentFile = currentFile.decode('utf-8') + # Normalize to string, because we need to use this in WINDOW(key), + # where key can only be string + currentFile = normalize('NFKD', currentFile).encode('ascii', 'ignore') + log('Normalized filename: %s' % currentFile, 1) - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { + # Save currentFile for cleanup later and for references + self.currentFile = currentFile + window('plex_lastPlayedFiled', value=currentFile) + # We may need to wait for info to be set in kodi monitor + itemId = window("emby_%s.itemid" % currentFile) + count = 0 + while not itemId: + xbmc.sleep(200) + itemId = window("emby_%s.itemid" % currentFile) + # try 20 times or about 10 seconds + if count == 20: + log("Could not find itemId, cancelling playback report...", -1) + return + count += 1 - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) - result = json.loads(result) - result = result.get('result') + log("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0) - try: # Audio tracks - indexAudio = result['currentaudiostream']['index'] - except (KeyError, TypeError): - indexAudio = 0 - - try: # Subtitles tracks - indexSubs = result['currentsubtitle']['index'] - except (KeyError, TypeError): - indexSubs = 0 + embyitem = "emby_%s" % currentFile + runtime = window("%s.runtime" % embyitem) + refresh_id = window("%s.refreshid" % embyitem) + playMethod = window("%s.playmethod" % embyitem) + itemType = window("%s.type" % embyitem) + window('emby_skipWatched%s' % itemId, value="true") - try: # If subtitles are enabled - subsEnabled = result['subtitleenabled'] - except (KeyError, TypeError): - subsEnabled = "" + log("Playing itemtype is: %s" % itemType, 1) + # Suspend library sync thread while movie is playing + if itemType in ('movie', 'episode'): + log("Suspending library sync while playing", 1) + window('suspend_LibraryThread', value='true') - # Postdata for the audio - postdata['AudioStreamIndex'] = indexAudio + 1 - - # Postdata for the subtitles - if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: - - # Number of audiotracks to help get Emby Index - audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = window("%s.indexMapping" % embyitem) + customseek = window('emby_customPlaylist.seektime') + if (window('emby_customPlaylist') == "true" and customseek): + # Start at, when using custom playlist (play to Kodi from webclient) + log("Seeking to: %s" % customseek, 1) + xbmcplayer.seekTime(int(customseek)) + window('emby_customPlaylist.seektime', clear=True) - if mapping: # Set in playbackutils.py - - log("Mapping for external subtitles index: %s" % mapping, 2) - externalIndex = json.loads(mapping) + seekTime = xbmcplayer.getTime() - if externalIndex.get(str(indexSubs)): - # If the current subtitle is in the mapping - postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] - else: - # Internal subtitle currently selected - subindex = indexSubs - len(externalIndex) + audioTracks + 1 - postdata['SubtitleStreamIndex'] = subindex - - else: # Direct paths enabled scenario or no external subtitles set - postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 - else: - postdata['SubtitleStreamIndex'] = "" - + # Get playback volume + volume_query = { + "jsonrpc": "2.0", + "id": 1, + "method": "Application.GetProperties", + "params": { + "properties": ["volume", "muted"] + } + } + result = xbmc.executeJSONRPC(json.dumps(volume_query)) + result = json.loads(result) + result = result.get('result') + + volume = result.get('volume') + muted = result.get('muted') - # Post playback to server - # log("Sending POST play started: %s." % postdata, 2) - # self.doUtils(url, postBody=postdata, type="POST") - - # Ensure we do have a runtime - try: - runtime = int(runtime) - except ValueError: - runtime = xbmcplayer.getTotalTime() - log("Runtime is missing, Kodi runtime: %s" % runtime, 1) + # Postdata structure to send to Emby server + url = "{server}/:/timeline?" + postdata = { - playQueueVersion = utils.window( - 'playQueueVersion') - playQueueID = utils.window( - 'playQueueID') - playQueueItemID = utils.window( - 'plex_%s.playQueueItemID' % currentFile) - # Save data map for updates and position calls - data = { - 'playQueueVersion': playQueueVersion, - 'playQueueID': playQueueID, - 'playQueueItemID': playQueueItemID, - 'runtime': runtime * 1000, - 'item_id': itemId, - 'refresh_id': refresh_id, - 'currentfile': currentFile, - 'AudioStreamIndex': postdata['AudioStreamIndex'], - 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], - 'playmethod': playMethod, - 'Type': itemType, - 'currentPosition': int(seekTime) + 'QueueableMediaTypes': "Video", + 'CanSeek': True, + 'ItemId': itemId, + 'MediaSourceId': itemId, + 'PlayMethod': playMethod, + 'VolumeLevel': volume, + 'PositionTicks': int(seekTime * 10000000), + 'IsMuted': muted + } + + # Get the current audio track and subtitles + if playMethod == "Transcode": + # property set in PlayUtils.py + postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) + postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) + else: + # Get the current kodi audio and subtitles and convert to Emby equivalent + tracks_query = { + "jsonrpc": "2.0", + "id": 1, + "method": "Player.GetProperties", + "params": { + + "playerid": 1, + "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] } - - self.played_info[currentFile] = data - log("ADDING_FILE: %s" % self.played_info, 1) + } + result = xbmc.executeJSONRPC(json.dumps(tracks_query)) + result = json.loads(result) + result = result.get('result') - # log some playback stats - '''if(itemType != None): - if(self.playStats.get(itemType) != None): - count = self.playStats.get(itemType) + 1 - self.playStats[itemType] = count + try: # Audio tracks + indexAudio = result['currentaudiostream']['index'] + except (KeyError, TypeError): + indexAudio = 0 + + try: # Subtitles tracks + indexSubs = result['currentsubtitle']['index'] + except (KeyError, TypeError): + indexSubs = 0 + + try: # If subtitles are enabled + subsEnabled = result['subtitleenabled'] + except (KeyError, TypeError): + subsEnabled = "" + + # Postdata for the audio + postdata['AudioStreamIndex'] = indexAudio + 1 + + # Postdata for the subtitles + if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: + + # Number of audiotracks to help get Emby Index + audioTracks = len(xbmc.Player().getAvailableAudioStreams()) + mapping = window("%s.indexMapping" % embyitem) + + if mapping: # Set in playbackutils.py + + log("Mapping for external subtitles index: %s" % mapping, 2) + externalIndex = json.loads(mapping) + + if externalIndex.get(str(indexSubs)): + # If the current subtitle is in the mapping + postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] else: - self.playStats[itemType] = 1 - - if(playMethod != None): - if(self.playStats.get(playMethod) != None): - count = self.playStats.get(playMethod) + 1 - self.playStats[playMethod] = count - else: - self.playStats[playMethod] = 1''' + # Internal subtitle currently selected + subindex = indexSubs - len(externalIndex) + audioTracks + 1 + postdata['SubtitleStreamIndex'] = subindex + + else: # Direct paths enabled scenario or no external subtitles set + postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 + else: + postdata['SubtitleStreamIndex'] = "" + + + # Post playback to server + # log("Sending POST play started: %s." % postdata, 2) + # self.doUtils(url, postBody=postdata, type="POST") + + # Ensure we do have a runtime + try: + runtime = int(runtime) + except ValueError: + runtime = xbmcplayer.getTotalTime() + log("Runtime is missing, Kodi runtime: %s" % runtime, 1) + + playQueueVersion = window('playQueueVersion') + playQueueID = window('playQueueID') + playQueueItemID = window('plex_%s.playQueueItemID' % currentFile) + # Save data map for updates and position calls + data = { + 'playQueueVersion': playQueueVersion, + 'playQueueID': playQueueID, + 'playQueueItemID': playQueueItemID, + 'runtime': runtime * 1000, + 'item_id': itemId, + 'refresh_id': refresh_id, + 'currentfile': currentFile, + 'AudioStreamIndex': postdata['AudioStreamIndex'], + 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], + 'playmethod': playMethod, + 'Type': itemType, + 'currentPosition': int(seekTime) + } + + self.played_info[currentFile] = data + log("ADDING_FILE: %s" % self.played_info, 1) + + # log some playback stats + '''if(itemType != None): + if(self.playStats.get(itemType) != None): + count = self.playStats.get(itemType) + 1 + self.playStats[itemType] = count + else: + self.playStats[itemType] = 1 + + if(playMethod != None): + if(self.playStats.get(playMethod) != None): + count = self.playStats.get(playMethod) + 1 + self.playStats[playMethod] = count + else: + self.playStats[playMethod] = 1''' def reportPlayback(self): # Don't use if Plex Companion is enabled @@ -281,7 +282,7 @@ class Player(xbmc.Player): # only report playback if emby has initiated the playback (item_id has value) if data: - # Get playback information + # Get playback inforation itemId = data['item_id'] audioindex = data['AudioStreamIndex'] subtitleindex = data['SubtitleStreamIndex'] @@ -451,18 +452,13 @@ class Player(xbmc.Player): window = utils.window # Will be called when user stops xbmc playing a file log("ONPLAYBACK_STOPPED", 2) - window('emby_customPlaylist', clear=True) - window('emby_customPlaylist.seektime', clear=True) - window('emby_playbackProps', clear=True) - window('suspend_LibraryThread', clear=True) log("Clear playlist properties.", 1) self.stopAll() def onPlayBackEnded(self): # Will be called when xbmc stops playing a file self.logMsg("ONPLAYBACK_ENDED", 2) - utils.window('emby_customPlaylist.seektime', clear=True) - utils.window('suspend_LibraryThread', clear=True) + self.stopAll() def stopAll(self): diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index b11d762c..e4040f46 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -76,10 +76,10 @@ class SubscriptionManager: keyid = None count = 0 while not keyid: - if count > 10: + if count > 300: break keyid = WINDOW.getProperty('Plex_currently_playing_itemid') - xbmc.sleep(1000) + xbmc.sleep(100) count += 1 if keyid: self.lastkey = "/library/metadata/%s"%keyid diff --git a/service.py b/service.py index 325c61f9..bb561ed6 100644 --- a/service.py +++ b/service.py @@ -109,6 +109,7 @@ class Service(): # ws = wsc.WebSocket_Client() library = librarysync.LibrarySync() kplayer = player.Player() + xplayer = xbmc.Player() plx = PlexAPI.PlexAPI() plexCompanion = PlexCompanion.PlexCompanion() @@ -136,11 +137,11 @@ class Service(): if (user.currUser is not None) and user.HasAccess: # If an item is playing - if xbmc.Player().isPlaying(): + if xplayer.isPlaying(): try: # Update and report progress - playtime = xbmc.Player().getTime() - totalTime = xbmc.Player().getTotalTime() + playtime = xplayer.getTime() + totalTime = xplayer.getTotalTime() currentFile = kplayer.currentFile # Update positionticks