From 56a3cdbbd8348b4252939521a811ffd0ce3c7bdf Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 08:58:09 +0200 Subject: [PATCH 01/19] Use super() for subclassing BaseHTTPRequestHandler --- resources/lib/plexbmchelper/listener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 58efff9a..4bf61e10 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -45,7 +45,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): ''' From f2e03e878ea261d4aacb642eb326c53478ccbe1b Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 08:59:56 +0200 Subject: [PATCH 02/19] Do not close http output stream when responding --- resources/lib/plexbmchelper/listener.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 4bf61e10..329b053f 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -78,7 +78,6 @@ 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) @@ -93,7 +92,6 @@ class MyHandler(BaseHTTPRequestHandler): self.send_header('Connection', "close") self.end_headers() self.wfile.write(body.encode('utf-8')) - self.wfile.close() except Exception: pass From d8db4634232f1849edede1e0e08ee29b2e9e9d3d Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 09:05:48 +0200 Subject: [PATCH 03/19] Improve logging for http client --- resources/lib/plexbmchelper/listener.py | 10 ++++++---- resources/lib/plexbmchelper/plexgdm.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 329b053f..2da964a6 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -62,6 +62,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) @@ -92,8 +93,8 @@ class MyHandler(BaseHTTPRequestHandler): self.send_header('Connection', "close") self.end_headers() self.wfile.write(body.encode('utf-8')) - except Exception: - pass + except Exception as exc: + LOG.debug('Exception encountered while responding: %s', exc) def answer_request(self, send_data): self.serverlist = self.server.client.getServerList() @@ -106,10 +107,11 @@ 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]), + 'X-Plex-Client-Identifier', self.client_address[0]), params.get('commandID')) if request_path == "version": self.response( diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index bbab06f1..857d985c 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -97,8 +97,8 @@ class plexgdm(object): % (self.client_header, self.client_data), 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, From 4d3e36fbdb275233e00b2555510ebfb2f8d8e103 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 09:40:07 +0200 Subject: [PATCH 04/19] Add missing HTTP headers that Plex for Window's Plex Companion uses --- resources/lib/clientinfo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 41f81697..8039cb11 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -42,6 +42,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') From 492e235a5338c184061378fe62cc7139f8f85c47 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 09:41:00 +0200 Subject: [PATCH 05/19] If receiving a Companion request, reply with a code 200 xml --- resources/lib/plexbmchelper/listener.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 2da964a6..62386918 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -210,7 +210,10 @@ class MyHandler(BaseHTTPRequestHandler): else: # Throw it to companion.py companion.process_command(request_path, params) - self.response('', clientinfo.getXArgsDeviceInfo(include_token=False)) + headers = clientinfo.getXArgsDeviceInfo(include_token=False) + headers['Content-Type'] = 'text/xml' + self.response(XML_OK, headers) + self.response(v.COMPANION_OK_MESSAGE, headers) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): From c107eb2ed89817215d0a348cdf032b0c17bb4cc2 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 09:41:38 +0200 Subject: [PATCH 06/19] Do not send a Connection: close header --- resources/lib/plexbmchelper/listener.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 62386918..ee0e28f0 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -90,9 +90,9 @@ class MyHandler(BaseHTTPRequestHandler): 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() - self.wfile.write(body.encode('utf-8')) + if body: + self.wfile.write(body.encode('utf-8')) except Exception as exc: LOG.debug('Exception encountered while responding: %s', exc) From d37fbb6c1a116ed08dc689399d3a95fe6aa98083 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 09:58:16 +0200 Subject: [PATCH 07/19] Remove obsolete BaseHTTPRequestHandler method --- resources/lib/plexbmchelper/listener.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index ee0e28f0..28274ed4 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -80,9 +80,6 @@ class MyHandler(BaseHTTPRequestHandler): 'x-plex-device, x-plex-device-screen-resolution') self.end_headers() - def sendOK(self): - self.send_response(200) - def response(self, body, headers=None, code=200): headers = {} if headers is None else headers try: From e6171127dc8c155fba8412d20ac0e16fa689a883 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 13:29:32 +0200 Subject: [PATCH 08/19] Don't simply swallow all http server exceptions --- resources/lib/plexbmchelper/listener.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 28274ed4..057220d2 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -82,16 +82,13 @@ class MyHandler(BaseHTTPRequestHandler): 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.end_headers() - if body: - self.wfile.write(body.encode('utf-8')) - except Exception as exc: - LOG.debug('Exception encountered while responding: %s', exc) + self.send_response(code) + for key in headers: + self.send_header(key, headers[key]) + self.send_header('Content-Length', len(body)) + self.end_headers() + if body: + self.wfile.write(body.encode('utf-8')) def answer_request(self, send_data): self.serverlist = self.server.client.getServerList() From 8c64e2a17c40578535cfc6c944367c5d92829440 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 15:44:00 +0200 Subject: [PATCH 09/19] Fix http server headers Connection:keep-alive and Content-Type for XMLs --- resources/lib/clientinfo.py | 1 - resources/lib/plexbmchelper/listener.py | 43 +++++++++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 8039cb11..92b7ebf5 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -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), diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 057220d2..f89a9e23 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -107,18 +107,31 @@ class MyHandler(BaseHTTPRequestHandler): 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 @@ -153,7 +166,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') @@ -167,7 +180,7 @@ 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 self.response( @@ -180,11 +193,13 @@ class MyHandler(BaseHTTPRequestHandler): 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', 'Content-Type': 'text/xml;charset=utf-8' - }, + }.update(headers), code=500) 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') @@ -196,17 +211,19 @@ 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) - headers = clientinfo.getXArgsDeviceInfo(include_token=False) - headers['Content-Type'] = 'text/xml' - self.response(XML_OK, headers) + headers['Content-Type'] = 'text/xml;charset=utf-8' + headers = clientinfo.getXArgsDeviceInfo(options=headers, + include_token=False) self.response(v.COMPANION_OK_MESSAGE, headers) From f3754fa2e323fc3b6582c839f1fa46da9fbb3939 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 15:48:44 +0200 Subject: [PATCH 10/19] Use ThreadingHTTPServer instead of own threaded HTTPServer --- resources/lib/plex_companion.py | 2 +- resources/lib/plexbmchelper/listener.py | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 25cf12aa..fb55a637 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -300,7 +300,7 @@ class PlexCompanion(backgroundthread.KillableThread): start_count = 0 while True: try: - httpd = listener.ThreadedHTTPServer( + httpd = listener.PKCHTTPServer( client, subscription_manager, ('', v.COMPANION_PORT), diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index f89a9e23..1b6ce3a3 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -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 @@ -227,12 +226,7 @@ class MyHandler(BaseHTTPRequestHandler): 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- @@ -242,4 +236,4 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """ self.client = client self.subscription_manager = subscription_manager - HTTPServer.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) From aa14e8259fda0cbb28bb61a404bfb941dfea0cca Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 16:08:09 +0200 Subject: [PATCH 11/19] Fix Plex Web's Plex Companion connection terminating suddenly --- resources/lib/plexbmchelper/listener.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 1b6ce3a3..6473bfed 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -181,9 +181,9 @@ class MyHandler(BaseHTTPRequestHandler): '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', @@ -192,8 +192,7 @@ class MyHandler(BaseHTTPRequestHandler): 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', 'Content-Type': 'text/xml;charset=utf-8' - }.update(headers), - code=500) + }.update(headers)) elif "/subscribe" in request_path: headers['Content-Type'] = 'text/xml;charset=utf-8' headers = clientinfo.getXArgsDeviceInfo(options=headers, From 0d41321d7f3571254da46bf2fdb26d2b167a1b51 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 18 Oct 2021 16:45:19 +0200 Subject: [PATCH 12/19] Fix PKC not being able to register as a PMS client --- resources/lib/plexbmchelper/plexgdm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index 857d985c..652d1bbc 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -93,8 +93,8 @@ 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 as exc: From 4f7e54591c71bc070dcc2a4e1e1ed4d211a66d94 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 19 Oct 2021 08:36:05 +0200 Subject: [PATCH 13/19] Fix TypeError for Plex Companion switching audio or subtitle stream --- resources/lib/playlist_func.py | 21 +++++++++++++++++++++ resources/lib/plex_companion.py | 14 +------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index c548dea0..3d2068fd 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -376,6 +376,27 @@ 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') + app.APP.player.setAudioStream(kodi_index) + self.current_kodi_audio_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): """ diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index fb55a637..e6400dd8 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -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): From 0385c25e2ff8a765379f855c1dcc9a523ce6711e Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 20 Oct 2021 14:53:12 +0200 Subject: [PATCH 14/19] Beta version bump 3.5.6 --- addon.xml | 7 +++++-- changelog.txt | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index e108a66f..a34d42b5 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -91,7 +91,10 @@ Plex를 Kodi에 기본 통합 Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오! 자신의 책임하에 사용 - version 3.5.5: + version 3.5.6 (beta only): +- Fix Plex Companion not working by fixing some issues with PKC's http.server's BaseHTTPRequestHandler + +version 3.5.5: - Lost patience with Kodi 19: drop use of Python multiprocessing entirely version 3.5.4: diff --git a/changelog.txt b/changelog.txt index 01de4e0f..2021abfc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +version 3.5.6 (beta only): +- Fix Plex Companion not working by fixing some issues with PKC's http.server's BaseHTTPRequestHandler + version 3.5.5: - Lost patience with Kodi 19: drop use of Python multiprocessing entirely From 49fce5a2cd640dc4975988f6168e16d59b939765 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 20 Oct 2021 14:55:39 +0200 Subject: [PATCH 15/19] Track video streams as well; refactor reporting of playback progress; only switch audio stream if different --- resources/lib/json_rpc.py | 9 +++ resources/lib/kodimonitor.py | 2 + resources/lib/playlist_func.py | 94 +++++++++++++++++++++- resources/lib/plex_functions.py | 14 ++++ resources/lib/plexbmchelper/subscribers.py | 41 +--------- 5 files changed, 119 insertions(+), 41 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 5e970191..8a6f00a6 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -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 diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 6ec36967..eae5f77e 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -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': diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 3d2068fd..697823b5 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -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) @@ -383,8 +464,13 @@ class PlaylistItem(object): if 'audioStreamID' in plex_data: plex_index = int(plex_data['audioStreamID']) kodi_index = self.kodi_stream_index(plex_index, 'audio') - app.APP.player.setAudioStream(kodi_index) + 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: diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index 23ab6814..ea5791d8 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -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') diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 52fb44b7..4e9767f2 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -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 From 20f5d9d561e71e5b1f04faa2961c2cc9f6db7865 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 22 Oct 2021 14:36:13 +0200 Subject: [PATCH 16/19] Fix Kodi JSON racing condition on playback startup and KeyError --- resources/lib/kodimonitor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index eae5f77e..95765a59 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -390,6 +390,10 @@ 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 + # Wait a bit because JSON responses won't be ready otherwise + if app.APP.monitor.waitForAbort(2): + # In case PKC needs to quit + return item.init_kodi_streams() item.switch_to_plex_stream('video') if utils.settings('audioStreamPick') == '0': From d605cfd6858138af5414cfd25a6762622492327b Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 22 Oct 2021 21:22:32 +0200 Subject: [PATCH 17/19] Beta version bump 3.5.7 --- addon.xml | 7 +++++-- changelog.txt | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index a34d42b5..294be6df 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -91,7 +91,10 @@ Plex를 Kodi에 기본 통합 Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오! 자신의 책임하에 사용 - version 3.5.6 (beta only): + version 3.5.7 (beta only): +- Fix Kodi JSON racing condition on playback startup and KeyError + +version 3.5.6 (beta only): - Fix Plex Companion not working by fixing some issues with PKC's http.server's BaseHTTPRequestHandler version 3.5.5: diff --git a/changelog.txt b/changelog.txt index 2021abfc..1a003ef3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +version 3.5.7 (beta only): +- Fix Kodi JSON racing condition on playback startup and KeyError + version 3.5.6 (beta only): - Fix Plex Companion not working by fixing some issues with PKC's http.server's BaseHTTPRequestHandler From ab52521f7358f813fe07fc7f04004e499d9df6e5 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 30 Oct 2021 17:43:42 +0200 Subject: [PATCH 18/19] Fix UnboundLocalError: local variable 'identifier' referenced before assignment --- resources/lib/kodi_db/video.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 36b5fd9a..887d8c4f 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -611,6 +611,8 @@ class KodiVideoDB(common.KodiDBBase): identifier = 'idMovie' elif kodi_type == v.KODI_TYPE_EPISODE: identifier = 'idEpisode' + else: + return self.cursor.execute('SELECT idFile FROM %s WHERE %s = ? LIMIT 1' % (kodi_type, identifier), (kodi_id, )) try: From 59a3dd0010d8c67eca07ada0479371f04fbed20f Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 30 Oct 2021 18:43:38 +0200 Subject: [PATCH 19/19] Stable and beta version bump 3.5.8 --- addon.xml | 8 ++++++-- changelog.txt | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index 294be6df..02ae8290 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -91,7 +91,11 @@ Plex를 Kodi에 기본 통합 Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오! 자신의 책임하에 사용 - version 3.5.7 (beta only): + version 3.5.8: +- Fix UnboundLocalError: local variable 'identifier' referenced before assignment +- versions 3.5.6-3.5.7 for everyone + +version 3.5.7 (beta only): - Fix Kodi JSON racing condition on playback startup and KeyError version 3.5.6 (beta only): diff --git a/changelog.txt b/changelog.txt index 1a003ef3..66b4595f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +version 3.5.8: +- Fix UnboundLocalError: local variable 'identifier' referenced before assignment +- versions 3.5.6-3.5.7 for everyone + version 3.5.7 (beta only): - Fix Kodi JSON racing condition on playback startup and KeyError