Merge branch 'develop'

This commit is contained in:
tomkat83 2016-03-09 18:38:38 +01:00
commit a76b5397f6
13 changed files with 370 additions and 191 deletions

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" <addon id="plugin.video.plexkodiconnect"
name="PlexKodiConnect" name="PlexKodiConnect"
version="1.0.4" version="1.0.5"
provider-name="croneter"> provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>

View file

@ -1,3 +1,8 @@
version 1.0.5
- Catch exceptions in itemtypes and log them
- Slightly increased download timeouts
- Overhaul userclient
version 1.0.4 version 1.0.4
- Sleep for a while in loops - drastically reduces CPU load - Sleep for a while in loops - drastically reduces CPU load
- Connect to remote PMS! - Connect to remote PMS!

View file

@ -298,6 +298,7 @@
<string id="30533">Duration of the music library pop up (in seconds)</string> <string id="30533">Duration of the music library pop up (in seconds)</string>
<string id="30534">Server messages</string> <string id="30534">Server messages</string>
<string id="30535">Generate a new device Id</string> <string id="30535">Generate a new device Id</string>
<string id="30536">Users must log in every time when Kodi restarts</string>
<!-- service add-on --> <!-- service add-on -->
<string id="33000">Welcome</string> <string id="33000">Welcome</string>

View file

@ -24,6 +24,8 @@
<string id="30521">Bei Wiederaufnahme zurückspulen (in Sekunden)</string> <string id="30521">Bei Wiederaufnahme zurückspulen (in Sekunden)</string>
<string id="30505">[COLOR yellow]Anzahl Login-Versuche zurücksetzen[/COLOR]</string> <string id="30505">[COLOR yellow]Anzahl Login-Versuche zurücksetzen[/COLOR]</string>
<string id="30536">Benutzer müssen sich bei jedem Neustart von Kodi neu anmelden</string>
<string id="30014">Verbindung</string> <string id="30014">Verbindung</string>
<string id="30015">Netzwerk</string> <string id="30015">Netzwerk</string>

View file

