From 3480c8fb49b944c38c51e399e48af8cac0b91649 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 19 Sep 2021 13:38:25 +0200 Subject: [PATCH] Tell the PMS if a video's audio stream or potentially subtitle stream has changed. For subtitles, this functionality is broken due to a Kodi bug --- resources/lib/json_rpc.py | 35 +++++++ resources/lib/kodimonitor.py | 68 +++++++++---- resources/lib/playlist_func.py | 169 +++++++++++++++++++------------- resources/lib/plex_functions.py | 16 +++ resources/lib/variables.py | 4 + 5 files changed, 206 insertions(+), 86 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 1f6fa56a..5e970191 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -420,6 +420,41 @@ def get_item(playerid): 'properties': ['title', 'file']})['result']['item'] +def get_current_audio_stream_index(playerid): + """ + Returns the currently active audio stream index [int] + """ + return JsonRPC('Player.GetProperties').execute({ + 'playerid': playerid, + 'properties': ['currentaudiostream']})['result']['currentaudiostream']['index'] + + +def get_current_subtitle_stream_index(playerid): + """ + Returns the currently active subtitle stream index [int] or None if there + are no subs + PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The + JSON reply won't change even though subtitles are changed :-( + """ + try: + return JsonRPC('Player.GetProperties').execute({ + 'playerid': playerid, + 'properties': ['currentsubtitle', ]})['result']['currentsubtitle']['index'] + except KeyError: + pass + + +def get_subtitle_enabled(playerid): + """ + Returns True if a subtitle is currently enabled, False otherwise. + PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The + JSON reply won't change even though subtitles are changed :-( + """ + return JsonRPC('Player.GetProperties').execute({ + 'playerid': playerid, + 'properties': ['subtitleenabled', ]})['result']['subtitleenabled'] + + def get_player_props(playerid): """ Returns a dict for the active Kodi player with the following values: diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 7c518b17..de88a3c0 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -374,10 +374,39 @@ class KodiMonitor(xbmc.Monitor): Example data as returned by Kodi: {'item': {'id': 5, 'type': 'movie'}, 'player': {'playerid': 1, 'speed': 1}} + + PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! + Kodi subs will never change. Also see json_rpc.py """ + playerid = data['player']['playerid'] + if not playerid == v.KODI_VIDEO_PLAYER_ID: + # We're just messing with Kodi's videoplayer + return if not self._switched_to_plex_streams: + # We need to switch to the Plex streams ONCE upon playback start + # after onavchange has been fired self.switch_to_plex_streams() self._switched_to_plex_streams = True + else: + item = app.PLAYSTATE.item + if item is None: + # Player might've quit + return + kodi_audio_stream = js.get_current_audio_stream_index(playerid) + sub_enabled = js.get_subtitle_enabled(playerid) + kodi_sub_stream = js.get_current_subtitle_stream_index(playerid) + # Audio + if kodi_audio_stream != item.current_kodi_audio_stream: + item.on_kodi_audio_stream_change(kodi_audio_stream) + # Subtitles - CURRENTLY BROKEN ON THE KODI SIDE! + # current_kodi_sub_stream may also be zero + subs_off = (None, False) + if ((sub_enabled and item.current_kodi_sub_stream in subs_off) + or (not sub_enabled and item.current_kodi_sub_stream not in subs_off) + or (kodi_sub_stream is not None + and kodi_sub_stream != item.current_kodi_sub_stream)): + item.on_kodi_subtitle_stream_change(kodi_sub_stream, + sub_enabled) @staticmethod def switch_to_plex_streams(): @@ -392,30 +421,33 @@ class KodiMonitor(xbmc.Monitor): try: plex_index, language_tag = item.active_plex_stream_index(typus) except TypeError: - if typus == 'subtitle': - LOG.info('Deactivating Kodi subtitles because the PMS ' - 'told us to not show any subtitles') - app.APP.player.showSubtitles(False) + LOG.debug('Deactivating Kodi subtitles because the PMS ' + 'told us to not show any subtitles') + app.APP.player.showSubtitles(False) + item.current_kodi_sub_stream = False continue - LOG.info('The PMS wants to display %s stream with Plex id %s and ' - 'languageTag %s', - typus, plex_index, language_tag) - kodi_index = item.kodi_stream_index(plex_index, - typus) + LOG.debug('The PMS wants to display %s stream with Plex id %s and ' + 'languageTag %s', + typus, plex_index, language_tag) + kodi_index = item.kodi_stream_index(plex_index, typus) if kodi_index is None: - LOG.info('Leaving Kodi %s stream settings untouched since we ' - 'could not parse Plex %s stream with id %s to a Kodi ' - 'index', typus, typus, plex_index) + LOG.debug('Leaving Kodi %s stream settings untouched since we ' + 'could not parse Plex %s stream with id %s to a Kodi' + ' index', typus, typus, plex_index) else: - LOG.info('Switching to Kodi %s stream number %s because the ' - 'PMS told us to show stream with Plex id %s', - typus, kodi_index, plex_index) + LOG.debug('Switching to Kodi %s stream number %s because the ' + 'PMS told us to show stream with Plex id %s', + typus, kodi_index, plex_index) # If we're choosing an "illegal" index, this function does # need seem to fail nor log any errors - if typus == 'subtitle': - app.APP.player.setSubtitleStream(kodi_index) - else: + if typus == 'audio': app.APP.player.setAudioStream(kodi_index) + else: + app.APP.player.setSubtitleStream(kodi_index) + if typus == 'audio': + item.current_kodi_audio_stream = kodi_index + else: + item.current_kodi_sub_stream = kodi_index def _playback_cleanup(ended=False): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index a0faf18c..153b0aed 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -11,6 +11,7 @@ from . import plex_functions as PF from .kodi_db import kodiid_from_filename from .downloadutils import DownloadUtils as DU from . import utils +from .utils import cast from . import json_rpc as js from . import variables as v from . import app @@ -175,6 +176,16 @@ class PlaylistItem(object): # False: do NOT resume, don't ask user # True: do resume, don't ask user self.resume = None + # Get the Plex audio and subtitle streams in the same order as Kodi + # uses them (Kodi uses indexes to activate them, not ids like Plex) + self._streams_have_been_processed = False + self._audio_streams = None + self._subtitle_streams = None + # Which Kodi streams are active? + self.current_kodi_audio_stream = None + # False means "deactivated", None means "we do not have a Kodi + # equivalent for this Plex subtitle" + self.current_kodi_sub_stream = None @property def plex_id(self): @@ -190,6 +201,18 @@ class PlaylistItem(object): def uri(self): return self._uri + @property + def audio_streams(self): + if not self._streams_have_been_processed: + self._process_streams() + return self._audio_streams + + @property + def subtitle_streams(self): + if not self._streams_have_been_processed: + self._process_streams() + return self._subtitle_streams + def __repr__(self): return ("{{" "'id': {self.id}, " @@ -206,85 +229,100 @@ class PlaylistItem(object): "'force_transcode': {self.force_transcode}, " "'part': {self.part}".format(self=self)) + def _process_streams(self): + """ + Builds audio and subtitle streams and enables matching between Plex + and Kodi using self.audio_streams and self.subtitle_streams + """ + # The playqueue response from the PMS does not contain a stream filename + # thanks Plex + self._subtitle_streams = accessible_plex_subtitles( + self.playmethod, + self.file, + self.api.plex_media_streams()) + # Audio streams are much easier - they're always available and sorted + # the same in Kodi and Plex + self._audio_streams = [x for x in self.api.plex_media_streams() + if x.get('streamType') == '2'] + self._streams_have_been_processed = True + + def _get_iterator(self, stream_type): + if stream_type == 'audio': + return self.audio_streams + elif stream_type == 'subtitle': + return self.subtitle_streams + def plex_stream_index(self, kodi_stream_index, stream_type): """ Pass in the kodi_stream_index [int] in order to receive the Plex stream - index. - + index [int]. stream_type: 'video', 'audio', 'subtitle' - Returns None if unsuccessful """ - stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] - count = 0 - if kodi_stream_index == -1: - # Kodi telling us "it's the last one" - iterator = list(reversed(self.api.plex_media_streams())) - kodi_stream_index = 0 - else: - iterator = self.api.plex_media_streams() - # Kodi indexes differently than Plex - for stream in iterator: - if (stream.get('streamType') == stream_type and - 'key' in stream.attrib): - if count == kodi_stream_index: - return stream.get('id') - count += 1 - for stream in iterator: - if (stream.get('streamType') == stream_type and - 'key' not in stream.attrib): - if count == kodi_stream_index: - return stream.get('id') - count += 1 + if stream_type == 'audio': + return int(self.audio_streams[kodi_stream_index].get('id')) + elif stream_type == 'subtitle': + try: + return int(self.subtitle_streams[kodi_stream_index].get('id')) + except (IndexError, TypeError): + pass def kodi_stream_index(self, plex_stream_index, stream_type): """ - Pass in the kodi_stream_index [int] in order to receive the Plex stream - index. - + Pass in the plex_stream_index [int] in order to receive the Kodi stream + index [int]. stream_type: 'video', 'audio', 'subtitle' - Returns None if unsuccessful """ if plex_stream_index is None: return - stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] - count = 0 - streams = self.sorted_accessible_plex_subtitles(stream_type) - for stream in streams: - if utils.cast(int, stream.get('id')) == plex_stream_index: - return count - count += 1 + for i, stream in enumerate(self._get_iterator(stream_type)): + if cast(int, stream.get('id')) == plex_stream_index: + return i def active_plex_stream_index(self, stream_type): """ Returns the following tuple for the active stream on the Plex side: - (id [int], languageTag [str]) + (Plex stream id [int], languageTag [str] or None) Returns None if no stream has been selected """ - stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] - for stream in self.api.plex_media_streams(): - if stream.get('streamType') == stream_type \ - and stream.get('selected') == '1': - return (utils.cast(int, stream.get('id')), - stream.get('languageTag')) + for i, stream in enumerate(self._get_iterator(stream_type)): + if stream.get('selected') == '1': + return (int(stream.get('id')), stream.get('languageTag')) - def sorted_accessible_plex_subtitles(self, stream_type): + def on_kodi_subtitle_stream_change(self, kodi_stream_index, subs_enabled): """ - Returns only the subtitles that Kodi can access when PKC Direct Paths - are used; i.e. Kodi has access to a video's directory. - NOT supported: additional subtitles downloaded using the Plex interface + Call this method if Kodi changed its subtitle and you want Plex to + know. """ - # The playqueue response from the PMS does not contain a stream filename - # thanks Plex - if stream_type == '3': - streams = accessible_plex_subtitles(self.playmethod, - self.file, - self.api.plex_media_streams()) + if subs_enabled: + try: + plex_stream_index = int(self.subtitle_streams[kodi_stream_index].get('id')) + except (IndexError, TypeError): + LOG.debug('Kodi subtitle change detected to a sub %s that is ' + 'NOT available on the Plex side', kodi_stream_index) + self.current_kodi_sub_stream = None + return + LOG.debug('Kodi subtitle change detected: telling Plex about ' + 'switch to index %s, Plex stream id %s', + kodi_stream_index, plex_stream_index) + self.current_kodi_sub_stream = kodi_stream_index else: - streams = [x for x in self.api.plex_media_streams() - if x.get('streamType') == stream_type] - return streams + plex_stream_index = 0 + LOG.debug('Kodi subtitle has been deactivated, telling Plex') + self.current_kodi_sub_stream = False + PF.change_subtitle(plex_stream_index, self.api.part_id()) + + def on_kodi_audio_stream_change(self, kodi_stream_index): + """ + Call this method if Kodi changed its audio stream and you want Plex to + know. kodi_stream_index [int] + """ + plex_stream_index = int(self.audio_streams[kodi_stream_index].get('id')) + LOG.debug('Changing Plex audio stream to %s, Kodi index %s', + plex_stream_index, kodi_stream_index) + PF.change_audio_stream(plex_stream_index, self.api.part_id()) + self.current_kodi_audio_stream = kodi_stream_index def playlist_item_from_kodi(kodi_item): @@ -310,7 +348,7 @@ def playlist_item_from_kodi(kodi_item): except IndexError: query = '' query = dict(utils.parse_qsl(query)) - item.plex_id = utils.cast(int, query.get('plex_id')) + item.plex_id = cast(int, query.get('plex_id')) item.plex_type = query.get('itemType') LOG.debug('Made playlist item from Kodi: %s', item) return item @@ -437,18 +475,13 @@ def get_playlist_details_from_xml(playlist, xml): """ if xml is None: raise PlaylistError('No playlist received for playlist %s' % playlist) - playlist.id = utils.cast(int, - xml.get('%sID' % playlist.kind)) - playlist.version = utils.cast(int, - xml.get('%sVersion' % playlist.kind)) - playlist.shuffled = utils.cast(int, - xml.get('%sShuffled' % playlist.kind)) - playlist.selectedItemID = utils.cast(int, - xml.get('%sSelectedItemID' - % playlist.kind)) - playlist.selectedItemOffset = utils.cast(int, - xml.get('%sSelectedItemOffset' - % playlist.kind)) + playlist.id = cast(int, xml.get('%sID' % playlist.kind)) + playlist.version = cast(int, xml.get('%sVersion' % playlist.kind)) + playlist.shuffled = cast(int, xml.get('%sShuffled' % playlist.kind)) + playlist.selectedItemID = cast(int, xml.get('%sSelectedItemID' + % playlist.kind)) + playlist.selectedItemOffset = cast(int, xml.get('%sSelectedItemOffset' + % playlist.kind)) LOG.debug('Updated playlist from xml: %s', playlist) diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index 03deaf6c..23ab6814 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -1132,3 +1132,19 @@ def change_subtitle(plex_stream_id, part_id): url = '{server}/library/parts/%s' % part_id return DU().downloadUrl(utils.extend_url(url, arguments), action_type='PUT') + + +def change_audio_stream(plex_stream_id, part_id): + """ + Tell the PMS to display/burn-in the subtitle stream with id plex_stream_id + for the Part (PMS XML etree tag "Part") with unique id part_id. + - plex_stream_id = 0 will deactivate the subtitle + - We always do this for ALL parts of a video + """ + arguments = { + 'audioStreamID': plex_stream_id, + 'allParts': 1 + } + url = '{server}/library/parts/%s' % part_id + return DU().downloadUrl(utils.extend_url(url, arguments), + action_type='PUT') diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 0da3dd67..5771d886 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -29,6 +29,10 @@ ADDON_PATH = _ADDON.getAddonInfo('path') ADDON_FOLDER = xbmcvfs.translatePath('special://home') ADDON_PROFILE = xbmcvfs.translatePath(_ADDON.getAddonInfo('profile')) +# Used e.g. for json_rpc +KODI_VIDEO_PLAYER_ID = 1 +KODI_AUDIO_PLAYER_ID = 0 + KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')