Merge pull request #1674 from croneter/py3-fix-socketserver

Fix Plex Companion not working by fixing some issues with PKC's http.server's BaseHTTPRequestHandler
This commit is contained in:
croneter 2021-10-20 14:55:58 +02:00 committed by GitHub
commit 937265dfc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 194 additions and 99 deletions

View file

@ -29,7 +29,6 @@ def getXArgsDeviceInfo(options=None, include_token=True):
"""
xargs = {
'Accept': '*/*',
'Connection': 'keep-alive',
"Content-Type": "application/x-www-form-urlencoded",
# "Access-Control-Allow-Origin": "*",
'Accept-Language': xbmc.getLanguage(xbmc.ISO_639_1),
@ -42,6 +41,8 @@ def getXArgsDeviceInfo(options=None, include_token=True):
'X-Plex-Version': v.ADDON_VERSION,
'X-Plex-Client-Identifier': getDeviceId(),
'X-Plex-Provides': 'client,controller,player,pubsub-player',
'X-Plex-Protocol': '1.0',
'Cache-Control': 'no-cache'
}
if include_token and utils.window('pms_token'):
xargs['X-Plex-Token'] = utils.window('pms_token')

View file

@ -429,6 +429,15 @@ def get_current_audio_stream_index(playerid):
'properties': ['currentaudiostream']})['result']['currentaudiostream']['index']
def get_current_video_stream_index(playerid):
"""
Returns the currently active video stream index [int]
"""
return JsonRPC('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['currentvideostream']})['result']['currentvideostream']['index']
def get_current_subtitle_stream_index(playerid):
"""
Returns the currently active subtitle stream index [int] or None if there

View file

@ -390,6 +390,8 @@ class KodiMonitor(xbmc.Monitor):
if not self._switched_to_plex_streams:
# We need to switch to the Plex streams ONCE upon playback start
# after onavchange has been fired
item.init_kodi_streams()
item.switch_to_plex_stream('video')
if utils.settings('audioStreamPick') == '0':
item.switch_to_plex_stream('audio')
if utils.settings('subtitleStreamPick') == '0':

View file