@ -92,9 +92,11 @@ class PlexAPI():
'plexToken': utils.settings('plexToken'), 'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'), 'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'), 'plexid': utils.settings('plexid'),
'myplexlogin': utils.settings('myplexlogin') 'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
plexLogin is unicode or empty unicode string u'' Returns strings or unicode
Returns empty strings '' for a setting if not found. Returns empty strings '' for a setting if not found.
@ -102,11 +104,13 @@ class PlexAPI():
plexhome is 'true' if plex home is used (the default) plexhome is 'true' if plex home is used (the default)
""" """
return { return {
'plexLogin': utils.settings('plexLogin').decode('utf-8'), 'plexLogin': utils.settings('plexLogin'),
'plexToken': utils.settings('plexToken'), 'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'), 'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'), 'plexid': utils.settings('plexid'),
'myplexlogin': utils.settings('myplexlogin') 'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
} }
def GetPlexLoginAndPassword(self): def GetPlexLoginAndPassword(self):
@ -158,12 +162,14 @@ class PlexAPI():
""" """
Prompts user to sign in by visiting https://plex.tv/pin Prompts user to sign in by visiting https://plex.tv/pin
Writes plexhome, username and token to Kodi settings file. Returns: Writes to Kodi settings file. Also returns:
{ {
'plexhome': 'true' if Plex Home, 'false' otherwise 'plexhome': 'true' if Plex Home, 'false' otherwise
'username': 'username':
'avatar': URL to user avator 'avatar': URL to user avator
'token': 'token':
'plexid': Plex user ID
'homesize': Number of Plex home users (defaults to '1')
} }
Returns False if authentication did not work. Returns False if authentication did not work.
""" """
@ -202,19 +208,23 @@ class PlexAPI():
else: else:
home = 'false' home = 'false'
username = xml.get('username', '') username = xml.get('username', '')
avatar = xml.get('thumb') avatar = xml.get('thumb', '')
token = xml.findtext('authentication-token') token = xml.findtext('authentication-token')
homeSize = xml.get('homeSize', '1')
result = { result = {
'plexhome': home, 'plexhome': home,
'username': username, 'username': username,
'avatar': avatar, 'avatar': avatar,
'token': token, 'token': token,
'plexid': userid 'plexid': userid,
'homesize': homeSize
} }
utils.settings('plexLogin', username) utils.settings('plexLogin', username)
utils.settings('plexToken', token) utils.settings('plexToken', token)
utils.settings('plexhome', home) utils.settings('plexhome', home)
utils.settings('plexid', userid) utils.settings('plexid', userid)
utils.settings('plexAvatar', avatar)
utils.settings('plexHomeSize', homeSize)
# Let Kodi log into plex.tv on startup from now on # Let Kodi log into plex.tv on startup from now on
utils.settings('myplexlogin', 'true') utils.settings('myplexlogin', 'true')
return result return result
@ -334,7 +344,7 @@ class PlexAPI():
# Add '/clients' to URL because then an authentication is necessary # Add '/clients' to URL because then an authentication is necessary
# If a plex.tv URL was passed, this does not work. # If a plex.tv URL was passed, this does not work.
header = self.getXArgsDeviceInfo() header = self.getXArgsDeviceInfo()
if token is not None: if token:
header['X-Plex-Token'] = token header['X-Plex-Token'] = token
sslverify = utils.settings('sslverify') sslverify = utils.settings('sslverify')
if sslverify == "true": if sslverify == "true":
@ -780,7 +790,7 @@ class PlexAPI():
XML = etree.parse(response) XML = etree.parse(response)
# Log received XML if debugging enabled. # Log received XML if debugging enabled.
self.logMsg("====== received PMS-XML ======", 1) self.logMsg("====== received PMS-XML ======", 1)
self.logMsg(XML.getroot(), 1) self.logMsg(XML, 1)
self.logMsg("====== PMS-XML finished ======", 1) self.logMsg("====== PMS-XML finished ======", 1)
return XML return XML
@ -1050,42 +1060,41 @@ class PlexAPI():
self.logMsg("Avatar url for user %s is: %s" % (username, url), 1) self.logMsg("Avatar url for user %s is: %s" % (username, url), 1)
return url return url
def ChoosePlexHomeUser(self): def ChoosePlexHomeUser(self, plexToken):
""" """
Let's user choose from a list of Plex home users. Will switch to that Let's user choose from a list of Plex home users. Will switch to that
user accordingly. user accordingly.
Output: Returns a dict:
username {
userid 'username': Unicode
authtoken 'userid': '' Plex ID of the user
'token': '' User's token
'protected': True if PIN is needed, else False
}
Will return empty strings if failed. Will return False if something went wrong (wrong PIN, no connection)
""" """
string = self.__language__ string = self.__language__
dialog = xbmcgui.Dialog() dialog = xbmcgui.Dialog()
plexLogin = utils.settings('plexLogin')
plexToken = utils.settings('plexToken')
machineIdentifier = utils.settings('plex_machineIdentifier')
self.logMsg("Getting user list.", 1)
# Get list of Plex home users # Get list of Plex home users
users = self.MyPlexListHomeUsers(plexToken) users = self.MyPlexListHomeUsers(plexToken)
# Download users failed. Set username to Plex login
if not users: if not users:
utils.settings('username', value=plexLogin) self.logMsg("User download failed.", -1)
self.logMsg("User download failed. Set username = plexlogin", 0) return False
return ('', '', '')
userlist = [] userlist = []
userlistCoded = [] userlistCoded = []
for user in users: for user in users:
username = user['title'] username = user['title']
userlist.append(username) userlist.append(username)
# To take care of non-ASCII usernames
userlistCoded.append(username.encode('utf-8')) userlistCoded.append(username.encode('utf-8'))
usernumber = len(userlist) usernumber = len(userlist)
username = ''
usertoken = '' usertoken = ''
# Plex home not in use: only 1 user returned
trials = 0 trials = 0
while trials < 3: while trials < 3:
if usernumber > 1: if usernumber > 1:
@ -1094,58 +1103,62 @@ class PlexAPI():
self.addonName + string(39306), self.addonName + string(39306),
userlistCoded) userlistCoded)
if user_select == -1: if user_select == -1:
self.logMsg("No user selected.", 1) self.logMsg("No user selected.", 0)
utils.settings('username', value='')
xbmc.executebuiltin('Addon.OpenSettings(%s)' xbmc.executebuiltin('Addon.OpenSettings(%s)'
% self.addonId) % self.addonId)
return ('', '', '') return False
# No Plex home in use - only 1 user # Only 1 user received, choose that one
else: else:
user_select = 0 user_select = 0
selected_user = userlist[user_select] selected_user = userlist[user_select]
self.logMsg("Selected user: %s" % selected_user, 1) self.logMsg("Selected user: %s" % selected_user, 0)
utils.settings('username', value=selected_user)
user = users[user_select] user = users[user_select]
# Ask for PIN, if protected: # Ask for PIN, if protected:
pin = None
if user['protected'] == '1': if user['protected'] == '1':
# Please enter pin for user
self.logMsg('Asking for users PIN', 1) self.logMsg('Asking for users PIN', 1)
pin = dialog.input( pin = dialog.input(
string(39307) + selected_user, string(39307) + selected_user,
type=xbmcgui.INPUT_NUMERIC, type=xbmcgui.INPUT_NUMERIC,
option=xbmcgui.ALPHANUM_HIDE_INPUT) option=xbmcgui.ALPHANUM_HIDE_INPUT)
# User chose to cancel # User chose to cancel
if pin is None: # Plex bug: don't call url for protected user with empty PIN
break if not pin:
else: trials += 1
pin = None continue
# Switch to this Plex Home user, if applicable # Switch to this Plex Home user, if applicable
# Plex bug: don't call url for protected user with empty PIN
if user['protected'] == '1' and not pin:
break
username, usertoken = self.PlexSwitchHomeUser( username, usertoken = self.PlexSwitchHomeUser(
user['id'], user['id'],
pin, pin,
plexToken, plexToken,
machineIdentifier utils.settings('plex_machineIdentifier'))
)
# Couldn't get user auth # Couldn't get user auth
if not username: if not username:
trials += 1
# Could not login user, please try again # Could not login user, please try again
if not dialog.yesno(self.addonName, if not dialog.yesno(self.addonName,
string(39308) + selected_user, string(39308) + selected_user,
string(39309)): string(39309)):
# User chose to cancel # User chose to cancel
break break
# Successfully retrieved: break out of while loop
else: else:
# Successfully retrieved username: break out of while loop
break break
trials += trials
if not username:
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId)
return ('', '', '')
return (username, user['id'], usertoken)
def PlexSwitchHomeUser(self, userId, pin, token, machineId): if not username:
self.logMsg('Failed signing in a user to plex.tv', -1)
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId)
return False
return {
'username': username,
'userid': user['id'],
'protected': True if user['protected'] == '1' else False,
'token': usertoken
}
def PlexSwitchHomeUser(self, userId, pin, token, machineIdentifier):
""" """
Retrieves Plex home token for a Plex home user. Retrieves Plex home token for a Plex home user.
@ -1153,7 +1166,6 @@ class PlexAPI():
userId id of the Plex home user userId id of the Plex home user
pin PIN of the Plex home user, if protected pin PIN of the Plex home user, if protected
token token for plex.tv token token for plex.tv
machineId Plex PMS machineIdentifier
Output: Output:
(username, token) (username, token)
@ -1165,7 +1177,9 @@ class PlexAPI():
url += '?pin=' + pin url += '?pin=' + pin
self.logMsg('Switching to user %s' % userId, 0) self.logMsg('Switching to user %s' % userId, 0)
answer = self.TalkToPlexServer(url, talkType="POST", token=token) answer = self.TalkToPlexServer(url, talkType="POST", token=token)
if not answer: try:
answer.attrib
except:
self.logMsg('Error: plex.tv switch HomeUser change failed', -1) self.logMsg('Error: plex.tv switch HomeUser change failed', -1)
return ('', '') return ('', '')
@ -1173,28 +1187,25 @@ class PlexAPI():
token = answer.attrib.get('authenticationToken', '') token = answer.attrib.get('authenticationToken', '')
# Get final token # Get final token
url = 'https://plex.tv/pms/servers.xml' url = 'https://plex.tv/api/resources?includeHttps=1'
answer = self.TalkToPlexServer(url, talkType="GET", token=token) xml = self.TalkToPlexServer(url, talkType="GET", token=token)
if not answer: try:
self.logMsg('Error: plex.tv switch HomeUser change failed', -1) xml.attrib
except:
self.logMsg('switch HomeUser failed - plex.tv answer wrong', -1)
return ('', '') return ('', '')
found = 0 found = 0
for child in answer: for device in xml:
if child.attrib['machineIdentifier'] == machineId: if device.attrib.get('clientIdentifier') == machineIdentifier:
token = child.attrib['accessToken']
self.logMsg('Found a plex home user token', 1)
found += 1 found += 1
token = device.attrib.get('accessToken')
if found == 0: if found == 0:
self.logMsg('Error: plex.tv switch HomeUser change failed', -1) self.logMsg('No tokens found for your server!', -1)
return ('', '') return ('', '')
self.logMsg('Plex.tv switch HomeUser change successfull', 0) self.logMsg('Plex.tv switch HomeUser change successfull', 0)
self.logMsg("username: %s, token: xxxx. " self.logMsg("username: %s, token: xxxx. " % username, 0)
"Saving to window and file settings" % username, 0)
utils.window('emby_currUser', value=userId)
utils.settings('userId', value=userId)
utils.settings('username', value=username)
utils.window('emby_accessToken%s' % userId, value=token)
return (username, token) return (username, token)
def MyPlexListHomeUsers(self, authtoken): def MyPlexListHomeUsers(self, authtoken):
@ -1218,9 +1229,11 @@ class PlexAPI():
If any value is missing, None is returned instead (or "" from plex.tv) If any value is missing, None is returned instead (or "" from plex.tv)
If an error is encountered, False is returned If an error is encountered, False is returned
""" """
XML = self.getXMLFromPMS('https://plex.tv', '/api/home/users/', {}, authtoken) XML = self.getXMLFromPMS(
'https://plex.tv', '/api/home/users/', {}, authtoken)
if not XML: if not XML:
# Download failed; quitting with False self.logMsg('Download of Plex home users failed.', -1)
self.logMsg('plex.tv xml received was: %s' % XML, -1)
return False return False
# analyse response # analyse response
root = XML.getroot() root = XML.getroot()

