5a39208aa3
With elementetree instead
423 lines
No EOL
14 KiB
Python
423 lines
No EOL
14 KiB
Python
#################################################################################################
|
|
# utils
|
|
#################################################################################################
|
|
|
|
import xbmc
|
|
import xbmcgui
|
|
import xbmcaddon
|
|
import xbmcvfs
|
|
import json
|
|
import os
|
|
import cProfile
|
|
import pstats
|
|
import time
|
|
import inspect
|
|
import sqlite3
|
|
import string
|
|
import unicodedata
|
|
import xml.etree.ElementTree as etree
|
|
|
|
from API import API
|
|
from PlayUtils import PlayUtils
|
|
from DownloadUtils import DownloadUtils
|
|
|
|
downloadUtils = DownloadUtils()
|
|
addon = xbmcaddon.Addon()
|
|
language = addon.getLocalizedString
|
|
|
|
|
|
def logMsg(title, msg, level = 1):
|
|
|
|
WINDOW = xbmcgui.Window(10000)
|
|
# Get the logLevel set in UserClient
|
|
logLevel = int(WINDOW.getProperty('getLogLevel'))
|
|
|
|
if(logLevel >= level):
|
|
if(logLevel == 2): # inspect.stack() is expensive
|
|
try:
|
|
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg))
|
|
except UnicodeEncodeError:
|
|
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8')))
|
|
else:
|
|
try:
|
|
xbmc.log(title + " -> " + str(msg))
|
|
except UnicodeEncodeError:
|
|
xbmc.log(title + " -> " + str(msg.encode('utf-8')))
|
|
|
|
def convertEncoding(data):
|
|
#nasty hack to make sure we have a unicode string
|
|
try:
|
|
return data.decode('utf-8')
|
|
except:
|
|
return data
|
|
|
|
def KodiSQL(type="video"):
|
|
|
|
if type == "music":
|
|
dbPath = getKodiMusicDBPath()
|
|
elif type == "texture":
|
|
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
|
|
else:
|
|
dbPath = getKodiVideoDBPath()
|
|
|
|
connection = sqlite3.connect(dbPath)
|
|
return connection
|
|
|
|
def getKodiVideoDBPath():
|
|
|
|
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
|
dbVersion = {
|
|
|
|
"13": 78, # Gotham
|
|
"14": 90, # Helix
|
|
"15": 93, # Isengard
|
|
"16": 99 # Jarvis
|
|
}
|
|
|
|
dbPath = xbmc.translatePath(
|
|
"special://database/MyVideos%s.db"
|
|
% dbVersion.get(kodibuild, "")).decode('utf-8')
|
|
return dbPath
|
|
|
|
def getKodiMusicDBPath():
|
|
|
|
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
|
dbVersion = {
|
|
|
|
"13": 46, # Gotham
|
|
"14": 48, # Helix
|
|
"15": 52, # Isengard
|
|
"16": 56 # Jarvis
|
|
}
|
|
|
|
dbPath = xbmc.translatePath(
|
|
"special://database/MyMusic%s.db"
|
|
% dbVersion.get(kodibuild, "")).decode('utf-8')
|
|
return dbPath
|
|
|
|
def prettifyXml(elem):
|
|
rough_string = etree.tostring(elem, "utf-8")
|
|
reparsed = minidom.parseString(rough_string)
|
|
return reparsed.toprettyxml(indent="\t")
|
|
|
|
def startProfiling():
|
|
pr = cProfile.Profile()
|
|
pr.enable()
|
|
return pr
|
|
|
|
def stopProfiling(pr, profileName):
|
|
pr.disable()
|
|
ps = pstats.Stats(pr)
|
|
|
|
addondir = xbmc.translatePath(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile'))
|
|
|
|
fileTimeStamp = time.strftime("%Y-%m-%d %H-%M-%S")
|
|
tabFileNamepath = os.path.join(addondir, "profiles")
|
|
tabFileName = os.path.join(addondir, "profiles" , profileName + "_profile_(" + fileTimeStamp + ").tab")
|
|
|
|
if not xbmcvfs.exists(tabFileNamepath):
|
|
xbmcvfs.mkdir(tabFileNamepath)
|
|
|
|
f = open(tabFileName, 'wb')
|
|
f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n")
|
|
for (key, value) in ps.stats.items():
|
|
(filename, count, func_name) = key
|
|
(ccalls, ncalls, total_time, cumulative_time, callers) = value
|
|
try:
|
|
f.write(str(ncalls) + "\t" + "{:10.4f}".format(total_time) + "\t" + "{:10.4f}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n")
|
|
except ValueError:
|
|
f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n")
|
|
f.close()
|
|
|
|
def indent(elem, level=0):
|
|
# Prettify xml trees
|
|
i = "\n" + level*" "
|
|
if len(elem):
|
|
if not elem.text or not elem.text.strip():
|
|
elem.text = i + " "
|
|
if not elem.tail or not elem.tail.strip():
|
|
elem.tail = i
|
|
for elem in elem:
|
|
indent(elem, level+1)
|
|
if not elem.tail or not elem.tail.strip():
|
|
elem.tail = i
|
|
else:
|
|
if level and (not elem.tail or not elem.tail.strip()):
|
|
elem.tail = i
|
|
|
|
def createSources():
|
|
# To make Master lock compatible
|
|
path = xbmc.translatePath("special://profile/").decode("utf-8")
|
|
xmlpath = "%ssources.xml" % path
|
|
|
|
if xbmcvfs.exists(xmlpath):
|
|
# Modify the existing file
|
|
try:
|
|
xmlparse = etree.parse(xmlpath)
|
|
except:
|
|
root = etree.Element('sources')
|
|
else:
|
|
root = xmlparse.getroot()
|
|
|
|
video = root.find('video')
|
|
if video is None:
|
|
video = etree.SubElement(root, 'video')
|
|
else:
|
|
# We need to create the file
|
|
root = etree.Element('sources')
|
|
video = etree.SubElement(root, 'video')
|
|
|
|
|
|
# Add elements
|
|
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
|
|
|
|
# First dummy source
|
|
source_one = etree.SubElement(video, 'source')
|
|
etree.SubElement(source_one, 'name').text = "Emby"
|
|
etree.SubElement(source_one, 'path', attrib={'pathversion': "1"}).text = (
|
|
|
|
"smb://embydummy/dummypath1/"
|
|
)
|
|
etree.SubElement(source_one, 'allowsharing').text = "true"
|
|
|
|
# Second dummy source
|
|
source_two = etree.SubElement(video, 'source')
|
|
etree.SubElement(source_two, 'name').text = "Emby"
|
|
etree.SubElement(source_two, 'path', attrib={'pathversion': "1"}).text = (
|
|
|
|
"smb://embydummy/dummypath2/"
|
|
)
|
|
etree.SubElement(source_two, 'allowsharing').text = "true"
|
|
|
|
indent(root)
|
|
etree.ElementTree(root).write(xmlpath, method="html")
|
|
|
|
def pathsubstitution(add=True):
|
|
|
|
path = xbmc.translatePath('special://userdata').decode('utf-8')
|
|
xmlpath = "%sadvancedsettings.xml" % path
|
|
xmlpathexists = xbmcvfs.exists(xmlpath)
|
|
|
|
# original address
|
|
originalServer = settings('ipaddress')
|
|
originalPort = settings('port')
|
|
originalHttp = settings('https') == "true"
|
|
|
|
if originalHttp:
|
|
originalHttp = "https"
|
|
else:
|
|
originalHttp = "http"
|
|
|
|
# Process add or deletion
|
|
if add:
|
|
# second address
|
|
secondServer = settings('secondipaddress')
|
|
secondPort = settings('secondport')
|
|
secondHttp = settings('secondhttps') == "true"
|
|
|
|
if secondHttp:
|
|
secondHttp = "https"
|
|
else:
|
|
secondHttp = "http"
|
|
|
|
logMsg("EMBY", "Original address: %s://%s:%s, alternate is: %s://%s:%s" % (originalHttp, originalServer, originalPort, secondHttp, secondServer, secondPort), 1)
|
|
|
|
if xmlpathexists:
|
|
# we need to modify the file.
|
|
try:
|
|
xmlparse = etree.parse(xmlpath)
|
|
except: # Document is blank
|
|
root = etree.Element('advancedsettings')
|
|
else:
|
|
root = xmlparse.getroot()
|
|
|
|
pathsubs = root.find('pathsubstitution')
|
|
if pathsubs is None:
|
|
pathsubs = etree.SubElement(root, 'pathsubstitution')
|
|
else:
|
|
# we need to create the file.
|
|
root = etree.Element('advancedsettings')
|
|
pathsubs = etree.SubElement(root, 'pathsubstitution')
|
|
|
|
substitute = etree.SubElement(pathsubs, 'substitute')
|
|
# From original address
|
|
etree.SubElement(substitute, 'from').text = "%s://%s:%s" % (originalHttp, originalServer, originalPort)
|
|
# To secondary address
|
|
etree.SubElement(substitute, 'to').text = "%s://%s:%s" % (secondHttp, secondServer, secondPort)
|
|
|
|
etree.ElementTree(root).write(xmlpath)
|
|
settings('pathsub', "true")
|
|
|
|
else: # delete the path substitution, we don't need it anymore.
|
|
logMsg("EMBY", "Alternate address is disabled, removing path substitution for: %s://%s:%s" % (originalHttp, originalServer, originalPort), 1)
|
|
|
|
xmlparse = etree.parse(xmlpath)
|
|
root = xmlparse.getroot()
|
|
|
|
iterator = root.getiterator("pathsubstitution")
|
|
|
|
for substitutes in iterator:
|
|
for substitute in substitutes:
|
|
frominsert = substitute.find(".//from").text == "%s://%s:%s" % (originalHttp, originalServer, originalPort)
|
|
|
|
if frominsert:
|
|
# Found a match, in case there's more than one substitution.
|
|
substitutes.remove(substitute)
|
|
|
|
etree.ElementTree(root).write(xmlpath)
|
|
settings('pathsub', "false")
|
|
|
|
|
|
def settings(setting, value = None):
|
|
# Get or add addon setting
|
|
addon = xbmcaddon.Addon()
|
|
if value:
|
|
addon.setSetting(setting, value)
|
|
else:
|
|
return addon.getSetting(setting)
|
|
|
|
def window(property, value = None, clear = False):
|
|
# Get or set window property
|
|
WINDOW = xbmcgui.Window(10000)
|
|
if clear:
|
|
WINDOW.clearProperty(property)
|
|
elif value:
|
|
WINDOW.setProperty(property, value)
|
|
else:
|
|
return WINDOW.getProperty(property)
|
|
|
|
def normalize_string(text):
|
|
# For theme media, do not modify unless
|
|
# modified in TV Tunes
|
|
text = text.replace(":", "")
|
|
text = text.replace("/", "-")
|
|
text = text.replace("\\", "-")
|
|
text = text.replace("<", "")
|
|
text = text.replace(">", "")
|
|
text = text.replace("*", "")
|
|
text = text.replace("?", "")
|
|
text = text.replace('|', "")
|
|
text = text.strip()
|
|
# Remove dots from the last character as windows can not have directories
|
|
# with dots at the end
|
|
text = text.rstrip('.')
|
|
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
|
|
|
return text
|
|
|
|
def normalize_nodes(text):
|
|
# For video nodes
|
|
text = text.replace(":", "")
|
|
text = text.replace("/", "-")
|
|
text = text.replace("\\", "-")
|
|
text = text.replace("<", "")
|
|
text = text.replace(">", "")
|
|
text = text.replace("*", "")
|
|
text = text.replace("?", "")
|
|
text = text.replace('|', "")
|
|
text = text.replace('(', "")
|
|
text = text.replace(')', "")
|
|
text = text.strip()
|
|
# Remove dots from the last character as windows can not have directories
|
|
# with dots at the end
|
|
text = text.rstrip('.')
|
|
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
|
|
|
return text
|
|
|
|
def reloadProfile():
|
|
# Useful to reload the add-on without restarting Kodi.
|
|
profile = xbmc.getInfoLabel('System.ProfileName')
|
|
xbmc.executebuiltin("LoadProfile(%s)" % profile)
|
|
|
|
|
|
def reset():
|
|
|
|
WINDOW = xbmcgui.Window( 10000 )
|
|
return_value = xbmcgui.Dialog().yesno("Warning", "Are you sure you want to reset your local Kodi database?")
|
|
|
|
if return_value == 0:
|
|
return
|
|
|
|
# Because the settings dialog could be open
|
|
# it seems to override settings so we need to close it before we reset settings.
|
|
xbmc.executebuiltin("Dialog.Close(all,true)")
|
|
|
|
#cleanup video nodes
|
|
import shutil
|
|
path = "special://profile/library/video/"
|
|
if xbmcvfs.exists(path):
|
|
allDirs, allFiles = xbmcvfs.listdir(path)
|
|
for dir in allDirs:
|
|
if dir.startswith("Emby "):
|
|
shutil.rmtree(xbmc.translatePath("special://profile/library/video/" + dir))
|
|
for file in allFiles:
|
|
if file.startswith("emby"):
|
|
xbmcvfs.delete(path + file)
|
|
|
|
settings('SyncInstallRunDone', "false")
|
|
|
|
# Ask if user information should be deleted too.
|
|
return_user = xbmcgui.Dialog().yesno("Warning", "Reset all Emby Addon settings?")
|
|
if return_user == 1:
|
|
WINDOW.setProperty('deletesettings', "true")
|
|
addon = xbmcaddon.Addon()
|
|
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
|
|
dataPath = "%ssettings.xml" % addondir
|
|
xbmcvfs.delete(dataPath)
|
|
logMsg("EMBY", "Deleting: settings.xml", 1)
|
|
|
|
# first stop any db sync
|
|
WINDOW.setProperty("SyncDatabaseShouldStop", "true")
|
|
|
|
count = 0
|
|
while(WINDOW.getProperty("SyncDatabaseRunning") == "true"):
|
|
xbmc.log("Sync Running, will wait : " + str(count))
|
|
count += 1
|
|
if(count > 10):
|
|
dialog = xbmcgui.Dialog()
|
|
dialog.ok('Warning', 'Could not stop DB sync, you should try again.')
|
|
return
|
|
xbmc.sleep(1000)
|
|
|
|
# delete video db table data
|
|
print "Doing Video DB Reset"
|
|
connection = KodiSQL("video")
|
|
cursor = connection.cursor( )
|
|
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
|
rows = cursor.fetchall()
|
|
for row in rows:
|
|
tableName = row[0]
|
|
if(tableName != "version"):
|
|
cursor.execute("DELETE FROM " + tableName)
|
|
cursor.execute("DROP TABLE IF EXISTS emby")
|
|
connection.commit()
|
|
cursor.close()
|
|
|
|
if settings('enableMusicSync') == "true":
|
|
# delete video db table data
|
|
print "Doing Music DB Reset"
|
|
connection = KodiSQL("music")
|
|
cursor = connection.cursor( )
|
|
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
|
rows = cursor.fetchall()
|
|
for row in rows:
|
|
tableName = row[0]
|
|
if(tableName != "version"):
|
|
cursor.execute("DELETE FROM " + tableName)
|
|
cursor.execute("DROP TABLE IF EXISTS emby")
|
|
connection.commit()
|
|
cursor.close()
|
|
|
|
|
|
# reset the install run flag
|
|
#settings('SyncInstallRunDone', "false")
|
|
#WINDOW.setProperty("SyncInstallRunDone", "false")
|
|
|
|
dialog = xbmcgui.Dialog()
|
|
# Reload would work instead of restart since the add-on is a service.
|
|
#dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.')
|
|
#WINDOW.clearProperty("SyncDatabaseShouldStop")
|
|
#reloadProfile()
|
|
dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.')
|
|
xbmc.executebuiltin("RestartApp") |