373 lines
9.3 KiB
Python
373 lines
9.3 KiB
Python
|
"""
|
||
|
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()
|