View file

@ -37,7 +37,7 @@ class DownloadUtils():
# Requests session # Requests session
s = None s = None
timeout = 3 timeout = 10
def __init__(self): def __init__(self):
@ -224,7 +224,8 @@ class DownloadUtils():
# Get user information # Get user information
self.userId = utils.window('emby_currUser') self.userId = utils.window('emby_currUser')
self.server = utils.window('emby_server%s' % self.userId) self.server = utils.window('emby_server%s' % self.userId)
self.token = utils.window('emby_accessToken%s' % self.userId) self.token = utils.window(
'emby_accessToken%s' % self.userId)
header = self.getHeader(options=headerOptions) header = self.getHeader(options=headerOptions)
verifyssl = False verifyssl = False
cert = None cert = None

View file

@ -79,41 +79,61 @@ def reConnect():
string = xbmcaddon.Addon().getLocalizedString string = xbmcaddon.Addon().getLocalizedString
utils.logMsg("entrypoint reConnect", utils.logMsg("entrypoint reConnect",
"Connection resets requested", 0) "Connection resets requested", 0)
# Pause library sync thread - user needs to be auth in order to sync
utils.window('suspend_LibraryThread', value='true')
# Suspend the user client during procedure
utils.window('suspend_Userclient', value='true')
dialog = xbmcgui.Dialog() dialog = xbmcgui.Dialog()
dialog.notification( dialog.notification(
heading=addonName, heading=addonName,
message=string(39207), message=string(39207),
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
sound=False) sound=False)
# Pause library sync thread - user needs to be auth in order to sync
utils.window('suspend_LibraryThread', value='true')
# Wait max for 20 seconds for all lib scans to finish # Delete plex credentials in settings
utils.settings('myplexlogin', value="true")
utils.settings('plexLogin', value="")
utils.settings('plexToken', value=""),
utils.settings('plexid', value="")
utils.settings('plexHomeSize', value="")
utils.settings('plexAvatar', value="")
# Wait max for 5 seconds for all lib scans to finish
counter = 0 counter = 0
while utils.window('emby_dbScan') == 'true': while utils.window('emby_dbScan') == 'true':
xbmc.sleep(1000) if counter > 100:
counter += 1
if counter > 20:
dialog.ok( dialog.ok(
heading=addonName, heading=addonName,
message=string(39208), message=string(39208),
) )
# Resuming threads, just in case # Resuming threads, just in case
utils.window('suspend_LibraryThread', clear=True) utils.window('suspend_LibraryThread', clear=True)
utils.window('suspend_Userclient', clear=True)
# Abort reConnection # Abort reConnection
return return
counter += 1
xbmc.sleep(50)
# Log out currently signed in user:
utils.window('emby_serverStatus', value="401")
# Above method needs to have run its course! Hence wait
counter = 0
while utils.window('emby_serverStatus') == "401":
if counter > 100:
dialog.ok(
heading=addonName,
message=string(39208),
)
# Abort reConnection
return
counter += 1
xbmc.sleep(50)
# Suspend the user client during procedure
utils.window('suspend_Userclient', value='true')
import initialsetup import initialsetup
initialsetup.InitialSetup().setup(forcePlexTV=True) initialsetup.InitialSetup().setup(forcePlexTV=True)
# Log out currently signed in user:
utils.window('emby_serverStatus', value="401")
# Restart user client
utils.window('suspend_Userclient', clear=True)
# Request lib sync to get user view data (e.g. watched/unwatched) # Request lib sync to get user view data (e.g. watched/unwatched)
utils.window('plex_runLibScan', value='full') utils.window('plex_runLibScan', value='full')
# Restart user client
utils.window('suspend_Userclient', clear=True)
def PassPlaylist(xml, resume=None): def PassPlaylist(xml, resume=None):
@ -222,7 +242,7 @@ def doMainListing():
addDirectoryItem(label, path) addDirectoryItem(label, path)
# Plex user switch, if Plex home is in use # Plex user switch, if Plex home is in use
if utils.settings('plexhome') == 'true': if int(utils.settings('plexHomeSize')) > 1:
addDirectoryItem(string(39200), addDirectoryItem(string(39200),
"plugin://plugin.video.plexkodiconnect/" "plugin://plugin.video.plexkodiconnect/"
"?mode=switchuser") "?mode=switchuser")
@ -401,8 +421,17 @@ def switchPlexUser():
# Pause library sync thread - user needs to be auth in order to sync # Pause library sync thread - user needs to be auth in order to sync
utils.window('suspend_LibraryThread', value='true') utils.window('suspend_LibraryThread', value='true')
# Wait to ensure that any sync already going on has finished # Wait to ensure that any sync already going on has finished
counter = 0
while utils.window('emby_dbScan') == 'true': while utils.window('emby_dbScan') == 'true':
xbmc.sleep(1000) if counter > 100:
# Something went wrong, aborting
# Resuming threads, just in case
utils.window('suspend_LibraryThread', clear=True)
# Abort reConnection
return
counter += 1
xbmc.sleep(50)
# Log out currently signed in user: # Log out currently signed in user:
utils.window('emby_serverStatus', value="401") utils.window('emby_serverStatus', value="401")
# Request lib sync to get user view data (e.g. watched/unwatched) # Request lib sync to get user view data (e.g. watched/unwatched)

