PlexKodiConnect/resources/lib/utils.py

1144 lines
35 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
###############################################################################
2017-12-10 00:35:08 +11:00
from logging import getLogger
2017-01-29 23:52:46 +11:00
from cProfile import Profile
from pstats import Stats
from sqlite3 import connect, OperationalError
from datetime import datetime, timedelta
2017-01-29 23:52:46 +11:00
from StringIO import StringIO
from time import localtime, strftime, strptime
from unicodedata import normalize
2015-12-25 07:07:00 +11:00
import xml.etree.ElementTree as etree
from functools import wraps, partial
from calendar import timegm
2017-05-12 01:51:14 +10:00
from os.path import join
from os import remove, walk, makedirs
from shutil import rmtree
from urllib import quote_plus
2015-12-25 07:07:00 +11:00
import xbmc
import xbmcaddon
import xbmcgui
from xbmcvfs import exists, delete
2015-12-25 07:07:00 +11:00
2017-01-29 23:40:34 +11:00
from variables import DB_VIDEO_PATH, DB_MUSIC_PATH, DB_TEXTURE_PATH, \
DB_PLEX_PATH, KODI_PROFILE, KODIVERSION
import state
2017-01-29 23:40:34 +11:00
2016-02-12 00:03:04 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
2017-12-10 00:35:08 +11:00
log = getLogger("PLEX."+__name__)
2016-09-03 03:31:27 +10:00
WINDOW = xbmcgui.Window(10000)
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
###############################################################################
# Main methods
def window(property, value=None, clear=False, windowid=10000):
"""
Get or set window property - thread safe!
Returns unicode.
Property and value may be string or unicode
"""
if windowid != 10000:
win = xbmcgui.Window(windowid)
else:
win = WINDOW
if clear:
win.clearProperty(property)
elif value is not None:
win.setProperty(tryEncode(property), tryEncode(value))
else:
return tryDecode(win.getProperty(property))
def plex_command(key, value):
"""
Used to funnel states between different Python instances. NOT really thread
safe - let's hope the Kodi user can't click fast enough
key: state.py variable
value: either 'True' or 'False'
"""
while window('plex_command'):
2017-09-03 21:30:50 +10:00
xbmc.sleep(20)
window('plex_command', value='%s-%s' % (key, value))
2016-09-03 03:31:27 +10:00
def settings(setting, value=None):
"""
Get or add addon setting. Returns unicode
setting and value can either be unicode or string
"""
2016-11-01 05:42:52 +11:00
# We need to instantiate every single time to read changed variables!
addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
2016-09-03 03:31:27 +10:00
if value is not None:
# Takes string or unicode by default!
2016-11-01 05:42:52 +11:00
addon.setSetting(tryEncode(setting), tryEncode(value))
2016-09-03 03:31:27 +10:00
else:
# Should return unicode by default, but just in case
2016-11-01 05:42:52 +11:00
return tryDecode(addon.getSetting(setting))
2016-09-03 03:31:27 +10:00
def exists_dir(path):
"""
Safe way to check whether the directory path exists already (broken in Kodi
<17)
Feed with encoded string or unicode
"""
if KODIVERSION >= 17:
answ = exists(tryEncode(path))
else:
dummyfile = join(tryDecode(path), 'dummyfile.txt')
try:
with open(dummyfile, 'w') as f:
f.write('text')
except IOError:
# folder does not exist yet
answ = 0
else:
# Folder exists. Delete file again.
delete(tryEncode(dummyfile))
answ = 1
return answ
2016-09-03 03:31:27 +10:00
def language(stringid):
# Central string retrieval
return ADDON.getLocalizedString(stringid)
2017-01-25 04:48:13 +11:00
def dialog(typus, *args, **kwargs):
"""
Displays xbmcgui Dialog. Pass a string as typus:
'yesno', 'ok', 'notification', 'input', 'select', 'numeric'
2017-08-18 17:53:10 +10:00
kwargs:
heading='{plex}' title bar (here PlexKodiConnect)
message=lang(30128), Actual dialog content. Don't use with OK
line1=str(), For 'OK' and 'yesno' dialogs use line1...line3!
time=5000,
sound=True,
nolabel=str(), For 'yesno' dialogs
yeslabel=str(), For 'yesno' dialogs
2017-01-25 04:48:13 +11:00
Icons:
icon='{plex}' Display Plex standard icon
icon='{info}' xbmcgui.NOTIFICATION_INFO
icon='{warning}' xbmcgui.NOTIFICATION_WARNING
icon='{error}' xbmcgui.NOTIFICATION_ERROR
2017-01-25 05:59:38 +11:00
Input Types:
type='{alphanum}' xbmcgui.INPUT_ALPHANUM (standard keyboard)
type='{numeric}' xbmcgui.INPUT_NUMERIC (format: #)
type='{date}' xbmcgui.INPUT_DATE (format: DD/MM/YYYY)
type='{time}' xbmcgui.INPUT_TIME (format: HH:MM)
type='{ipaddress}' xbmcgui.INPUT_IPADDRESS (format: #.#.#.#)
type='{password}' xbmcgui.INPUT_PASSWORD
(return md5 hash of input, input is masked)
2017-01-25 04:48:13 +11:00
"""
2016-10-23 02:15:10 +11:00
d = xbmcgui.Dialog()
if "icon" in kwargs:
2017-01-25 05:59:38 +11:00
types = {
2017-01-25 04:48:13 +11:00
'{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png',
'{info}': xbmcgui.NOTIFICATION_INFO,
'{warning}': xbmcgui.NOTIFICATION_WARNING,
'{error}': xbmcgui.NOTIFICATION_ERROR
}
2017-01-25 05:59:38 +11:00
for key, value in types.iteritems():
2017-01-25 04:48:13 +11:00
kwargs['icon'] = kwargs['icon'].replace(key, value)
2017-01-25 05:59:38 +11:00
if 'type' in kwargs:
types = {
'{alphanum}': xbmcgui.INPUT_ALPHANUM,
'{numeric}': xbmcgui.INPUT_NUMERIC,
'{date}': xbmcgui.INPUT_DATE,
'{time}': xbmcgui.INPUT_TIME,
'{ipaddress}': xbmcgui.INPUT_IPADDRESS,
'{password}': xbmcgui.INPUT_PASSWORD
}
kwargs['type'] = types[kwargs['type']]
2016-10-23 02:15:10 +11:00
if "heading" in kwargs:
kwargs['heading'] = kwargs['heading'].replace("{plex}",
language(29999))
types = {
'yesno': d.yesno,
'ok': d.ok,
'notification': d.notification,
'input': d.input,
'select': d.select,
'numeric': d.numeric
}
2017-01-25 04:48:13 +11:00
return types[typus](*args, **kwargs)
2016-10-23 02:15:10 +11:00
2017-12-09 23:47:19 +11:00
def millis_to_kodi_time(milliseconds):
2017-12-08 17:53:01 +11:00
"""
2017-12-09 23:47:19 +11:00
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
2017-12-08 17:53:01 +11:00
Pass in the time in milliseconds as an int
"""
seconds = milliseconds / 1000
minutes = seconds / 60
hours = minutes / 60
seconds = seconds % 60
minutes = minutes % 60
milliseconds = milliseconds % 1000
return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': milliseconds}
2017-12-09 23:47:19 +11:00
def kodi_time_to_millis(time):
"""
Converts the Kodi time dict
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
2017-12-14 20:21:30 +11:00
to milliseconds [int]. Will not return negative results but 0!
2017-12-09 23:47:19 +11:00
"""
2017-12-14 20:21:30 +11:00
ret = (time['hours'] * 3600 +
time['minutes'] * 60 +
time['seconds']) * 1000 + time['milliseconds']
ret = 0 if ret < 0 else ret
return ret
2017-12-09 23:47:19 +11:00
2016-09-03 03:31:27 +10:00
def tryEncode(uniString, encoding='utf-8'):
"""
Will try to encode uniString (in unicode) to encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
string.encode()
"""
if isinstance(uniString, str):
# already encoded
return uniString
try:
uniString = uniString.encode(encoding, "ignore")
except TypeError:
uniString = uniString.encode()
return uniString
def tryDecode(string, encoding='utf-8'):
"""
Will try to decode string (encoded) using encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
string.encode()
"""
if isinstance(string, unicode):
# already decoded
return string
try:
string = string.decode(encoding, "ignore")
except TypeError:
string = string.decode()
return string
2016-01-27 03:20:13 +11:00
2015-12-25 07:07:00 +11:00
def slugify(text):
"""
Normalizes text (in unicode or string) to e.g. enable safe filenames.
Returns unicode
"""
if not isinstance(text, unicode):
text = unicode(text)
return unicode(normalize('NFKD', text).encode('ascii', 'ignore'))
2017-05-07 02:36:24 +10:00
def escape_html(string):
"""
Escapes the following:
< to &lt;
> to &gt;
& to &amp;
"""
escapes = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;'
}
for key, value in escapes.iteritems():
string = string.replace(key, value)
return string
2016-03-12 00:42:14 +11:00
def DateToKodi(stamp):
2016-12-28 03:33:52 +11:00
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
2016-03-12 00:42:14 +11:00
2016-12-28 03:33:52 +11:00
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
2016-12-28 03:33:52 +11:00
None if an error was encountered
"""
try:
stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET
2017-01-29 23:52:46 +11:00
date_time = localtime(stamp)
localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
2016-12-28 03:33:52 +11:00
except:
localdate = None
return localdate
2016-03-12 00:42:14 +11:00
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())
def kodiSQL(media_type="video"):
2017-01-05 06:09:09 +11:00
if media_type == "plex":
2017-01-29 23:40:34 +11:00
dbPath = DB_PLEX_PATH
elif media_type == "music":
2017-01-29 23:40:34 +11:00
dbPath = DB_MUSIC_PATH
elif media_type == "texture":
2017-01-29 23:40:34 +11:00
dbPath = DB_TEXTURE_PATH
2015-12-25 07:07:00 +11:00
else:
2017-01-29 23:40:34 +11:00
dbPath = DB_VIDEO_PATH
2017-01-29 23:52:46 +11:00
return connect(dbPath, timeout=60.0)
2015-12-25 07:07:00 +11:00
2016-12-21 02:13:19 +11:00
def create_actor_db_index():
"""
Index the "actors" because we got a TON - speed up SELECT and WHEN
"""
conn = kodiSQL('video')
cursor = conn.cursor()
try:
cursor.execute("""
CREATE UNIQUE INDEX index_name
ON actor (name);
""")
2017-01-29 23:52:46 +11:00
except OperationalError:
2016-12-21 02:13:19 +11:00
# Index already exists
pass
conn.commit()
conn.close()
def reset():
# Are you sure you want to reset your local Kodi database?
if not dialog('yesno',
heading='{plex} %s ' % language(30132),
line1=language(39600)):
2015-12-25 07:07:00 +11:00
return
# first stop any db sync
plex_command('STOP_SYNC', 'True')
2015-12-25 07:07:00 +11:00
count = 10
2016-05-31 16:06:42 +10:00
while window('plex_dbScan') == "true":
2016-09-03 03:31:27 +10:00
log.debug("Sync is running, will retry: %s..." % count)
2015-12-25 07:07:00 +11:00
count -= 1
if count == 0:
# Could not stop the database from running. Please try again later.
dialog('ok',
heading='{plex} %s' % language(30132),
line1=language(39601))
2015-12-25 07:07:00 +11:00
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
2016-09-03 03:31:27 +10:00
log.info("Resetting the Kodi video database.")
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 %s" % tablename)
2015-12-25 07:07:00 +11:00
connection.commit()
cursor.close()
2016-01-01 10:16:16 +11:00
if settings('enableMusic') == "true":
2016-09-03 03:31:27 +10:00
log.info("Resetting the Kodi music database.")
2015-12-25 07:07:00 +11:00
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 %s" % tablename)
2015-12-25 07:07:00 +11:00
connection.commit()
cursor.close()
# Wipe the Plex database
2016-09-03 03:31:27 +10:00
log.info("Resetting the Plex database.")
2017-01-05 06:09:09 +11:00
connection = kodiSQL('plex')
2015-12-25 07:07:00 +11:00
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 %s" % tablename)
cursor.execute('DROP table IF EXISTS plex')
cursor.execute('DROP table IF EXISTS view')
2015-12-25 07:07:00 +11:00
connection.commit()
cursor.close()
# Remove all cached artwork? (recommended!)
if dialog('yesno',
heading='{plex} %s ' % language(30132),
line1=language(39602)):
2016-09-03 03:31:27 +10:00
log.info("Resetting all cached artwork.")
# Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/")
if exists(path):
rmtree(tryDecode(path), ignore_errors=True)
# remove all existing data from texture DB
connection = kodiSQL('texture')
cursor = connection.cursor()
query = 'SELECT tbl_name FROM sqlite_master WHERE type=?'
cursor.execute(query, ("table", ))
rows = cursor.fetchall()
for row in rows:
tableName = row[0]
if(tableName != "version"):
cursor.execute("DELETE FROM %s" % tableName)
connection.commit()
cursor.close()
# reset the install run flag
2015-12-25 07:07:00 +11:00
settings('SyncInstallRunDone', value="false")
# Reset all PlexKodiConnect Addon settings? (this is usually NOT
# recommended and unnecessary!)
if dialog('yesno',
heading='{plex} %s ' % language(30132),
line1=language(39603)):
2015-12-25 07:07:00 +11:00
# Delete the settings
addon = xbmcaddon.Addon()
addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile')))
2015-12-25 07:07:00 +11:00
dataPath = "%ssettings.xml" % addondir
2016-09-03 03:31:27 +10:00
log.info("Deleting: settings.xml")
remove(dataPath)
2015-12-25 07:07:00 +11:00
# Kodi will now restart to apply the changes.
dialog('ok',
heading='{plex} %s ' % language(30132),
line1=language(33033))
2015-12-25 07:07:00 +11:00
xbmc.executebuiltin('RestartApp')
def profiling(sortby="cumulative"):
# Will print results to Kodi log
def decorator(func):
def wrapper(*args, **kwargs):
2017-01-29 23:52:46 +11:00
pr = Profile()
2015-12-25 07:07:00 +11:00
pr.enable()
result = func(*args, **kwargs)
pr.disable()
2015-12-25 07:07:00 +11:00
2017-01-29 23:52:46 +11:00
s = StringIO()
ps = Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
2016-12-21 02:13:19 +11:00
log.info(s.getvalue())
2015-12-25 07:07:00 +11:00
return result
return wrapper
return decorator
2015-12-25 07:07:00 +11:00
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
2017-01-29 23:52:46 +11:00
date = datetime(*(strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
return date
2017-05-30 01:05:22 +10:00
def compare_version(current, minimum):
"""
2017-05-30 01:11:04 +10:00
Returns True if current is >= then minimum. False otherwise. Returns True
if there was no valid input for current!
2017-05-30 01:05:22 +10:00
Input strings: e.g. "1.2.3"; always with Major, Minor and Patch!
"""
log.info("current DB: %s minimum DB: %s" % (current, minimum))
try:
currMajor, currMinor, currPatch = current.split(".")
except ValueError:
# there WAS no current DB, e.g. deleted.
return True
minMajor, minMinor, minPatch = minimum.split(".")
currMajor = int(currMajor)
currMinor = int(currMinor)
currPatch = int(currPatch)
minMajor = int(minMajor)
minMinor = int(minMinor)
minPatch = int(minPatch)
if currMajor > minMajor:
return True
elif currMajor < minMajor:
return False
if currMinor > minMinor:
return True
elif currMinor < minMinor:
return False
if currPatch >= minPatch:
return True
else:
return False
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('.')
2017-01-29 23:52:46 +11:00
text = tryEncode(normalize('NFKD', unicode(text, 'utf-8')))
2015-12-25 07:07:00 +11:00
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('.')
2017-01-29 23:52:46 +11:00
text = tryEncode(normalize('NFKD', unicode(text, 'utf-8')))
2015-12-25 07:07:00 +11:00
return text
2015-12-25 07:07:00 +11:00
def indent(elem, level=0):
"""
Prettifies xml trees. Pass the etree root in
"""
2015-12-25 07:07:00 +11:00
i = "\n" + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
2015-12-25 07:07:00 +11:00
if not elem.tail or not elem.tail.strip():
elem.tail = i
2015-12-25 07:07:00 +11:00
for elem in elem:
indent(elem, level+1)
2015-12-25 07:07:00 +11:00
if not elem.tail or not elem.tail.strip():
elem.tail = i
2015-12-25 07:07:00 +11:00
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
2015-12-25 07:07:00 +11:00
def guisettingsXML():
"""
Returns special://userdata/guisettings.xml as an etree xml root element
"""
path = tryDecode(xbmc.translatePath("special://profile/"))
xmlpath = "%sguisettings.xml" % path
try:
xmlparse = etree.parse(xmlpath)
2017-05-23 05:31:19 +10:00
except IOError:
# Document is blank or missing
root = etree.Element('settings')
2017-05-23 05:31:19 +10:00
except etree.ParseError:
log.error('Error parsing %s' % xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog('ok', language(29999), language(39716).format(
'guisettings.xml', 'http://kodi.wiki/view/userdata'))
return
else:
root = xmlparse.getroot()
return root
def __setXMLTag(element, tag, value, attrib=None):
"""
Looks for an element's subelement and sets its value.
If "subelement" does not exist, create it using attrib and value.
element : etree element
tag : unicode for subelement
value : unicode
attrib : dict; will use etree attrib method
Returns the subelement
"""
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
else:
etree.SubElement(element, tag, attrib=attrib).text = value
else:
subelement.text = value
return subelement
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 advancedsettings_xml(node_list, new_value=None, attrib=None,
force_create=False):
"""
Returns
etree element, tree
or
None, None
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:
<busydialogdelayms>750</busydialogdelayms>
for the following example xml:
<?xml version="1.0" encoding="UTF-8" ?>
<advancedsettings>
<video>
<busydialogdelayms>750</busydialogdelayms>
</video>
</advancedsettings>
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
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'))
2017-05-23 05:31:19 +10:00
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:
for node in node_list:
element = element.find(node)
if element is None:
break
return element, tree
# 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
2015-12-25 07:07:00 +11:00
def sourcesXML():
# To make Master lock compatible
path = tryDecode(xbmc.translatePath("special://profile/"))
2015-12-25 07:07:00 +11:00
xmlpath = "%ssources.xml" % path
try:
xmlparse = etree.parse(xmlpath)
2017-05-23 05:31:19 +10:00
except IOError: # Document is blank or missing
2015-12-25 07:07:00 +11:00
root = etree.Element('sources')
2017-05-23 05:31:19 +10:00
except etree.ParseError:
log.error('Error parsing %s' % xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog('ok', language(29999), language(39716).format(
'sources.xml', 'http://kodi.wiki/view/sources.xml'))
return
2015-12-25 07:07:00 +11:00
else:
root = xmlparse.getroot()
2015-12-25 07:07:00 +11:00
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 = "Plex"
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, encoding="UTF-8")
2015-12-25 07:07:00 +11:00
2016-03-16 19:55:19 +11:00
def passwordsXML():
2015-12-25 07:07:00 +11:00
# To add network credentials
path = tryDecode(xbmc.translatePath("special://userdata/"))
2015-12-25 07:07:00 +11:00
xmlpath = "%spasswords.xml" % path
2017-05-23 05:31:19 +10:00
dialog = xbmcgui.Dialog()
2015-12-25 07:07:00 +11:00
try:
xmlparse = etree.parse(xmlpath)
2017-05-23 05:31:19 +10:00
except IOError:
# Document is blank or missing
2015-12-25 07:07:00 +11:00
root = etree.Element('passwords')
2016-03-16 19:55:19 +11:00
skipFind = True
2017-05-23 05:31:19 +10:00
except etree.ParseError:
log.error('Error parsing %s' % xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog.ok(language(29999), language(39716).format(
'passwords.xml', 'http://forum.kodi.tv/'))
return
2015-12-25 07:07:00 +11:00
else:
root = xmlparse.getroot()
2016-03-16 19:55:19 +11:00
skipFind = False
2015-12-25 07:07:00 +11:00
credentials = settings('networkCreds')
if credentials:
# Present user with options
2016-03-16 19:55:19 +11:00
option = dialog.select(
"Modify/Remove network credentials", ["Modify", "Remove"])
2015-12-25 07:07:00 +11:00
if option < 0:
# User cancelled dialog
return
elif option == 1:
# User selected remove
for paths in root.getiterator('passwords'):
2015-12-25 07:07:00 +11:00
for path in paths:
if path.find('.//from').text == "smb://%s/" % credentials:
paths.remove(path)
2016-09-03 03:31:27 +10:00
log.info("Successfully removed credentials for: %s"
% credentials)
etree.ElementTree(root).write(xmlpath,
encoding="UTF-8")
2015-12-25 07:07:00 +11:00
break
else:
2016-09-03 03:31:27 +10:00
log.error("Failed to find saved server: %s in passwords.xml"
% credentials)
2015-12-25 07:07:00 +11:00
settings('networkCreds', value="")
xbmcgui.Dialog().notification(
2016-03-16 19:55:19 +11:00
heading='PlexKodiConnect',
message="%s removed from passwords.xml" % credentials,
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
time=1000,
sound=False)
2015-12-25 07:07:00 +11:00
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. "
2016-03-16 19:55:19 +11:00
'For example, the server name: \\\\SERVER-PC\\path\\ or smb://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
server = quote_plus(server)
2015-12-25 07:07:00 +11:00
# Network username
user = dialog.input("Enter the network username")
if not user:
return
user = quote_plus(user)
2015-12-25 07:07:00 +11:00
# Network password
2016-03-16 19:55:19 +11:00
password = dialog.input("Enter the network password",
'', # Default input
xbmcgui.INPUT_ALPHANUM,
xbmcgui.ALPHANUM_HIDE_INPUT)
# Need to url-encode the password
password = quote_plus(password)
2016-03-16 19:55:19 +11:00
# Add elements. Annoying etree bug where findall hangs forever
if skipFind is False:
skipFind = True
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)
skipFind = False
break
if skipFind:
2015-12-25 07:07:00 +11:00
# 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
# Add credentials
2015-12-25 07:07:00 +11:00
settings('networkCreds', value="%s" % server)
2016-09-03 03:31:27 +10:00
log.info("Added server: %s to passwords.xml" % server)
2015-12-25 07:07:00 +11:00
# Prettify and write to file
try:
indent(root)
except:
pass
etree.ElementTree(root).write(xmlpath, encoding="UTF-8")
# dialog.notification(
# heading="PlexKodiConnect",
# message="Added to passwords.xml",
# icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
# time=5000,
# sound=False)
2015-12-25 07:07:00 +11:00
def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
2016-03-08 01:31:07 +11:00
"""
Feed with tagname as unicode
"""
path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
2015-12-25 07:07:00 +11:00
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 exists(tryEncode(path)):
2016-09-03 03:31:27 +10:00
log.info("Creating directory: %s" % path)
makedirs(path)
2015-12-25 07:07:00 +11:00
# Only add the playlist if it doesn't already exists
if exists(tryEncode(xsppath)):
2016-09-03 03:31:27 +10:00
log.info('Path %s does exist' % xsppath)
2015-12-25 07:07:00 +11:00
if delete:
remove(xsppath)
2016-09-03 03:31:27 +10:00
log.info("Successfully removed playlist: %s." % tagname)
2015-12-25 07:07:00 +11:00
return
# Using write process since there's no guarantee the xml declaration works
# with etree
2015-12-25 07:07:00 +11:00
itemtypes = {
'homevideos': 'movies',
'movie': 'movies',
'show': 'tvshows'
2015-12-25 07:07:00 +11:00
}
2016-09-03 03:31:27 +10:00
log.info("Writing playlist file to: %s" % xsppath)
with open(xsppath, 'wb') as f:
f.write(tryEncode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
'<name>Plex %s</name>\n\t'
'<match>all</match>\n\t'
'<rule field="tag" operator="is">\n\t\t'
'<value>%s</value>\n\t'
'</rule>\n'
'</smartplaylist>\n'
% (itemtypes.get(mediatype, mediatype), plname, tagname)))
2016-09-03 03:31:27 +10:00
log.info("Successfully added playlist: %s" % tagname)
def deletePlaylists():
# Clean up the playlists
path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
for root, _, files in walk(path):
for file in files:
if file.startswith('Plex'):
remove(join(root, file))
def deleteNodes():
# Clean up video nodes
path = tryDecode(xbmc.translatePath("special://profile/library/video/"))
for root, dirs, _ in walk(path):
for directory in dirs:
if directory.startswith('Plex-'):
rmtree(join(root, directory))
break
2016-09-03 03:31:27 +10:00
###############################################################################
# WRAPPERS
def CatchExceptions(warnuser=False):
"""
2016-09-03 03:31:27 +10:00
Decorator for methods to catch exceptions and log them. Useful for e.g.
librarysync threads using itemtypes.py, because otherwise we would not
get informed of crashes
warnuser=True: sets the window flag 'plex_scancrashed' to true
which will trigger a Kodi infobox to inform user
"""
2016-09-03 03:31:27 +10:00
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
2016-09-05 01:14:52 +10:00
log.error('%s has crashed. Error: %s' % (func.__name__, e))
2016-09-03 03:31:27 +10:00
import traceback
2016-09-05 01:14:52 +10:00
log.error("Traceback:\n%s" % traceback.format_exc())
2016-09-03 03:31:27 +10:00
if warnuser:
window('plex_scancrashed', value='true')
return
return wrapper
return decorate
2016-09-03 03:31:27 +10:00
def LogTime(func):
"""
2016-09-03 03:31:27 +10:00
Decorator for functions and methods to log the time it took to run the code
"""
2016-09-03 03:31:27 +10:00
@wraps(func)
def wrapper(*args, **kwargs):
starttotal = datetime.now()
result = func(*args, **kwargs)
elapsedtotal = datetime.now() - starttotal
2016-12-21 02:13:19 +11:00
log.info('It took %s to run the function %s'
% (elapsedtotal, func.__name__))
2016-09-03 03:31:27 +10:00
return result
return wrapper
2017-05-17 21:55:24 +10:00
def thread_methods(cls=None, add_stops=None, add_suspends=None):
2016-09-03 03:31:27 +10:00
"""
Decorator to add the following methods to a threading class:
suspend_thread(): pauses the thread
resume_thread(): resumes the thread
stop_thread(): stopps/kills the thread
2016-09-03 03:31:27 +10:00
2017-05-17 21:55:24 +10:00
thread_suspended(): returns True if thread is suspended
thread_stopped(): returns True if thread is stopped (or should stop ;-))
2017-05-17 21:55:24 +10:00
ALSO returns True if PKC should exit
2016-09-03 03:31:27 +10:00
Also adds the following class attributes:
2017-05-17 21:55:24 +10:00
__thread_stopped
__thread_suspended
__stops
__suspends
invoke with either
2017-05-17 21:55:24 +10:00
@Newthread_methods
class MyClass():
or
2017-05-17 21:55:24 +10:00
@Newthread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'],
add_suspends=['DB_SCAN', 'WHATEVER'])
class MyClass():
"""
2017-05-17 21:55:24 +10:00
# So we don't need to invoke with ()
if cls is None:
2017-05-17 21:55:24 +10:00
return partial(thread_methods,
add_stops=add_stops,
add_suspends=add_suspends)
2017-05-17 21:55:24 +10:00
# Because we need a reference, not a copy of the immutable objects in
# state, we need to look up state every time explicitly
cls.__stops = ['STOP_PKC']
if add_stops is not None:
cls.__stops.extend(add_stops)
cls.__suspends = add_suspends or []
2016-09-03 03:31:27 +10:00
# Attach new attributes to class
2017-05-17 21:55:24 +10:00
cls.__thread_stopped = False
cls.__thread_suspended = False
2016-09-03 03:31:27 +10:00
# Define new class methods and attach them to class
def stop_thread(self):
2017-05-17 21:55:24 +10:00
self.__thread_stopped = True
cls.stop_thread = stop_thread
2016-09-03 03:31:27 +10:00
def suspend_thread(self):
2017-05-17 21:55:24 +10:00
self.__thread_suspended = True
cls.suspend_thread = suspend_thread
2016-09-03 03:31:27 +10:00
def resume_thread(self):
2017-05-17 21:55:24 +10:00
self.__thread_suspended = False
cls.resume_thread = resume_thread
2016-09-03 03:31:27 +10:00
def thread_suspended(self):
2017-05-17 21:55:24 +10:00
if self.__thread_suspended is True:
return True
for suspend in self.__suspends:
if getattr(state, suspend):
return True
return False
cls.thread_suspended = thread_suspended
2016-09-03 03:31:27 +10:00
def thread_stopped(self):
2017-05-17 21:55:24 +10:00
if self.__thread_stopped is True:
return True
for stop in self.__stops:
if getattr(state, stop):
return True
return False
cls.thread_stopped = thread_stopped
2016-09-03 03:31:27 +10:00
# Return class to render this a decorator
return cls
2017-12-21 19:28:06 +11:00
class Lock_Function(object):
2016-12-28 03:33:52 +11:00
"""
Decorator for class methods and functions to lock them with lock.
Initialize this class first
lockfunction = Lock_Function(lock), where lock is a threading.Lock() object
To then lock a function or method:
@lockfunction.lockthis
def some_function(args, kwargs)
"""
def __init__(self, lock):
self.lock = lock
def lockthis(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
with self.lock:
result = func(*args, **kwargs)
return result
return wrapper
2016-09-03 03:31:27 +10:00
###############################################################################
# UNUSED METHODS
2016-12-28 03:33:52 +11:00
2017-12-09 06:32:10 +11:00
# def changePlayState(itemType, kodiId, playCount, lastplayed):
# """
# YET UNUSED
# kodiId: int or str
# playCount: int or str
# lastplayed: str or int unix timestamp
# """
# lastplayed = DateToKodi(lastplayed)
# kodiId = int(kodiId)
# playCount = int(playCount)
# method = {
# 'movie': ' VideoLibrary.SetMovieDetails',
# 'episode': 'VideoLibrary.SetEpisodeDetails',
# 'musicvideo': ' VideoLibrary.SetMusicVideoDetails', # TODO
# 'show': 'VideoLibrary.SetTVShowDetails', # TODO
# '': 'AudioLibrary.SetAlbumDetails', # TODO
# '': 'AudioLibrary.SetArtistDetails', # TODO
# 'track': 'AudioLibrary.SetSongDetails'
# }
# params = {
# 'movie': {
# 'movieid': kodiId,
# 'playcount': playCount,
# 'lastplayed': lastplayed
# },
# 'episode': {
# 'episodeid': kodiId,
# 'playcount': playCount,
# 'lastplayed': lastplayed
# }
# }
# result = jsonrpc(method[itemType]).execute(params[itemType])
# log.debug("JSON result was: %s" % result)