jellyfin-kodi/resources/lib/utils.py

632 lines
20 KiB
Python
Raw Normal View History

2016-03-31 19:39:32 +00:00
# -*- coding: utf-8 -*-
#################################################################################################
import cProfile
import inspect
import json
import logging
2016-03-31 19:39:32 +00:00
import pstats
import sqlite3
import StringIO
import os
import time
import unicodedata
import xml.etree.ElementTree as etree
2016-06-22 19:29:53 +00:00
from datetime import datetime
2016-03-31 19:39:32 +00:00
import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs
#################################################################################################
2016-06-16 05:43:36 +00:00
log = logging.getLogger("EMBY."+__name__)
2016-06-16 05:43:36 +00:00
#################################################################################################
# Main methods
2016-06-16 05:43:36 +00:00
def window(property, value=None, clear=False, window_id=10000):
# Get or set window property
WINDOW = xbmcgui.Window(window_id)
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00:00
if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value)
2016-06-16 05:43:36 +00:00
else:
return WINDOW.getProperty(property)
2016-03-31 19:39:32 +00:00
def settings(setting, value=None):
# Get or add addon setting
2016-06-16 05:43:36 +00:00
addon = xbmcaddon.Addon(id='plugin.video.emby')
2016-03-31 19:39:32 +00:00
if value is not None:
2016-06-16 05:43:36 +00:00
addon.setSetting(setting, value)
else: # returns unicode object
return addon.getSetting(setting)
2016-03-31 19:39:32 +00:00
2016-06-16 05:43:36 +00:00
def language(string_id):
# Central string retrieval - unicode
2016-07-18 20:47:42 +00:00
return xbmcaddon.Addon(id='plugin.video.emby').getLocalizedString(string_id)
2016-03-31 19:39:32 +00:00
2016-08-08 00:57:11 +00: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
return json.dumps(query)
def execute(self, params=None):
self.params = params
return json.loads(xbmc.executeJSONRPC(self._query()))
2016-06-16 05:43:36 +00:00
#################################################################################################
# Database related methods
2016-03-31 19:39:32 +00:00
def kodiSQL(media_type="video"):
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00:00
if media_type == "emby":
dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
elif media_type == "texture":
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
2016-06-16 05:43:36 +00:00
elif media_type == "music":
dbPath = getKodiMusicDBPath()
2016-03-31 19:39:32 +00:00
else:
dbPath = getKodiVideoDBPath()
2016-04-04 00:54:36 +00:00
if settings('dblock') == "true":
connection = sqlite3.connect(dbPath, isolation_level=None, timeout=20)
else:
connection = sqlite3.connect(dbPath, timeout=20)
2016-03-31 19:39:32 +00:00
return connection
def getKodiVideoDBPath():
dbVersion = {
"13": 78, # Gotham
"14": 90, # Helix
"15": 93, # Isengard
"16": 99, # Jarvis
2016-08-06 15:20:15 +00:00
"17": 107 # Krypton
2016-03-31 19:39:32 +00:00
}
dbPath = xbmc.translatePath(
2016-06-16 05:43:36 +00:00
"special://database/MyVideos%s.db"
% dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8')
2016-03-31 19:39:32 +00:00
return dbPath
def getKodiMusicDBPath():
dbVersion = {
"13": 46, # Gotham
"14": 48, # Helix
"15": 52, # Isengard
"16": 56, # Jarvis
"17": 60 # Krypton
2016-03-31 19:39:32 +00:00
}
dbPath = xbmc.translatePath(
2016-06-16 05:43:36 +00:00
"special://database/MyMusic%s.db"
% dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8')
2016-03-31 19:39:32 +00:00
return dbPath
def querySQL(query, args=None, cursor=None, conntype=None):
result = None
manualconn = False
failed = False
if cursor is None:
if conntype is None:
log.info("New connection type is missing.")
return result
else:
manualconn = True
connection = kodiSQL(conntype)
cursor = connection.cursor()
attempts = 0
while attempts < 3:
try:
log.debug("Query: %s Args: %s" % (query, args))
if args is None:
result = cursor.execute(query)
else:
result = cursor.execute(query, args)
break # Query successful, break out of while loop
except sqlite3.OperationalError as e:
if "database is locked" in e:
log.warn("%s...Attempt: %s" % (e, attempts))
attempts += 1
xbmc.sleep(1000)
else:
log.error(e)
if manualconn:
cursor.close()
raise
except sqlite3.Error as e:
log.error(e)
if manualconn:
cursor.close()
raise
else:
failed = True
log.info("FAILED // Query: %s Args: %s" % (query, args))
if manualconn:
if failed:
cursor.close()
else:
connection.commit()
cursor.close()
log.debug(result)
return result
2016-06-16 05:43:36 +00:00
#################################################################################################
# Utility methods
2016-03-31 19:39:32 +00:00
def getScreensaver():
# Get the current screensaver value
query = {
'jsonrpc': "2.0",
'id': 0,
'method': "Settings.getSettingValue",
'params': {
'setting': "screensaver.mode"
}
}
return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value']
def setScreensaver(value):
# Toggle the screensaver
query = {
'jsonrpc': "2.0",
'id': 0,
'method': "Settings.setSettingValue",
'params': {
'setting': "screensaver.mode",
'value': value
}
}
2016-06-16 05:43:36 +00:00
result = xbmc.executeJSONRPC(json.dumps(query))
log.info("Toggling screensaver: %s %s" % (value, result))
2016-06-16 05:43:36 +00:00
2016-06-18 03:03:28 +00:00
def convertDate(date):
2016-06-16 05:43:36 +00:00
try:
date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
except (ImportError, TypeError):
2016-06-16 05:43:36 +00:00
# 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
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 profiling(sortby="cumulative"):
# Will print results to Kodi log
def decorator(func):
def wrapper(*args, **kwargs):
pr = cProfile.Profile()
pr.enable()
result = func(*args, **kwargs)
pr.disable()
s = StringIO.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
log.info(s.getvalue())
2016-06-16 05:43:36 +00:00
return result
return wrapper
return decorator
#################################################################################################
# Addon utilities
2016-03-31 19:39:32 +00:00
def reset():
dialog = xbmcgui.Dialog()
2016-06-21 01:57:29 +00:00
if not dialog.yesno(language(29999), language(33074)):
2016-03-31 19:39:32 +00:00
return
# first stop any db sync
window('emby_shouldStop', value="true")
count = 10
while window('emby_dbScan') == "true":
log.info("Sync is running, will retry: %s..." % count)
2016-03-31 19:39:32 +00:00
count -= 1
if count == 0:
2016-06-21 01:57:29 +00:00
dialog.ok(language(29999), language(33085))
2016-03-31 19:39:32 +00:00
return
xbmc.sleep(1000)
# Clean up the playlists
deletePlaylists()
# Clean up the video nodes
deleteNodes()
# Wipe the kodi databases
log.warn("Resetting the Kodi video database.")
2016-03-31 19:39:32 +00: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()
if settings('enableMusic') == "true":
log.warn("Resetting the Kodi music database.")
2016-03-31 19:39:32 +00: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 emby database
log.warn("Resetting the Emby database.")
2016-03-31 19:39:32 +00: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')
connection.commit()
cursor.close()
# Offer to wipe cached thumbnails
2016-06-21 01:57:29 +00:00
resp = dialog.yesno(language(29999), language(33086))
2016-03-31 19:39:32 +00:00
if resp:
log.warn("Resetting all cached artwork.")
2016-03-31 19:39:32 +00:00
# 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))
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00:00
# 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()
2016-04-04 00:54:36 +00:00
# reset the install run flag
2016-03-31 19:39:32 +00:00
settings('SyncInstallRunDone', value="false")
# Remove emby info
2016-06-21 01:57:29 +00:00
resp = dialog.yesno(language(29999), language(33087))
2016-03-31 19:39:32 +00:00
if resp:
# Delete the settings
addon = xbmcaddon.Addon()
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
dataPath = "%ssettings.xml" % addondir
xbmcvfs.delete(dataPath)
log.info("Deleting: settings.xml")
2016-03-31 19:39:32 +00:00
2016-06-21 01:57:29 +00:00
dialog.ok(heading=language(29999), line1=language(33088))
2016-03-31 19:39:32 +00:00
xbmc.executebuiltin('RestartApp')
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()
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00: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
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00:00
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"
# 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
2016-06-21 01:57:29 +00:00
option = dialog.select(language(33075), [language(33076), language(33077)])
2016-03-31 19:39:32 +00:00
if option < 0:
# User cancelled dialog
return
elif option == 1:
# User selected remove
for paths in root.getiterator('passwords'):
for path in paths:
if path.find('.//from').text == "smb://%s/" % credentials:
paths.remove(path)
log.info("Successfully removed credentials for: %s" % credentials)
2016-03-31 19:39:32 +00:00
etree.ElementTree(root).write(xmlpath)
break
else:
log.info("Failed to find saved server: %s in passwords.xml" % credentials)
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00:00
settings('networkCreds', value="")
xbmcgui.Dialog().notification(
2016-06-21 01:57:29 +00:00
heading=language(29999),
message="%s %s" % (language(33078), credentials),
2016-03-31 19:39:32 +00:00
icon="special://home/addons/plugin.video.emby/icon.png",
time=1000,
sound=False)
return
elif option == 0:
# User selected to modify
2016-06-21 01:57:29 +00:00
server = dialog.input(language(33083), credentials)
2016-03-31 19:39:32 +00:00
if not server:
return
else:
# No credentials added
2016-06-21 01:57:29 +00:00
dialog.ok(heading=language(29999), line1=language(33082))
server = dialog.input(language(33084))
2016-03-31 19:39:32 +00:00
if not server:
return
# Network username
2016-06-21 01:57:29 +00:00
user = dialog.input(language(33079))
2016-03-31 19:39:32 +00:00
if not user:
return
# Network password
2016-06-21 01:57:29 +00:00
password = dialog.input(heading=language(33080), option=xbmcgui.ALPHANUM_HIDE_INPUT)
2016-03-31 19:39:32 +00:00
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)
2016-04-04 00:54:36 +00:00
# Add credentials
2016-03-31 19:39:32 +00:00
settings('networkCreds', value="%s" % server)
log.info("Added server: %s to passwords.xml" % server)
2016-03-31 19:39:32 +00:00
# Prettify and write to file
try:
indent(root)
except: pass
etree.ElementTree(root).write(xmlpath)
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00:00
dialog.notification(
2016-06-21 01:57:29 +00:00
heading=language(29999),
message="%s %s" % (language(33081), server),
2016-03-31 19:39:32 +00:00
icon="special://home/addons/plugin.video.emby/icon.png",
time=1000,
sound=False)
def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
# Tagname is in unicode - actions: add or delete
tagname = tagname.encode('utf-8')
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sEmby %s - %s.xsp" % (path, viewid, mediatype)
else:
plname = tagname
xsppath = "%sEmby %s.xsp" % (path, viewid)
# Create the playlist directory
if not xbmcvfs.exists(path):
log.info("Creating directory: %s" % path)
2016-03-31 19:39:32 +00:00
xbmcvfs.mkdirs(path)
# Only add the playlist if it doesn't already exists
if xbmcvfs.exists(xsppath):
if delete:
xbmcvfs.delete(xsppath)
log.info("Successfully removed playlist: %s." % tagname)
2016-04-04 00:54:36 +00:00
2016-03-31 19:39:32 +00:00
return
# Using write process since there's no guarantee the xml declaration works with etree
itemtypes = {
'homevideos': "movies"
}
log.info("Writing playlist file to: %s" % xsppath)
2016-03-31 19:39:32 +00:00
try:
f = xbmcvfs.File(xsppath, 'w')
except:
log.info("Failed to create playlist: %s" % xsppath)
2016-03-31 19:39:32 +00:00
return
else:
f.write(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
'<name>Emby %s</name>\n\t'
'<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()
log.info("Successfully added playlist: %s" % tagname)
2016-03-31 19:39:32 +00:00
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:
log.warn("Failed to delete directory: %s" % dir.decode('utf-8'))
2016-03-31 19:39:32 +00:00
for file in files:
if file.decode('utf-8').startswith('emby'):
try:
xbmcvfs.delete("%s%s" % (path, file.decode('utf-8')))
except:
log.warn("Failed to delete file: %s" % file.decode('utf-8'))