diff --git a/addon.xml b/addon.xml index e108a66f..02ae8290 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -91,7 +91,17 @@ Plex를 Kodi에 기본 통합 Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오! 자신의 책임하에 사용 - version 3.5.5: + 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): +- 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..66b4595f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,13 @@ +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): +- 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 diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 41f81697..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), @@ -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') 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/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: diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 6ec36967..95765a59 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -390,6 +390,12 @@ 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': 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 c548dea0..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) @@ -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): """ diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 25cf12aa..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): @@ -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), 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/listener.py b/resources/lib/plexbmchelper/listener.py index 58efff9a..6473bfed 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 @@ -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() + 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')) - 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]), + '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) diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index bbab06f1..652d1bbc 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -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, 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