View file

@ -40,7 +40,7 @@ class image_cache_thread(threading.Thread):
"http://%s:%s/image/image://%s" "http://%s:%s/image/image://%s"
% (self.xbmc_host, self.xbmc_port, self.urlToProcess)), % (self.xbmc_host, self.xbmc_port, self.urlToProcess)),
auth=(self.xbmc_username, self.xbmc_password), auth=(self.xbmc_username, self.xbmc_password),
timeout=(0.1, 0.1)) timeout=(2, 2))
# We don't need the result # We don't need the result
except: pass except: pass

View file

@ -68,6 +68,24 @@ class InitialSetup():
# Problems connecting to plex.tv. Network or internet issue? # Problems connecting to plex.tv. Network or internet issue?
dialog.ok(self.addonName, dialog.ok(self.addonName,
string(39010)) string(39010))
else:
# Successful connected to plex.tv
# Refresh the info from Plex.tv
url = 'https://plex.tv/'
path = 'users/account'
xml = self.plx.getXMLFromPMS(url, path, authtoken=plexToken)
if xml:
xml = xml.getroot()
plexLogin = xml.attrib.get('title')
utils.settings('plexLogin', value=plexLogin)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
utils.settings('plexhome', value=home)
utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
utils.settings(
'plexHomeSize', value=xml.attrib.get('homeSize', '1'))
self.logMsg('Updated Plex info from plex.tv', 0)
else:
self.logMsg('Failed to update Plex info from plex.tv', -1)
# If a Plex server IP has already been set, return. # If a Plex server IP has already been set, return.
if server and forcePlexTV is False: if server and forcePlexTV is False:
self.logMsg("Server is already set.", 0) self.logMsg("Server is already set.", 0)
@ -175,6 +193,7 @@ class InitialSetup():
return return
# Write to Kodi settings file # Write to Kodi settings file
utils.settings('plex_machineIdentifier', activeServer) utils.settings('plex_machineIdentifier', activeServer)
utils.settings('plex_servername', server['name'])
if server['local'] == '1': if server['local'] == '1':
scheme = server['scheme'] scheme = server['scheme']
utils.settings('ipaddress', server['ip']) utils.settings('ipaddress', server['ip'])

View file

