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:
parent
bce51224f2
commit
3480c8fb49
5 changed files with 206 additions and 86 deletions
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue