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

This commit is contained in:
croneter 2021-09-19 13:38:25 +02:00
parent bce51224f2
commit 3480c8fb49
5 changed files with 206 additions and 86 deletions

View file

@ -420,6 +420,41 @@ def get_item(playerid):
'properties': ['title', 'file']})['result']['item'] '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): def get_player_props(playerid):
""" """
Returns a dict for the active Kodi player with the following values: Returns a dict for the active Kodi player with the following values:

View file

@ -374,10 +374,39 @@ class KodiMonitor(xbmc.Monitor):
Example data as returned by Kodi: Example data as returned by Kodi:
{'item': {'id': 5, 'type': 'movie'}, {'item': {'id': 5, 'type': 'movie'},
'player': {'playerid': 1, 'speed': 1}} '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: 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.switch_to_plex_streams()
self._switched_to_plex_streams = True 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 @staticmethod
def switch_to_plex_streams(): def switch_to_plex_streams():
@ -392,30 +421,33 @@ class KodiMonitor(xbmc.Monitor):
try: try:
plex_index, language_tag = item.active_plex_stream_index(typus) plex_index, language_tag = item.active_plex_stream_index(typus)
except TypeError: except TypeError:
if typus == 'subtitle': LOG.debug('Deactivating Kodi subtitles because the PMS '
LOG.info('Deactivating Kodi subtitles because the PMS '
'told us to not show any subtitles') 'told us to not show any subtitles')
app.APP.player.showSubtitles(False) app.APP.player.showSubtitles(False)
item.current_kodi_sub_stream = False
continue continue
LOG.info('The PMS wants to display %s stream with Plex id %s and ' LOG.debug('The PMS wants to display %s stream with Plex id %s and '
'languageTag %s', 'languageTag %s',
typus, plex_index, language_tag) typus, plex_index, language_tag)
kodi_index = item.kodi_stream_index(plex_index, kodi_index = item.kodi_stream_index(plex_index, typus)
typus)
if kodi_index is None: if kodi_index is None:
LOG.info('Leaving Kodi %s stream settings untouched since we ' LOG.debug('Leaving Kodi %s stream settings untouched since we '
'could not parse Plex %s stream with id %s to a Kodi ' 'could not parse Plex %s stream with id %s to a Kodi'
'index', typus, typus, plex_index) ' index', typus, typus, plex_index)
else: else:
LOG.info('Switching to Kodi %s stream number %s because the ' LOG.debug('Switching to Kodi %s stream number %s because the '
'PMS told us to show stream with Plex id %s', 'PMS told us to show stream with Plex id %s',
typus, kodi_index, plex_index) typus, kodi_index, plex_index)
# If we're choosing an "illegal" index, this function does # If we're choosing an "illegal" index, this function does
# need seem to fail nor log any errors # need seem to fail nor log any errors
if typus == 'subtitle': if typus == 'audio':
app.APP.player.setSubtitleStream(kodi_index)
else:
app.APP.player.setAudioStream(kodi_index) 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): def _playback_cleanup(ended=False):

View file

