diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 92b1eafc..767f1abc 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -2,12 +2,13 @@ ############################################################################### from logging import getLogger from Queue import Queue +from xml.etree.ElementTree import ParseError import xbmc import xbmcgui from utils import settings, window, language as lang, tryEncode, \ - advancedsettings_xml + XmlKodiSetting, reboot_kodi import downloadutils from userclient import UserClient @@ -402,17 +403,28 @@ class InitialSetup(): """ LOG.info("Initial setup called.") dialog = self.dialog - - # Get current Kodi video cache setting - cache, _ = advancedsettings_xml(['cache', 'memorysize']) + try: + with XmlKodiSetting('advancedsettings.xml', + force_create=True, + top_element='advancedsettings') as xml: + # Get current Kodi video cache setting + cache = xml.get_setting(['cache', 'memorysize']) + # Disable foreground "Loading media information from files" + # (still used by Kodi, even though the Wiki says otherwise) + xml.set_setting(['musiclibrary', 'backgroundupdate'], + value='true') + # Disable cleaning of library - not compatible with PKC + xml.set_setting(['videolibrary', 'cleanonupdate'], + value='false') + reboot = xml.write_xml + except ParseError: + cache = None + reboot = False # Kodi default cache if no setting is set cache = str(cache.text) if cache is not None else '20971520' LOG.info('Current Kodi video memory cache in bytes: %s', cache) settings('kodi_video_cache', value=cache) - # Disable foreground "Loading media information from files" - # (still used by Kodi, even though the Wiki says otherwise) - advancedsettings_xml(['musiclibrary', 'backgroundupdate'], - new_value='true') + # Do we need to migrate stuff? check_migration() # Optionally sign into plex.tv. Will not be called on very first run @@ -436,6 +448,8 @@ class InitialSetup(): LOG.info("Using PMS %s with machineIdentifier %s", self.server, self.serverid) self._write_PMS_settings(self.server, self.pms_token) + if reboot is True: + reboot_kodi() return # If not already retrieved myplex info, optionally let user sign in @@ -450,6 +464,8 @@ class InitialSetup(): # User already answered the installation questions if settings('InstallQuestionsAnswered') == 'true': + if reboot is True: + reboot_kodi() return # Additional settings where the user needs to choose @@ -517,3 +533,5 @@ class InitialSetup(): state.PMS_STATUS = 'Stop' xbmc.executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') + elif reboot is True: + reboot_kodi() diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 28d173b0..82f21f4b 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -458,14 +458,8 @@ class LibrarySync(Thread): Compare the views to Plex """ if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True: - if music.set_excludefromscan_music_folders() is True: - log.info('Detected new Music library - restarting now') - # 'New Plex music library detected. Sorry, but we need to - # restart Kodi now due to the changes made.' - dialog('ok', heading='{plex}', line1=lang(39711)) - from xbmc import executebuiltin - executebuiltin('RestartApp') - return False + # Will reboot Kodi is new library detected + music.excludefromscan_music_folders() self.views = [] vnodes = self.vnodes diff --git a/resources/lib/music.py b/resources/lib/music.py index ab229f0e..136c0a7a 100644 --- a/resources/lib/music.py +++ b/resources/lib/music.py @@ -1,57 +1,33 @@ # -*- coding: utf-8 -*- from logging import getLogger from re import compile as re_compile -import xml.etree.ElementTree as etree +from xml.etree.ElementTree import ParseError -from utils import advancedsettings_xml, indent, tryEncode +from utils import XmlKodiSetting, reboot_kodi, language as lang from PlexFunctions import get_plex_sections from PlexAPI import API import variables as v ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''') ############################################################################### -def get_current_music_folders(): - """ - Returns a list of encoded strings as paths to the currently "blacklisted" - excludefromscan music folders in the advancedsettings.xml - """ - paths = [] - root, _ = advancedsettings_xml(['audio', 'excludefromscan']) - if root is None: - return paths - - for element in root: - try: - path = REGEX_MUSICPATH.findall(element.text)[0] - except IndexError: - log.error('Could not parse %s of xml element %s' - % (element.text, element.tag)) - continue - else: - paths.append(path) - return paths - - -def set_excludefromscan_music_folders(): +def excludefromscan_music_folders(): """ Gets a complete list of paths for music libraries from the PMS. Sets them to be excluded in the advancedsettings.xml from being scanned by Kodi. Existing keys will be replaced - Returns False if no new Plex libraries needed to be exluded, True otherwise + Reboots Kodi if new library detected """ - changed = False - write_xml = False xml = get_plex_sections() try: xml[0].attrib except (TypeError, IndexError, AttributeError): - log.error('Could not get Plex sections') + LOG.error('Could not get Plex sections') return # Build paths paths = [] @@ -66,39 +42,38 @@ def set_excludefromscan_music_folders(): typus=v.PLEX_TYPE_ARTIST, omitCheck=True) paths.append(__turn_to_regex(path)) - # Get existing advancedsettings - root, tree = advancedsettings_xml(['audio', 'excludefromscan'], - force_create=True) - - for path in paths: - for element in root: - if element.text == path: - # Path already excluded - break - else: - changed = True - write_xml = True - log.info('New Plex music library detected: %s' % path) - element = etree.Element(tag='regexp') - element.text = path - root.append(element) - - # Delete obsolete entries (unlike above, we don't change 'changed' to not - # enforce a restart) - for element in root: - for path in paths: - if element.text == path: - break - else: - log.info('Deleting Plex music library from advancedsettings: %s' - % element.text) - root.remove(element) - write_xml = True - - if write_xml is True: - indent(tree.getroot()) - tree.write('%sadvancedsettings.xml' % v.KODI_PROFILE, encoding="UTF-8") - return changed + try: + with XmlKodiSetting('advancedsettings.xml', + force_create=True, + top_element='advancedsettings') as xml: + parent = xml.set_setting(['audio', 'excludefromscan']) + for path in paths: + for element in parent: + if element.text == path: + # Path already excluded + break + else: + LOG.info('New Plex music library detected: %s', path) + xml.set_setting(['audio', 'excludefromscan', 'regexp'], + value=path, check_existing=False) + # We only need to reboot if we ADD new paths! + reboot = xml.write_xml + # Delete obsolete entries + for element in parent: + for path in paths: + if element.text == path: + break + else: + LOG.info('Deleting music library from advancedsettings: %s', + element.text) + parent.remove(element) + except (ParseError, IOError): + LOG.error('Could not adjust advancedsettings.xml') + reboot = False + if reboot is True: + # 'New Plex music library detected. Sorry, but we need to + # restart Kodi now due to the changes made.' + reboot_kodi(lang(39711)) def __turn_to_regex(path): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 6dfdd941..68ab881b 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -37,6 +37,17 @@ ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') # Main methods +def reboot_kodi(message=None): + """ + Displays an OK prompt with 'Kodi will now restart to apply the changes' + Kodi will then reboot. + + Set optional custom message + """ + message = message or language(33033) + dialog('ok', heading='{plex}', line1=message) + xbmc.executebuiltin('RestartApp') + def window(property, value=None, clear=False, windowid=10000): """ Get or set window property - thread safe! @@ -457,12 +468,7 @@ def reset(): dataPath = "%ssettings.xml" % addondir log.info("Deleting: settings.xml") remove(dataPath) - - # Kodi will now restart to apply the changes. - dialog('ok', - heading='{plex} %s ' % language(30132), - line1=language(33033)) - xbmc.executebuiltin('RestartApp') + reboot_kodi() def profiling(sortby="cumulative"): @@ -617,114 +623,166 @@ def guisettingsXML(): return root -def __setXMLTag(element, tag, value, attrib=None): +class XmlKodiSetting(object): """ - Looks for an element's subelement and sets its value. - If "subelement" does not exist, create it using attrib and value. + Used to load a Kodi XML settings file from special://profile as an etree + object to read settings or set them. Usage: + with XmlKodiSetting(filename, + path=None, + force_create=False, + top_element=None) as xml: + xml.get_setting('test') - element : etree element - tag : unicode for subelement - value : unicode - attrib : dict; will use etree attrib method + filename [str]: filename of the Kodi settings file under + path [str]: if set, replace special://profile path with custom + path + force_create: will create the XML file if it does not exist + top_element [str]: Name of the top xml element; used if xml does not + yet exist - Returns the subelement + Raises IOError if the file does not exist or is empty and force_create + has been set to False. + Raises etree.ParseError if the file could not be parsed by etree + + xml.write_xml Set to True if we need to write the XML to disk """ - subelement = element.find(tag) - if subelement is None: - # Setting does not exist yet; create it - if attrib is None: - etree.SubElement(element, tag).text = value + def __init__(self, filename, path=None, force_create=False, + top_element=None): + self.filename = filename + if path is None: + self.path = join(KODI_PROFILE, filename) else: - etree.SubElement(element, tag, attrib=attrib).text = value - else: - subelement.text = value - return subelement + self.path = join(path, filename) + self.force_create = force_create + self.top_element = top_element + self.tree = None + self.root = None + self.write_xml = False + def __enter__(self): + try: + self.tree = etree.parse(self.path) + except IOError: + # Document is blank or missing + if self.force_create is False: + log.debug('%s does not seem to exist; not creating', self.path) + # This will abort __enter__ + self.__exit__(IOError, None, None) + # Create topmost xml entry + self.tree = etree.ElementTree( + element=etree.Element(self.top_element)) + self.write_xml = True + except etree.ParseError: + log.error('Error parsing %s', self.path) + # "Kodi cannot parse {0}. PKC will not function correctly. Please + # visit {1} and correct your file!" + dialog('ok', language(29999), language(39716).format( + self.filename, + 'http://kodi.wiki')) + self.__exit__(etree.ParseError, None, None) + self.root = self.tree.getroot() + return self -def __setSubElement(element, subelement): - """ - Returns an etree element's subelement. Creates one if not exist - """ - answ = element.find(subelement) - if answ is None: - answ = etree.SubElement(element, subelement) - return answ + def __exit__(self, e_typ, e_val, trcbak): + if e_typ: + raise + # Only safe to file if we did not botch anything + if self.write_xml is True: + # Indent and make readable + indent(self.root) + # Safe the changed xml + self.tree.write(self.path, encoding="UTF-8") + @staticmethod + def _set_sub_element(element, subelement): + """ + Returns an etree element's subelement. Creates one if not exist + """ + answ = element.find(subelement) + if answ is None: + answ = etree.SubElement(element, subelement) + return answ -def advancedsettings_xml(node_list, new_value=None, attrib=None, - force_create=False): - """ - Returns - etree element, tree - or - None, None + def get_setting(self, node_list): + """ + node_list is a list of node names starting from the outside, ignoring + the outter advancedsettings. + Example nodelist=['video', 'busydialogdelayms'] for the following xml + would return the etree Element: - node_list is a list of node names starting from the outside, ignoring the - outter advancedsettings. Example nodelist=['video', 'busydialogdelayms'] - for the following xml would return the etree Element: - - 750 - - for the following example xml: - - - - - - If new_value is set, '750' will be replaced accordingly, returning the new - etree Element. Advancedsettings might be generated if it did not exist - already + for the following example xml: - If the dict attrib is set, the Element's attributs will be appended - accordingly + + + - force_create=True will forcibly create the key even if no value is provided - """ - path = '%sadvancedsettings.xml' % KODI_PROFILE - try: - tree = etree.parse(path) - except IOError: - # Document is blank or missing - if new_value is None and attrib is None and force_create is False: - log.debug('Could not parse advancedsettings.xml, returning None') - return None, None - # Create topmost xml entry - tree = etree.ElementTree(element=etree.Element('advancedsettings')) - except etree.ParseError: - log.error('Error parsing %s' % path) - # "Kodi cannot parse {0}. PKC will not function correctly. Please visit - # {1} and correct your file!" - dialog('ok', language(29999), language(39716).format( - 'advancedsettings.xml', - 'http://kodi.wiki/view/Advancedsettings.xml')) - return None, None - root = tree.getroot() - element = root - - # Reading values - if new_value is None and attrib is None and force_create is False: + Returns the etree element or None if not found + """ + element = self.root for node in node_list: element = element.find(node) if element is None: break - return element, tree + return element - # Setting new values. Get correct element first - for node in node_list: - element = __setSubElement(element, node) - # Write new values - element.text = new_value or '' - if attrib is not None: - for key, attribute in attrib.iteritems(): - element.set(key, attribute) - # Indent and make readable - indent(root) - # Safe the changed xml - tree.write(path, encoding="UTF-8") - return element, tree + def set_setting(self, node_list, value=None, attrib=None, + check_existing=True): + """ + node_list is a list of node names starting from the outside, ignoring + the outter advancedsettings. + Example nodelist=['video', 'busydialogdelayms'] for the following xml + would return the etree Element: + + 750 + + for the following example xml: + + + + + + value, e.g. '750' will be set accordingly, returning the new + etree Element. Advancedsettings might be generated if it did not exist + already + + If the dict attrib is set, the Element's attributs will be appended + accordingly + + If check_existing is True, it will return the FIRST matching element of + node_list. Set to False if there are several elements of the same tag! + + Returns the (last) etree element + """ + attrib = attrib or {} + value = value or '' + if check_existing is True: + old = self.get_setting(node_list) + if old is not None: + already_set = True + if old.text.strip() != value: + already_set = False + elif old.attrib != attrib: + already_set = False + if already_set is True: + log.debug('Element has already been found') + return old + # Need to set new setting, indeed + self.write_xml = True + element = self.root + for node in node_list: + element = self._set_sub_element(element, node) + # Write new values + element.text = value + if attrib: + for key, attribute in attrib.iteritems(): + element.set(key, attribute) + return element def sourcesXML():