PlexKodiConnect/resources/lib/utils.py

711 lines
23 KiB
Python
Raw Normal View History

2015-12-25 07:07:00 +11:00
# -*- coding: utf-8 -*-
2016-02-12 00:03:04 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
import cProfile
import inspect
import pstats
import sqlite3
2016-02-12 00:03:04 +11:00
from datetime import datetime, time, timedelta
2015-12-25 07:07:00 +11:00
import unicodedata
import xml.etree.ElementTree as etree
2016-01-30 06:07:21 +11:00
from functools import wraps
from calendar import timegm
2016-03-01 20:40:30 +11:00
import os
2015-12-25 07:07:00 +11:00
import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs
2016-02-12 00:03:04 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
2016-01-27 03:20:13 +11:00
addonName = xbmcaddon.Addon().getAddonInfo('name')
2015-12-25 07:07:00 +11:00
2016-01-30 06:07:21 +11:00
def LogTime(func):
2016-01-25 20:36:24 +11:00
"""
2016-01-30 06:07:21 +11:00
Decorator for functions and methods to log the time it took to run the code
"""
@wraps(func)
def wrapper(*args, **kwargs):
starttotal = datetime.now()
result = func(*args, **kwargs)
elapsedtotal = datetime.now() - starttotal
logMsg('%s %s' % (addonName, func.__name__),
'It took %s to run the function.' % (elapsedtotal), 1)
return result
return wrapper
2016-01-27 01:13:03 +11:00
2016-01-30 06:07:21 +11:00
def ThreadMethodsAdditionalStop(windowAttribute):
2016-01-27 01:13:03 +11:00
"""
2016-01-30 06:07:21 +11:00
Decorator to replace stopThread method to include the Kodi windowAttribute
Use with any sync threads. @ThreadMethods still required FIRST
"""
def wrapper(cls):
def threadStopped(self):
return (self._threadStopped or
(window('terminateNow') == "true") or
2016-01-30 06:07:21 +11:00
window(windowAttribute) == "true")
cls.threadStopped = threadStopped
return cls
return wrapper
2016-01-27 01:13:03 +11:00
2016-01-27 22:18:54 +11:00
def ThreadMethodsAdditionalSuspend(windowAttribute):
"""
Decorator to replace threadSuspended(): thread now also suspends if a
Kodi windowAttribute is set to 'true', e.g. 'suspend_LibraryThread'
Use with any library sync threads. @ThreadMethods still required FIRST
"""
def wrapper(cls):
def threadSuspended(self):
2016-01-30 06:07:21 +11:00
return (self._threadSuspended or
window(windowAttribute) == 'true')
2016-01-27 22:18:54 +11:00
cls.threadSuspended = threadSuspended
return cls
return wrapper
2016-01-27 01:13:03 +11:00
def ThreadMethods(cls):
"""
Decorator to add the following methods to a threading class:
suspendThread(): pauses the thread
resumeThread(): resumes the thread
stopThread(): stopps/kills the thread
2016-01-27 01:13:03 +11:00
threadSuspended(): returns True if thread is suspend_thread
threadStopped(): returns True if thread is stopped (or should stop ;-))
ALSO stops if Kodi is exited
Also adds the following class attributes:
_threadStopped
_threadSuspended
2016-01-25 20:36:24 +11:00
"""
2016-01-27 01:13:03 +11:00
# Attach new attributes to class
cls._threadStopped = False
cls._threadSuspended = False
# Define new class methods and attach them to class
def stopThread(self):
self._threadStopped = True
cls.stopThread = stopThread
def suspendThread(self):
self._threadSuspended = True
cls.suspendThread = suspendThread
def resumeThread(self):
self._threadSuspended = False
cls.resumeThread = resumeThread
def threadSuspended(self):
return self._threadSuspended
cls.threadSuspended = threadSuspended
def threadStopped(self):
return self._threadStopped or (window('terminateNow') == 'true')
2016-01-27 01:13:03 +11:00
cls.threadStopped = threadStopped
2016-01-25 20:36:24 +11:00
2016-01-27 01:13:03 +11:00
# Return class to render this a decorator
2016-01-25 20:36:24 +11:00
return cls
2016-01-27 03:20:13 +11:00
def logging(cls):
"""
2016-01-27 22:18:54 +11:00
A decorator adding logging capabilities to classes.
Also adds self.addonName to the class
Syntax: self.logMsg(message, loglevel)
Loglevel: -2 (Error) to 2 (DB debug)
"""
2016-01-27 03:20:13 +11:00
# Attach new attributes to class
cls.addonName = addonName
# Define new class methods and attach them to class
def newFunction(self, msg, lvl=0):
title = "%s %s" % (addonName, cls.__name__)
2016-01-27 03:20:13 +11:00
logMsg(title, msg, lvl)
cls.logMsg = newFunction
# Return class to render this a decorator
return cls
def IntFromStr(string):
"""
Returns an int from string or the int 0 if something happened
"""
try:
result = int(string)
except:
result = 0
return result
def getUnixTimestamp(secondsIntoTheFuture=None):
"""
Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as
an integer.
Optionally, pass secondsIntoTheFuture: positive int's will result in a
future timestamp, negative the past
"""
if secondsIntoTheFuture:
future = datetime.utcnow() + timedelta(seconds=secondsIntoTheFuture)
else:
future = datetime.utcnow()
return timegm(future.timetuple())
2015-12-25 07:07:00 +11:00
def logMsg(title, msg, level=1):
# Get the logLevel set in UserClient
try:
logLevel = int(window('emby_logLevel'))
except ValueError:
logLevel = 0
if logLevel >= level:
2016-01-27 03:20:13 +11:00
if logLevel == 2: # inspect is expensive
func = inspect.currentframe().f_back.f_back.f_code
2015-12-25 07:07:00 +11:00
try:
2016-01-27 03:20:13 +11:00
xbmc.log("%s -> %s : %s" % (
title, func.co_name, msg))
2015-12-25 07:07:00 +11:00
except UnicodeEncodeError:
2016-01-30 06:07:21 +11:00
try:
xbmc.log("%s -> %s : %s" % (
title, func.co_name, msg.encode('utf-8')))
except:
xbmc.log("%s -> %s : %s" % (title, func.co_name, 'COULDNT LOG'))
2015-12-25 07:07:00 +11:00
else:
try:
xbmc.log("%s -> %s" % (title, msg))
except UnicodeEncodeError:
2016-01-30 06:07:21 +11:00
try:
xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
except:
xbmc.log("%s -> %s " % (title, 'COULDNT LOG'))
2015-12-25 07:07:00 +11:00
2016-01-27 03:20:13 +11:00
2015-12-25 07:07:00 +11:00
def window(property, value=None, clear=False, windowid=10000):
# Get or set window property
WINDOW = xbmcgui.Window(windowid)
#setproperty accepts both string and unicode but utf-8 strings are adviced by kodi devs because some unicode can give issues
'''if isinstance(property, unicode):
property = property.encode("utf-8")
if isinstance(value, unicode):
value = value.encode("utf-8")'''
2015-12-25 07:07:00 +11:00
if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value)
else: #getproperty returns string so convert to unicode
return WINDOW.getProperty(property)#.decode("utf-8")
2015-12-25 07:07:00 +11:00
def settings(setting, value=None):
# Get or add addon setting
addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
2015-12-25 07:07:00 +11:00
if value is not None:
addon.setSetting(setting, value)
else:
return addon.getSetting(setting) #returns unicode object
2015-12-25 07:07:00 +11:00
def language(stringid):
# Central string retrieval
addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
string = addon.getLocalizedString(stringid) #returns unicode object
2015-12-25 07:07:00 +11:00
return string
def kodiSQL(type="video"):
if type == "emby":
dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
elif 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 reset():
dialog = xbmcgui.Dialog()
resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?")
if resp == 0:
return
# first stop any db sync
window('emby_shouldStop', value="true")
count = 10
while window('emby_dbScan') == "true":
logMsg("EMBY", "Sync is running, will retry: %s..." % count)
count -= 1
if count == 0:
dialog.ok("Warning", "Could not stop the database from running. Try again.")
return
xbmc.sleep(1000)
# Clean up the playlists
deletePlaylists()
2015-12-25 07:07:00 +11:00
# Clean up the video nodes
deleteNodes()
2015-12-25 07:07:00 +11:00
# Wipe the kodi databases
logMsg("EMBY", "Resetting the Kodi video database.", 0)
2015-12-25 07:07:00 +11:00
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)
connection.commit()
cursor.close()
2016-01-01 10:16:16 +11:00
if settings('enableMusic') == "true":
2015-12-25 07:07:00 +11:00
logMsg("EMBY", "Resetting the Kodi music database.")
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)
connection.commit()
cursor.close()
# Wipe the emby database
logMsg("EMBY", "Resetting the Emby database.", 0)
2015-12-25 07:07:00 +11:00
connection = kodiSQL('emby')
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')
cursor.execute('DROP table IF EXISTS view')
2015-12-25 07:07:00 +11:00
connection.commit()
cursor.close()
# Offer to wipe cached thumbnails
resp = dialog.yesno("Warning", "Removed all cached artwork?")
if resp:
logMsg("EMBY", "Resetting all cached artwork.", 0)
# Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
allDirs, allFiles = xbmcvfs.listdir(path+dir)
for file in allFiles:
if os.path.supports_unicode_filenames:
xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
else:
xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
# remove all existing data from texture DB
connection = kodiSQL('texture')
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)
connection.commit()
cursor.close()
2015-12-25 07:07:00 +11:00
# reset the install run flag
settings('SyncInstallRunDone', value="false")
# Remove emby info
resp = dialog.yesno("Warning", "Reset all Emby Addon settings?")
if resp:
2015-12-25 07:07:00 +11:00
# Delete the settings
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)
dialog.ok(
heading="Emby for Kodi",
line1="Database reset has completed, Kodi will now restart to apply the changes.")
xbmc.executebuiltin('RestartApp')
def startProfiling():
pr = cProfile.Profile()
pr.enable()
return pr
def stopProfiling(pr, profileName):
pr.disable()
ps = pstats.Stats(pr)
profiles = xbmc.translatePath("%sprofiles/"
% xbmcaddon.Addon().getAddonInfo('profile')).decode('utf-8')
if not xbmcvfs.exists(profiles):
# Create the profiles folder
xbmcvfs.mkdir(profiles)
timestamp = time.strftime("%Y-%m-%d %H-%M-%S")
profile = "%s%s_profile_(%s).tab" % (profiles, profileName, timestamp)
f = xbmcvfs.File(profile, 'w')
2015-12-25 07:07:00 +11:00
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(
"%s\t%s\t%s\t%s\t%s\r\n"
% (ncalls, "{:10.4f}".format(total_time),
"{:10.4f}".format(cumulative_time), func_name, filename))
except ValueError:
f.write(
"%s\t%s\t%s\t%s\t%s\r\n"
% (ncalls, "{0}".format(total_time),
"{0}".format(cumulative_time), func_name, filename))
f.close()
def convertdate(date):
try:
date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
except TypeError:
# TypeError: attribute of type 'NoneType' is not callable
# Known Kodi/python error
date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
return date
2015-12-25 07:07:00 +11:00
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 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 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 sourcesXML():
# To make Master lock compatible
path = xbmc.translatePath("special://profile/").decode('utf-8')
xmlpath = "%ssources.xml" % path
try:
xmlparse = etree.parse(xmlpath)
except: # Document is blank or missing
root = etree.Element('sources')
else:
root = xmlparse.getroot()
video = root.find('video')
if video is None:
video = etree.SubElement(root, 'video')
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
# Add elements
count = 2
for source in root.findall('.//path'):
if source.text == "smb://":
count -= 1
if count == 0:
# sources already set
break
else:
# Missing smb:// occurences, re-add.
for i in range(0, count):
source = etree.SubElement(video, 'source')
etree.SubElement(source, 'name').text = "Emby"
etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://"
etree.SubElement(source, 'allowsharing').text = "true"
2015-12-25 07:07:00 +11:00
# Prettify and write to file
try:
indent(root)
except: pass
etree.ElementTree(root).write(xmlpath)
def passwordsXML():
# To add network credentials
path = xbmc.translatePath("special://userdata/").decode('utf-8')
xmlpath = "%spasswords.xml" % path
try:
xmlparse = etree.parse(xmlpath)
except: # Document is blank or missing
root = etree.Element('passwords')
else:
root = xmlparse.getroot()
dialog = xbmcgui.Dialog()
credentials = settings('networkCreds')
if credentials:
# Present user with options
option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"])
if option < 0:
# User cancelled dialog
return
elif option == 1:
# User selected remove
iterator = root.getiterator('passwords')
for paths in iterator:
for path in paths:
if path.find('.//from').text == "smb://%s/" % credentials:
paths.remove(path)
logMsg("EMBY", "Successfully removed credentials for: %s"
% credentials, 1)
etree.ElementTree(root).write(xmlpath)
break
else:
logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1)
settings('networkCreds', value="")
xbmcgui.Dialog().notification(
2016-01-25 20:36:24 +11:00
heading='PlexKodiConnect',
2016-01-02 16:38:26 +11:00
message="%s removed from passwords.xml" % credentials,
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
2015-12-25 07:07:00 +11:00
time=1000,
sound=False)
return
elif option == 0:
# User selected to modify
server = dialog.input("Modify the computer name or ip address", credentials)
if not server:
return
else:
# No credentials added
dialog.ok(
heading="Network credentials",
line1= (
2016-01-25 20:36:24 +11:00
"Input the server name or IP address as indicated in your plex library paths. "
2015-12-25 07:07:00 +11:00
'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".'))
server = dialog.input("Enter the server name or IP address")
2015-12-25 07:07:00 +11:00
if not server:
return
# Network username
user = dialog.input("Enter the network username")
if not user:
return
# Network password
password = dialog.input(
heading="Enter the network password",
option=xbmcgui.ALPHANUM_HIDE_INPUT)
if not password:
return
# Add elements
for path in root.findall('.//path'):
if path.find('.//from').text.lower() == "smb://%s/" % server.lower():
# Found the server, rewrite credentials
path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server)
break
else:
# Server not found, add it.
path = etree.SubElement(root, 'path')
etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server
topath = "smb://%s:%s@%s/" % (user, password, server)
etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath
# Force Kodi to see the credentials without restarting
xbmcvfs.exists(topath)
# Add credentials
settings('networkCreds', value="%s" % server)
logMsg("EMBY", "Added server: %s to passwords.xml" % server, 1)
# Prettify and write to file
try:
indent(root)
except: pass
etree.ElementTree(root).write(xmlpath)
dialog.notification(
2016-01-25 20:36:24 +11:00
heading="PlexKodiConnect",
2016-01-02 16:38:26 +11:00
message="%s added to passwords.xml" % server,
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
2015-12-25 07:07:00 +11:00
time=1000,
sound=False)
def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
2015-12-25 07:07:00 +11:00
# Tagname is in unicode - actions: add or delete
tagname = tagname.encode('utf-8')
2015-12-25 07:07:00 +11:00
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype)
2016-03-03 03:27:21 +11:00
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
2015-12-25 07:07:00 +11:00
else:
plname = tagname
2016-03-03 03:27:21 +11:00
xsppath = "%sPlex %s.xsp" % (path, viewid)
2015-12-25 07:07:00 +11:00
# Create the playlist directory
if not xbmcvfs.exists(path):
2016-03-03 03:27:21 +11:00
logMsg("PLEX", "Creating directory: %s" % path, 1)
2015-12-25 07:07:00 +11:00
xbmcvfs.mkdirs(path)
# Only add the playlist if it doesn't already exists
if xbmcvfs.exists(xsppath):
if delete:
xbmcvfs.delete(xsppath)
2016-03-03 03:27:21 +11:00
logMsg("PLEX", "Successfully removed playlist: %s." % tagname, 1)
2015-12-25 07:07:00 +11:00
return
# Using write process since there's no guarantee the xml declaration works with etree
itemtypes = {
2016-03-03 03:27:21 +11:00
'homevideos': "movie"
2015-12-25 07:07:00 +11:00
}
2016-03-03 03:27:21 +11:00
logMsg("Plex", "Writing playlist file to: %s" % xsppath, 1)
2015-12-26 20:09:47 +11:00
try:
f = xbmcvfs.File(xsppath, 'w')
2015-12-26 20:09:47 +11:00
except:
2016-03-03 03:27:21 +11:00
logMsg("Plex", "Failed to create playlist: %s" % xsppath, -1)
2015-12-26 20:09:47 +11:00
return
else:
f.write(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
2016-03-03 03:27:21 +11:00
'<name>Plex %s</name>\n\t'
2015-12-26 20:09:47 +11:00
'<match>all</match>\n\t'
'<rule field="tag" operator="is">\n\t\t'
'<value>%s</value>\n\t'
'</rule>'
'</smartplaylist>'
% (itemtypes.get(mediatype, mediatype), plname, tagname))
f.close()
2016-03-03 03:27:21 +11:00
logMsg("Plex", "Successfully added playlist: %s" % tagname)
def deletePlaylists():
# Clean up the playlists
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
dirs, files = xbmcvfs.listdir(path)
for file in files:
if file.decode('utf-8').startswith('Emby'):
xbmcvfs.delete("%s%s" % (path, file))
def deleteNodes():
# Clean up video nodes
import shutil
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
dirs, files = xbmcvfs.listdir(path)
for dir in dirs:
if dir.decode('utf-8').startswith('Emby'):
try:
shutil.rmtree("%s%s" % (path, dir.decode('utf-8')))
except:
logMsg("EMBY", "Failed to delete directory: %s" % dir.decode('utf-8'))
for file in files:
if file.decode('utf-8').startswith('emby'):
try:
xbmcvfs.delete("%s%s" % (path, file.decode('utf-8')))
except:
logMsg("EMBY", "Failed to file: %s" % file.decode('utf-8'))