@ -11,6 +11,7 @@ from . import plex_functions as PF
from .kodi_db import kodiid_from_filename from .kodi_db import kodiid_from_filename
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils from . import utils
from .utils import cast
from . import json_rpc as js from . import json_rpc as js
from . import variables as v from . import variables as v
from . import app from . import app
@ -175,6 +176,16 @@ class PlaylistItem(object):
# False: do NOT resume, don't ask user # False: do NOT resume, don't ask user
# True: do resume, don't ask user # True: do resume, don't ask user
self.resume = None 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 @property
def plex_id(self): def plex_id(self):
@ -190,6 +201,18 @@ class PlaylistItem(object):
def uri(self): def uri(self):
return self._uri 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): def __repr__(self):
return ("{{" return ("{{"
"'id': {self.id}, " "'id': {self.id}, "
@ -206,85 +229,100 @@ class PlaylistItem(object):
"'force_transcode': {self.force_transcode}, " "'force_transcode': {self.force_transcode}, "
"'part': {self.part}".format(self=self)) "'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): def plex_stream_index(self, kodi_stream_index, stream_type):
""" """
Pass in the kodi_stream_index [int] in order to receive the Plex stream Pass in the kodi_stream_index [int] in order to receive the Plex stream
index. index [int].
stream_type: 'video', 'audio', 'subtitle' stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful Returns None if unsuccessful
""" """
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] if stream_type == 'audio':
count = 0 return int(self.audio_streams[kodi_stream_index].get('id'))
if kodi_stream_index == -1: elif stream_type == 'subtitle':
# Kodi telling us "it's the last one" try:
iterator = list(reversed(self.api.plex_media_streams())) return int(self.subtitle_streams[kodi_stream_index].get('id'))
kodi_stream_index = 0 except (IndexError, TypeError):
else: pass
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
def kodi_stream_index(self, plex_stream_index, stream_type): def kodi_stream_index(self, plex_stream_index, stream_type):
""" """
Pass in the kodi_stream_index [int] in order to receive the Plex stream Pass in the plex_stream_index [int] in order to receive the Kodi stream
index. index [int].
stream_type: 'video', 'audio', 'subtitle' stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful Returns None if unsuccessful
""" """
if plex_stream_index is None: if plex_stream_index is None:
return return
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] for i, stream in enumerate(self._get_iterator(stream_type)):
count = 0 if cast(int, stream.get('id')) == plex_stream_index:
streams = self.sorted_accessible_plex_subtitles(stream_type) return i
for stream in streams:
if utils.cast(int, stream.get('id')) == plex_stream_index:
return count
count += 1
def active_plex_stream_index(self, stream_type): def active_plex_stream_index(self, stream_type):
""" """
Returns the following tuple for the active stream on the Plex side: 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 Returns None if no stream has been selected
""" """
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] for i, stream in enumerate(self._get_iterator(stream_type)):
for stream in self.api.plex_media_streams(): if stream.get('selected') == '1':
if stream.get('streamType') == stream_type \ return (int(stream.get('id')), stream.get('languageTag'))
and stream.get('selected') == '1':
return (utils.cast(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 Call this method if Kodi changed its subtitle and you want Plex to
are used; i.e. Kodi has access to a video's directory. know.
NOT supported: additional subtitles downloaded using the Plex interface
""" """
# The playqueue response from the PMS does not contain a stream filename if subs_enabled:
# thanks Plex try:
if stream_type == '3': plex_stream_index = int(self.subtitle_streams[kodi_stream_index].get('id'))
streams = accessible_plex_subtitles(self.playmethod, except (IndexError, TypeError):
self.file, LOG.debug('Kodi subtitle change detected to a sub %s that is '
self.api.plex_media_streams()) '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: else:
streams = [x for x in self.api.plex_media_streams() plex_stream_index = 0
if x.get('streamType') == stream_type] LOG.debug('Kodi subtitle has been deactivated, telling Plex')
return streams 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): def playlist_item_from_kodi(kodi_item):
@ -310,7 +348,7 @@ def playlist_item_from_kodi(kodi_item):
except IndexError: except IndexError:
query = '' query = ''
query = dict(utils.parse_qsl(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') item.plex_type = query.get('itemType')
LOG.debug('Made playlist item from Kodi: %s', item) LOG.debug('Made playlist item from Kodi: %s', item)
return item return item
@ -437,17 +475,12 @@ def get_playlist_details_from_xml(playlist, xml):
""" """
if xml is None: if xml is None:
raise PlaylistError('No playlist received for playlist %s' % playlist) raise PlaylistError('No playlist received for playlist %s' % playlist)
playlist.id = utils.cast(int, playlist.id = cast(int, xml.get('%sID' % playlist.kind))
xml.get('%sID' % playlist.kind)) playlist.version = cast(int, xml.get('%sVersion' % playlist.kind))
playlist.version = utils.cast(int, playlist.shuffled = cast(int, xml.get('%sShuffled' % playlist.kind))
xml.get('%sVersion' % playlist.kind)) playlist.selectedItemID = cast(int, xml.get('%sSelectedItemID'
playlist.shuffled = utils.cast(int,
xml.get('%sShuffled' % playlist.kind))
playlist.selectedItemID = utils.cast(int,
xml.get('%sSelectedItemID'
% playlist.kind)) % playlist.kind))
playlist.selectedItemOffset = utils.cast(int, playlist.selectedItemOffset = cast(int, xml.get('%sSelectedItemOffset'
xml.get('%sSelectedItemOffset'
% playlist.kind)) % playlist.kind))
LOG.debug('Updated playlist from xml: %s', playlist) LOG.debug('Updated playlist from xml: %s', playlist)

View file

@ -1132,3 +1132,19 @@ def change_subtitle(plex_stream_id, part_id):
url = '{server}/library/parts/%s' % part_id url = '{server}/library/parts/%s' % part_id
return DU().downloadUrl(utils.extend_url(url, arguments), return DU().downloadUrl(utils.extend_url(url, arguments),
action_type='PUT') 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')

View file

@ -29,6 +29,10 @@ ADDON_PATH = _ADDON.getAddonInfo('path')
ADDON_FOLDER = xbmcvfs.translatePath('special://home') ADDON_FOLDER = xbmcvfs.translatePath('special://home')
ADDON_PROFILE = xbmcvfs.translatePath(_ADDON.getAddonInfo('profile')) 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) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion') KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')