From 48ba7f086940207f3fbd6f9cdc21c0b006baebe0 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 22 Jan 2016 15:37:20 +0100 Subject: [PATCH] Plex Companien (Plexbmc helper) version 0.1 --- default.py | 21 ++-- resources/lib/PlexAPI.py | 5 +- resources/lib/PlexCompanion.py | 111 ++++++++++++++++++ resources/lib/downloadutils.py | 3 + resources/lib/entrypoint.py | 24 ++++ resources/lib/librarysync.py | 49 ++------ .../__init__.py | 0 .../functions.py | 6 +- .../httppersist.py | 0 .../listener.py | 2 +- .../plexgdm.py | 9 +- .../settings.py | 28 +++-- .../subscribers.py | 18 ++- resources/lib/utils.py | 30 +++++ resources/settings.xml | 4 + service.py | 50 +++++--- 16 files changed, 274 insertions(+), 86 deletions(-) create mode 100644 resources/lib/PlexCompanion.py rename resources/lib/{plexbmc.helper => plexbmchelper}/__init__.py (100%) rename resources/lib/{plexbmc.helper => plexbmchelper}/functions.py (96%) rename resources/lib/{plexbmc.helper => plexbmchelper}/httppersist.py (100%) rename resources/lib/{plexbmc.helper => plexbmchelper}/listener.py (99%) rename resources/lib/{plexbmc.helper => plexbmchelper}/plexgdm.py (97%) rename resources/lib/{plexbmc.helper => plexbmchelper}/settings.py (53%) rename resources/lib/{plexbmc.helper => plexbmchelper}/subscribers.py (88%) diff --git a/default.py b/default.py index 27c87e9a..ee617922 100644 --- a/default.py +++ b/default.py @@ -8,7 +8,6 @@ import urlparse import xbmc import xbmcaddon -import xbmcgui ################################################################################################# @@ -22,9 +21,6 @@ sys.path.append(base_resource) import entrypoint import utils -import PlexAPI -import userclient - ################################################################################################# enableProfiling = False @@ -34,8 +30,8 @@ class Main: # MAIN ENTRY POINT def __init__(self): - plx = PlexAPI.PlexAPI() # Parse parameters + xbmc.log("Full sys.argv received: %s" % sys.argv) base_url = sys.argv[0] addon_handle = int(sys.argv[1]) params = urlparse.parse_qs(sys.argv[2][1:]) @@ -44,11 +40,15 @@ class Main: mode = params['mode'][0] itemid = params.get('id') if itemid: - itemid = itemid[0] + try: + itemid = itemid[0] + except: + pass except: params = {} mode = "" - + xbmc.log("mode: %s, itemid: %s, base_url: %s, addon_handle: %s" + % (mode, itemid, base_url, addon_handle), 2) modes = { @@ -65,7 +65,8 @@ class Main: 'nextup': entrypoint.getNextUpEpisodes, 'inprogressepisodes': entrypoint.getInProgressEpisodes, 'recentepisodes': entrypoint.getRecentEpisodes, - 'refreshplaylist': entrypoint.refreshPlaylist + 'refreshplaylist': entrypoint.refreshPlaylist, + 'companion': entrypoint.plexCompanion } if "extrafanart" in sys.argv[0]: @@ -90,7 +91,9 @@ class Main: elif mode == "channelsfolder": folderid = params['folderid'][0] modes[mode](itemid, folderid) - + elif mode == "companion": + resume = params.get('resume', '') + modes[mode](itemid, resume=resume) else: modes[mode]() else: diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 10f94bcc..928e9fc5 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -53,15 +53,13 @@ import requests import re import json -import uuid +from urllib import urlencode, quote_plus try: import xml.etree.cElementTree as etree except ImportError: import xml.etree.ElementTree as etree -from urllib import urlencode, quote_plus - # from Version import __VERSION__ # from Debug import * # dprint(), prettyXML() @@ -1499,6 +1497,7 @@ class PlexAPI(): xml = self.doUtils.downloadUrl(url, headerOptions=headerOptions) if not xml: self.logMsg("Error retrieving metadata for %s" % url, -1) + xml = None return xml diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py new file mode 100644 index 00000000..d0fd3a98 --- /dev/null +++ b/resources/lib/PlexCompanion.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +import threading +import traceback +import socket +import requests + +import xbmc + +import clientinfo +import utils +from plexbmchelper import listener, plexgdm, subscribers +from plexbmchelper.settings import settings + + +class PlexCompanion(threading.Thread): + def __init__(self): + self._shouldStop = threading.Event() + self.port = int(utils.settings('companionPort')) + ci = clientinfo.ClientInfo() + self.addonName = ci.getAddonName() + self.clientId = ci.getDeviceId() + self.deviceName = ci.getDeviceName() + self.logMsg("----===## Starting PlexBMC Helper ##===----", 1) + + # Start GDM for server/client discovery + self.client = plexgdm.plexgdm(debug=settings['gdm_debug']) + self.client.clientDetails(self.clientId, # UUID + self.deviceName, # clientName + self.port, + self.addonName, + '1.0') # Version + self.logMsg("Registration string is: %s " + % self.client.getClientDetails(), 1) + + threading.Thread.__init__(self) + + def logMsg(self, msg, lvl=1): + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + def stopClient(self): + # When emby for kodi terminates + self._shouldStop.set() + + def stopped(self): + return self._shouldStop.isSet() + + def run(self): + start_count = 0 + while True: + try: + httpd = listener.ThreadedHTTPServer( + ('', self.port), + listener.MyHandler) + httpd.timeout = 0.95 + break + except: + self.logMsg("Unable to start PlexCompanion. Traceback:", -1) + self.logMsg(traceback.print_exc(), -1) + + xbmc.sleep(3000) + + if start_count == 3: + self.logMsg("Error: Unable to start web helper.", -1) + httpd = False + break + + start_count += 1 + + if not httpd: + return + + self.client.start_all() + message_count = 0 + is_running = False + while not self.stopped(): + try: + + httpd.handle_request() + message_count += 1 + + if message_count > 30: + if self.stopped(): + break + if self.client.check_client_registration(): + self.logMsg("Client is still registered", 1) + else: + self.logMsg("Client is no longer registered", + 1) + self.logMsg("PlexBMC Helper still running on " + "port %s" % self.port, 1) + message_count = 0 + + if not is_running: + self.logMsg("PleXBMC Helper has started", 0) + + is_running = True + if message_count % 1 == 0: + subscribers.subMgr.notify() + settings['serverList'] = self.client.getServerList() + except: + self.logMsg("Error in loop, continuing anyway", 1) + self.logMsg(traceback.print_exc(), 1) + + self.client.stop_all() + try: + httpd.socket.shutdown(socket.SHUT_RDWR) + finally: + httpd.socket.close() + requests.dumpConnections() + self.logMsg("----===## STOP PlexBMC Helper ##===----", 0) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index b8ddc3a4..6afc1c22 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -218,6 +218,9 @@ class DownloadUtils(): r = s.delete(url, json=postBody, timeout=timeout, headers=header) elif type == "OPTIONS": r = s.options(url, json=postBody, timeout=timeout, headers=header) + # For Plex Companion + elif type == "POSTXML": + r = s.post(url, postBody, timeout=timeout, headers=header) except AttributeError: # request session does not exists diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 5f42d2ef..6c106191 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -6,6 +6,7 @@ import json import os import sys import urlparse +import re import xbmc import xbmcaddon @@ -26,10 +27,33 @@ import playutils import api import PlexAPI +import embydb_functions ################################################################################################# +def plexCompanion(fullurl, resume=""): + regex = re.compile(r'''/(\d+)$''') + itemid = regex.findall(fullurl) + try: + itemid = itemid[0] + except IndexError: + # No matches found, url not like: + # http://192.168.0.2:32400/library/metadata/243480 + return False + # TODO: direct play an URL + # Initialize embydb + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby = embydb_functions.Embydb_Functions(embycursor) + # Get dbid using itemid + dbid = emby.getItem_byId(itemid)[0] + embyconn.close() + # Start playing + item = PlexAPI.PlexAPI().GetPlexMetadata(itemid) + pbutils.PlaybackUtils(item).play(itemid, dbid) + + def doPlayback(itemid, dbid): # Get a first XML to get the librarySectionUUID item = PlexAPI.PlexAPI().GetPlexMetadata(itemid) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 3635e05b..11085b1d 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -4,6 +4,7 @@ import threading from datetime import datetime, timedelta +import Queue import xbmc import xbmcgui @@ -21,7 +22,6 @@ import userclient import videonodes import PlexAPI -import Queue ################################################################################################## @@ -46,14 +46,9 @@ class ThreadedGetMetadata(threading.Thread): self.lock = lock self.userStop = userStop self._shouldstop = threading.Event() - self.addonName = clientinfo.ClientInfo().getAddonName() threading.Thread.__init__(self) - def logMsg(self, msg, lvl=1): - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - def run_internal(self): + def run(self): plx = PlexAPI.PlexAPI() global getMetadataCount while self.stopped() is False: @@ -68,25 +63,22 @@ class ThreadedGetMetadata(threading.Thread): plexXML = plx.GetPlexMetadata(updateItem['itemId']) except: raise - updateItem['XML'] = plexXML - # place item into out queue - self.out_queue.put(updateItem) + # check whether valid XML + try: + # .tag works for XML + plexXML.tag + updateItem['XML'] = plexXML + # place item into out queue + self.out_queue.put(updateItem) + # If we don't have a valid XML, don't put that into the queue + except AttributeError: + pass # Keep track of where we are at with self.lock: getMetadataCount += 1 # signals to queue job is done self.queue.task_done() - def run(self): - try: - self.run_internal() - except Exception as e: - xbmcgui.Dialog().ok(self.addonName, - "A sync thread has exited! " - "You should restart Kodi now. " - "Please report this on the forum.") - raise - def stopThread(self): self._shouldstop.set() @@ -113,14 +105,9 @@ class ThreadedProcessMetadata(threading.Thread): self.itemType = itemType self.userStop = userStop self._shouldstop = threading.Event() - self.addonName = clientinfo.ClientInfo().getAddonName() threading.Thread.__init__(self) - def logMsg(self, msg, lvl=1): - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - def run_internal(self): + def run(self): # Constructs the method name, e.g. itemtypes.Movies itemFkt = getattr(itemtypes, self.itemType) global processMetadataCount @@ -152,16 +139,6 @@ class ThreadedProcessMetadata(threading.Thread): # signals to queue job is done self.queue.task_done() - def run(self): - try: - self.run_internal() - except Exception as e: - xbmcgui.Dialog().ok(self.addonName, - "A sync thread has exited! " - "You should restart Kodi now. " - "Please report this on the forum.") - raise - def stopThread(self): self._shouldstop.set() diff --git a/resources/lib/plexbmc.helper/__init__.py b/resources/lib/plexbmchelper/__init__.py similarity index 100% rename from resources/lib/plexbmc.helper/__init__.py rename to resources/lib/plexbmchelper/__init__.py diff --git a/resources/lib/plexbmc.helper/functions.py b/resources/lib/plexbmchelper/functions.py similarity index 96% rename from resources/lib/plexbmc.helper/functions.py rename to resources/lib/plexbmchelper/functions.py index 83948e41..90bfcb7d 100644 --- a/resources/lib/plexbmc.helper/functions.py +++ b/resources/lib/plexbmchelper/functions.py @@ -73,7 +73,9 @@ def jsonrpc(action, arguments = {}): elif action.lower() == "playmedia": fullurl=arguments[0] resume=arguments[1] - xbmc.Player().play("plugin://plugin.video.plexbmc/?mode=5&force="+resume+"&url="+fullurl) + xbmc.Player().play("plugin://plugin.video.plexkodiconnect/" + "?mode=companion&resume=%s&id=%s" + % (resume, fullurl)) return True elif arguments: request=json.dumps({ "id" : 1, @@ -127,7 +129,7 @@ def getPlexHeaders(): "X-Plex-Version": settings['version'], "X-Plex-Client-Identifier": settings['uuid'], "X-Plex-Provides": "player", - "X-Plex-Product": "PleXBMC", + "X-Plex-Product": "PlexKodiConnect", "X-Plex-Device-Name": settings['client_name'], "X-Plex-Platform": "XBMC", "X-Plex-Model": getPlatform(), diff --git a/resources/lib/plexbmc.helper/httppersist.py b/resources/lib/plexbmchelper/httppersist.py similarity index 100% rename from resources/lib/plexbmc.helper/httppersist.py rename to resources/lib/plexbmchelper/httppersist.py diff --git a/resources/lib/plexbmc.helper/listener.py b/resources/lib/plexbmchelper/listener.py similarity index 99% rename from resources/lib/plexbmc.helper/listener.py rename to resources/lib/plexbmchelper/listener.py index 20b0ef87..471c99a2 100644 --- a/resources/lib/plexbmc.helper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -74,7 +74,7 @@ class MyHandler(BaseHTTPRequestHandler): resp += ' protocolVersion="1"' resp += ' protocolCapabilities="navigation,playback,timeline"' resp += ' machineIdentifier="%s"' % settings['uuid'] - resp += ' product="PleXBMC"' + resp += ' product="PlexKodiConnect"' resp += ' platform="%s"' % getPlatform() resp += ' platformVersion="%s"' % settings['plexbmc_version'] resp += ' deviceClass="pc"' diff --git a/resources/lib/plexbmc.helper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py similarity index 97% rename from resources/lib/plexbmc.helper/plexgdm.py rename to resources/lib/plexbmchelper/plexgdm.py index 209ccf66..f355212c 100644 --- a/resources/lib/plexbmc.helper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -31,6 +31,7 @@ import re import threading import time import urllib2 +import downloadutils class plexgdm: @@ -140,9 +141,11 @@ class plexgdm: media_port=self.server_list[0]['port'] self.__printDebug("Checking server [%s] on port [%s]" % (media_server, media_port) ,2) - f = urllib2.urlopen('http://%s:%s/clients' % (media_server, media_port)) - client_result = f.read() - if self.client_id in client_result: + client_result = downloadutils.DownloadUtils().downloadUrl( + 'http://%s:%s/clients' % (media_server, media_port)) + # f = urllib2.urlopen('http://%s:%s/clients' % (media_server, media_port)) + # client_result = f.read() + if self.client_id in str(client_result): self.__printDebug("Client registration successful",1) self.__printDebug("Client data is: %s" % client_result, 3) return True diff --git a/resources/lib/plexbmc.helper/settings.py b/resources/lib/plexbmchelper/settings.py similarity index 53% rename from resources/lib/plexbmc.helper/settings.py rename to resources/lib/plexbmchelper/settings.py index 46bc1ca2..3e0272b7 100644 --- a/resources/lib/plexbmc.helper/settings.py +++ b/resources/lib/plexbmchelper/settings.py @@ -7,7 +7,7 @@ settings = {} try: guidoc = parse(xbmc.translatePath('special://userdata/guisettings.xml')) except: - print "Unable to read XBMC's guisettings.xml" + print "Unable to read XBMC's guisettings.xml" def getGUI(name): global guidoc @@ -19,24 +19,28 @@ def getGUI(name): return "" addon = xbmcaddon.Addon() -plexbmc = xbmcaddon.Addon('plugin.video.plexbmc') +plexbmc = xbmcaddon.Addon('plugin.video.plexkodiconnect') -settings['debug'] = addon.getSetting('debug') == "true" -settings['gdm_debug'] = addon.getSetting('gdm_debug') == "true" -if addon.getSetting('use_xbmc_name') == "true": - settings['client_name'] = getGUI('devicename') +if plexbmc.getSetting('logLevel') == '2' or \ + plexbmc.getSetting('logLevel') == '1': + settings['debug'] = 'true' + settings['gdm_debug'] = 'true' else: - settings['client_name'] = addon.getSetting('c_name') + settings['debug'] = 'false' + settings['gdm_debug'] = 'false' + +settings['client_name'] = plexbmc.getSetting('deviceName') + # XBMC web server settings settings['webserver_enabled'] = (getGUI('webserver') == "true") settings['port'] = int(getGUI('webserverport')) settings['user'] = getGUI('webserverusername') settings['passwd'] = getGUI('webserverpassword') -settings['uuid'] = str(addon.getSetting('uuid')) or str(uuid.uuid4()) -addon.setSetting('uuid', settings['uuid']) -settings['version'] = addon.getAddonInfo('version') +settings['uuid'] = plexbmc.getSetting('plex_client_Id') + +settings['version'] = plexbmc.getAddonInfo('version') settings['plexbmc_version'] = plexbmc.getAddonInfo('version') -settings['myplex_user'] = plexbmc.getSetting('myplex_user') +settings['myplex_user'] = plexbmc.getSetting('username') settings['serverList'] = [] -settings['myport'] = 3005 +settings['myport'] = addon.getSetting('companionPort') diff --git a/resources/lib/plexbmc.helper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py similarity index 88% rename from resources/lib/plexbmc.helper/subscribers.py rename to resources/lib/plexbmchelper/subscribers.py index cf5ebd2f..56c7386d 100644 --- a/resources/lib/plexbmc.helper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -5,6 +5,7 @@ from xml.dom.minidom import parseString from functions import * from settings import settings from httppersist import requests +import downloadutils class SubscriptionManager: def __init__(self): @@ -19,6 +20,7 @@ class SubscriptionManager: self.port = "" self.playerprops = {} self.sentstopped = True + self.download = downloadutils.DownloadUtils() def getVolume(self): self.volume = getVolume() @@ -113,7 +115,11 @@ class SubscriptionManager: params['time'] = info['time'] params['duration'] = info['duration'] serv = getServerByHost(self.server) - requests.getwithparams(serv.get('server', 'localhost'), serv.get('port', 32400), "/:/timeline", params, getPlexHeaders(), serv.get('protocol', 'http')) + url = serv.get('protocol', 'http') + '://' \ + + serv.get('server', 'localhost') + ':' \ + + serv.get('port', 32400) + "/:/timeline" + self.download.downloadUrl(url, type="GET", parameters=params) + # requests.getwithparams(serv.get('server', 'localhost'), serv.get('port', 32400), "/:/timeline", params, getPlexHeaders(), serv.get('protocol', 'http')) printDebug("sent server notification with state = %s" % params['state']) WINDOW = xbmcgui.Window(10000) WINDOW.setProperty('plexbmc.nowplaying.sent', '1') @@ -175,6 +181,7 @@ class Subscriber: self.commandID = int(commandID) or 0 self.navlocationsent = False self.age = 0 + self.download = downloadutils.DownloadUtils() def __eq__(self, other): return self.uuid == other.uuid def tostr(self): @@ -191,7 +198,14 @@ class Subscriber: self.navlocationsent = True msg = re.sub(r"INSERTCOMMANDID", str(self.commandID), msg) printDebug("sending xml to subscriber %s: %s" % (self.tostr(), msg)) - if not requests.post(self.host, self.port, "/:/timeline", msg, getPlexHeaders(), self.protocol): + url = self.protocol + '://' + self.host + ':' + self.port \ + + "/:/timeline" + response = self.download.downloadUrl(url, + postBody=msg, + type="POSTXML") + # if not requests.post(self.host, self.port, "/:/timeline", msg, getPlexHeaders(), self.protocol): + # subMgr.removeSubscriber(self.uuid) + if response in [False, 401]: subMgr.removeSubscriber(self.uuid) subMgr = SubscriptionManager() diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 4465c1b2..2a93b208 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -15,9 +15,39 @@ import xbmcaddon import xbmcgui import xbmcvfs +import clientinfo + ################################################################################################# +class logDecor(object): + """ + A decorator adding logging capabilities. + + Syntax: self.logMsg(message, loglevel) + + Loglevel: -2 (Error) to 2 (DB debug) + """ + def __init__(self, f): + """ + If there are no decorator arguments, the function to be decorated is + passed to the constructor. + """ + self.f = f + self.addonName = clientinfo.ClientInfo().getAddonName() + + def __call__(self, *args, **kwargs): + """ + The __call__ method is not called until the + decorated function is called. + """ + def decorLog(self, msg, lvl=1): + className = self.__class__.__name__ + logMsg("%s %s" % (self.addonName, className), msg, lvl) + # The function itself: + self.f(*args, **kwargs) + + def logMsg(title, msg, level=1): # Get the logLevel set in UserClient diff --git a/resources/settings.xml b/resources/settings.xml index 728b40dd..5f054eb7 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -75,4 +75,8 @@ + + + + diff --git a/service.py b/service.py index ad45aa06..1aff254e 100644 --- a/service.py +++ b/service.py @@ -4,13 +4,13 @@ import os import sys -import time +# import time from datetime import datetime import xbmc import xbmcaddon import xbmcgui -import xbmcvfs +# import xbmcvfs ################################################################################################# @@ -29,9 +29,10 @@ import librarysync import player import utils import videonodes -import websocket_client as wsc +# import websocket_client as wsc import PlexAPI +import PlexCompanion ################################################################################################# @@ -46,6 +47,7 @@ class Service(): websocket_running = False library_running = False kodimonitor_running = False + plexCompanion_running = False def __init__(self): @@ -106,6 +108,9 @@ class Service(): # ws = wsc.WebSocket_Client() library = librarysync.LibrarySync() kplayer = player.Player() + plx = PlexAPI.PlexAPI() + plexCompanion = PlexCompanion.PlexCompanion() + plexCompanionDesired = utils.settings('plexCompanion') # Sync and progress report lastProgressUpdate = datetime.today() @@ -223,10 +228,7 @@ class Service(): # No server info set in add-on settings pass - elif PlexAPI.PlexAPI().CheckConnection( - server, - plexToken - ) != 200: + elif plx.CheckConnection(server, plexToken) != 200: # Server is offline. # Alert the user and suppress future warning if self.server_online: @@ -234,10 +236,12 @@ class Service(): utils.window('emby_online', value="false") xbmcgui.Dialog().notification( - heading="Error connecting", - message="%s Server is unreachable." % self.addonName, - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - sound=False) + heading="Error connecting", + message="%s Server is unreachable." + % self.addonName, + icon="special://home/addons/plugin.video." + "plexkodiconnect/icon.png", + sound=False) self.server_online = False @@ -251,11 +255,12 @@ class Service(): break # Alert the user that server is online. xbmcgui.Dialog().notification( - heading="Emby server", - message="Server is online.", - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - time=2000, - sound=False) + heading="Emby server", + message="Server is online.", + icon="special://home/addons/plugin.video." + "plexkodiconnect/icon.png", + time=2000, + sound=False) self.server_online = True self.logMsg("Server is online and ready.", 1) @@ -266,6 +271,12 @@ class Service(): self.userclient_running = True user.start() + # Start the Plex Companion thread + if not self.plexCompanion_running and \ + plexCompanionDesired == "true": + self.plexCompanion_running = True + plexCompanion.start() + break if monitor.waitForAbort(1): @@ -278,11 +289,14 @@ class Service(): ##### Emby thread is terminating. ##### + if self.plexCompanion_running: + plexCompanion.stopClient() + if self.library_running: library.stopThread() - if self.websocket_running: - ws.stopClient() + # if self.websocket_running: + # ws.stopClient() if self.userclient_running: user.stopClient()