@ -284,6 +284,17 @@ class Movies(Items):
self.add_updateBoxset(boxset) self.add_updateBoxset(boxset)
def add_update(self, item, viewtag=None, viewid=None): def add_update(self, item, viewtag=None, viewid=None):
try:
self.run_add_update(item, viewtag, viewid)
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('itemtypes.py for movies has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
# skip this item for now
return
def run_add_update(self, item, viewtag=None, viewid=None):
# Process single movie # Process single movie
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
@ -296,6 +307,7 @@ class Movies(Items):
# If the item doesn't exist, we'll add it to the database # If the item doesn't exist, we'll add it to the database
update_item = True update_item = True
itemid = API.getRatingKey() itemid = API.getRatingKey()
self.logMsg("Processing item %s" % itemid, 1)
# Cannot parse XML, abort # Cannot parse XML, abort
if not itemid: if not itemid:
self.logMsg("Cannot parse XML data for movie", -1) self.logMsg("Cannot parse XML data for movie", -1)
@ -360,6 +372,7 @@ class Movies(Items):
studio = studios[0] studio = studios[0]
except IndexError: except IndexError:
studio = None studio = None
self.logMsg('Read all attributes', 1)
# Find one trailer # Find one trailer
trailer = None trailer = None
@ -408,6 +421,7 @@ class Movies(Items):
'mode': "play" 'mode': "play"
} }
filename = "%s?%s" % (path, urllib.urlencode(params)) filename = "%s?%s" % (path, urllib.urlencode(params))
self.logMsg('Path set for item', 1)
##### UPDATE THE MOVIE ##### ##### UPDATE THE MOVIE #####
if update_item: if update_item:
self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1) self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1)
@ -454,6 +468,7 @@ class Movies(Items):
# Create the reference in emby table # Create the reference in emby table
emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid) emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid)
self.logMsg('Done add or update for item', 1)
# Update the path # Update the path
query = ' '.join(( query = ' '.join((
@ -902,6 +917,17 @@ class TVShows(Items):
self.contentPop(title, self.newvideo_time) self.contentPop(title, self.newvideo_time)
def add_update(self, item, viewtag=None, viewid=None): def add_update(self, item, viewtag=None, viewid=None):
try:
self.run_add_update(item, viewtag, viewid)
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('itemtypes.py for tv show has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
# skip this item for now
return
def run_add_update(self, item, viewtag=None, viewid=None):
# Process single tvshow # Process single tvshow
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
@ -911,6 +937,8 @@ class TVShows(Items):
update_item = True update_item = True
itemid = API.getRatingKey() itemid = API.getRatingKey()
self.logMsg("Processing item %s" % itemid, 1)
if not itemid: if not itemid:
self.logMsg("Cannot parse XML data for TV show", -1) self.logMsg("Cannot parse XML data for TV show", -1)
return return
@ -964,6 +992,8 @@ class TVShows(Items):
except IndexError: except IndexError:
studio = None studio = None
self.logMsg('Read all attributes', 1)
# GET THE FILE AND PATH ##### # GET THE FILE AND PATH #####
playurl = API.getKey() playurl = API.getKey()
@ -997,7 +1027,7 @@ class TVShows(Items):
# Set plugin path # Set plugin path
toplevelpath = "plugin://plugin.video.plexkodiconnect.tvshows/" toplevelpath = "plugin://plugin.video.plexkodiconnect.tvshows/"
path = "%s%s/" % (toplevelpath, itemid) path = "%s%s/" % (toplevelpath, itemid)
self.logMsg('Path set for item', 1)
# UPDATE THE TVSHOW ##### # UPDATE THE TVSHOW #####
if update_item: if update_item:
self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1) self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1)
@ -1052,7 +1082,7 @@ class TVShows(Items):
# Create the reference in emby table # Create the reference in emby table
emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid,
checksum=checksum, mediafolderid=viewid) checksum=checksum, mediafolderid=viewid)
self.logMsg('Done add or update for item', 1)
# Update the path # Update the path
query = ' '.join(( query = ' '.join((
@ -1083,13 +1113,25 @@ class TVShows(Items):
all_episodes = embyserver.getEpisodesbyShow(itemid) all_episodes = embyserver.getEpisodesbyShow(itemid)
self.added_episode(all_episodes['Items'], None) self.added_episode(all_episodes['Items'], None)
def add_updateSeason(self, item, viewid=None, viewtag=None): def add_updateSeason(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateSeason(item, viewtag, viewid)
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('itemtypes.py for tv seasons has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
# skip this item for now
return
def run_add_updateSeason(self, item, viewid=None, viewtag=None):
API = PlexAPI.API(item) API = PlexAPI.API(item)
showid = viewid showid = viewid
itemid = API.getRatingKey() itemid = API.getRatingKey()
if not itemid: if not itemid:
self.logMsg('Error getting itemid for season, skipping', -1) self.logMsg('Error getting itemid for season, skipping', -1)
return return
self.logMsg("Processing item %s" % itemid, 1)
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db kodi_db = self.kodi_db
@ -1117,6 +1159,17 @@ class TVShows(Items):
emby_db.addReference(itemid, seasonid, "Season", "season", parentid=showid, checksum=checksum) emby_db.addReference(itemid, seasonid, "Season", "season", parentid=showid, checksum=checksum)
def add_updateEpisode(self, item, viewtag=None, viewid=None): def add_updateEpisode(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateEpisode(item, viewtag, viewid)
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('itemtypes.py for tv episode has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
# skip this item for now
return
def run_add_updateEpisode(self, item, viewtag=None, viewid=None):
""" """
viewtag and viewid are irrelevant! viewtag and viewid are irrelevant!
""" """
@ -1136,6 +1189,7 @@ class TVShows(Items):
if not itemid: if not itemid:
self.logMsg('Error getting itemid for episode, skipping', -1) self.logMsg('Error getting itemid for episode, skipping', -1)
return return
self.logMsg("Processing item %s" % itemid, 1)
emby_dbitem = emby_db.getItem_byId(itemid) emby_dbitem = emby_db.getItem_byId(itemid)
try: try:
episodeid = emby_dbitem[0] episodeid = emby_dbitem[0]
@ -1584,7 +1638,19 @@ class Music(Items):
if not pdialog and self.contentmsg: if not pdialog and self.contentmsg:
self.contentPop(title, self.newmusic_time) self.contentPop(title, self.newmusic_time)
def add_updateArtist(self, item, viewtag=None, viewid=None, def add_updateArtist(self, item, viewtag=None, viewid=None, artisttype="MusicArtist"):
try:
self.run_add_updateArtist(item, viewtag, viewid, artisttype)
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('itemtypes.py for music artist has crashed for '
'item %s. Error:'
% item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
# skip this item for now
return
def run_add_updateArtist(self, item, viewtag=None, viewid=None,
artisttype="MusicArtist"): artisttype="MusicArtist"):
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
@ -1594,6 +1660,7 @@ class Music(Items):
update_item = True update_item = True
itemid = API.getRatingKey() itemid = API.getRatingKey()
self.logMsg("Processing item %s" % itemid, 1)
emby_dbitem = emby_db.getItem_byId(itemid) emby_dbitem = emby_db.getItem_byId(itemid)
try: try:
artistid = emby_dbitem[0] artistid = emby_dbitem[0]
@ -1668,6 +1735,18 @@ class Music(Items):
artwork.addArtwork(artworks, artistid, "artist", kodicursor) artwork.addArtwork(artworks, artistid, "artist", kodicursor)
def add_updateAlbum(self, item, viewtag=None, viewid=None): def add_updateAlbum(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateAlbum(item, viewtag, viewid)
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('itemtypes.py for music album has crashed for '
'item %s. Error:'
% item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
# skip this item for now
return
def run_add_updateAlbum(self, item, viewtag=None, viewid=None):
kodiversion = self.kodiversion kodiversion = self.kodiversion
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
@ -1680,6 +1759,7 @@ class Music(Items):
if not itemid: if not itemid:
self.logMsg('Error processing Album, skipping', -1) self.logMsg('Error processing Album, skipping', -1)
return return
self.logMsg("Processing item %s" % itemid, 1)
emby_dbitem = emby_db.getItem_byId(itemid) emby_dbitem = emby_db.getItem_byId(itemid)
try: try:
albumid = emby_dbitem[0] albumid = emby_dbitem[0]
@ -1853,6 +1933,18 @@ class Music(Items):
artwork.addArtwork(artworks, albumid, "album", kodicursor) artwork.addArtwork(artworks, albumid, "album", kodicursor)
def add_updateSong(self, item, viewtag=None, viewid=None): def add_updateSong(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateSong(item, viewtag, viewid)
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('itemtypes.py for music song has crashed for '
'item %s. Error:'
% item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
# skip this item for now
return
def run_add_updateSong(self, item, viewtag=None, viewid=None):
# Process single song # Process single song
kodiversion = self.kodiversion kodiversion = self.kodiversion
kodicursor = self.kodicursor kodicursor = self.kodicursor
@ -1866,6 +1958,7 @@ class Music(Items):
if not itemid: if not itemid:
self.logMsg('Error processing Song; skipping', -1) self.logMsg('Error processing Song; skipping', -1)
return return
self.logMsg("Processing item %s" % itemid, 1)
emby_dbitem = emby_db.getItem_byId(itemid) emby_dbitem = emby_db.getItem_byId(itemid)
try: try:
songid = emby_dbitem[0] songid = emby_dbitem[0]

View file

@ -111,6 +111,8 @@ class UserClient(threading.Thread):
settings = utils.settings settings = utils.settings
# Original host # Original host
self.machineIdentifier = utils.settings('plex_machineIdentifier')
self.servername = utils.settings('plex_servername')
HTTPS = settings('https') == "true" HTTPS = settings('https') == "true"
host = settings('ipaddress') host = settings('ipaddress')
port = settings('port') port = settings('port')
@ -192,29 +194,17 @@ class UserClient(threading.Thread):
def setUserPref(self): def setUserPref(self):
self.logMsg('Setting user preferences', 0) self.logMsg('Setting user preferences', 0)
url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser) # Only try to get user avatar if there is a token
if url: if self.currToken:
utils.window('EmbyUserImage', value=url) url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser)
if url:
utils.window('EmbyUserImage', value=url)
# Set resume point max # Set resume point max
# url = "{server}/emby/System/Configuration?format=json" # url = "{server}/emby/System/Configuration?format=json"
# result = doUtils.downloadUrl(url) # result = doUtils.downloadUrl(url)
# utils.settings('markPlayed', value=str(result['MaxResumePct'])) # utils.settings('markPlayed', value=str(result['MaxResumePct']))
def getPublicUsers(self):
server = self.getServer()
# Get public Users
url = "%s/emby/Users/Public?format=json" % server
result = self.doUtils.downloadUrl(url, authenticate=False)
if result != "":
return result
else:
# Server connection failed
return False
def hasAccess(self): def hasAccess(self):
# Plex: always return True for now # Plex: always return True for now
return True return True
@ -241,18 +231,15 @@ class UserClient(threading.Thread):
xbmcgui.Dialog().notification(self.addonName, xbmcgui.Dialog().notification(self.addonName,
utils.language(33007)) utils.language(33007))
def loadCurrUser(self, authenticated=False): def loadCurrUser(self, username, userId, usertoken, authenticated=False):
self.logMsg('Loading current user', 0) self.logMsg('Loading current user', 0)
window = utils.window window = utils.window
settings = utils.settings
doUtils = self.doUtils doUtils = self.doUtils
username = self.getUsername()
userId = self.getUserId()
self.currUserId = userId self.currUserId = userId
self.currToken = usertoken
self.currServer = self.getServer() self.currServer = self.getServer()
self.currToken = self.getToken()
self.machineIdentifier = utils.settings('plex_machineIdentifier')
self.ssl = self.getSSLverify() self.ssl = self.getSSLverify()
self.sslcert = self.getSSL() self.sslcert = self.getSSL()
@ -268,7 +255,6 @@ class UserClient(threading.Thread):
return False return False
elif res == 401: elif res == 401:
self.logMsg('Token is no longer valid', -1) self.logMsg('Token is no longer valid', -1)
self.resetClient()
return False return False
elif res >= 400: elif res >= 400:
self.logMsg('Answer from PMS is not as expected. Retrying', -1) self.logMsg('Answer from PMS is not as expected. Retrying', -1)
@ -280,9 +266,7 @@ class UserClient(threading.Thread):
window('emby_accessToken%s' % userId, value=self.currToken) window('emby_accessToken%s' % userId, value=self.currToken)
window('emby_server%s' % userId, value=self.currServer) window('emby_server%s' % userId, value=self.currServer)
window('plex_machineIdentifier', value=self.machineIdentifier) window('plex_machineIdentifier', value=self.machineIdentifier)
window('plex_servername', value=self.servername)
window('emby_serverStatus', clear=True)
window('suspend_LibraryThread', clear=True)
# Set DownloadUtils values # Set DownloadUtils values
doUtils.setUsername(username) doUtils.setUsername(username)
@ -290,8 +274,6 @@ class UserClient(threading.Thread):
doUtils.setServer(self.currServer) doUtils.setServer(self.currServer)
doUtils.setToken(self.currToken) doUtils.setToken(self.currToken)
doUtils.setSSL(self.ssl, self.sslcert) doUtils.setSSL(self.ssl, self.sslcert)
# parental control - let's verify if access is restricted
# self.hasAccess()
# Start DownloadUtils session # Start DownloadUtils session
doUtils.startSession() doUtils.startSession()
@ -299,6 +281,23 @@ class UserClient(threading.Thread):
# Set user preferences in settings # Set user preferences in settings
self.currUser = username self.currUser = username
self.setUserPref() self.setUserPref()
# Writing values to settings file
settings('username', value=username)
settings('userid', value=userId)
settings('accessToken', value=usertoken)
dialog = xbmcgui.Dialog()
if username:
dialog.notification(
heading=self.addonName,
message="Welcome " + username,
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png")
else:
dialog.notification(
heading=self.addonName,
message="Welcome",
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png")
return True return True
def authenticate(self): def authenticate(self):
@ -309,6 +308,15 @@ class UserClient(threading.Thread):
settings = utils.settings settings = utils.settings
dialog = xbmcgui.Dialog() dialog = xbmcgui.Dialog()
# Give attempts at entering password / selecting user
if self.retry >= 2:
log("Too many retries to login.", -1)
window('emby_serverStatus', value="Stop")
dialog.ok(lang(33001),
lang(39023))
xbmc.executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
# Get /profile/addon_data # Get /profile/addon_data
addondir = xbmc.translatePath( addondir = xbmc.translatePath(
self.addon.getAddonInfo('profile')).decode('utf-8') self.addon.getAddonInfo('profile')).decode('utf-8')
@ -318,98 +326,94 @@ class UserClient(threading.Thread):
if not hasSettings: if not hasSettings:
log("Error, no settings.xml found.", -1) log("Error, no settings.xml found.", -1)
self.auth = False self.auth = False
return return False
server = self.getServer() server = self.getServer()
# If no user information # If there is no server we can connect to
if not server: if not server:
log("Missing server information.", 0) log("Missing server information.", 0)
self.auth = False self.auth = False
return return False
username = self.getUsername() # If there is a username in the settings, try authenticating
userId = self.getUserId(username) username = settings('username')
# If there's a token, load the user userId = settings('userid')
if self.getToken(username=username, userId=userId): usertoken = settings('accessToken')
if self.loadCurrUser() is False: enforceLogin = settings('enforceUserLogin')
pass # Found a user in the settings, try to authenticate
if username and enforceLogin == 'false':
log('Trying to authenticate with old settings', 0)
if self.loadCurrUser(username,
userId,
usertoken,
authenticated=False):
# SUCCESS: loaded a user from the settings
return True
else: else:
# We successfully loaded a user # Failed to use the settings - delete them!
log("Current user: %s" % self.currUser, 1) log("Failed to use the settings credentials. Deleting them", 1)
log("Current userId: %s" % self.currUserId, 1) settings('username', value='')
log("Current accessToken: xxxx", 1) settings('userid', value='')
return settings('accessToken', value='')
# AUTHENTICATE USER #####
plx = PlexAPI.PlexAPI() plx = PlexAPI.PlexAPI()
# Choose Plex user login
plexdict = plx.GetPlexLoginFromSettings()
myplexlogin = plexdict['myplexlogin']
plexhome = plexdict['plexhome']
if myplexlogin == "true" and plexhome == 'true': # Could not use settings - try to get Plex user list from plex.tv
username, userId, accessToken = plx.ChoosePlexHomeUser() plextoken = settings('plexToken')
else: if plextoken:
log("Trying to connect to PMS without a token", 0) log("Trying to connect to plex.tv to get a user list", 0)
accessToken = '' userInfo = plx.ChoosePlexHomeUser(plextoken)
# Check connection if userInfo is False:
if plx.CheckConnection(server, accessToken) == 200: # FAILURE: Something went wrong, try again
self.currUser = username
settings('accessToken', value=accessToken)
settings('userId', value=userId)
log("User authenticated with an access token", 1)
if self.loadCurrUser(authenticated=True) is False:
# Something went really wrong, return and try again
self.auth = True self.auth = True
self.currUser = None self.retry += 1
return return False
# Success! username = userInfo['username']
if username: userId = userInfo['userid']
dialog.notification( usertoken = userInfo['token']
heading=self.addonName,
message="Welcome " + username,
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png")
else:
dialog.notification(
heading=self.addonName,
message="Welcome",
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png")
self.retry = 0
# Make sure that lib sync thread is not paused
else: else:
self.logMsg("Error: user authentication failed.", -1) log("Trying to authenticate without a token", 0)
settings('accessToken', value="") username = ''
settings('userId', value="") userId = ''
usertoken = ''
# Give attempts at entering password / selecting user
if self.retry >= 2:
log("Too many retries to login.", -1)
window('emby_serverStatus', value="Stop")
dialog.ok(lang(33001),
lang(39023))
xbmc.executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
if self.loadCurrUser(username, userId, usertoken, authenticated=False):
# SUCCESS: loaded a user from the settings
return True
else:
# FAILUR: Something went wrong, try again
self.auth = True
self.retry += 1 self.retry += 1
self.auth = False return False
def resetClient(self): def resetClient(self):
self.logMsg("Reset UserClient authentication.", 1) self.logMsg("Reset UserClient authentication.", 1)
settings = utils.settings
window = utils.window
window('emby_accessToken%s' % self.currUserId, clear=True)
window('emby_server%s' % self.currUserId, clear=True)
window('emby_currUser', clear=True)
window('plex_username', clear=True)
settings('username', value='')
settings('userid', value='')
settings('accessToken', value='')
# Reset token in downloads
self.doUtils.setToken('')
self.doUtils.setUserId('')
self.doUtils.setUsername('')
utils.settings('accessToken', value="")
utils.window('emby_accessToken%s' % self.currUserId, clear=True)
self.currToken = None self.currToken = None
self.logMsg("User token has been removed. Pausing Lib sync thread", 1)
utils.window('suspend_LibraryThread', value="true")
self.auth = True self.auth = True
self.currUser = None self.currUser = None
self.currUserId = None self.currUserId = None
self.retry = 0
def run(self): def run(self):
log = self.logMsg log = self.logMsg
window = utils.window window = utils.window
# Start library sync thread in a suspended mode, until signed in
utils.window('suspend_LibraryThread', value="true")
log("----===## Starting UserClient ##===----", 0) log("----===## Starting UserClient ##===----", 0)
while not self.threadStopped(): while not self.threadStopped():
@ -429,6 +433,7 @@ class UserClient(threading.Thread):
# Unauthorized access, revoke token # Unauthorized access, revoke token
window('emby_serverStatus', value="Auth") window('emby_serverStatus', value="Auth")
self.resetClient() self.resetClient()
xbmc.sleep(2000)
if self.auth and (self.currUser is None): if self.auth and (self.currUser is None):
# Try to authenticate user # Try to authenticate user
@ -436,7 +441,14 @@ class UserClient(threading.Thread):
# Set auth flag because we no longer need # Set auth flag because we no longer need
# to authenticate the user # to authenticate the user
self.auth = False self.auth = False
self.authenticate() if self.authenticate():
# Successfully authenticated and loaded a user
log("Current user: %s" % self.currUser, 1)
log("Current userId: %s" % self.currUserId, 1)
log("Current accessToken: xxxx", 1)
self.retry = 0
window('suspend_LibraryThread', clear=True)
window('emby_serverStatus', clear=True)
if not self.auth and (self.currUser is None): if not self.auth and (self.currUser is None):
# Loop if no server found # Loop if no server found

View file

@ -16,7 +16,9 @@
<setting id="secondsslcert" subsetting="true" label="30501" type="file" default="None" visible="eq(-2,true)" /> <setting id="secondsslcert" subsetting="true" label="30501" type="file" default="None" visible="eq(-2,true)" />
<!-- User settings --> <!-- User settings -->
<setting type="sep" /> <setting type="sep" />
<setting id="deviceName" label="30016" type="text" visible="eq(-1,true)" default="Kodi" />
<setting id="enforceUserLogin" label="30536" type="bool" default="false" />
<setting label="30505" type="action" visible="eq(1,) + !eq(-15,)" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=resetauth)" option="close" /> <setting label="30505" type="action" visible="eq(1,) + !eq(-15,)" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=resetauth)" option="close" />
<setting label="30517" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=passwords)" option="close" /><!-- Network credentials --> <setting label="30517" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=passwords)" option="close" /><!-- Network credentials -->
<setting id="accessToken" type="text" visible="false" default="" /> <setting id="accessToken" type="text" visible="false" default="" />
@ -32,14 +34,16 @@
<setting id="plexLogin" label="plex.tv username" type="text" default="" visible="false" /> <setting id="plexLogin" label="plex.tv username" type="text" default="" visible="false" />
<setting id="plexhome" label="Plex home in use" type="bool" default="" visible="false" /> <setting id="plexhome" label="Plex home in use" type="bool" default="" visible="false" />
<setting id="plexToken" label="plexToken" type="text" default="" visible="false" /> <setting id="plexToken" label="plexToken" type="text" default="" visible="false" />
<setting id="plexHomeSize" type="number" default="1" visible="false" />
</category> </category>
<category label="Plex Companion"> <category label="Plex Companion">
<setting type="lsep" label="39008" /> <setting type="lsep" label="39008" />
<setting id="plexCompanion" label="39004" type="bool" default="true" /> <setting id="plexCompanion" label="39004" type="bool" default="true" />
<setting id="deviceNameOpt" label="30504" type="bool" default="false" /> <setting id="deviceNameOpt" label="30504" type="bool" default="false" />
<setting id="companionPort" label="39005" type="number" default="3005" option="int" visible="eq(-1,true)"/> <setting id="deviceName" label="30016" type="text" visible="eq(-1,true)" default="Kodi" />
<setting id="companionDebugging" label="39006" type="bool" default="false" visible="eq(-3,true)"/> <setting id="companionPort" label="39005" type="number" default="3005" option="int" visible="eq(-3,true)"/>
<setting id="companionGDMDebugging" label="39007" type="bool" default="false" visible="eq(-4,true)"/> <setting id="companionDebugging" label="39006" type="bool" default="false" visible="eq(-4,true)"/>
<setting id="companionGDMDebugging" label="39007" type="bool" default="false" visible="eq(-5,true)"/>
</category> </category>
<category label="30506"><!-- Sync Options --> <category label="30506"><!-- Sync Options -->
<setting id="serverSync" type="bool" label="30514" default="true" visible="false"/><!-- Enable fast startup (requires server plugin) --> <setting id="serverSync" type="bool" label="30514" default="true" visible="false"/><!-- Enable fast startup (requires server plugin) -->