@ -179,9 +179,11 @@ class PlaylistItem(object):
# 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._video_streams = None
self._audio_streams = None
self._subtitle_streams = None
# Which Kodi streams are active?
self.current_kodi_video_stream = None
self.current_kodi_audio_stream = None
# False means "deactivated", None means "we do not have a Kodi
# equivalent for this Plex subtitle"
@ -201,6 +203,12 @@ class PlaylistItem(object):
def uri(self):
return self._uri
@property
def video_streams(self):
if not self._streams_have_been_processed:
self._process_streams()
return self._video_streams
@property
def audio_streams(self):
if not self._streams_have_been_processed:
@ -213,6 +221,18 @@ class PlaylistItem(object):
self._process_streams()
return self._subtitle_streams
@property
def current_plex_video_stream(self):
return self.plex_stream_index(self.current_kodi_video_stream, 'video')
@property
def current_plex_audio_stream(self):
return self.plex_stream_index(self.current_kodi_audio_stream, 'audio')
@property
def current_plex_sub_stream(self):
return self.plex_stream_index(self.current_kodi_sub_stream, 'subtitle')
def __repr__(self):
return ("{{"
"'id': {self.id}, "
@ -244,6 +264,13 @@ class PlaylistItem(object):
# the same in Kodi and Plex
self._audio_streams = [x for x in self.api.plex_media_streams()
if x.get('streamType') == '2']
# Same for video streams
self._video_streams = [x for x in self.api.plex_media_streams()
if x.get('streamType') == '1']
if len(self._video_streams) == 1:
# Add a selected = "1" attribute to let our logic stand!
# Missing if there is only 1 video stream present
self._video_streams[0].set('selected', '1')
self._streams_have_been_processed = True
def _get_iterator(self, stream_type):
@ -251,6 +278,17 @@ class PlaylistItem(object):
return self.audio_streams
elif stream_type == 'subtitle':
return self.subtitle_streams
elif stream_type == 'video':
return self.video_streams
def init_kodi_streams(self):
"""
Initializes all streams after Kodi has started playing this video
"""
self.current_kodi_video_stream = js.get_current_video_stream_index(v.KODI_VIDEO_PLAYER_ID)
self.current_kodi_audio_stream = js.get_current_audio_stream_index(v.KODI_VIDEO_PLAYER_ID)
self.current_kodi_sub_stream = False if not js.get_subtitle_enabled(v.KODI_VIDEO_PLAYER_ID) \
else js.get_current_subtitle_stream_index(v.KODI_VIDEO_PLAYER_ID)
def plex_stream_index(self, kodi_stream_index, stream_type):
"""
@ -261,6 +299,8 @@ class PlaylistItem(object):
"""
if stream_type == 'audio':
return int(self.audio_streams[kodi_stream_index].get('id'))
elif stream_type == 'video':
return int(self.video_streams[kodi_stream_index].get('id'))
elif stream_type == 'subtitle':
try:
return int(self.subtitle_streams[kodi_stream_index].get('id'))
@ -324,10 +364,39 @@ class PlaylistItem(object):
PF.change_audio_stream(plex_stream_index, self.api.part_id())
self.current_kodi_audio_stream = kodi_stream_index
def on_kodi_video_stream_change(self, kodi_stream_index):
"""
Call this method if Kodi changed its video stream and you want Plex to
know. kodi_stream_index [int]
"""
plex_stream_index = int(self.video_streams[kodi_stream_index].get('id'))
LOG.debug('Changing Plex video stream to %s, Kodi index %s',
plex_stream_index, kodi_stream_index)
PF.change_video_stream(plex_stream_index, self.api.part_id())
self.current_kodi_video_stream = kodi_stream_index
def switch_to_plex_streams(self):
self.switch_to_plex_stream('video')
self.switch_to_plex_stream('audio')
self.switch_to_plex_stream('subtitle')
@staticmethod
def _set_kodi_stream_if_different(kodi_index, typus):
if typus == 'video':
current = js.get_current_video_stream_index(v.KODI_VIDEO_PLAYER_ID)
if current != kodi_index:
LOG.debug('Switching video stream')
app.APP.player.setVideoStream(kodi_index)
else:
LOG.debug('Not switching video stream (no change)')
elif typus == 'audio':
current = js.get_current_audio_stream_index(v.KODI_VIDEO_PLAYER_ID)
if current != kodi_index:
LOG.debug('Switching audio stream')
app.APP.player.setAudioStream(kodi_index)
else:
LOG.debug('Not switching audio stream (no change)')
def switch_to_plex_stream(self, typus):
try:
plex_index, language_tag = self.active_plex_stream_index(typus)
@ -351,22 +420,34 @@ class PlaylistItem(object):
# If we're choosing an "illegal" index, this function does
# need seem to fail nor log any errors
if typus == 'audio':
app.APP.player.setAudioStream(kodi_index)
else:
self._set_kodi_stream_if_different(kodi_index, 'audio')
elif typus == 'subtitle':
app.APP.player.setSubtitleStream(kodi_index)
app.APP.player.showSubtitles(True)
elif typus == 'video':
self._set_kodi_stream_if_different(kodi_index, 'video')
if typus == 'audio':
self.current_kodi_audio_stream = kodi_index
else:
elif typus == 'subtitle':
self.current_kodi_sub_stream = kodi_index
elif typus == 'video':
self.current_kodi_video_stream = kodi_index
def on_av_change(self, playerid):
"""
Call this method if Kodi reports an "AV-Change"
(event "Player.OnAVChange")
"""
kodi_video_stream = js.get_current_video_stream_index(playerid)
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 != self.current_kodi_audio_stream:
self.on_kodi_audio_stream_change(kodi_audio_stream)
# Video
if kodi_video_stream != self.current_kodi_video_stream:
self.on_kodi_video_stream_change(kodi_audio_stream)
# Subtitles - CURRENTLY BROKEN ON THE KODI SIDE!
# current_kodi_sub_stream may also be zero
subs_off = (None, False)
@ -376,6 +457,32 @@ class PlaylistItem(object):
and kodi_sub_stream != self.current_kodi_sub_stream)):
self.on_kodi_subtitle_stream_change(kodi_sub_stream, sub_enabled)
def on_plex_stream_change(self, plex_data):
"""
Call this method if Plex Companion wants to change streams
"""
if 'audioStreamID' in plex_data:
plex_index = int(plex_data['audioStreamID'])
kodi_index = self.kodi_stream_index(plex_index, 'audio')
self._set_kodi_stream_if_different(kodi_index, 'audio')
self.current_kodi_audio_stream = kodi_index
if 'videoStreamID' in plex_data:
plex_index = int(plex_data['videoStreamID'])
kodi_index = self.kodi_stream_index(plex_index, 'video')
self._set_kodi_stream_if_different(kodi_index, 'video')
self.current_kodi_video_stream = kodi_index
if 'subtitleStreamID' in plex_data:
plex_index = int(plex_data['subtitleStreamID'])
if plex_index == 0:
app.APP.player.showSubtitles(False)
kodi_index = False
else:
kodi_index = self.kodi_stream_index(plex_index, 'subtitle')
if kodi_index:
app.APP.player.setSubtitleStream(kodi_index)
app.APP.player.showSubtitles(True)
self.current_kodi_sub_stream = kodi_index
def playlist_item_from_kodi(kodi_item):
"""

View file

@ -191,19 +191,7 @@ class PlexCompanion(backgroundthread.KillableThread):
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['audioStreamID'], 'audio')
app.APP.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
app.APP.player.showSubtitles(False)
else:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
app.APP.player.setSubtitleStream(index)
else:
LOG.error('Unknown setStreams command: %s', data)
playqueue.items[pos].on_plex_stream_change(data)
@staticmethod
def _process_refresh(data):
@ -300,7 +288,7 @@ class PlexCompanion(backgroundthread.KillableThread):
start_count = 0
while True:
try:
httpd = listener.ThreadedHTTPServer(
httpd = listener.PKCHTTPServer(
client,
subscription_manager,
('', v.COMPANION_PORT),

View file

@ -1148,3 +1148,17 @@ def change_audio_stream(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_video_stream(plex_stream_id, part_id):
"""
Tell the PMS to display another video stream
- We always do this for ALL parts of a video
"""
arguments = {
'videoStreamID': 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

@ -5,8 +5,7 @@ Plex Companion listener
"""
from logging import getLogger
from re import sub
from socketserver import ThreadingMixIn
from http.server import HTTPServer, BaseHTTPRequestHandler
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from .. import utils, companion, json_rpc as js, clientinfo, variables as v
from .. import app
@ -45,7 +44,7 @@ class MyHandler(BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
self.serverlist = []
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
super().__init__(*args, **kwargs)
def log_message(self, format, *args):
'''
@ -62,6 +61,7 @@ class MyHandler(BaseHTTPRequestHandler):
self.answer_request(1)
def do_OPTIONS(self):
LOG.debug("Serving OPTIONS request...")
self.send_response(200)
self.send_header('Content-Length', '0')
self.send_header('X-Plex-Client-Identifier', v.PKC_MACHINE_IDENTIFIER)
@ -78,24 +78,16 @@ class MyHandler(BaseHTTPRequestHandler):
'x-plex-device-name, x-plex-platform, x-plex-product, accept, '
'x-plex-device, x-plex-device-screen-resolution')
self.end_headers()
self.wfile.close()
def sendOK(self):
self.send_response(200)
def response(self, body, headers=None, code=200):
headers = {} if headers is None else headers
try:
self.send_response(code)
for key in headers:
self.send_header(key, headers[key])
self.send_header('Content-Length', len(body))
self.send_header('Connection', "close")
self.end_headers()
if body:
self.wfile.write(body.encode('utf-8'))
self.wfile.close()
except Exception:
pass
def answer_request(self, send_data):
self.serverlist = self.server.client.getServerList()
@ -108,23 +100,37 @@ class MyHandler(BaseHTTPRequestHandler):
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]
LOG.debug("remote request_path: %s", request_path)
LOG.debug("remote request_path: %s, received from %s with headers: %s",
request_path, self.client_address, self.headers.items())
LOG.debug("params received from remote: %s", params)
sub_mgr.update_command_id(self.headers.get(
'X-Plex-Client-Identifier', self.client_address[0]),
params.get('commandID'))
conntype = self.headers.get('Connection', '')
if conntype.lower() == 'keep-alive':
headers = {
'Connection': 'Keep-Alive',
'Keep-Alive': 'timeout=20'
}
else:
headers = {'Connection': 'Close'}
if request_path == "version":
self.response(
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
% v.ADDON_VERSION)
% v.ADDON_VERSION,
headers)
elif request_path == "verify":
self.response("XBMC JSON connection test:\n" + js.ping())
self.response("XBMC JSON connection test:\n" + js.ping(),
headers)
elif request_path == 'resources':
self.response(
RESOURCES_XML.format(
title=v.DEVICENAME,
machineIdentifier=v.PKC_MACHINE_IDENTIFIER),
clientinfo.getXArgsDeviceInfo(include_token=False))
clientinfo.getXArgsDeviceInfo(options=headers,
include_token=False))
elif request_path == 'player/timeline/poll':
# Plex web does polling if connected to PKC via Companion
# Only reply if there is indeed something playing
@ -159,7 +165,7 @@ class MyHandler(BaseHTTPRequestHandler):
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Content-Type': 'text/xml;charset=utf-8'
})
}.update(headers))
elif not sub_mgr.stop_sent_to_web:
sub_mgr.stop_sent_to_web = True
LOG.debug('Signaling STOP to Plex Web')
@ -173,11 +179,11 @@ class MyHandler(BaseHTTPRequestHandler):
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Content-Type': 'text/xml;charset=utf-8'
})
}.update(headers))
else:
# Fail connection with HTTP 500 error - has been open too long
# We're not playing anything yet, just reply with a 200
self.response(
'Need to close this connection on the PKC side',
msg,
{
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Protocol': '1.0',
@ -186,11 +192,12 @@ class MyHandler(BaseHTTPRequestHandler):
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Content-Type': 'text/xml;charset=utf-8'
},
code=500)
}.update(headers))
elif "/subscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
clientinfo.getXArgsDeviceInfo(include_token=False))
headers['Content-Type'] = 'text/xml;charset=utf-8'
headers = clientinfo.getXArgsDeviceInfo(options=headers,
include_token=False)
self.response(v.COMPANION_OK_MESSAGE, headers)
protocol = params.get('protocol')
host = self.client_address[0]
port = params.get('port')
@ -202,23 +209,23 @@ class MyHandler(BaseHTTPRequestHandler):
uuid,
command_id)
elif "/unsubscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
clientinfo.getXArgsDeviceInfo(include_token=False))
headers['Content-Type'] = 'text/xml;charset=utf-8'
headers = clientinfo.getXArgsDeviceInfo(options=headers,
include_token=False)
self.response(v.COMPANION_OK_MESSAGE, headers)
uuid = self.headers.get('X-Plex-Client-Identifier') \
or self.client_address[0]
sub_mgr.remove_subscriber(uuid)
else:
# Throw it to companion.py
companion.process_command(request_path, params)
self.response('', clientinfo.getXArgsDeviceInfo(include_token=False))
headers['Content-Type'] = 'text/xml;charset=utf-8'
headers = clientinfo.getXArgsDeviceInfo(options=headers,
include_token=False)
self.response(v.COMPANION_OK_MESSAGE, headers)
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""
Using ThreadingMixIn Thread magic
"""
daemon_threads = True
class PKCHTTPServer(ThreadingHTTPServer):
def __init__(self, client, subscription_manager, *args, **kwargs):
"""
client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
@ -228,4 +235,4 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""
self.client = client
self.subscription_manager = subscription_manager
HTTPServer.__init__(self, *args, **kwargs)
super().__init__(*args, **kwargs)

View file

@ -93,12 +93,12 @@ class plexgdm(object):
try:
log.debug("Sending registration data: HELLO %s\n%s"
% (self.client_header, self.client_data))
self.update_sock.sendto("HELLO %s\n%s"
% (self.client_header, self.client_data),
msg = 'HELLO {}\n{}'.format(self.client_header, self.client_data)
self.update_sock.sendto(msg.encode('utf-8'),
self.client_register_group)
log.debug('(Re-)registering PKC Plex Companion successful')
except Exception:
log.error("Unable to send registration message")
except Exception as exc:
log.error("Unable to send registration message. Error: %s", exc)
def client_update(self):
self.update_sock = socket.socket(socket.AF_INET,

View file

@ -249,27 +249,10 @@ class SubscriptionMgr(object):
answ['token'] = playqueue.plex_transient_token
# Process audio and subtitle streams
if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
strm_id = self._plex_stream_index(playerid, 'audio')
if strm_id:
answ['audioStreamID'] = strm_id
else:
LOG.error('We could not select a Plex audiostream')
strm_id = self._plex_stream_index(playerid, 'video')
if strm_id:
answ['videoStreamID'] = strm_id
else:
LOG.error('We could not select a Plex videostream')
if info['subtitleenabled']:
try:
strm_id = self._plex_stream_index(playerid, 'subtitle')
except KeyError:
# subtitleenabled can be True while currentsubtitle can
# still be {}
strm_id = None
if strm_id is not None:
# If None, then the subtitle is only present on Kodi
# side
answ['subtitleStreamID'] = strm_id
answ['videoStreamID'] = str(item.current_plex_video_stream)
answ['audioStreamID'] = str(item.current_plex_audio_stream)
# Mind the zero - meaning subs are deactivated
answ['subtitleStreamID'] = str(item.current_plex_sub_stream or 0)
return answ
def signal_stop(self):
@ -285,22 +268,6 @@ class SubscriptionMgr(object):
self.last_params,
timeout=0.0001)
def _plex_stream_index(self, playerid, stream_type):
"""
Returns the current Plex stream index [str] for the player playerid
stream_type: 'video', 'audio', 'subtitle'
"""
playqueue = PQ.PLAYQUEUES[playerid]
info = app.PLAYSTATE.player_states[playerid]
position = self._get_correct_position(info, playqueue)
if info[STREAM_DETAILS[stream_type]] == -1:
kodi_stream_index = -1
else:
kodi_stream_index = info[STREAM_DETAILS[stream_type]]['index']
return playqueue.items[position].plex_stream_index(kodi_stream_index,
stream_type)
def update_command_id(self, uuid, command_id):
"""
Updates the Plex Companien client with the machine identifier uuid with