PlexKodiConnect/resources/lib/utils.py

1038 lines
32 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
###############################################################################
2016-09-03 03:31:27 +10:00
import logging
2017-01-29 23:52:46 +11:00
from cProfile import Profile
from json import loads, dumps
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
2016-01-30 06:07:21 +11:00
from functools import wraps
from calendar import timegm
2017-01-29 23:52:46 +11:00
from os import path as os_path
2015-12-25 07:07:00 +11:00
import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs
2017-01-29 23:40:34 +11:00
from variables import DB_VIDEO_PATH, DB_MUSIC_PATH, DB_TEXTURE_PATH, \
DB_PLEX_PATH
2016-02-12 00:03:04 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
2016-09-03 03:31:27 +10:00
log = logging.getLogger("PLEX."+__name__)
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))
2017-01-03 00:07:24 +11:00
def pickl_window(property, value=None, clear=False, windowid=10000):
"""
Get or set window property - thread safe! For use with Pickle
Property and value must be string
"""
if windowid != 10000:
win = xbmcgui.Window(windowid)
else:
win = WINDOW
if clear:
win.clearProperty(property)
elif value is not None:
win.setProperty(property, value)
else:
return win.getProperty(property)
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 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'
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
}
for key, value in types.iteritems():
kwargs['type'] = kwargs['type'].replace(key, value)
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
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
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) + float(window('kodiplextimeoffset'))
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 IfExists(path):
"""
Kodi's xbmcvfs.exists is broken - it caches the results for directories.
path: path to a directory (with a slash at the end)
Returns True if path exists, else false
"""
2017-01-29 23:52:46 +11:00
dummyfile = tryEncode(os_path.join(path, 'dummyfile.txt'))
try:
etree.ElementTree(etree.Element('test')).write(dummyfile)
except:
# folder does not exist yet
answer = False
else:
# Folder exists. Delete file again.
xbmcvfs.delete(dummyfile)
answer = True
return answer
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 getScreensaver():
# Get the current screensaver value
params = {'setting': "screensaver.mode"}
return JSONRPC('Settings.getSettingValue').execute(params)['result']['value']
def setScreensaver(value):
# Toggle the screensaver
params = {'setting': "screensaver.mode", 'value': value}
log.debug('Toggling screensaver to "%s": %s'
% (value, JSONRPC('Settings.setSettingValue').execute(params)))
2015-12-25 07:07:00 +11:00
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
2016-05-31 16:06:42 +10:00
window('plex_shouldStop', value="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 " + tablename)
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 " + tablename)
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 " + 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 = tryDecode(xbmc.translatePath("special://thumbnails/"))
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
allDirs, allFiles = xbmcvfs.listdir(path+dir)
for file in allFiles:
2017-01-29 23:52:46 +11:00
if os_path.supports_unicode_filenames:
xbmcvfs.delete(os_path.join(
path + tryDecode(dir),
tryDecode(file)))
else:
2017-01-29 23:52:46 +11:00
xbmcvfs.delete(os_path.join(
tryEncode(path) + 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()
# 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")
xbmcvfs.delete(tryEncode(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
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
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 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)
except:
# Document is blank or missing
root = etree.Element('settings')
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 : string/unicode for subelement
value : string/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 advancedSettingsXML():
"""
Kodi tweaks
Changes advancedsettings.xml, musiclibrary:
backgroundupdate set to "true"
Overrides guisettings.xml in Kodi userdata folder:
updateonstartup : set to "false"
usetags : set to "false"
findremotethumbs : set to "false"
"""
path = tryDecode(xbmc.translatePath("special://profile/"))
xmlpath = "%sadvancedsettings.xml" % path
try:
xmlparse = etree.parse(xmlpath)
except:
# Document is blank or missing
root = etree.Element('advancedsettings')
else:
root = xmlparse.getroot()
music = __setSubElement(root, 'musiclibrary')
__setXMLTag(music, 'backgroundupdate', "true")
# __setXMLTag(music, 'updateonstartup', "false")
# Subtag 'musicfiles'
# music = __setSubElement(root, 'musicfiles')
# __setXMLTag(music, 'usetags', "false")
# __setXMLTag(music, 'findremotethumbs', "false")
# Prettify and write to file
try:
indent(root)
except:
pass
etree.ElementTree(root).write(xmlpath)
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)
except: # Document is blank or missing
root = etree.Element('sources')
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)
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
try:
xmlparse = etree.parse(xmlpath)
except: # Document is blank or missing
root = etree.Element('passwords')
2016-03-16 19:55:19 +11:00
skipFind = True
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
dialog = xbmcgui.Dialog()
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)
2015-12-25 07:07:00 +11:00
etree.ElementTree(root).write(xmlpath)
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
# Network username
user = dialog.input("Enter the network username")
if not user:
return
# 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
from urllib import quote_plus
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
# Force Kodi to see the credentials without restarting
xbmcvfs.exists(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)
# 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 xbmcvfs.exists(tryEncode(path)):
2016-09-03 03:31:27 +10:00
log.info("Creating directory: %s" % path)
xbmcvfs.mkdirs(tryEncode(path))
2015-12-25 07:07:00 +11:00
# Only add the playlist if it doesn't already exists
if xbmcvfs.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:
xbmcvfs.delete(tryEncode(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
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)
2015-12-26 20:09:47 +11:00
try:
f = xbmcvfs.File(tryEncode(xsppath), 'wb')
2015-12-26 20:09:47 +11:00
except:
2016-09-03 03:31:27 +10:00
log.error("Failed to create playlist: %s" % xsppath)
2015-12-26 20:09:47 +11:00
return
else:
f.write(tryEncode(
2015-12-26 20:09:47 +11:00
'<?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'
2016-03-08 01:31:07 +11:00
'</rule>\n'
'</smartplaylist>\n'
% (itemtypes.get(mediatype, mediatype), plname, tagname)))
2015-12-26 20:09:47 +11:00
f.close()
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/"))
dirs, files = xbmcvfs.listdir(tryEncode(path))
for file in files:
if tryDecode(file).startswith('Plex'):
xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))
def deleteNodes():
# Clean up video nodes
import shutil
path = tryDecode(xbmc.translatePath("special://profile/library/video/"))
dirs, files = xbmcvfs.listdir(tryEncode(path))
for dir in dirs:
if tryDecode(dir).startswith('Plex'):
try:
shutil.rmtree("%s%s" % (path, tryDecode(dir)))
except:
2016-09-03 03:31:27 +10:00
log.error("Failed to delete directory: %s" % tryDecode(dir))
for file in files:
if tryDecode(file).startswith('plex'):
try:
xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))
except:
2016-09-03 03:31:27 +10:00
log.error("Failed to file: %s" % tryDecode(file))
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
def ThreadMethodsAdditionalStop(windowAttribute):
"""
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('plex_terminateNow') == "true") or
window(windowAttribute) == "true")
cls.threadStopped = threadStopped
return cls
return wrapper
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):
return (self._threadSuspended or
window(windowAttribute) == 'true')
cls.threadSuspended = threadSuspended
return cls
return wrapper
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
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
"""
# 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('plex_terminateNow') == 'true')
cls.threadStopped = threadStopped
# Return class to render this a decorator
return cls
2016-12-28 03:33:52 +11:00
class Lock_Function:
"""
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
2016-09-03 03:31:27 +10: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
}
}
query = {
"jsonrpc": "2.0",
"id": 1,
}
query['method'] = method[itemType]
query['params'] = params[itemType]
2017-01-29 23:52:46 +11:00
result = xbmc.executeJSONRPC(dumps(query))
result = loads(result)
2016-09-03 03:31:27 +10:00
result = result.get('result')
log.debug("JSON result was: %s" % result)
2016-12-03 21:50:05 +11:00
class JSONRPC(object):
id_ = 1
jsonrpc = "2.0"
def __init__(self, method, **kwargs):
self.method = method
for arg in kwargs: # id_(int), jsonrpc(str)
self.arg = arg
def _query(self):
query = {
'jsonrpc': self.jsonrpc,
'id': self.id_,
'method': self.method,
}
if self.params is not None:
query['params'] = self.params
2017-01-29 23:52:46 +11:00
return dumps(query)
2016-12-03 21:50:05 +11:00
def execute(self, params=None):
self.params = params
2017-01-29 23:52:46 +11:00
return loads(xbmc.executeJSONRPC(self._query()))