""" Created on 09/08/2011 @author: mikel """ __all__ = ["fonts", "includes"] import logging import os import re import shutil import sys import time import xml.etree.ElementTree as ET from datetime import datetime from os import listdir from os.path import isdir, dirname, basename import xbmc import xbmcgui class SkinUtilsError(Exception): pass def reload_skin(): xbmc.executebuiltin("XBMC.ReloadSkin()") def setup_logging(): # Keep comaptibility with Python2.6 if hasattr(logging, 'NullHandler'): logger = logging.getLogger('skinutils') logger.addHandler(logging.NullHandler()) def get_logger(): return logging.getLogger('skinutils') def debug_log(msg): get_logger().debug(msg) def get_sha1_obj(): # SHA1 lib 2.4 compatibility try: from hashlib import sha1 return sha1() except: import sha return sha.new() def sha1_file(file, block_size=2 ** 20): f = open(file, 'rb') sha1 = get_sha1_obj() while True: data = f.read(block_size) if not data: break sha1.update(data) f.close() return sha1.hexdigest() def try_remove_file(file, wait=0.5, tries=10): removed = False num_try = 0 while num_try < tries and not removed: try: os.remove(file) return True except OSError: num_try += 1 time.sleep(wait) return False def case_file_exists(file): if not os.path.isfile(file): return False else: file_dir = dirname(file) if not isdir(file_dir): return False else: dir_contents = listdir(file_dir) return basename(file) in dir_contents def get_current_skin_path(): return os.path.normpath(xbmc.translatePath("special://skin/")) def get_skin_name(): return os.path.basename(get_current_skin_path()) def get_local_skin_path(): user_addons_path = xbmc.translatePath("special://home/addons") return os.path.normpath( os.path.join(user_addons_path, get_skin_name()) ) def copy_skin_to_userdata(ask_user=True): # Warn user before doing this weird thing d = xbmcgui.Dialog() msg1 = "This addon needs to install some extra resources." msg2 = "This installation requires a manual XBMC restart." msg3 = "Begin installation now? After that it will exit." make_copy = ( not ask_user or d.yesno("Notice", msg1, msg2, msg3) ) if make_copy: # Get skin dest name local_skin_path = get_local_skin_path() # If it was not copied before... if not os.path.exists(local_skin_path): shutil.copytree(get_current_skin_path(), local_skin_path) return make_copy def is_invalid_local_skin(): # Get skin paths current_skin_path = get_current_skin_path() local_skin_path = get_local_skin_path() # If the local path does not exist if not os.path.isdir(local_skin_path): return False else: # Get addon xml paths current_xml = os.path.join(current_skin_path, 'addon.xml') local_xml = os.path.join(local_skin_path, 'addon.xml') # Both files must exist if not os.path.isfile(current_xml) or not os.path.isfile(local_xml): return True # If sum of both files mismatch, got it! elif sha1_file(current_xml) != sha1_file(local_xml): return True # Otherwise everything is ok else: return False def fix_invalid_local_skin(): local_skin_path = get_local_skin_path() time_suffix = datetime.now().strftime('%Y%m%d%H%M%S') backup_skin_path = local_skin_path + '-skinutils-' + time_suffix # Just move the skin, if it already exists someone is trolling us... shutil.move(local_skin_path, backup_skin_path) # And now do the real copy copy_skin_to_userdata(ask_user=False) # Inform the user about the operation... d = xbmcgui.Dialog() l1 = "Your local skin is not in use (probably outdated)." l2 = "Press OK to apply a fix (archiving the old skin)." l3 = "You will need to restart XBMC once more." d.ok("Notice", l1 + '\n' + '\n' + l2 + '\n' + l3) sys.exit() # Skin was copied but XBMC was not restarted def check_needs_restart(): # Get skin paths current_skin_path = get_current_skin_path() local_skin_path = get_local_skin_path() # Local skin exists and does not match current skin path if os.path.isdir(local_skin_path) and current_skin_path != local_skin_path: # Check if the local skin is a leftover from a previous XBMC install if is_invalid_local_skin(): fix_invalid_local_skin() # Local skin is correct, a restart is needed else: d = xbmcgui.Dialog() d.ok("Notice", "Restart XBMC to complete the installation.") sys.exit() def do_write_test(path): test_file = os.path.join(path, 'write_test.txt') get_logger().debug('performing write test: %s' % test_file) try: # Open and cleanup open(test_file, 'w').close() os.remove(test_file) return True except Exception: return False def skin_is_local(): return get_current_skin_path() == get_local_skin_path() def check_skin_writability(): # Some debug info debug_log("-- skinutils debug info --") debug_log("current skin path: %s\n" % get_current_skin_path()) debug_log("local path should be: %s" % get_local_skin_path()) # Check if XBMC needs a restart check_needs_restart() # Get the current skin's path skin_path = get_local_skin_path() # Check if it's local or not (contained in userdata) if not skin_is_local(): copy_skin_to_userdata() sys.exit() # Check if this path is writable elif not os.access(skin_path, os.W_OK) or not do_write_test(skin_path): raise IOError("Skin directory is not writable.") def make_backup(path): backup_path = path + '-skinutilsbackup' # If the backup already exists, don't overwrite it if not os.path.exists(backup_path): shutil.copy(path, backup_path) def restore_backup(path): backup_path = path + '-skinutilsbackup' # Do nothing if no backup exists if os.path.exists(backup_path): # os.rename is atomic on unix, and it will overwrite silently if os.name != 'nt': os.rename(backup_path, path) # Windows will complain if the file exists else: os.remove(path) os.rename(backup_path, path) def is_invalid_xml(file): contents = open(file, 'r').read() # Check for invalid comments pattern = re.compile('', re.MULTILINE | re.DOTALL) group_pattern = re.compile('^-|--|-$') for match in re.finditer(pattern, contents): if re.match(group_pattern, match.group(1)) is not None: return True # Check also for whitespace prior to declaration whitespace_pattern = re.compile('^\s+', re.MULTILINE) return whitespace_pattern.match(contents) is not None def sanitize_xml(file): contents = open(file, 'r').read() # Remove leading whitespace to declaration contents = contents.lstrip() # Strip invalid comments p = re.compile('', re.MULTILINE | re.DOTALL) clean_contents, num_repl = re.subn(p, '', contents) open(file, 'w').write(clean_contents) def install_resources(): pass class DocumentCache: __cached_docs = None def __init__(self): self.__cached_docs = {} def _check_file_exists(self, file): if not os.path.isfile(file): raise IOError('File not found: %s' % file) def contains(self, file): return file in self.__cached_docs def _check_file_known(self, file): if not self.contains(file): raise KeyError('Unknown file: %s' % file) def list_files(self): return self.__cached_docs.keys() def items(self): return self.__cached_docs.items() def add(self, file): self._check_file_exists(file) self.__cached_docs[file] = None def read(self, file): self._check_file_exists(file) # If there is no cached data... if not self.contains(file) or self.__cached_docs[file] is None: # Check if the file about to load is sane if is_invalid_xml(file): make_backup(file) sanitize_xml(file) # Parse the document self.__cached_docs[file] = ET.parse(file) return self.__cached_docs[file] def write(self, file): self._check_file_known(file) # If there is a document in cache it may contain modifications if self.__cached_docs[file] is not None: make_backup(file) self.__cached_docs[file].write(file) def write_all(self): for item in self.__cached_docs: self.write(item) def clear(self, file): self._check_file_known(file) self.__cached_docs[file] = None def clear_all(self): for item in self.__cached_docs: self.clear(item) def rollback(self, file): self._check_file_known(file) restore_backup(file) self.clear(file) def rollback_all(self): for item in self.__cached_docs: self.rollback(item) setup_logging()