Seems I have to rename the filename to lowercase manually.
This commit is contained in:
angelblue05 2015-12-24 13:54:34 -06:00
parent 3f6fe0a9e7
commit f28c9fe069
10 changed files with 3695 additions and 3364 deletions

View file

@ -1,32 +1,38 @@
import xbmc # -*- coding: utf-8 -*-
import xbmcgui
import xbmcaddon ##################################################################################################
import requests
import json import json
import requests
import logging import logging
import Utils as utils import xbmc
from ClientInformation import ClientInformation import xbmcgui
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import utils
import clientinfo
##################################################################################################
# Disable requests logging # Disable requests logging
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
#logging.getLogger("requests").setLevel(logging.WARNING) #logging.getLogger('requests').setLevel(logging.WARNING)
##################################################################################################
class DownloadUtils(): class DownloadUtils():
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
_shared_state = {} _shared_state = {}
clientInfo = ClientInformation() clientInfo = clientinfo.ClientInfo()
addonName = clientInfo.getAddonName() addonName = clientInfo.getAddonName()
addon = xbmcaddon.Addon()
WINDOW = xbmcgui.Window(10000)
# Requests session # Requests session
s = None s = None
timeout = 60 timeout = 30
def __init__(self): def __init__(self):
@ -34,41 +40,44 @@ class DownloadUtils():
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
self.className = self.__class__.__name__ className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl)) utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
def setUsername(self, username): def setUsername(self, username):
# Reserved for UserClient only # Reserved for userclient only
self.username = username self.username = username
self.logMsg("Set username: %s" % username, 2) self.logMsg("Set username: %s" % username, 2)
def setUserId(self, userId): def setUserId(self, userId):
# Reserved for UserClient only # Reserved for userclient only
self.userId = userId self.userId = userId
self.logMsg("Set userId: %s" % userId, 2) self.logMsg("Set userId: %s" % userId, 2)
def setServer(self, server): def setServer(self, server):
# Reserved for UserClient only # Reserved for userclient only
self.server = server self.server = server
self.logMsg("Set server: %s" % server, 2) self.logMsg("Set server: %s" % server, 2)
def setToken(self, token): def setToken(self, token):
# Reserved for UserClient only # Reserved for userclient only
self.token = token self.token = token
self.logMsg("Set token: %s" % token, 2) self.logMsg("Set token: %s" % token, 2)
def setSSL(self, ssl, sslclient): def setSSL(self, ssl, sslclient):
# Reserved for UserClient only # Reserved for userclient only
self.sslverify = ssl self.sslverify = ssl
self.sslclient = sslclient self.sslclient = sslclient
self.logMsg("Verify SSL host certificate: %s" % ssl, 2) self.logMsg("Verify SSL host certificate: %s" % ssl, 2)
self.logMsg("SSL client side certificate: %s" % sslclient, 2) self.logMsg("SSL client side certificate: %s" % sslclient, 2)
def postCapabilities(self, deviceId): def postCapabilities(self, deviceId):
# Post settings to session # Post settings to session
url = "{server}/mediabrowser/Sessions/Capabilities/Full" url = "{server}/emby/Sessions/Capabilities/Full?format=json"
data = { data = {
'PlayableMediaTypes': "Audio,Video", 'PlayableMediaTypes': "Audio,Video",
'SupportsMediaControl': True, 'SupportsMediaControl': True,
'SupportedCommands': ( 'SupportedCommands': (
@ -86,49 +95,57 @@ class DownloadUtils():
} }
self.logMsg("Capabilities URL: %s" % url, 2) self.logMsg("Capabilities URL: %s" % url, 2)
self.logMsg("PostData: %s" % data, 2) self.logMsg("Postdata: %s" % data, 2)
try:
self.downloadUrl(url, postBody=data, type="POST") self.downloadUrl(url, postBody=data, type="POST")
self.logMsg("Posted capabilities to %s" % self.server, 1) self.logMsg("Posted capabilities to %s" % self.server, 2)
except:
self.logMsg("Posted capabilities failed.")
# Attempt at getting sessionId # Attempt at getting sessionId
url = "{server}/mediabrowser/Sessions?DeviceId=%s&format=json" % deviceId url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
result = self.downloadUrl(url)
try: try:
result = self.downloadUrl(url) sessionId = result[0]['Id']
self.logMsg("Session: %s" % result, 2)
sessionId = result[0][u'Id'] except (KeyError, TypeError):
self.logMsg("SessionId: %s" % sessionId)
self.WINDOW.setProperty("sessionId%s" % self.username, sessionId)
except:
self.logMsg("Failed to retrieve sessionId.", 1) self.logMsg("Failed to retrieve sessionId.", 1)
else: else:
self.logMsg("Session: %s" % result, 2)
self.logMsg("SessionId: %s" % sessionId, 1)
utils.window('emby_sessionId', value=sessionId)
# Post any permanent additional users # Post any permanent additional users
additionalUsers = utils.settings('additionalUsers').split(',') additionalUsers = utils.settings('additionalUsers')
self.logMsg("List of permanent users that should be added to the session: %s" % str(additionalUsers), 1) if additionalUsers:
additionalUsers = additionalUsers.split(',')
self.logMsg(
"List of permanent users added to the session: %s"
% additionalUsers, 1)
# Get the user list from server to get the userId # Get the user list from server to get the userId
url = "{server}/mediabrowser/Users?format=json" url = "{server}/emby/Users?format=json"
result = self.downloadUrl(url) result = self.downloadUrl(url)
if result:
for user in result:
username = user['Name'].lower()
userId = user['Id']
for additional in additionalUsers: for additional in additionalUsers:
addUser = additional.decode('utf-8').lower() addUser = additional.decode('utf-8').lower()
# Compare to server users to list of permanent additional users
for user in result:
username = user['Name'].lower()
if username in addUser: if username in addUser:
url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, userId) userId = user['Id']
postdata = {} url = (
self.downloadUrl(url, postBody=postdata, type="POST") "{server}/emby/Sessions/%s/Users/%s?format=json"
#xbmcgui.Dialog().notification("Success!", "%s added to viewing session" % username, time=1000) % (sessionId, userId)
)
self.downloadUrl(url, postBody={}, type="POST")
def startSession(self): def startSession(self):
self.deviceId = self.clientInfo.getMachineId() self.deviceId = self.clientInfo.getDeviceId()
# User is identified from this point # User is identified from this point
# Attach authenticated header to the session # Attach authenticated header to the session
@ -152,7 +169,7 @@ class DownloadUtils():
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
self.logMsg("Requests session started on: %s" % self.server) self.logMsg("Requests session started on: %s" % self.server, 1)
def stopSession(self): def stopSession(self):
try: try:
@ -165,93 +182,116 @@ class DownloadUtils():
clientInfo = self.clientInfo clientInfo = self.clientInfo
deviceName = clientInfo.getDeviceName() deviceName = clientInfo.getDeviceName()
deviceId = clientInfo.getMachineId() deviceId = clientInfo.getDeviceId()
version = clientInfo.getVersion() version = clientInfo.getVersion()
if not authenticate: if not authenticate:
# If user is not authenticated # If user is not authenticated
auth = 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (deviceName, deviceId, version) auth = (
header = {'Content-type': 'application/json', 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth} 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
% (deviceName, deviceId, version))
header = {
'Content-type': 'application/json',
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
'Authorization': auth
}
self.logMsg("Header: %s" % header, 2) self.logMsg("Header: %s" % header, 2)
return header
else: else:
userId = self.userId userId = self.userId
token = self.token token = self.token
# Attached to the requests session # Attached to the requests session
auth = 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (userId, deviceName, deviceId, version) auth = (
header = {'Content-type': 'application/json', 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth, 'X-MediaBrowser-Token': token} 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
% (userId, deviceName, deviceId, version))
header = {
'Content-type': 'application/json',
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
'Authorization': auth,
'X-MediaBrowser-Token': token
}
self.logMsg("Header: %s" % header, 2) self.logMsg("Header: %s" % header, 2)
return header return header
def downloadUrl(self, url, postBody=None, type="GET", authenticate=True): def downloadUrl(self, url, postBody=None, type="GET", parameters=None, authenticate=True):
self.logMsg("=== ENTER downloadUrl ===", 2) self.logMsg("=== ENTER downloadUrl ===", 2)
WINDOW = self.WINDOW
timeout = self.timeout timeout = self.timeout
default_link = "" default_link = ""
try: try:
# If user is authenticated # If user is authenticated
if (authenticate): if (authenticate):
# Get requests session # Get requests session
try: try:
s = self.s s = self.s
# Replace for the real values and append api_key # Replace for the real values
url = url.replace("{server}", self.server, 1) url = url.replace("{server}", self.server)
url = url.replace("{UserId}", self.userId, 1) url = url.replace("{UserId}", self.userId)
self.logMsg("URL: %s" % url, 2)
# Prepare request # Prepare request
if type == "GET": if type == "GET":
r = s.get(url, json=postBody, timeout=timeout) r = s.get(url, json=postBody, params=parameters, timeout=timeout)
elif type == "POST": elif type == "POST":
r = s.post(url, json=postBody, timeout=timeout) r = s.post(url, json=postBody, timeout=timeout)
elif type == "DELETE": elif type == "DELETE":
r = s.delete(url, json=postBody, timeout=timeout) r = s.delete(url, json=postBody, timeout=timeout)
except AttributeError: except AttributeError:
# request session does not exists
# Get user information # Get user information
self.username = WINDOW.getProperty('currUser') self.userId = utils.window('emby_currUser')
self.userId = WINDOW.getProperty('userId%s' % self.username) self.server = utils.window('emby_server%s' % self.userId)
self.server = WINDOW.getProperty('server%s' % self.username) self.token = utils.window('emby_accessToken%s' % self.userId)
self.token = WINDOW.getProperty('accessToken%s' % self.username)
header = self.getHeader() header = self.getHeader()
verifyssl = False verifyssl = False
cert = None cert = None
# IF user enables ssl verification # IF user enables ssl verification
try:
if utils.settings('sslverify') == "true": if utils.settings('sslverify') == "true":
verifyssl = True verifyssl = True
if utils.settings('sslcert') != "None": if utils.settings('sslcert') != "None":
cert = utils.settings('sslcert') cert = utils.settings('sslcert')
except:
self.logMsg("Could not load SSL settings.", 1)
pass
# Replace for the real values and append api_key # Replace for the real values
url = url.replace("{server}", self.server, 1) url = url.replace("{server}", self.server)
url = url.replace("{UserId}", self.userId, 1) url = url.replace("{UserId}", self.userId)
self.logMsg("URL: %s" % url, 2)
# Prepare request # Prepare request
if type == "GET": if type == "GET":
r = requests.get(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl) r = requests.get(url,
json=postBody,
params=parameters,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
elif type == "POST": elif type == "POST":
r = requests.post(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl) r = requests.post(url,
json=postBody,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
elif type == "DELETE": elif type == "DELETE":
r = requests.delete(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl) r = requests.delete(url,
json=postBody,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
# If user is not authenticated # If user is not authenticated
elif not authenticate: elif not authenticate:
self.logMsg("URL: %s" % url, 2)
header = self.getHeader(authenticate=False) header = self.getHeader(authenticate=False)
verifyssl = False verifyssl = False
@ -263,41 +303,49 @@ class DownloadUtils():
# Prepare request # Prepare request
if type == "GET": if type == "GET":
r = requests.get(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl) r = requests.get(url,
elif type == "POST": json=postBody,
r = requests.post(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl) params=parameters,
headers=header,
timeout=timeout,
verify=verifyssl)
# Process the response elif type == "POST":
r = requests.post(url,
json=postBody,
headers=header,
timeout=timeout,
verify=verifyssl)
##### THE RESPONSE #####
self.logMsg(r.url, 2)
if r.status_code == 204: if r.status_code == 204:
# No body in the response # No body in the response
self.logMsg("====== 204 Success ======", 2) self.logMsg("====== 204 Success ======", 2)
return default_link
elif r.status_code == requests.codes.ok: elif r.status_code == requests.codes.ok:
try: try:
# UTF-8 - JSON object # UTF-8 - JSON object
r = r.json() r = r.json()
self.logMsg("====== 200 Success ======", 2) self.logMsg("====== 200 Success ======", 2)
self.logMsg("Response: %s" % r, 2) self.logMsg("Response: %s" % r, 2)
return r return r
except: except:
if r.headers.get('content-type') == "text/html": if r.headers.get('content-type') != "text/html":
pass
else:
self.logMsg("Unable to convert the response for: %s" % url, 1) self.logMsg("Unable to convert the response for: %s" % url, 1)
else: else:
r.raise_for_status() r.raise_for_status()
return default_link ##### EXCEPTIONS #####
# TO REVIEW EXCEPTIONS
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
# Make the addon aware of status # Make the addon aware of status
if WINDOW.getProperty("Server_online") != "false": if utils.window('emby_online') != "false":
self.logMsg("Server unreachable at: %s" % url, 0) self.logMsg("Server unreachable at: %s" % url, 0)
self.logMsg(e, 2) self.logMsg(e, 2)
WINDOW.setProperty("Server_online", "false") utils.window('emby_online', value="false")
pass
except requests.exceptions.ConnectTimeout as e: except requests.exceptions.ConnectTimeout as e:
self.logMsg("Server timeout at: %s" % url, 0) self.logMsg("Server timeout at: %s" % url, 0)
@ -307,29 +355,35 @@ class DownloadUtils():
if r.status_code == 401: if r.status_code == 401:
# Unauthorized # Unauthorized
status = WINDOW.getProperty("Server_status") status = utils.window('emby_serverStatus')
if 'x-application-error-code' in r.headers: if 'X-Application-Error-Code' in r.headers:
# Emby server errors
if r.headers['X-Application-Error-Code'] == "ParentalControl": if r.headers['X-Application-Error-Code'] == "ParentalControl":
# Parental control - access restricted # Parental control - access restricted
WINDOW.setProperty("Server_status", "restricted") utils.window('emby_serverStatus', value="restricted")
xbmcgui.Dialog().notification("Emby server", "Access restricted.", xbmcgui.NOTIFICATION_ERROR, time=5000) xbmcgui.Dialog().notification(
heading="Emby server",
message="Access restricted.",
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
return False return False
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException": elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
# User tried to do something his emby account doesn't allow - admin restricted in some way # User tried to do something his emby account doesn't allow
pass pass
elif (status == "401") or (status == "Auth"): elif status not in ("401", "Auth"):
pass # Tell userclient token has been revoked.
utils.window('emby_serverStatus', value="401")
else:
# Tell UserClient token has been revoked.
WINDOW.setProperty("Server_status", "401")
self.logMsg("HTTP Error: %s" % e, 0) self.logMsg("HTTP Error: %s" % e, 0)
xbmcgui.Dialog().notification("Error connecting", "Unauthorized.", xbmcgui.NOTIFICATION_ERROR) xbmcgui.Dialog().notification(
heading="Error connecting",
message="Unauthorized.",
icon=xbmcgui.NOTIFICATION_ERROR)
return 401 return 401
elif (r.status_code == 301) or (r.status_code == 302): elif r.status_code in (301, 302):
# Redirects # Redirects
pass pass
elif r.status_code == 400: elif r.status_code == 400:

File diff suppressed because it is too large Load diff

View file

@ -1,72 +1,82 @@
# -*- coding: utf-8 -*-
################################################################################################# #################################################################################################
# Kodi Monitor
# Watched events that occur in Kodi, like setting media watched import json
#################################################################################################
import xbmc import xbmc
import xbmcgui import xbmcgui
import xbmcaddon
import json
import Utils as utils import clientinfo
from WriteKodiVideoDB import WriteKodiVideoDB import downloadutils
from ReadKodiDB import ReadKodiDB import embydb_functions as embydb
from PlayUtils import PlayUtils import playbackutils as pbutils
from DownloadUtils import DownloadUtils import utils
from PlaybackUtils import PlaybackUtils
#################################################################################################
class Kodi_Monitor( xbmc.Monitor ): class KodiMonitor(xbmc.Monitor):
WINDOW = xbmcgui.Window(10000)
def __init__(self, *args, **kwargs): def __init__(self):
xbmc.Monitor.__init__(self)
self.clientInfo = clientinfo.ClientInfo()
self.addonName = self.clientInfo.getAddonName()
self.doUtils = downloadutils.DownloadUtils()
self.logMsg("Kodi monitor started.", 1)
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__ self.className = self.__class__.__name__
utils.logMsg("%s %s" % ("EMBY", className), msg, int(lvl)) utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
def onScanStarted(self, library): def onScanStarted(self, library):
utils.window('kodiScan', value="true") self.logMsg("Kodi library scan %s running." % library, 2)
self.logMsg("Kodi library scan running.", 2) if library == "video":
utils.window('emby_kodiScan', value="true")
def onScanFinished(self, library): def onScanFinished(self, library):
utils.window('kodiScan', clear=True) self.logMsg("Kodi library scan %s finished." % library, 2)
self.logMsg("Kodi library scan finished.", 2) if library == "video":
utils.window('emby_kodiScan', clear=True)
#this library monitor is used to detect a watchedstate change by the user through the library
#as well as detect when a library item has been deleted to pass the delete to the Emby server
def onNotification(self, sender, method, data): def onNotification(self, sender, method, data):
WINDOW = self.WINDOW doUtils = self.doUtils
downloadUtils = DownloadUtils() if method not in ("Playlist.OnAdd"):
#player started playing an item - self.logMsg("Method: %s Data: %s" % (method, data), 1)
if ("Playlist.OnAdd" in method or "Player.OnPlay" in method):
jsondata = json.loads(data) if data:
if jsondata: data = json.loads(data)
if jsondata.has_key("item"):
if jsondata.get("item").has_key("id") and jsondata.get("item").has_key("type"):
id = jsondata.get("item").get("id")
type = jsondata.get("item").get("type")
if (utils.settings('useDirectPaths')=='true' and not type == "song") or (type == "song" and utils.settings('enableMusicSync') == "true"):
if type == "song": if method == "Player.OnPlay":
connection = utils.KodiSQL('music') # Set up report progress for emby playback
cursor = connection.cursor() item = data.get('item')
embyid = ReadKodiDB().getEmbyIdByKodiId(id, type, connection, cursor) try:
cursor.close() kodiid = item['id']
type = item['type']
except (KeyError, TypeError):
self.logMsg("Properties already set for item.", 1)
else: else:
embyid = ReadKodiDB().getEmbyIdByKodiId(id,type) if ((utils.settings('useDirectPaths') == "1" and not type == "song") or
(type == "song" and utils.settings('disableMusic') == "false")):
if embyid: # Set up properties for player
embyconn = utils.kodiSQL('emby')
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % embyid embycursor = embyconn.cursor()
result = downloadUtils.downloadUrl(url) emby_db = embydb.Embydb_Functions(embycursor)
self.logMsg("Result: %s" % result, 2) emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
try:
itemid = emby_dbitem[0]
except TypeError:
self.logMsg("No kodiid returned.", 1)
else:
url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
result = doUtils.downloadUrl(url)
self.logMsg("Item: %s" % result, 2)
playurl = None playurl = None
count = 0 count = 0
@ -74,77 +84,112 @@ class Kodi_Monitor( xbmc.Monitor ):
try: try:
playurl = xbmc.Player().getPlayingFile() playurl = xbmc.Player().getPlayingFile()
except RuntimeError: except RuntimeError:
count += 1
xbmc.sleep(200) xbmc.sleep(200)
else: else:
listItem = xbmcgui.ListItem() listItem = xbmcgui.ListItem()
PlaybackUtils().setProperties(playurl, result, listItem) playback = pbutils.PlaybackUtils(result)
if type == "song" and utils.settings('directstreammusic') == "true": if type == "song" and utils.settings('streamMusic') == "true":
utils.window('%splaymethod' % playurl, value="DirectStream") utils.window('emby_%s.playmethod' % playurl,
value="DirectStream")
else: else:
utils.window('%splaymethod' % playurl, value="DirectPlay") utils.window('emby_%s.playmethod' % playurl,
value="DirectPlay")
# Set properties for player.py
playback.setProperties(playurl, listItem)
finally:
embycursor.close()
count += 1
if method == "VideoLibrary.OnUpdate": elif method == "VideoLibrary.OnUpdate":
# Triggers 4 times, the following is only for manually marking as watched/unwatched # Manually marking as watched/unwatched
jsondata = json.loads(data) playcount = data.get('playcount')
item = data.get('item')
try:
kodiid = item['id']
type = item['type']
except (KeyError, TypeError):
self.logMsg("Item is invalid for playstate update.", 1)
else:
# Send notification to the server.
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
try:
itemid = emby_dbitem[0]
except TypeError:
self.logMsg("Could not find itemid in emby database.", 1)
else:
# Stop from manually marking as watched unwatched, with actual playback.
if utils.window('emby_skipWatched%s' % itemid) == "true":
# property is set in player.py
utils.window('emby_skipWatched%s' % itemid, clear=True)
else:
# notify the server
url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid
if playcount != 0:
doUtils.downloadUrl(url, type="POST")
self.logMsg("Mark as watched for itemid: %s" % itemid, 1)
else:
doUtils.downloadUrl(url, type="DELETE")
self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1)
finally:
embycursor.close()
elif method == "VideoLibrary.OnRemove":
try: try:
playcount = jsondata.get('playcount') kodiid = data['id']
item = jsondata['item']['id'] type = data['type']
type = jsondata['item']['type'] except (KeyError, TypeError):
prop = utils.window('Played%s%s' % (type, item)) self.logMsg("Item is invalid for emby deletion.", 1)
except:
self.logMsg("Could not process VideoLibrary.OnUpdate data.", 1)
else: else:
self.logMsg("VideoLibrary.OnUpdate: %s" % data, 2) # Send the delete action to the server.
if prop != "true": offerDelete = False
# Set property to prevent the multi triggering
utils.window('Played%s%s' % (type, item), "true")
WriteKodiVideoDB().updatePlayCountFromKodi(item, type, playcount)
self.clearProperty(type, item) if type == "episode" and utils.settings('deleteTV') == "true":
offerDelete = True
elif type == "movie" and utils.settings('deleteMovies') == "true":
offerDelete = True
if method == "System.OnWake": if utils.settings('offerDelete') != "true":
xbmc.sleep(10000) #Allow network to wake up # Delete could be disabled, even if the subsetting is enabled.
WINDOW.setProperty("OnWakeSync", "true") offerDelete = False
if method == "VideoLibrary.OnRemove": if offerDelete:
xbmc.log('Intercepted remove from sender: ' + sender + ' method: ' + method + ' data: ' + data) embyconn = utils.kodiSQL('emby')
jsondata = json.loads(data) embycursor = embyconn.cursor()
id = ReadKodiDB().getEmbyIdByKodiId(jsondata.get("id"), jsondata.get("type")) emby_db = embydb.Embydb_Functions(embycursor)
if id == None: emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
try:
itemid = emby_dbitem[0]
except TypeError:
self.logMsg("Could not find itemid in emby database.", 1)
else:
if utils.settings('skipConfirmDelete') != "true":
resp = xbmcgui.Dialog().yesno(
heading="Confirm delete",
line1="Delete file on Emby Server?")
if not resp:
self.logMsg("User skipped deletion.", 1)
embycursor.close()
return return
xbmc.log("Deleting Emby ID: " + id + " from database") url = "{server}/emby/Items/%s?format=json" % itemid
connection = utils.KodiSQL() self.logMsg("Deleting request: %s" % itemid)
cursor = connection.cursor() doUtils.downloadUrl(url, type="DELETE")
cursor.execute("DELETE FROM emby WHERE emby_id = ?", (id,)) finally:
connection.commit() embycursor.close()
cursor.close
if jsondata:
if jsondata.get("type") == "episode" or "movie": elif method == "System.OnWake":
url='{server}/mediabrowser/Items?Ids=' + id + '&format=json' # Allow network to wake up
#This is a check to see if the item exists on the server, if it doesn't it may have already been deleted by another client xbmc.sleep(10000)
result = DownloadUtils().downloadUrl(url) utils.window('emby_onWake', value="true")
item = result.get("Items")[0]
if data:
return_value = xbmcgui.Dialog().yesno("Confirm Delete", "Delete file on Emby Server?")
if return_value:
url='{server}/mediabrowser/Items/' + id
xbmc.log('Deleting via URL: ' + url)
DownloadUtils().downloadUrl(url, type="DELETE")
elif method == "Playlist.OnClear": elif method == "Playlist.OnClear":
self.logMsg("Clear playback properties.", 2) utils.window('emby_customPlaylist', clear=True, windowid=10101)
utils.window('propertiesPlayback', clear=True) #xbmcgui.Window(10101).clearProperties()
self.logMsg("Clear playlist properties.")
def clearProperty(self, type, id):
# The sleep is necessary since VideoLibrary.OnUpdate
# triggers 4 times in a row.
xbmc.sleep(100)
utils.window('Played%s%s' % (type,id), clear=True)
# Clear the widget cache
utils.window('clearwidgetcache', value="clear")

File diff suppressed because it is too large Load diff

View file

@ -6,117 +6,156 @@ import xbmc
import xbmcgui import xbmcgui
import xbmcvfs import xbmcvfs
from ClientInformation import ClientInformation import clientinfo
import Utils as utils import utils
################################################################################################# #################################################################################################
class PlayUtils(): class PlayUtils():
clientInfo = ClientInformation()
addonName = clientInfo.getAddonName() def __init__(self, item):
self.item = item
self.clientInfo = clientinfo.ClientInfo()
self.addonName = self.clientInfo.getAddonName()
self.userid = utils.window('emby_currUser')
self.server = utils.window('emby_server%s' % self.userid)
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__ self.className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
def getPlayUrl(self, server, id, result):
if self.isDirectPlay(result,True): def getPlayUrl(self):
# Try direct play
playurl = self.directPlay(result) item = self.item
if playurl: playurl = None
if item['MediaSources'][0]['Protocol'] == "Http":
# Only play as http
self.logMsg("File protocol is http.", 1)
playurl = self.httpPlay()
utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
elif self.isDirectPlay():
self.logMsg("File is direct playing.", 1) self.logMsg("File is direct playing.", 1)
utils.window("%splaymethod" % playurl.encode('utf-8'), value="DirectPlay") playurl = self.directPlay()
playurl = playurl.encode('utf-8')
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, value="DirectPlay")
elif self.isDirectStream():
elif self.isDirectStream(result):
# Try direct stream
playurl = self.directStream(result, server, id)
if playurl:
self.logMsg("File is direct streaming.", 1) self.logMsg("File is direct streaming.", 1)
utils.window("%splaymethod" % playurl, value="DirectStream") playurl = self.directStream()
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
elif self.isTranscoding():
elif self.isTranscoding(result):
# Try transcoding
playurl = self.transcoding(result, server, id)
if playurl:
self.logMsg("File is transcoding.", 1) self.logMsg("File is transcoding.", 1)
utils.window("%splaymethod" % playurl, value="Transcode") playurl = self.transcoding()
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
else: # Error return playurl
utils.window("playurlFalse", value="true")
return
return playurl.encode('utf-8') def httpPlay(self):
# Audio, Video, Photo
item = self.item
server = self.server
itemid = item['Id']
mediatype = item['MediaType']
def isDirectPlay(self, result, dialog = False): if type == "Audio":
# Requirements for Direct play: playurl = "%s/emby/Audio/%s/stream" % (server, itemid)
# FileSystem, Accessible path else:
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
return playurl
def isDirectPlay(self):
item = self.item
# Requirement: Filesystem, Accessible path
if utils.settings('playFromStream') == "true": if utils.settings('playFromStream') == "true":
# User forcing to play via HTTP instead of SMB # User forcing to play via HTTP
self.logMsg("Can't direct play: Play from HTTP is enabled.", 1) self.logMsg("Can't direct play, play from HTTP enabled.", 1)
return False return False
# Avoid H265 1080p
if (utils.settings('transcodeH265') == "true" and if (utils.settings('transcodeH265') == "true" and
result['MediaSources'][0]['Name'].startswith("1080P/H265")): result['MediaSources'][0]['Name'].startswith("1080P/H265")):
# Avoid H265 1080p
self.logMsg("Option to transcode 1080P/H265 enabled.", 1) self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
return False return False
canDirectPlay = result['MediaSources'][0]['SupportsDirectPlay'] canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay']
# Make sure it's supported by server # Make sure direct play is supported by the server
if not canDirectPlay: if not canDirectPlay:
self.logMsg("Can't direct play: Server does not allow or support it.", 1) self.logMsg("Can't direct play, server doesn't allow/support it.", 1)
return False return False
location = result['LocationType'] location = item['LocationType']
# File needs to be "FileSystem" if location == "FileSystem":
if 'FileSystem' in location: # Verify the path
# Verify if path is accessible if not self.fileExists():
if self.fileExists(result): self.logMsg("Unable to direct play.")
return True try:
else: count = int(utils.settings('failCount'))
self.logMsg("Unable to direct play. Verify the following path is accessible by the device: %s. You might also need to add SMB credentials in the add-on settings." % result['MediaSources'][0]['Path'], 1) except ValueError:
if dialog: count = 0
self.logMsg("Direct play failed: %s times." % count, 1)
failCount = int(utils.settings('directSteamFailedCount')) if count < 2:
self.logMsg("Direct Play failCount: %s." % failCount, 1) # Let the user know that direct play failed
utils.settings('failCount', value=str(count+1))
if failCount < 2: xbmcgui.Dialog().notification(
# Let user know that direct play failed heading="Emby server",
utils.settings('directSteamFailedCount', value=str(failCount + 1)) message="Unable to direct play.",
xbmcgui.Dialog().notification("Emby server", "Unable to direct play. Verify your log for more information.", icon="special://home/addons/plugin.video.emby/icon.png", sound=False) icon="special://home/addons/plugin.video.emby/icon.png",
sound=False)
elif utils.settings('playFromStream') != "true": elif utils.settings('playFromStream') != "true":
# Permanently set direct stream as true # Permanently set direct stream as true
utils.settings('playFromStream', value="true") utils.settings('playFromStream', value="true")
xbmcgui.Dialog().notification("Emby server", "Enabled play from HTTP in add-on settings.", icon="special://home/addons/plugin.video.emby/icon.png", sound=False) utils.settings('failCount', value="0")
xbmcgui.Dialog().notification(
heading="Emby server",
message=("Direct play failed 3 times. Enabled play "
"from HTTP in the add-on settings."),
icon="special://home/addons/plugin.video.emby/icon.png",
sound=False)
return False return False
def directPlay(self, result): return True
def directPlay(self):
item = self.item
try: try:
playurl = result['MediaSources'][0]['Path'] playurl = item['MediaSources'][0]['Path']
except KeyError: except (IndexError, KeyError):
playurl = result['Path'] playurl = item['Path']
if 'VideoType' in result: if item.get('VideoType'):
# Specific format modification # Specific format modification
if 'Dvd' in result['VideoType']: type = item['VideoType']
if type == "Dvd":
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
elif 'BluRay' in result['VideoType']: elif type == "Bluray":
playurl = "%s/BDMV/index.bdmv" % playurl playurl = "%s/BDMV/index.bdmv" % playurl
# Network - SMB protocol # Assign network protocol
if "\\\\" in playurl: if playurl.startswith('\\\\'):
smbuser = utils.settings('smbusername')
smbpass = utils.settings('smbpassword')
# Network share
if smbuser:
playurl = playurl.replace("\\\\", "smb://%s:%s@" % (smbuser, smbpass))
else:
playurl = playurl.replace("\\\\", "smb://") playurl = playurl.replace("\\\\", "smb://")
playurl = playurl.replace("\\", "/") playurl = playurl.replace("\\", "/")
@ -126,107 +165,120 @@ class PlayUtils():
return playurl return playurl
def fileExists(self):
def isDirectStream(self, result): if 'Path' not in self.item:
# Requirements for Direct stream: # File has no path defined in server
# FileSystem or Remote, BitRate, supported encoding return False
# Convert path to direct play
path = self.directPlay()
self.logMsg("Verifying path: %s" % path, 1)
if xbmcvfs.exists(path):
self.logMsg("Path exists.", 1)
return True
elif ":" not in path:
self.logMsg("Can't verify path, assumed linux. Still try to direct play.", 1)
return True
else:
self.logMsg("Failed to find file.")
return False
def isDirectStream(self):
item = self.item
# Avoid H265 1080p
if (utils.settings('transcodeH265') == "true" and if (utils.settings('transcodeH265') == "true" and
result['MediaSources'][0]['Name'].startswith("1080P/H265")): result['MediaSources'][0]['Name'].startswith("1080P/H265")):
# Avoid H265 1080p
self.logMsg("Option to transcode 1080P/H265 enabled.", 1) self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
return False return False
canDirectStream = result['MediaSources'][0]['SupportsDirectStream'] # Requirement: BitRate, supported encoding
# Make sure it's supported by server canDirectStream = item['MediaSources'][0]['SupportsDirectStream']
# Make sure the server supports it
if not canDirectStream: if not canDirectStream:
return False return False
location = result['LocationType'] # Verify the bitrate
# File can be FileSystem or Remote, not Virtual if not self.isNetworkSufficient():
if 'Virtual' in location: self.logMsg("The network speed is insufficient to direct stream file.", 1)
self.logMsg("File location is virtual. Can't proceed.", 1)
return False
# Verify BitRate
if not self.isNetworkQualitySufficient(result):
self.logMsg("The network speed is insufficient to playback the file.", 1)
return False return False
return True return True
def directStream(self, result, server, id, type = "Video"): def directStream(self):
if result['Path'].endswith('.strm'): item = self.item
server = self.server
itemid = item['Id']
type = item['Type']
if 'Path' in item and item['Path'].endswith('.strm'):
# Allow strm loading when direct streaming # Allow strm loading when direct streaming
playurl = self.directPlay(result) playurl = self.directPlay()
return playurl elif type == "Audio":
playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid)
if "ThemeVideo" in type: else:
playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
elif "Video" in type:
playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id)
elif "Audio" in type:
playurl = "%s/mediabrowser/Audio/%s/stream.mp3" % (server, id)
return playurl return playurl
def isNetworkSufficient(self):
def isTranscoding(self, result): settings = self.getBitrate()*1000
# Last resort, no requirements
# BitRate try:
canTranscode = result['MediaSources'][0]['SupportsTranscoding'] sourceBitrate = int(self.item['MediaSources'][0]['Bitrate'])
# Make sure it's supported by server except (KeyError, TypeError):
self.logMsg("Bitrate value is missing.", 1)
else:
self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s"
% (settings, sourceBitrate), 1)
if settings < sourceBitrate:
return False
return True
def isTranscoding(self):
item = self.item
canTranscode = item['MediaSources'][0]['SupportsTranscoding']
# Make sure the server supports it
if not canTranscode: if not canTranscode:
return False return False
location = result['LocationType']
# File can be FileSystem or Remote, not Virtual
if 'Virtual' in location:
return False
return True return True
def transcoding(self, result, server, id): def transcoding(self):
if result['Path'].endswith('.strm'): item = self.item
if 'Path' in item and item['Path'].endswith('.strm'):
# Allow strm loading when transcoding # Allow strm loading when transcoding
playurl = self.directPlay(result) playurl = self.directPlay()
return playurl
# Play transcoding
deviceId = self.clientInfo.getMachineId()
playurl = "%s/mediabrowser/Videos/%s/master.m3u8?mediaSourceId=%s" % (server, id, id)
playurl = "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" % (playurl, deviceId, self.getVideoBitRate()*1000)
playurl = self.audioSubsPref(playurl, result.get('MediaSources'))
self.logMsg("Playurl: %s" % playurl, 1)
return playurl
def isNetworkQualitySufficient(self, result):
# Works out if the network quality can play directly or if transcoding is needed
settingsVideoBitRate = self.getVideoBitRate()
settingsVideoBitRate = settingsVideoBitRate * 1000
try:
mediaSources = result['MediaSources']
sourceBitRate = int(mediaSources[0]['Bitrate'])
except KeyError:
self.logMsg("Bitrate value is missing.", 1)
else: else:
self.logMsg("The video quality selected is: %s, the video bitrate required to direct stream is: %s." % (settingsVideoBitRate, sourceBitRate), 1) itemid = item['Id']
if settingsVideoBitRate < sourceBitRate: deviceId = self.clientInfo.getDeviceId()
return False playurl = (
"%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
% (self.server, itemid, itemid)
)
playurl = (
"%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
% (playurl, deviceId, self.getBitrate()*1000))
return True return playurl
def getBitrate(self):
def getVideoBitRate(self):
# get the addon video quality # get the addon video quality
videoQuality = utils.settings('videoBitRate') videoQuality = utils.settings('videoBitrate')
bitrate = { bitrate = {
'0': 664, '0': 664,
@ -253,34 +305,7 @@ class PlayUtils():
# max bit rate supported by server (max signed 32bit integer) # max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483) return bitrate.get(videoQuality, 2147483)
def fileExists(self, result): def audioSubsPref(self, url):
if 'Path' not in result:
# File has no path in server
return False
# Convert Emby path to a path we can verify
path = self.directPlay(result)
try:
pathexists = xbmcvfs.exists(path)
except:
pathexists = False
# Verify the device has access to the direct path
if pathexists:
# Local or Network path
self.logMsg("Path exists.", 2)
return True
elif ":" not in path:
# Give benefit of the doubt for nfs.
self.logMsg("Can't verify path (assumed NFS). Still try direct play.", 2)
return True
else:
self.logMsg("Path is detected as follow: %s. Try direct streaming." % path, 2)
return False
def audioSubsPref(self, url, mediaSources):
# For transcoding only # For transcoding only
# Present the list of audio to select from # Present the list of audio to select from
audioStreamsList = {} audioStreamsList = {}
@ -292,15 +317,21 @@ class PlayUtils():
selectSubsIndex = "" selectSubsIndex = ""
playurlprefs = "%s" % url playurlprefs = "%s" % url
mediaStream = mediaSources[0].get('MediaStreams') item = self.item
for stream in mediaStream: try:
mediasources = item['MediaSources'][0]
mediastreams = mediasources['MediaStreams']
except (TypeError, KeyError, IndexError):
return
for stream in mediastreams:
# Since Emby returns all possible tracks together, have to sort them. # Since Emby returns all possible tracks together, have to sort them.
index = stream['Index'] index = stream['Index']
type = stream['Type'] type = stream['Type']
if 'Audio' in type: if 'Audio' in type:
codec = stream['Codec'] codec = stream['Codec']
channelLayout = stream['ChannelLayout'] channelLayout = stream.get('ChannelLayout', "")
try: try:
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
@ -312,6 +343,8 @@ class PlayUtils():
audioStreams.append(track) audioStreams.append(track)
elif 'Subtitle' in type: elif 'Subtitle' in type:
if stream['IsExternal']:
continue
try: try:
track = "%s - %s" % (index, stream['Language']) track = "%s - %s" % (index, stream['Language'])
except: except:
@ -336,7 +369,7 @@ class PlayUtils():
selectAudioIndex = audioStreamsList[selected] selectAudioIndex = audioStreamsList[selected]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
else: # User backed out of selection else: # User backed out of selection
playurlprefs += "&AudioStreamIndex=%s" % mediaSources[0]['DefaultAudioStreamIndex'] playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex']
else: # There's only one audiotrack. else: # There's only one audiotrack.
selectAudioIndex = audioStreamsList[audioStreams[0]] selectAudioIndex = audioStreamsList[audioStreams[0]]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
@ -352,7 +385,7 @@ class PlayUtils():
selectSubsIndex = subtitleStreamsList[selected] selectSubsIndex = subtitleStreamsList[selected]
playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
else: # User backed out of selection else: # User backed out of selection
playurlprefs += "&SubtitleStreamIndex=%s" % mediaSources[0].get('DefaultSubtitleStreamIndex', "") playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "")
# Get number of channels for selected audio track # Get number of channels for selected audio track
audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)

View file

@ -2,71 +2,69 @@
################################################################################################# #################################################################################################
import datetime import json
import json as json
import sys import sys
import xbmc import xbmc
import xbmcaddon
import xbmcplugin
import xbmcgui import xbmcgui
import xbmcplugin
from API import API import api
from DownloadUtils import DownloadUtils import artwork
from PlayUtils import PlayUtils import clientinfo
from ClientInformation import ClientInformation import downloadutils
import Utils as utils import playutils as putils
import playlist
import read_embyserver as embyserver
import utils
################################################################################################# #################################################################################################
class PlaybackUtils(): class PlaybackUtils():
clientInfo = ClientInformation()
doUtils = DownloadUtils()
api = API()
addon = xbmcaddon.Addon() def __init__(self, item):
language = addon.getLocalizedString
addonName = clientInfo.getAddonName() self.item = item
self.API = api.API(self.item)
self.clientInfo = clientinfo.ClientInfo()
self.addonName = self.clientInfo.getAddonName()
self.doUtils = downloadutils.DownloadUtils()
self.userid = utils.window('emby_currUser')
self.server = utils.window('emby_server%s' % self.userid)
self.artwork = artwork.Artwork()
self.emby = embyserver.Read_EmbyServer()
self.pl = playlist.Playlist()
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__ self.className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
def PLAY(self, result, setup = "service"):
self.logMsg("PLAY Called", 1) def play(self, itemid, dbid=None):
self.logMsg("Play called.", 1)
api = self.api
doUtils = self.doUtils doUtils = self.doUtils
username = utils.window('currUser') item = self.item
server = utils.window('server%s' % username) API = self.API
listitem = xbmcgui.ListItem()
playutils = putils.PlayUtils(item)
id = result['Id'] playurl = playutils.getPlayUrl()
userdata = result['UserData'] if not playurl:
# Get the playurl - direct play, direct stream or transcoding return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
playurl = PlayUtils().getPlayUrl(server, id, result)
listItem = xbmcgui.ListItem()
if utils.window('playurlFalse') == "true":
# Playurl failed - set in PlayUtils.py
utils.window('playurlFalse', clear=True)
self.logMsg("Failed to retrieve the playback path/url or dialog was cancelled.", 1)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem)
############### -- SETUP MAIN ITEM ################
# Set listitem and properties for main item
self.logMsg("Returned playurl: %s" % playurl, 1)
listItem.setPath(playurl)
self.setProperties(playurl, result, listItem)
mainArt = API().getArtwork(result, "Primary")
listItem.setThumbnailImage(mainArt)
listItem.setIconImage(mainArt)
if dbid is None:
# Item is not in Kodi database
listitem.setPath(playurl)
self.setProperties(playurl, listitem)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
############### ORGANIZE CURRENT PLAYLIST ################ ############### ORGANIZE CURRENT PLAYLIST ################
@ -74,58 +72,45 @@ class PlaybackUtils():
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
startPos = max(playlist.getposition(), 0) # Can return -1 startPos = max(playlist.getposition(), 0) # Can return -1
sizePlaylist = playlist.size() sizePlaylist = playlist.size()
propertiesPlayback = utils.window('propertiesPlayback') == "true"
introsPlaylist = False
dummyPlaylist = False
currentPosition = startPos currentPosition = startPos
self.logMsg("Playlist start position: %s" % startPos, 2) propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true"
self.logMsg("Playlist plugin position: %s" % currentPosition, 2) introsPlaylist = False
self.logMsg("Playlist size: %s" % sizePlaylist, 2) dummyPlaylist = False
self.logMsg("Playlist start position: %s" % startPos, 1)
self.logMsg("Playlist plugin position: %s" % currentPosition, 1)
self.logMsg("Playlist size: %s" % sizePlaylist, 1)
############### RESUME POINT ################ ############### RESUME POINT ################
# Resume point for widget only userdata = API.getUserData()
timeInfo = api.getTimeInfo(result) seektime = API.adjustResume(userdata['Resume'])
jumpBackSec = int(utils.settings('resumeJumpBack'))
seekTime = round(float(timeInfo.get('ResumeTime')), 6)
if seekTime > jumpBackSec:
# To avoid negative bookmark
seekTime = seekTime - jumpBackSec
# Show the additional resume dialog if launched from a widget
if homeScreen and seekTime:
# Dialog presentation
displayTime = str(datetime.timedelta(seconds=(int(seekTime))))
display_list = ["%s %s" % (self.language(30106), displayTime), self.language(30107)]
resume_result = xbmcgui.Dialog().select(self.language(30105), display_list)
if resume_result == 0:
# User selected to resume, append resume point to listitem
listItem.setProperty('StartOffset', str(seekTime))
elif resume_result > 0:
# User selected to start from beginning
seekTime = 0
else: # User cancelled the dialog
self.logMsg("User cancelled resume dialog.", 1)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem)
# We need to ensure we add the intro and additional parts only once. # We need to ensure we add the intro and additional parts only once.
# Otherwise we get a loop. # Otherwise we get a loop.
if not propertiesPlayback: if not propertiesPlayback:
utils.window('propertiesPlayback', value="true") utils.window('emby_playbackProps', value="true", windowid=10101)
self.logMsg("Setting up properties in playlist.") self.logMsg("Setting up properties in playlist.", 1)
if (not homeScreen and not seektime and
utils.window('emby_customPlaylist', windowid=10101) != "true"):
self.logMsg("Adding dummy file to playlist.", 2)
dummyPlaylist = True
playlist.add(playurl, listitem, index=startPos)
# Remove the original item from playlist
self.pl.removefromPlaylist(startPos+1)
# Readd the original item to playlist - via jsonrpc so we have full metadata
self.pl.insertintoPlaylist(currentPosition+1, dbid, item['Type'].lower())
currentPosition += 1
############### -- CHECK FOR INTROS ################ ############### -- CHECK FOR INTROS ################
if utils.settings('disableCinema') == "false" and not seekTime: if utils.settings('enableCinema') == "true" and not seektime:
# if we have any play them when the movie/show is not being resumed # if we have any play them when the movie/show is not being resumed
url = "{server}/mediabrowser/Users/{UserId}/Items/%s/Intros?format=json&ImageTypeLimit=1&Fields=Etag" % id url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid
intros = doUtils.downloadUrl(url) intros = doUtils.downloadUrl(url)
if intros['TotalRecordCount'] != 0: if intros['TotalRecordCount'] != 0:
@ -141,17 +126,15 @@ class PlaybackUtils():
if getTrailers: if getTrailers:
for intro in intros['Items']: for intro in intros['Items']:
# The server randomly returns intros, process them. # The server randomly returns intros, process them.
introId = intro['Id']
introPlayurl = PlayUtils().getPlayUrl(server, introId, intro)
introListItem = xbmcgui.ListItem() introListItem = xbmcgui.ListItem()
introPlayurl = putils.PlayUtils(intro).getPlayUrl()
self.logMsg("Adding Intro: %s" % introPlayurl, 1) self.logMsg("Adding Intro: %s" % introPlayurl, 1)
# Set listitem and properties for intros # Set listitem and properties for intros
self.setProperties(introPlayurl, intro, introListItem) pbutils = PlaybackUtils(intro)
self.setListItemProps(server, introId, introListItem, intro) pbutils.setProperties(introPlayurl, introListItem)
playlist.add(introPlayurl, introListItem, index=currentPosition) self.pl.insertintoPlaylist(currentPosition, url=introPlayurl)
introsPlaylist = True introsPlaylist = True
currentPosition += 1 currentPosition += 1
@ -159,109 +142,126 @@ class PlaybackUtils():
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ###############
if homeScreen and not sizePlaylist: if homeScreen and not sizePlaylist:
# Extend our current playlist with the actual item to play only if there's no playlist first # Extend our current playlist with the actual item to play
# only if there's no playlist first
self.logMsg("Adding main item to playlist.", 1) self.logMsg("Adding main item to playlist.", 1)
self.setListItemProps(server, id, listItem, result) self.pl.addtoPlaylist(dbid, item['Type'].lower())
playlist.add(playurl, listItem, index=currentPosition)
# Ensure that additional parts are played after the main item # Ensure that additional parts are played after the main item
currentPosition += 1 currentPosition += 1
############### -- CHECK FOR ADDITIONAL PARTS ################ ############### -- CHECK FOR ADDITIONAL PARTS ################
if result.get('PartCount'): if item.get('PartCount'):
# Only add to the playlist after intros have played # Only add to the playlist after intros have played
partcount = result['PartCount'] partcount = item['PartCount']
url = "{server}/mediabrowser/Videos/%s/AdditionalParts" % id url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid
parts = doUtils.downloadUrl(url) parts = doUtils.downloadUrl(url)
for part in parts['Items']: for part in parts['Items']:
partId = part['Id']
additionalPlayurl = PlayUtils().getPlayUrl(server, partId, part)
additionalListItem = xbmcgui.ListItem() additionalListItem = xbmcgui.ListItem()
additionalPlayurl = putils.PlayUtils(part).getPlayUrl()
self.logMsg("Adding additional part: %s" % partcount, 1) self.logMsg("Adding additional part: %s" % partcount, 1)
# Set listitem and properties for each additional parts # Set listitem and properties for each additional parts
self.setProperties(additionalPlayurl, part, additionalListItem) pbutils = PlaybackUtils(part)
self.setListItemProps(server, partId, additionalListItem, part) pbutils.setProperties(additionalPlayurl, additionalListItem)
pbutils.setArtwork(additionalListItem)
playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
self.pl.verifyPlaylist()
currentPosition += 1 currentPosition += 1
if dummyPlaylist:
############### ADD DUMMY TO PLAYLIST ################# # Added a dummy file to the playlist,
# because the first item is going to fail automatically.
if (not homeScreen and introsPlaylist) or (homeScreen and sizePlaylist > 0): self.logMsg("Processed as a playlist. First item is skipped.", 1)
# Playlist will fail on the current position. Adding dummy url return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
dummyPlaylist = True
self.logMsg("Adding dummy url to counter the setResolvedUrl error.", 2)
playlist.add(playurl, index=startPos)
currentPosition += 1
# We just skipped adding properties. Reset flag for next time. # We just skipped adding properties. Reset flag for next time.
elif propertiesPlayback: elif propertiesPlayback:
self.logMsg("Resetting properties playback flag.", 2) self.logMsg("Resetting properties playback flag.", 2)
utils.window('propertiesPlayback', clear=True) utils.window('emby_playbackProps', clear=True, windowid=10101)
#self.pl.verifyPlaylist()
########## SETUP MAIN ITEM ##########
self.verifyPlaylist() # For transcoding only, ask for audio/subs pref
if utils.window('emby_%s.playmethod' % playurl) == "Transcode":
playurl = playutils.audioSubsPref(playurl)
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
listitem.setPath(playurl)
self.setProperties(playurl, listitem)
############### PLAYBACK ################ ############### PLAYBACK ################
if not homeScreen and not introsPlaylist: if homeScreen and seektime:
self.logMsg("Play as a widget item.", 1)
self.setListItem(listitem)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
self.logMsg("Processed as a single item.", 1) elif ((introsPlaylist and utils.window('emby_customPlaylist', windowid=10101) == "true") or
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem) (homeScreen and not sizePlaylist)):
# Playlist was created just now, play it.
elif dummyPlaylist: self.logMsg("Play playlist.", 1)
# Added a dummy file to the playlist because the first item is going to fail automatically. xbmc.Player().play(playlist, startpos=startPos)
self.logMsg("Processed as a playlist. First item is skipped.", 1)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem)
else: else:
self.logMsg("Play as a regular item.", 1) self.logMsg("Play as a regular item.", 1)
xbmc.Player().play(playlist, startpos=startPos) xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
def setProperties(self, playurl, listitem):
# Set all properties necessary for plugin path playback
item = self.item
itemid = item['Id']
itemtype = item['Type']
def verifyPlaylist(self): embyitem = "emby_%s" % playurl
utils.window('%s.runtime' % embyitem, value=str(item.get('RunTimeTicks')))
utils.window('%s.type' % embyitem, value=itemtype)
utils.window('%s.itemid' % embyitem, value=itemid)
playlistitems = '{"jsonrpc": "2.0", "method": "Playlist.GetItems", "params": { "playlistid": 1 }, "id": 1}' if itemtype == "Episode":
items = xbmc.executeJSONRPC(playlistitems) utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId'))
self.logMsg(items, 2) else:
utils.window('%s.refreshid' % embyitem, value=itemid)
def removeFromPlaylist(self, pos): # Append external subtitles to stream
playmethod = utils.window('%s.playmethod' % embyitem)
# Only for direct play and direct stream
subtitles = self.externalSubs(playurl)
if playmethod in ("DirectStream", "Transcode"):
# Direct play automatically appends external
listitem.setSubtitles(subtitles)
playlistremove = '{"jsonrpc": "2.0", "method": "Playlist.Remove", "params": { "playlistid": 1, "position": %d }, "id": 1}' % pos self.setArtwork(listitem)
result = xbmc.executeJSONRPC(playlistremove)
self.logMsg(result, 1)
def externalSubs(self, playurl):
def externalSubs(self, id, playurl, mediaSources):
username = utils.window('currUser')
server = utils.window('server%s' % username)
externalsubs = [] externalsubs = []
mapping = {} mapping = {}
mediaStream = mediaSources[0].get('MediaStreams') item = self.item
itemid = item['Id']
try:
mediastreams = item['MediaSources'][0]['MediaStreams']
except (TypeError, KeyError, IndexError):
return
kodiindex = 0 kodiindex = 0
for stream in mediaStream: for stream in mediastreams:
index = stream['Index'] index = stream['Index']
# Since Emby returns all possible tracks together, have to pull only external subtitles. # Since Emby returns all possible tracks together, have to pull only external subtitles.
# IsTextSubtitleStream if true, is available to download from emby. # IsTextSubtitleStream if true, is available to download from emby.
if "Subtitle" in stream['Type'] and stream['IsExternal'] and stream['IsTextSubtitleStream']: if (stream['Type'] == "Subtitle" and
stream['IsExternal'] and stream['IsTextSubtitleStream']):
playmethod = utils.window("%splaymethod" % playurl) # Direct stream
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
if "DirectPlay" in playmethod: % (self.server, itemid, itemid, index))
# Direct play, get direct path
url = PlayUtils().directPlay(stream)
elif "DirectStream" in playmethod: # Direct stream
url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt" % (server, id, id, index)
# map external subtitles for mapping # map external subtitles for mapping
mapping[kodiindex] = index mapping[kodiindex] = index
@ -269,69 +269,79 @@ class PlaybackUtils():
kodiindex += 1 kodiindex += 1
mapping = json.dumps(mapping) mapping = json.dumps(mapping)
utils.window('%sIndexMapping' % playurl, value=mapping) utils.window('emby_%s.indexMapping' % playurl, value=mapping)
return externalsubs return externalsubs
def setArtwork(self, listItem):
def setProperties(self, playurl, result, listItem):
# Set runtimeticks, type, refresh_id and item_id
id = result.get('Id')
type = result.get('Type', "")
utils.window("%sruntimeticks" % playurl, value=str(result.get('RunTimeTicks')))
utils.window("%stype" % playurl, value=type)
utils.window("%sitem_id" % playurl, value=id)
if type == "Episode":
utils.window("%srefresh_id" % playurl, value=result.get('SeriesId'))
else:
utils.window("%srefresh_id" % playurl, value=id)
if utils.window("%splaymethod" % playurl) != "Transcode":
# Only for direct play and direct stream
# Append external subtitles to stream
subtitleList = self.externalSubs(id, playurl, result['MediaSources'])
listItem.setSubtitles(subtitleList)
def setArt(self, list, name, path):
if name in ("thumb", "fanart_image", "small_poster", "tiny_poster", "medium_landscape", "medium_poster", "small_fanartimage", "medium_fanartimage", "fanart_noindicators"):
list.setProperty(name, path)
else:
list.setArt({name:path})
return list
def setListItemProps(self, server, id, listItem, result):
# Set up item and item info # Set up item and item info
api = self.api item = self.item
artwork = self.artwork
type = result.get('Type') allartwork = artwork.getAllArtwork(item, parentInfo=True)
people = api.getPeople(result) # Set artwork for listitem
studios = api.getStudios(result) arttypes = {
'poster': "Primary",
'tvshow.poster': "Primary",
'clearart': "Art",
'tvshow.clearart': "Art",
'clearlogo': "Logo",
'tvshow.clearlogo': "Logo",
'discart': "Disc",
'fanart_image': "Backdrop",
'landscape': "Thumb"
}
for arttype in arttypes:
art = arttypes[arttype]
if art == "Backdrop":
try: # Backdrop is a list, grab the first backdrop
self.setArtProp(listItem, arttype, allartwork[art][0])
except: pass
else:
self.setArtProp(listItem, arttype, allartwork[art])
def setArtProp(self, listItem, arttype, path):
if arttype in (
'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
'medium_landscape', 'medium_poster', 'small_fanartimage',
'medium_fanartimage', 'fanart_noindicators'):
listItem.setProperty(arttype, path)
else:
listItem.setArt({arttype: path})
def setListItem(self, listItem):
item = self.item
type = item['Type']
API = self.API
people = API.getPeople()
studios = API.getStudios()
metadata = { metadata = {
'title': result.get('Name', "Missing name"), 'title': item.get('Name', "Missing name"),
'year': result.get('ProductionYear'), 'year': item.get('ProductionYear'),
'plot': api.getOverview(result), 'plot': API.getOverview(),
'director': people.get('Director'), 'director': people.get('Director'),
'writer': people.get('Writer'), 'writer': people.get('Writer'),
'mpaa': api.getMpaa(result), 'mpaa': API.getMpaa(),
'genre': api.getGenre(result), 'genre': " / ".join(item['Genres']),
'studio': " / ".join(studios), 'studio': " / ".join(studios),
'aired': api.getPremiereDate(result), 'aired': API.getPremiereDate(),
'rating': result.get('CommunityRating'), 'rating': item.get('CommunityRating'),
'votes': result.get('VoteCount') 'votes': item.get('VoteCount')
} }
if "Episode" in type: if "Episode" in type:
# Only for tv shows # Only for tv shows
thumbId = result.get('SeriesId') thumbId = item.get('SeriesId')
season = result.get('ParentIndexNumber', -1) season = item.get('ParentIndexNumber', -1)
episode = result.get('IndexNumber', -1) episode = item.get('IndexNumber', -1)
show = result.get('SeriesName', "") show = item.get('SeriesName', "")
metadata['TVShowTitle'] = show metadata['TVShowTitle'] = show
metadata['season'] = season metadata['season'] = season
@ -341,122 +351,3 @@ class PlaybackUtils():
listItem.setProperty('IsFolder', 'false') listItem.setProperty('IsFolder', 'false')
listItem.setLabel(metadata['title']) listItem.setLabel(metadata['title'])
listItem.setInfo('video', infoLabels=metadata) listItem.setInfo('video', infoLabels=metadata)
# Set artwork for listitem
self.setArt(listItem,'poster', API().getArtwork(result, "Primary"))
self.setArt(listItem,'tvshow.poster', API().getArtwork(result, "SeriesPrimary"))
self.setArt(listItem,'clearart', API().getArtwork(result, "Art"))
self.setArt(listItem,'tvshow.clearart', API().getArtwork(result, "Art"))
self.setArt(listItem,'clearlogo', API().getArtwork(result, "Logo"))
self.setArt(listItem,'tvshow.clearlogo', API().getArtwork(result, "Logo"))
self.setArt(listItem,'discart', API().getArtwork(result, "Disc"))
self.setArt(listItem,'fanart_image', API().getArtwork(result, "Backdrop"))
self.setArt(listItem,'landscape', API().getArtwork(result, "Thumb"))
def seekToPosition(self, seekTo):
# Set a loop to wait for positive confirmation of playback
count = 0
while not xbmc.Player().isPlaying():
count += 1
if count >= 10:
return
else:
xbmc.sleep(500)
# Jump to seek position
count = 0
while xbmc.Player().getTime() < (seekToTime - 5) and count < 11: # only try 10 times
count += 1
xbmc.Player().seekTime(seekTo)
xbmc.sleep(100)
def PLAYAllItems(self, items, startPositionTicks):
self.logMsg("== ENTER: PLAYAllItems ==")
self.logMsg("Items: %s" % items)
doUtils = self.doUtils
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
started = False
for itemId in items:
self.logMsg("Adding Item to playlist: %s" % itemId, 1)
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemId
result = doUtils.downloadUrl(url)
addition = self.addPlaylistItem(playlist, result)
if not started and addition:
started = True
self.logMsg("Starting Playback Pre", 1)
xbmc.Player().play(playlist)
if not started:
self.logMsg("Starting Playback Post", 1)
xbmc.Player().play(playlist)
# Seek to position
if startPositionTicks:
seekTime = startPositionTicks / 10000000.0
self.seekToPosition(seekTime)
def AddToPlaylist(self, itemIds):
self.logMsg("== ENTER: PLAYAllItems ==")
doUtils = self.doUtils
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
for itemId in itemIds:
self.logMsg("Adding Item to Playlist: %s" % itemId)
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemId
result = doUtils.downloadUrl(url)
self.addPlaylistItem(playlist, result)
return playlist
def addPlaylistItem(self, playlist, item):
id = item['Id']
username = utils.window('currUser')
server = utils.window('server%s' % username)
playurl = PlayUtils().getPlayUrl(server, id, item)
if utils.window('playurlFalse') == "true":
# Playurl failed - set in PlayUtils.py
utils.window('playurlFalse', clear=True)
self.logMsg("Failed to retrieve the playback path/url or dialog was cancelled.", 1)
return
self.logMsg("Playurl: %s" % playurl)
thumb = API().getArtwork(item, "Primary")
listItem = xbmcgui.ListItem(path=playurl, iconImage=thumb, thumbnailImage=thumb)
self.setListItemProps(server, id, listItem, item)
self.setProperties(playurl, item, listItem)
playlist.add(playurl, listItem)
# Not currently being used
'''def PLAYAllEpisodes(self, items):
WINDOW = xbmcgui.Window(10000)
username = WINDOW.getProperty('currUser')
userid = WINDOW.getProperty('userId%s' % username)
server = WINDOW.getProperty('server%s' % username)
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
for item in items:
item_url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json&ImageTypeLimit=1" % item["Id"]
jsonData = self.downloadUtils.downloadUrl(item_url)
item_data = jsonData
self.addPlaylistItem(playlist, item_data, server, userid)
xbmc.Player().play(playlist)'''

View file

@ -2,45 +2,47 @@
################################################################################################# #################################################################################################
import json as json import json
import xbmc import xbmc
import xbmcgui import xbmcgui
from DownloadUtils import DownloadUtils import utils
from WebSocketClient import WebSocketThread import clientinfo
from ClientInformation import ClientInformation import downloadutils
from LibrarySync import LibrarySync import kodidb_functions as kodidb
import Utils as utils import websocket_client as wsc
################################################################################################# #################################################################################################
class Player(xbmc.Player): class Player(xbmc.Player):
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
_shared_state = {} _shared_state = {}
xbmcplayer = xbmc.Player() played_info = {}
doUtils = DownloadUtils()
clientInfo = ClientInformation()
ws = WebSocketThread()
librarySync = LibrarySync()
addonName = clientInfo.getAddonName()
played_information = {}
playStats = {} playStats = {}
currentFile = None currentFile = None
def __init__(self, *args):
def __init__(self):
self.__dict__ = self._shared_state self.__dict__ = self._shared_state
self.clientInfo = clientinfo.ClientInfo()
self.addonName = self.clientInfo.getAddonName()
self.doUtils = downloadutils.DownloadUtils()
self.ws = wsc.WebSocket_Client()
self.xbmcplayer = xbmc.Player()
self.logMsg("Starting playback monitor.", 2) self.logMsg("Starting playback monitor.", 2)
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
self.className = self.__class__.__name__ self.className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl)) utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
def GetPlayStats(self): def GetPlayStats(self):
return self.playStats return self.playStats
@ -74,31 +76,42 @@ class Player( xbmc.Player ):
self.currentFile = currentFile self.currentFile = currentFile
# We may need to wait for info to be set in kodi monitor # We may need to wait for info to be set in kodi monitor
itemId = utils.window("%sitem_id" % currentFile) itemId = utils.window("emby_%s.itemid" % currentFile)
tryCount = 0 tryCount = 0
while not itemId: while not itemId:
xbmc.sleep(200) xbmc.sleep(200)
itemId = utils.window("%sitem_id" % currentFile) itemId = utils.window("emby_%s.itemid" % currentFile)
if tryCount == 20: # try 20 times or about 10 seconds if tryCount == 20: # try 20 times or about 10 seconds
self.logMsg("Could not find itemId, cancelling playback report...", 1) self.logMsg("Could not find itemId, cancelling playback report...", 1)
break break
else: tryCount += 1 else: tryCount += 1
else: else:
self.logMsg("ONPLAYBACK_STARTED: %s ITEMID: %s" % (currentFile, itemId), 0) self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0)
# Only proceed if an itemId was found. # Only proceed if an itemId was found.
runtime = utils.window("%sruntimeticks" % currentFile) embyitem = "emby_%s" % currentFile
refresh_id = utils.window("%srefresh_id" % currentFile) runtime = utils.window("%s.runtime" % embyitem)
playMethod = utils.window("%splaymethod" % currentFile) refresh_id = utils.window("%s.refreshid" % embyitem)
itemType = utils.window("%stype" % currentFile) playMethod = utils.window("%s.playmethod" % embyitem)
itemType = utils.window("%s.type" % embyitem)
utils.window('emby_skipWatched%s' % itemId, value="true")
seekTime = xbmcplayer.getTime() seekTime = xbmcplayer.getTime()
# Get playback volume # Get playback volume
volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' volume_query = {
result = xbmc.executeJSONRPC(volume_query)
"jsonrpc": "2.0",
"id": 1,
"method": "Application.GetProperties",
"params": {
"properties": ["volume", "muted"]
}
}
result = xbmc.executeJSONRPC(json.dumps(volume_query))
result = json.loads(result) result = json.loads(result)
result = result.get('result') result = result.get('result')
@ -106,7 +119,7 @@ class Player( xbmc.Player ):
muted = result.get('muted') muted = result.get('muted')
# Postdata structure to send to Emby server # Postdata structure to send to Emby server
url = "{server}/mediabrowser/Sessions/Playing" url = "{server}/emby/Sessions/Playing"
postdata = { postdata = {
'QueueableMediaTypes': "Video", 'QueueableMediaTypes': "Video",
@ -123,12 +136,22 @@ class Player( xbmc.Player ):
if playMethod == "Transcode": if playMethod == "Transcode":
# property set in PlayUtils.py # property set in PlayUtils.py
postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile) postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile)
postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" % currentFile) postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex"
% currentFile)
else: else:
# Get the current kodi audio and subtitles and convert to Emby equivalent # Get the current kodi audio and subtitles and convert to Emby equivalent
track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' tracks_query = {
result = xbmc.executeJSONRPC(track_query)
"jsonrpc": "2.0",
"id": 1,
"method": "Player.GetProperties",
"params": {
"playerid": 1,
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
}
}
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
result = json.loads(result) result = json.loads(result)
result = result.get('result') result = result.get('result')
@ -155,9 +178,9 @@ class Player( xbmc.Player ):
# Number of audiotracks to help get Emby Index # Number of audiotracks to help get Emby Index
audioTracks = len(xbmc.Player().getAvailableAudioStreams()) audioTracks = len(xbmc.Player().getAvailableAudioStreams())
mapping = utils.window("%sIndexMapping" % currentFile) mapping = utils.window("%s.indexMapping" % embyitem)
if mapping: # Set in PlaybackUtils.py if mapping: # Set in playbackutils.py
self.logMsg("Mapping for external subtitles index: %s" % mapping, 2) self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
externalIndex = json.loads(mapping) externalIndex = json.loads(mapping)
@ -167,7 +190,8 @@ class Player( xbmc.Player ):
postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
else: else:
# Internal subtitle currently selected # Internal subtitle currently selected
postdata['SubtitleStreamIndex'] = indexSubs - len(externalIndex) + audioTracks + 1 subindex = indexSubs - len(externalIndex) + audioTracks + 1
postdata['SubtitleStreamIndex'] = subindex
else: # Direct paths enabled scenario or no external subtitles set else: # Direct paths enabled scenario or no external subtitles set
postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
@ -184,7 +208,7 @@ class Player( xbmc.Player ):
runtime = int(runtime) runtime = int(runtime)
except ValueError: except ValueError:
runtime = xbmcplayer.getTotalTime() runtime = xbmcplayer.getTotalTime()
self.logMsg("Runtime is missing, grabbing runtime from Kodi player: %s" % runtime, 1) self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1)
# Save data map for updates and position calls # Save data map for updates and position calls
data = { data = {
@ -200,8 +224,8 @@ class Player( xbmc.Player ):
'currentPosition': int(seekTime) 'currentPosition': int(seekTime)
} }
self.played_information[currentFile] = data self.played_info[currentFile] = data
self.logMsg("ADDING_FILE: %s" % self.played_information, 1) self.logMsg("ADDING_FILE: %s" % self.played_info, 1)
# log some playback stats # log some playback stats
'''if(itemType != None): '''if(itemType != None):
@ -225,7 +249,7 @@ class Player( xbmc.Player ):
# Get current file # Get current file
currentFile = self.currentFile currentFile = self.currentFile
data = self.played_information.get(currentFile) data = self.played_info.get(currentFile)
# only report playback if emby has initiated the playback (item_id has value) # only report playback if emby has initiated the playback (item_id has value)
if data: if data:
@ -239,15 +263,23 @@ class Player( xbmc.Player ):
# Get playback volume # Get playback volume
volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' volume_query = {
result = xbmc.executeJSONRPC(volume_query)
"jsonrpc": "2.0",
"id": 1,
"method": "Application.GetProperties",
"params": {
"properties": ["volume", "muted"]
}
}
result = xbmc.executeJSONRPC(json.dumps(volume_query))
result = json.loads(result) result = json.loads(result)
result = result.get('result') result = result.get('result')
volume = result.get('volume') volume = result.get('volume')
muted = result.get('muted') muted = result.get('muted')
# Postdata for the websocketclient report # Postdata for the websocketclient report
postdata = { postdata = {
@ -269,8 +301,18 @@ class Player( xbmc.Player ):
else: else:
# Get current audio and subtitles track # Get current audio and subtitles track
track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid":1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' tracks_query = {
result = xbmc.executeJSONRPC(track_query)
"jsonrpc": "2.0",
"id": 1,
"method": "Player.GetProperties",
"params": {
"playerid": 1,
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
}
}
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
result = json.loads(result) result = json.loads(result)
result = result.get('result') result = result.get('result')
@ -297,7 +339,7 @@ class Player( xbmc.Player ):
# Number of audiotracks to help get Emby Index # Number of audiotracks to help get Emby Index
audioTracks = len(xbmc.Player().getAvailableAudioStreams()) audioTracks = len(xbmc.Player().getAvailableAudioStreams())
mapping = utils.window("%sIndexMapping" % currentFile) mapping = utils.window("emby_%s.indexMapping" % currentFile)
if mapping: # Set in PlaybackUtils.py if mapping: # Set in PlaybackUtils.py
@ -306,13 +348,16 @@ class Player( xbmc.Player ):
if externalIndex.get(str(indexSubs)): if externalIndex.get(str(indexSubs)):
# If the current subtitle is in the mapping # If the current subtitle is in the mapping
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [externalIndex[str(indexSubs)]] * 2 subindex = [externalIndex[str(indexSubs)]] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else: else:
# Internal subtitle currently selected # Internal subtitle currently selected
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else: # Direct paths enabled scenario or no external subtitles set else: # Direct paths enabled scenario or no external subtitles set
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs + audioTracks + 1] * 2 subindex = [indexSubs + audioTracks + 1] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else: else:
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2 data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
@ -326,8 +371,8 @@ class Player( xbmc.Player ):
currentFile = self.currentFile currentFile = self.currentFile
self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2) self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2)
if self.played_information.get(currentFile): if self.played_info.get(currentFile):
self.played_information[currentFile]['paused'] = True self.played_info[currentFile]['paused'] = True
self.reportPlayback() self.reportPlayback()
@ -336,8 +381,8 @@ class Player( xbmc.Player ):
currentFile = self.currentFile currentFile = self.currentFile
self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2) self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2)
if self.played_information.get(currentFile): if self.played_info.get(currentFile):
self.played_information[currentFile]['paused'] = False self.played_info[currentFile]['paused'] = False
self.reportPlayback() self.reportPlayback()
@ -346,15 +391,17 @@ class Player( xbmc.Player ):
currentFile = self.currentFile currentFile = self.currentFile
self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2) self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2)
if self.played_information.get(currentFile): if self.played_info.get(currentFile):
position = self.xbmcplayer.getTime() position = self.xbmcplayer.getTime()
self.played_information[currentFile]['currentPosition'] = position self.played_info[currentFile]['currentPosition'] = position
self.reportPlayback() self.reportPlayback()
def onPlayBackStopped( self ): def onPlayBackStopped( self ):
# Will be called when user stops xbmc playing a file # Will be called when user stops xbmc playing a file
self.logMsg("ONPLAYBACK_STOPPED", 2) self.logMsg("ONPLAYBACK_STOPPED", 2)
xbmcgui.Window(10101).clearProperties()
self.logMsg("Clear playlist properties.")
self.stopAll() self.stopAll()
def onPlayBackEnded( self ): def onPlayBackEnded( self ):
@ -364,14 +411,16 @@ class Player( xbmc.Player ):
def stopAll(self): def stopAll(self):
if not self.played_information: doUtils = self.doUtils
if not self.played_info:
return return
self.logMsg("Played_information: %s" % self.played_information, 1) self.logMsg("Played_information: %s" % self.played_info, 1)
# Process each items # Process each items
for item in self.played_information: for item in self.played_info:
data = self.played_information.get(item) data = self.played_info.get(item)
if data: if data:
self.logMsg("Item path: %s" % item, 2) self.logMsg("Item path: %s" % item, 2)
@ -379,47 +428,61 @@ class Player( xbmc.Player ):
runtime = data['runtime'] runtime = data['runtime']
currentPosition = data['currentPosition'] currentPosition = data['currentPosition']
itemId = data['item_id'] itemid = data['item_id']
refresh_id = data['refresh_id'] refresh_id = data['refresh_id']
currentFile = data['currentfile'] currentFile = data['currentfile']
type = data['Type'] type = data['Type']
playMethod = data['playmethod'] playMethod = data['playmethod']
if currentPosition and runtime: if currentPosition and runtime:
try:
percentComplete = (currentPosition * 10000000) / int(runtime) percentComplete = (currentPosition * 10000000) / int(runtime)
markPlayedAt = float(utils.settings('markPlayed')) / 100 except ZeroDivisionError:
# Runtime is 0.
percentComplete = 0
self.logMsg("Percent complete: %s Mark played at: %s" % (percentComplete, markPlayedAt), 1) markPlayedAt = float(utils.settings('markPlayed')) / 100
# Prevent manually mark as watched in Kodi monitor > WriteKodiVideoDB().UpdatePlaycountFromKodi() self.logMsg(
utils.window('SkipWatched%s' % itemId, "true") "Percent complete: %s Mark played at: %s"
% (percentComplete, markPlayedAt), 1)
# Prevent manually mark as watched in Kodi monitor
utils.window('emby_skipWatched%s' % itemid, value="true")
self.stopPlayback(data) self.stopPlayback(data)
offerDelete = utils.settings('offerDelete') == "true"
offerTypeDelete = False
if type == "Episode" and utils.settings('offerDeleteTV') == "true":
offerTypeDelete = True
elif type == "Movie" and utils.settings('offerDeleteMovies') == "true":
offerTypeDelete = True
if percentComplete >= markPlayedAt and offerDelete and offerTypeDelete:
# Make the bigger setting be able to disable option easily.
self.logMsg("Offering deletion for: %s." % itemId, 1)
return_value = xbmcgui.Dialog().yesno("Offer Delete", "Delete %s" % currentFile.split("/")[-1], "on Emby Server?")
if return_value:
# Delete Kodi entry before Emby
listItem = [itemId]
LibrarySync().removefromDB(listItem, True)
# Stop transcoding # Stop transcoding
if playMethod == "Transcode": if playMethod == "Transcode":
self.logMsg("Transcoding for %s terminated." % itemId, 1) self.logMsg("Transcoding for %s terminated." % itemid, 1)
deviceId = self.clientInfo.getMachineId() deviceId = self.clientInfo.getDeviceId()
url = "{server}/mediabrowser/Videos/ActiveEncodings?DeviceId=%s" % deviceId url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId
self.doUtils.downloadUrl(url, type="DELETE") doUtils.downloadUrl(url, type="DELETE")
self.played_information.clear() # Send the delete action to the server.
offerDelete = False
if type == "Episode" and utils.settings('deleteTV') == "true":
offerDelete = True
elif type == "Movie" and utils.settings('deleteMovies') == "true":
offerDelete = True
if utils.settings('offerDelete') != "true":
# Delete could be disabled, even if the subsetting is enabled.
offerDelete = False
if percentComplete >= markPlayedAt and offerDelete:
if utils.settings('skipConfirmDelete') != "true":
resp = xbmcgui.Dialog().yesno(
heading="Confirm delete",
line1="Delete file on Emby Server?")
if not resp:
self.logMsg("User skipped deletion.", 1)
continue
url = "{server}/emby/Items/%s?format=json" % itemid
self.logMsg("Deleting request: %s" % itemid)
doUtils.downloadUrl(url, type="DELETE")
self.played_info.clear()
def stopPlayback(self, data): def stopPlayback(self, data):
@ -429,12 +492,11 @@ class Player( xbmc.Player ):
currentPosition = data['currentPosition'] currentPosition = data['currentPosition']
positionTicks = int(currentPosition * 10000000) positionTicks = int(currentPosition * 10000000)
url = "{server}/mediabrowser/Sessions/Playing/Stopped" url = "{server}/emby/Sessions/Playing/Stopped"
postdata = { postdata = {
'ItemId': itemId, 'ItemId': itemId,
'MediaSourceId': itemId, 'MediaSourceId': itemId,
'PositionTicks': positionTicks 'PositionTicks': positionTicks
} }
self.doUtils.downloadUrl(url, postBody=postdata, type="POST") self.doUtils.downloadUrl(url, postBody=postdata, type="POST")

View file

@ -1,22 +1,21 @@
################################################################################################# # -*- coding: utf-8 -*-
# UserClient thread
################################################################################################# ##################################################################################################
import hashlib
import threading
import xbmc import xbmc
import xbmcgui import xbmcgui
import xbmcaddon import xbmcaddon
import xbmcvfs import xbmcvfs
import threading import artwork
import hashlib import utils
import json as json import clientinfo
import downloadutils
import KodiMonitor ##################################################################################################
import Utils as utils
from ClientInformation import ClientInformation
from DownloadUtils import DownloadUtils
from Player import Player
from API import API
class UserClient(threading.Thread): class UserClient(threading.Thread):
@ -24,16 +23,7 @@ class UserClient(threading.Thread):
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
_shared_state = {} _shared_state = {}
clientInfo = ClientInformation()
doUtils = DownloadUtils()
KodiMonitor = KodiMonitor.Kodi_Monitor()
addonName = clientInfo.getAddonName()
addon = xbmcaddon.Addon()
WINDOW = xbmcgui.Window(10000)
stopClient = False stopClient = False
logLevel = int(addon.getSetting('logLevel'))
auth = True auth = True
retry = 0 retry = 0
@ -44,25 +34,25 @@ class UserClient(threading.Thread):
HasAccess = True HasAccess = True
AdditionalUser = [] AdditionalUser = []
def __init__(self, *args): userSettings = None
def __init__(self):
self.__dict__ = self._shared_state self.__dict__ = self._shared_state
threading.Thread.__init__(self, *args) self.addon = xbmcaddon.Addon()
self.addonName = clientinfo.ClientInfo().getAddonName()
self.doUtils = downloadutils.DownloadUtils()
self.logLevel = int(utils.settings('logLevel'))
threading.Thread.__init__(self)
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__ className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), str(msg), int(lvl)) utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
def getUsername(self):
username = utils.settings('username')
if (username == ""):
self.logMsg("No username saved.", 2)
return ""
return username
def getAdditionalUsers(self): def getAdditionalUsers(self):
@ -71,11 +61,21 @@ class UserClient(threading.Thread):
if additionalUsers: if additionalUsers:
self.AdditionalUser = additionalUsers.split(',') self.AdditionalUser = additionalUsers.split(',')
def getUsername(self):
username = utils.settings('username')
if not username:
self.logMsg("No username saved.", 2)
return ""
return username
def getLogLevel(self): def getLogLevel(self):
try: try:
logLevel = int(utils.settings('logLevel')) logLevel = int(utils.settings('logLevel'))
except: except ValueError:
logLevel = 0 logLevel = 0
return logLevel return logLevel
@ -83,71 +83,84 @@ class UserClient(threading.Thread):
def getUserId(self): def getUserId(self):
username = self.getUsername() username = self.getUsername()
w_userId = self.WINDOW.getProperty('userId%s' % username) w_userId = utils.window('emby_userId%s' % username)
s_userId = utils.settings('userId%s' % username) s_userId = utils.settings('userId%s' % username)
# Verify the window property # Verify the window property
if (w_userId != ""): if w_userId:
self.logMsg("Returning userId from WINDOW for username: %s UserId: %s" % (username, w_userId), 2) if not s_userId:
# Save access token if it's missing from settings
utils.settings('userId%s' % username, value=w_userId)
self.logMsg(
"Returning userId from WINDOW for username: %s UserId: %s"
% (username, w_userId), 2)
return w_userId return w_userId
# Verify the settings # Verify the settings
elif (s_userId != ""): elif s_userId:
self.logMsg("Returning userId from SETTINGS for username: %s userId: %s" % (username, s_userId), 2) self.logMsg(
"Returning userId from SETTINGS for username: %s userId: %s"
% (username, s_userId), 2)
return s_userId return s_userId
# No userId found # No userId found
else: else:
self.logMsg("No userId saved for username: %s." % username) self.logMsg("No userId saved for username: %s." % username, 1)
return
def getServer(self, prefix=True): def getServer(self, prefix=True):
alternate = utils.settings('altip') == "true" alternate = utils.settings('altip') == "true"
# For https support
HTTPS = utils.settings('https')
host = utils.settings('ipaddress')
port = utils.settings('port')
# Alternate host
if alternate: if alternate:
HTTPS = utils.settings('secondhttps') # Alternate host
HTTPS = utils.settings('secondhttps') == "true"
host = utils.settings('secondipaddress') host = utils.settings('secondipaddress')
port = utils.settings('secondport') port = utils.settings('secondport')
else:
# Original host
HTTPS = utils.settings('https') == "true"
host = utils.settings('ipaddress')
port = utils.settings('port')
server = host + ":" + port server = host + ":" + port
if host == "": if not host:
self.logMsg("No server information saved.", 2) self.logMsg("No server information saved.", 2)
return "" return False
# If https is true # If https is true
if prefix and (HTTPS == "true"): if prefix and HTTPS:
server = "https://%s" % server server = "https://%s" % server
return server return server
# If https is false # If https is false
elif prefix and (HTTPS == "false"): elif prefix and not HTTPS:
server = "http://%s" % server server = "http://%s" % server
return server return server
# If only the host:port is required # If only the host:port is required
elif (prefix == False): elif not prefix:
return server return server
def getToken(self): def getToken(self):
username = self.getUsername() username = self.getUsername()
w_token = self.WINDOW.getProperty('accessToken%s' % username) w_token = utils.window('emby_accessToken%s' % username)
s_token = utils.settings('accessToken') s_token = utils.settings('accessToken')
# Verify the window property # Verify the window property
if (w_token != ""): if w_token:
self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s" % (username, w_token), 2) if not s_token:
# Save access token if it's missing from settings
utils.settings('accessToken', value=w_token)
self.logMsg(
"Returning accessToken from WINDOW for username: %s accessToken: %s"
% (username, w_token), 2)
return w_token return w_token
# Verify the settings # Verify the settings
elif (s_token != ""): elif s_token:
self.logMsg("Returning accessToken from SETTINGS for username: %s accessToken: %s" % (username, s_token), 2) self.logMsg(
self.WINDOW.setProperty('accessToken%s' % username, s_token) "Returning accessToken from SETTINGS for username: %s accessToken: %s"
% (username, s_token), 2)
utils.window('emby_accessToken%s' % username, value=s_token)
return s_token return s_token
else: else:
self.logMsg("No token found.") self.logMsg("No token found.", 1)
return "" return ""
def getSSLverify(self): def getSSLverify(self):
@ -174,71 +187,63 @@ class UserClient(threading.Thread):
def setUserPref(self): def setUserPref(self):
player = Player() doUtils = self.doUtils
server = self.getServer()
userId = self.getUserId()
url = "{server}/mediabrowser/Users/{UserId}?format=json"
result = self.doUtils.downloadUrl(url)
url = "{server}/emby/Users/{UserId}?format=json"
result = doUtils.downloadUrl(url)
self.userSettings = result
# Set user image for skin display # Set user image for skin display
self.WINDOW.setProperty("EmbyUserImage",API().getUserArtwork(result,"Primary")) if result.get('PrimaryImageTag'):
utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result, 'Primary'))
# Load the resume point from Emby and set as setting # Set resume point max
url = "{server}/mediabrowser/System/Configuration?format=json" url = "{server}/emby/System/Configuration?format=json"
result = self.doUtils.downloadUrl(url) result = doUtils.downloadUrl(url)
utils.settings('markPlayed', value=str(result['MaxResumePct'])) utils.settings('markPlayed', value=str(result['MaxResumePct']))
return True
def getPublicUsers(self): def getPublicUsers(self):
server = self.getServer() server = self.getServer()
# Get public Users # Get public Users
url = "%s/mediabrowser/Users/Public?format=json" % server url = "%s/emby/Users/Public?format=json" % server
result = self.doUtils.downloadUrl(url, authenticate=False) result = self.doUtils.downloadUrl(url, authenticate=False)
users = [] if result != "":
return result
if (result != ""):
users = result
else: else:
# Server connection failed # Server connection failed
return False return False
return users
def hasAccess(self): def hasAccess(self):
# hasAccess is verified in service.py
url = "{server}/mediabrowser/Users" url = "{server}/emby/Users?format=json"
result = self.doUtils.downloadUrl(url) result = self.doUtils.downloadUrl(url)
if result is False: if result == False:
# Access is restricted # Access is restricted, set in downloadutils.py via exception
self.logMsg("Access is restricted.") self.logMsg("Access is restricted.", 1)
self.HasAccess = False self.HasAccess = False
return
elif self.WINDOW.getProperty('Server_online') != "true":
# Server connection failed
return
if self.WINDOW.getProperty("Server_status") == "restricted": elif utils.window('emby_online') != "true":
self.logMsg("Access is granted.") # Server connection failed
pass
elif utils.window('emby_serverStatus') == "restricted":
self.logMsg("Access is granted.", 1)
self.HasAccess = True self.HasAccess = True
self.WINDOW.setProperty("Server_status", "") utils.window('emby_serverStatus', clear=True)
xbmcgui.Dialog().notification("Emby server", "Access is enabled.") xbmcgui.Dialog().notification("Emby server", "Access is enabled.")
return
def loadCurrUser(self, authenticated=False): def loadCurrUser(self, authenticated=False):
WINDOW = self.WINDOW
doUtils = self.doUtils doUtils = self.doUtils
username = self.getUsername() username = self.getUsername()
userId = self.getUserId()
# Only to be used if token exists # Only to be used if token exists
self.currUserId = self.getUserId() self.currUserId = userId
self.currServer = self.getServer() self.currServer = self.getServer()
self.currToken = self.getToken() self.currToken = self.getToken()
self.ssl = self.getSSLverify() self.ssl = self.getSSLverify()
@ -246,21 +251,21 @@ class UserClient(threading.Thread):
# Test the validity of current token # Test the validity of current token
if authenticated == False: if authenticated == False:
url = "%s/mediabrowser/Users/%s" % (self.currServer, self.currUserId) url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
WINDOW.setProperty("currUser", username) utils.window('emby_currUser', value=userId)
WINDOW.setProperty("accessToken%s" % username, self.currToken) utils.window('emby_accessToken%s' % userId, value=self.currToken)
result = doUtils.downloadUrl(url) result = doUtils.downloadUrl(url)
if result == 401: if result == 401:
# Token is no longer valid # Token is no longer valid
self.resetClient() self.resetClient()
return False return False
# Set to windows property # Set to windows property
WINDOW.setProperty("currUser", username) utils.window('emby_currUser', value=userId)
WINDOW.setProperty("accessToken%s" % username, self.currToken) utils.window('emby_accessToken%s' % userId, value=self.currToken)
WINDOW.setProperty("server%s" % username, self.currServer) utils.window('emby_server%s' % userId, value=self.currServer)
WINDOW.setProperty("server_%s" % username, self.getServer(prefix=False)) utils.window('emby_server_%s' % userId, value=self.getServer(prefix=False))
WINDOW.setProperty("userId%s" % username, self.currUserId)
# Set DownloadUtils values # Set DownloadUtils values
doUtils.setUsername(username) doUtils.setUsername(username)
@ -273,188 +278,194 @@ class UserClient(threading.Thread):
# Start DownloadUtils session # Start DownloadUtils session
doUtils.startSession() doUtils.startSession()
self.getAdditionalUsers() self.getAdditionalUsers()
self.currUser = username
# Set user preferences in settings # Set user preferences in settings
self.currUser = username
self.setUserPref() self.setUserPref()
def authenticate(self): def authenticate(self):
# Get /profile/addon_data
WINDOW = self.WINDOW
addon = self.addon
username = self.getUsername()
server = self.getServer()
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
username = self.getUsername()
server = self.getServer()
# If there's no settings.xml # If there's no settings.xml
if (hasSettings == 0): if not hasSettings:
self.logMsg("No settings.xml found.") self.logMsg("No settings.xml found.", 1)
self.auth = False self.auth = False
return return
# If no user information # If no user information
if (server == "") or (username == ""): elif not server or not username:
self.logMsg("Missing server information.") self.logMsg("Missing server information.", 1)
self.auth = False self.auth = False
return return
# If there's a token # If there's a token, load the user
if (self.getToken() != ""): elif self.getToken():
result = self.loadCurrUser() result = self.loadCurrUser()
if result == False: if result == False:
pass pass
else: else:
self.logMsg("Current user: %s" % self.currUser, 0) self.logMsg("Current user: %s" % self.currUser, 1)
self.logMsg("Current userId: %s" % self.currUserId, 0) self.logMsg("Current userId: %s" % self.currUserId, 1)
self.logMsg("Current accessToken: %s" % self.currToken, 0) self.logMsg("Current accessToken: %s" % self.currToken, 2)
return return
##### AUTHENTICATE USER #####
users = self.getPublicUsers() users = self.getPublicUsers()
password = "" password = ""
# Find user in list # Find user in list
for user in users: for user in users:
name = user[u'Name'] name = user['Name']
userHasPassword = False
if (unicode(username, 'utf-8') in name): if username.decode('utf-8') in name:
# Verify if user has a password
if (user.get("HasPassword") == True):
userHasPassword = True
# If user has password # If user has password
if (userHasPassword): if user['HasPassword'] == True:
password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) password = xbmcgui.Dialog().input(
heading="Enter password for user: %s" % username,
option=xbmcgui.ALPHANUM_HIDE_INPUT)
# If password dialog is cancelled # If password dialog is cancelled
if (password == ""): if not password:
self.logMsg("No password entered.", 0) self.logMsg("No password entered.", 0)
self.WINDOW.setProperty("Server_status", "Stop") utils.window('emby_serverStatus', value="Stop")
self.auth = False self.auth = False
return return
break break
else: else:
# Manual login, user is hidden # Manual login, user is hidden
password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) password = xbmcgui.Dialog().input(
heading="Enter password for user: %s" % username,
option=xbmcgui.ALPHANUM_HIDE_INPUT)
sha1 = hashlib.sha1(password) sha1 = hashlib.sha1(password)
sha1 = sha1.hexdigest() sha1 = sha1.hexdigest()
# Authenticate username and password # Authenticate username and password
url = "%s/mediabrowser/Users/AuthenticateByName?format=json" % server url = "%s/emby/Users/AuthenticateByName?format=json" % server
data = {'username': username, 'password': sha1} data = {'username': username, 'password': sha1}
self.logMsg(data, 2) self.logMsg(data, 2)
result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False) result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False)
accessToken = None
try: try:
self.logMsg("Auth_Reponse: %s" % result, 1) self.logMsg("Auth response: %s" % result, 1)
accessToken = result[u'AccessToken'] accessToken = result['AccessToken']
except:
pass
if (result != None and accessToken != None): except (KeyError, TypeError):
self.logMsg("Failed to retrieve the api key.", 1)
accessToken = None
if accessToken is not None:
self.currUser = username self.currUser = username
xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser) xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser)
userId = result[u'User'][u'Id'] userId = result['User']['Id']
utils.settings("accessToken", accessToken) utils.settings('accessToken', value=accessToken)
utils.settings("userId%s" % username, userId) utils.settings('userId%s' % username, value=userId)
self.logMsg("User Authenticated: %s" % accessToken) self.logMsg("User Authenticated: %s" % accessToken, 1)
self.loadCurrUser(authenticated=True) self.loadCurrUser(authenticated=True)
self.WINDOW.setProperty("Server_status", "") utils.window('emby_serverStatus', clear=True)
self.retry = 0 self.retry = 0
return
else: else:
self.logMsg("User authentication failed.") self.logMsg("User authentication failed.", 1)
utils.settings("accessToken", "") utils.settings('accessToken', value="")
utils.settings("userId%s" % username, "") utils.settings('userId%s' % username, value="")
xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.") xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.")
# Give two attempts at entering password # Give two attempts at entering password
self.retry += 1
if self.retry == 2: if self.retry == 2:
self.logMsg("Too many retries. You can retry by selecting the option in the addon settings.") self.logMsg(
self.WINDOW.setProperty("Server_status", "Stop") """Too many retries. You can retry by resetting
xbmcgui.Dialog().ok("Error connecting", "Failed to authenticate too many times. You can retry by selecting the option in the addon settings.") attempts in the addon settings.""", 1)
utils.window('emby_serverStatus', value="Stop")
xbmcgui.Dialog().ok(
heading="Error connecting",
line1="Failed to authenticate too many times.",
line2="You can retry by resetting attempts in the addon settings.")
self.retry += 1
self.auth = False self.auth = False
return
def resetClient(self): def resetClient(self):
username = self.getUsername()
self.logMsg("Reset UserClient authentication.", 1) self.logMsg("Reset UserClient authentication.", 1)
if (self.currToken != None): username = self.getUsername()
if self.currToken is not None:
# In case of 401, removed saved token # In case of 401, removed saved token
utils.settings("accessToken", "") utils.settings('accessToken', value="")
self.WINDOW.setProperty("accessToken%s" % username, "") utils.window('emby_accessToken%s' % username, clear=True)
self.currToken = None self.currToken = None
self.logMsg("User token has been removed.", 1) self.logMsg("User token has been removed.", 1)
self.auth = True self.auth = True
self.currUser = None self.currUser = None
return
def run(self): def run(self):
self.logMsg("|---- Starting UserClient ----|", 0) monitor = xbmc.Monitor()
self.logMsg("----===## Starting UserClient ##===----", 0)
while not self.KodiMonitor.abortRequested(): while not monitor.abortRequested():
# Verify the log level # Verify the log level
currLogLevel = self.getLogLevel() currLogLevel = self.getLogLevel()
if self.logLevel != currLogLevel: if self.logLevel != currLogLevel:
# Set new log level # Set new log level
self.logLevel = currLogLevel self.logLevel = currLogLevel
utils.window('emby_logLevel', value=str(currLogLevel))
self.logMsg("New Log Level: %s" % currLogLevel, 0) self.logMsg("New Log Level: %s" % currLogLevel, 0)
self.WINDOW.setProperty('getLogLevel', str(currLogLevel))
if (self.WINDOW.getProperty("Server_status") != ""):
status = self.WINDOW.getProperty("Server_status")
status = utils.window('emby_serverStatus')
if status:
# Verify the connection status to server
if status == "restricted": if status == "restricted":
# Parental control is restricting access # Parental control is restricting access
self.HasAccess = False self.HasAccess = False
elif status == "401": elif status == "401":
self.WINDOW.setProperty("Server_status", "Auth") # Unauthorized access, revoke token
# Revoked token utils.window('emby_serverStatus', value="Auth")
self.resetClient() self.resetClient()
if self.auth and (self.currUser == None): if self.auth and (self.currUser is None):
status = self.WINDOW.getProperty("Server_status") # Try to authenticate user
status = utils.window('emby_serverStatus')
if (status == "") or (status == "Auth"): if not status or status == "Auth":
# Set auth flag because we no longer need
# to authenticate the user
self.auth = False self.auth = False
self.authenticate() self.authenticate()
if (self.auth == False) and (self.currUser == None):
# Only if there's information found to login if not self.auth and (self.currUser is None):
# If authenticate failed.
server = self.getServer() server = self.getServer()
username = self.getUsername() username = self.getUsername()
status = self.WINDOW.getProperty("Server_status") status = utils.window('emby_serverStatus')
# If user didn't enter a password when prompted # The status Stop is for when user cancelled password dialog.
if status == "Stop": if server and username and status != "Stop":
pass # Only if there's information found to login
self.logMsg("Server found: %s" % server, 2)
elif (server != "") and (username != ""): self.logMsg("Username found: %s" % username, 2)
self.logMsg("Server found: %s" % server)
self.logMsg("Username found: %s" % username)
self.auth = True self.auth = True
# If stopping the client didn't work
if self.stopClient == True: if self.stopClient == True:
# If stopping the client didn't work
break break
if self.KodiMonitor.waitForAbort(1): if monitor.waitForAbort(1):
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
break break
self.doUtils.stopSession() self.doUtils.stopSession()
self.logMsg("|---- UserClient Stopped ----|", 0) self.logMsg("##===---- UserClient Stopped ----===##", 0)
def stopClient(self): def stopClient(self):
# As last resort # When emby for kodi terminates
self.stopClient = True self.stopClient = True

View file

@ -1,59 +1,76 @@
################################################################################################# # -*- coding: utf-8 -*-
# utils
################################################################################################# #################################################################################################
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
import json
import os
import cProfile import cProfile
import pstats
import time
import inspect import inspect
import pstats
import sqlite3 import sqlite3
import string import time
import unicodedata import unicodedata
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from API import API import xbmc
from PlayUtils import PlayUtils import xbmcaddon
from DownloadUtils import DownloadUtils import xbmcgui
import xbmcvfs
downloadUtils = DownloadUtils() #################################################################################################
addon = xbmcaddon.Addon()
language = addon.getLocalizedString
def logMsg(title, msg, level=1): def logMsg(title, msg, level=1):
WINDOW = xbmcgui.Window(10000)
# Get the logLevel set in UserClient # Get the logLevel set in UserClient
logLevel = int(WINDOW.getProperty('getLogLevel'))
if(logLevel >= level):
if(logLevel == 2): # inspect.stack() is expensive
try: try:
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg)) logLevel = int(window('emby_logLevel'))
except ValueError:
logLevel = 0
if logLevel >= level:
if logLevel == 2: # inspect.stack() is expensive
try:
xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg))
except UnicodeEncodeError: except UnicodeEncodeError:
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8')))
else: else:
try: try:
xbmc.log(title + " -> " + str(msg)) xbmc.log("%s -> %s" % (title, msg))
except UnicodeEncodeError: except UnicodeEncodeError:
xbmc.log(title + " -> " + str(msg.encode('utf-8'))) xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
def convertEncoding(data): def window(property, value=None, clear=False, windowid=10000):
#nasty hack to make sure we have a unicode string # Get or set window property
try: WINDOW = xbmcgui.Window(windowid)
return data.decode('utf-8')
except:
return data
def KodiSQL(type="video"): if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value)
else:
return WINDOW.getProperty(property)
if type == "music": def settings(setting, value=None):
# Get or add addon setting
addon = xbmcaddon.Addon(id='plugin.video.emby')
if value is not None:
addon.setSetting(setting, value)
else:
return addon.getSetting(setting)
def language(stringid):
# Central string retrieval
addon = xbmcaddon.Addon(id='plugin.video.emby')
string = addon.getLocalizedString(stringid)
return string
def kodiSQL(type="video"):
if type == "emby":
dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
elif type == "music":
dbPath = getKodiMusicDBPath() dbPath = getKodiMusicDBPath()
elif type == "texture": elif type == "texture":
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
@ -95,218 +112,139 @@ def getKodiMusicDBPath():
% dbVersion.get(kodibuild, "")).decode('utf-8') % dbVersion.get(kodibuild, "")).decode('utf-8')
return dbPath return dbPath
def prettifyXml(elem): def reset():
rough_string = etree.tostring(elem, "utf-8")
reparsed = minidom.parseString(rough_string) dialog = xbmcgui.Dialog()
return reparsed.toprettyxml(indent="\t")
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
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
dirs, files = xbmcvfs.listdir(path)
for file in files:
if file.startswith('Emby'):
xbmcvfs.delete("%s%s" % (path, file))
# Clean up the 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.startswith('Emby'):
shutil.rmtree("%s%s" % (path, dir))
for file in files:
if file.startswith('emby'):
xbmcvfs.delete("%s%s" % (path, file))
# Wipe the kodi databases
logMsg("EMBY", "Resetting the Kodi video database.")
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('disableMusic') != "true":
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.")
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)
connection.commit()
cursor.close()
# reset the install run flag
settings('SyncInstallRunDone', value="false")
# Remove emby info
resp = dialog.yesno("Warning", "Reset all Emby Addon settings?")
if resp == 1:
# 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(): def startProfiling():
pr = cProfile.Profile() pr = cProfile.Profile()
pr.enable() pr.enable()
return pr return pr
def stopProfiling(pr, profileName): def stopProfiling(pr, profileName):
pr.disable() pr.disable()
ps = pstats.Stats(pr) ps = pstats.Stats(pr)
addondir = xbmc.translatePath(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile')) profiles = xbmc.translatePath("%sprofiles/"
% xbmcaddon.Addon().getAddonInfo('profile')).decode('utf-8')
fileTimeStamp = time.strftime("%Y-%m-%d %H-%M-%S") if not xbmcvfs.exists(profiles):
tabFileNamepath = os.path.join(addondir, "profiles") # Create the profiles folder
tabFileName = os.path.join(addondir, "profiles" , profileName + "_profile_(" + fileTimeStamp + ").tab") xbmcvfs.mkdir(profiles)
if not xbmcvfs.exists(tabFileNamepath): timestamp = time.strftime("%Y-%m-%d %H-%M-%S")
xbmcvfs.mkdir(tabFileNamepath) profile = "%s%s_profile_(%s).tab" % (profiles, profileName, timestamp)
f = open(tabFileName, 'wb') f = open(profile, 'wb')
f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n") f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n")
for (key, value) in ps.stats.items(): for (key, value) in ps.stats.items():
(filename, count, func_name) = key (filename, count, func_name) = key
(ccalls, ncalls, total_time, cumulative_time, callers) = value (ccalls, ncalls, total_time, cumulative_time, callers) = value
try: try:
f.write(str(ncalls) + "\t" + "{:10.4f}".format(total_time) + "\t" + "{:10.4f}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") 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: except ValueError:
f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") 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() f.close()
def indent(elem, level=0):
# Prettify xml trees
i = "\n" + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
indent(elem, level+1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def createSources():
# To make Master lock compatible
path = xbmc.translatePath("special://profile/").decode("utf-8")
xmlpath = "%ssources.xml" % path
if xbmcvfs.exists(xmlpath):
# Modify the existing file
try:
xmlparse = etree.parse(xmlpath)
except:
root = etree.Element('sources')
else:
root = xmlparse.getroot()
video = root.find('video')
if video is None:
video = etree.SubElement(root, 'video')
else:
# We need to create the file
root = etree.Element('sources')
video = etree.SubElement(root, 'video')
# Add elements
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
# First dummy source
source_one = etree.SubElement(video, 'source')
etree.SubElement(source_one, 'name').text = "Emby"
etree.SubElement(source_one, 'path', attrib={'pathversion': "1"}).text = (
"smb://embydummy/dummypath1/"
)
etree.SubElement(source_one, 'allowsharing').text = "true"
# Second dummy source
source_two = etree.SubElement(video, 'source')
etree.SubElement(source_two, 'name').text = "Emby"
etree.SubElement(source_two, 'path', attrib={'pathversion': "1"}).text = (
"smb://embydummy/dummypath2/"
)
etree.SubElement(source_two, 'allowsharing').text = "true"
try:
indent(root)
except:pass
etree.ElementTree(root).write(xmlpath)
def pathsubstitution(add=True):
path = xbmc.translatePath('special://userdata').decode('utf-8')
xmlpath = "%sadvancedsettings.xml" % path
xmlpathexists = xbmcvfs.exists(xmlpath)
# original address
originalServer = settings('ipaddress')
originalPort = settings('port')
originalHttp = settings('https') == "true"
if originalHttp:
originalHttp = "https"
else:
originalHttp = "http"
# Process add or deletion
if add:
# second address
secondServer = settings('secondipaddress')
secondPort = settings('secondport')
secondHttp = settings('secondhttps') == "true"
if secondHttp:
secondHttp = "https"
else:
secondHttp = "http"
logMsg("EMBY", "Original address: %s://%s:%s, alternate is: %s://%s:%s" % (originalHttp, originalServer, originalPort, secondHttp, secondServer, secondPort), 1)
if xmlpathexists:
# we need to modify the file.
try:
xmlparse = etree.parse(xmlpath)
except: # Document is blank
root = etree.Element('advancedsettings')
else:
root = xmlparse.getroot()
pathsubs = root.find('pathsubstitution')
if pathsubs is None:
pathsubs = etree.SubElement(root, 'pathsubstitution')
else:
# we need to create the file.
root = etree.Element('advancedsettings')
pathsubs = etree.SubElement(root, 'pathsubstitution')
substitute = etree.SubElement(pathsubs, 'substitute')
# From original address
etree.SubElement(substitute, 'from').text = "%s://%s:%s" % (originalHttp, originalServer, originalPort)
# To secondary address
etree.SubElement(substitute, 'to').text = "%s://%s:%s" % (secondHttp, secondServer, secondPort)
etree.ElementTree(root).write(xmlpath)
settings('pathsub', "true")
else: # delete the path substitution, we don't need it anymore.
logMsg("EMBY", "Alternate address is disabled, removing path substitution for: %s://%s:%s" % (originalHttp, originalServer, originalPort), 1)
xmlparse = etree.parse(xmlpath)
root = xmlparse.getroot()
iterator = root.getiterator("pathsubstitution")
for substitutes in iterator:
for substitute in substitutes:
frominsert = substitute.find(".//from").text == "%s://%s:%s" % (originalHttp, originalServer, originalPort)
if frominsert:
# Found a match, in case there's more than one substitution.
substitutes.remove(substitute)
etree.ElementTree(root).write(xmlpath)
settings('pathsub', "false")
def settings(setting, value = None):
# Get or add addon setting
addon = xbmcaddon.Addon()
if value:
addon.setSetting(setting, value)
else:
return addon.getSetting(setting)
def window(property, value = None, clear = False):
# Get or set window property
WINDOW = xbmcgui.Window(10000)
if clear:
WINDOW.clearProperty(property)
elif value:
WINDOW.setProperty(property, value)
else:
return WINDOW.getProperty(property)
def normalize_string(text):
# For theme media, do not modify unless
# modified in TV Tunes
text = text.replace(":", "")
text = text.replace("/", "-")
text = text.replace("\\", "-")
text = text.replace("<", "")
text = text.replace(">", "")
text = text.replace("*", "")
text = text.replace("?", "")
text = text.replace('|', "")
text = text.strip()
# Remove dots from the last character as windows can not have directories
# with dots at the end
text = text.rstrip('.')
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
return text
def normalize_nodes(text): def normalize_nodes(text):
# For video nodes # For video nodes
text = text.replace(":", "") text = text.replace(":", "")
@ -327,99 +265,223 @@ def normalize_nodes(text):
return text return text
def reloadProfile(): def normalize_string(text):
# Useful to reload the add-on without restarting Kodi. # For theme media, do not modify unless
profile = xbmc.getInfoLabel('System.ProfileName') # modified in TV Tunes
xbmc.executebuiltin("LoadProfile(%s)" % profile) 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()
def reset(): video = root.find('video')
if video is None:
video = etree.SubElement(root, 'video')
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
WINDOW = xbmcgui.Window( 10000 ) # Add elements
return_value = xbmcgui.Dialog().yesno("Warning", "Are you sure you want to reset your local Kodi database?") for i in range(1, 3):
if return_value == 0: for source in root.findall('.//path'):
return if source.text == "smb://embydummy/dummypath%s/" % i:
# Already there, skip
break
else:
source = etree.SubElement(video, 'source')
etree.SubElement(source, 'name').text = "Emby"
etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = (
# Because the settings dialog could be open "smb://embydummy/dummypath%s/" % i
# it seems to override settings so we need to close it before we reset settings. )
xbmc.executebuiltin("Dialog.Close(all,true)") etree.SubElement(source, 'allowsharing').text = "true"
# Prettify and write to file
try:
indent(root)
except: pass
etree.ElementTree(root).write(xmlpath)
#cleanup video nodes def passwordsXML():
import shutil
path = "special://profile/library/video/"
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
if dir.startswith("Emby "):
shutil.rmtree(xbmc.translatePath("special://profile/library/video/" + dir))
for file in allFiles:
if file.startswith("emby"):
xbmcvfs.delete(path + file)
settings('SyncInstallRunDone', "false") # To add network credentials
path = xbmc.translatePath("special://userdata/").decode('utf-8')
xmlpath = "%spasswords.xml" % path
# Ask if user information should be deleted too. try:
return_user = xbmcgui.Dialog().yesno("Warning", "Reset all Emby Addon settings?") xmlparse = etree.parse(xmlpath)
if return_user == 1: except: # Document is blank or missing
WINDOW.setProperty('deletesettings', "true") root = etree.Element('passwords')
addon = xbmcaddon.Addon() else:
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') root = xmlparse.getroot()
dataPath = "%ssettings.xml" % addondir
xbmcvfs.delete(dataPath)
logMsg("EMBY", "Deleting: settings.xml", 1)
# first stop any db sync
WINDOW.setProperty("SyncDatabaseShouldStop", "true")
count = 0
while(WINDOW.getProperty("SyncDatabaseRunning") == "true"):
xbmc.log("Sync Running, will wait : " + str(count))
count += 1
if(count > 10):
dialog = xbmcgui.Dialog()
dialog.ok('Warning', 'Could not stop DB sync, you should try again.')
return
xbmc.sleep(1000)
# delete video db table data
print "Doing Video DB Reset"
connection = KodiSQL("video")
cursor = connection.cursor( )
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor.fetchall()
for row in rows:
tableName = row[0]
if(tableName != "version"):
cursor.execute("DELETE FROM " + tableName)
cursor.execute("DROP TABLE IF EXISTS emby")
connection.commit()
cursor.close()
if settings('enableMusicSync') == "true":
# delete video db table data
print "Doing Music DB Reset"
connection = KodiSQL("music")
cursor = connection.cursor( )
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor.fetchall()
for row in rows:
tableName = row[0]
if(tableName != "version"):
cursor.execute("DELETE FROM " + tableName)
cursor.execute("DROP TABLE IF EXISTS emby")
connection.commit()
cursor.close()
# reset the install run flag
#settings('SyncInstallRunDone', "false")
#WINDOW.setProperty("SyncInstallRunDone", "false")
dialog = xbmcgui.Dialog() dialog = xbmcgui.Dialog()
# Reload would work instead of restart since the add-on is a service. credentials = settings('networkCreds')
#dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.') if credentials:
#WINDOW.clearProperty("SyncDatabaseShouldStop") # Present user with options
#reloadProfile() option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"])
dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.')
xbmc.executebuiltin("RestartApp") 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(
heading="Emby for Kodi",
message="%s removed from passwords.xml!" % credentials,
icon="special://home/addons/plugin.video.emby/icon.png",
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= (
"Input the server name or IP address as indicated in your emby library paths. "
'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".'))
server = dialog.input("Enter the server name or IP address", settings('ipaddress'))
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(
heading="Emby for Kodi",
message="%s added to passwords.xml!" % server,
icon="special://home/addons/plugin.video.emby/icon.png",
time=1000,
sound=False)
def playlistXSP(mediatype, tagname, viewtype="", delete=False):
# Tagname is in unicode - actions: add or delete
tagname = tagname.encode('utf-8')
cleantagname = normalize_nodes(tagname)
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sEmby %s - %s.xsp" % (path, cleantagname, mediatype)
else:
plname = tagname
xsppath = "%sEmby %s.xsp" % (path, cleantagname)
# Create the playlist directory
if not xbmcvfs.exists(path):
xbmcvfs.mkdirs(path)
# Only add the playlist if it doesn't already exists
if xbmcvfs.exists(xsppath):
if delete:
xbmcvfs.delete(xsppath)
logMsg("EMBY", "Successfully removed playlist: %s." % tagname, 1)
return
# Using write process since there's no guarantee the xml declaration works with etree
itemtypes = {
'homevideos': "movies"
}
f = open(xsppath, 'w')
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>'
% (itemtypes.get(mediatype, mediatype), plname, tagname))
f.close()
logMsg("EMBY", "Successfully added playlist: %s" % tagname)

View file

@ -1,466 +1,344 @@
################################################################################################# # -*- coding: utf-8 -*-
# VideoNodes - utils to create video nodes listings in kodi for the emby addon
################################################################################################# #################################################################################################
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
import json
import os
import shutil import shutil
#import common elementree because cElementree has issues with kodi
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
import Utils as utils import xbmc
import xbmcaddon
import xbmcvfs
from ReadEmbyDB import ReadEmbyDB import clientinfo
WINDOW = xbmcgui.Window(10000) import utils
addonSettings = xbmcaddon.Addon() #################################################################################################
language = addonSettings.getLocalizedString
class VideoNodes():
def buildVideoNodeForView(self, tagname, type, windowPropId): class VideoNodes(object):
#this method will build a video node for a particular Emby view (= tag in kodi)
#we set some window props here to for easy future reference and to be used in skins (for easy access only)
tagname_normalized = utils.normalize_nodes(tagname.encode('utf-8'))
libraryPath = xbmc.translatePath("special://profile/library/video/Emby - %s/" %tagname_normalized)
kodiVersion = 14
if xbmc.getInfoLabel("System.BuildVersion").startswith("15") or xbmc.getInfoLabel("System.BuildVersion").startswith("16"):
kodiVersion = 15
#create tag node - index def __init__(self):
xbmcvfs.mkdir(libraryPath)
nodefile = os.path.join(libraryPath, "index.xml")
root = etree.Element("node", {"order":"0"})
etree.SubElement(root, "label").text = tagname
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
path = "library://video/Emby - %s/" %tagname_normalized
WINDOW.setProperty("Emby.nodes.%s.index" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - all items clientInfo = clientinfo.ClientInfo()
nodefile = os.path.join(libraryPath, tagname_normalized + "_all.xml") self.addonName = clientInfo.getAddonName()
root = etree.Element("node", {"order":"1", "type":"filter"})
etree.SubElement(root, "label").text = tagname
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = type
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
WINDOW.setProperty("Emby.nodes.%s.title" %str(windowPropId),tagname)
path = "library://video/Emby - %s/%s_all.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.content" %str(windowPropId),path)
WINDOW.setProperty("Emby.nodes.%s.type" %str(windowPropId),type)
etree.SubElement(Rule, "value").text = tagname
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - recent items self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent.xml")
root = etree.Element("node", {"order":"2", "type":"filter"}) def logMsg(self, msg, lvl=1):
if type == "tvshows":
label = language(30170) className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
def commonRoot(self, order, label, tagname, roottype=1):
if roottype == 0:
# Index
root = etree.Element('node', attrib={'order': "%s" % order})
elif roottype == 1:
# Filter
root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"})
etree.SubElement(root, 'match').text = "all"
# Add tag rule
rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"})
etree.SubElement(rule, 'value').text = tagname
else: else:
label = language(30174) # Folder
etree.SubElement(root, "label").text = label root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"})
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = type
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
etree.SubElement(root, "order", {"direction":"descending"}).text = "dateadded"
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
#exclude watched items --> currently hardcoded --> TODO: add a setting for this ?
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule2, "value").text = "0"
WINDOW.setProperty("Emby.nodes.%s.recent.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_recent.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recent.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.recent.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - inprogress items etree.SubElement(root, 'label').text = label
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress.xml") etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png"
root = etree.Element("node", {"order":"3", "type":"filter"})
if type == "tvshows": return root
label = language(30171)
def viewNode(self, indexnumber, tagname, mediatype, viewtype, delete=False):
kodiversion = self.kodiversion
if mediatype == "homevideos":
# Treat homevideos as movies
mediatype = "movies"
tagname = tagname.encode('utf-8')
cleantagname = utils.normalize_nodes(tagname)
if viewtype == "mixed":
dirname = "%s - %s" % (cleantagname, mediatype)
else: else:
label = language(30177) dirname = cleantagname
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all" path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
etree.SubElement(root, "content").text = type nodepath = xbmc.translatePath(
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png" "special://profile/library/video/Emby - %s/" % dirname).decode('utf-8')
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname # Verify the video directory
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ? if not xbmcvfs.exists(path):
etree.SubElement(root, "limit").text = "25" shutil.copytree(
Rule2 = etree.SubElement(root, "rule", {"field":"inprogress","operator":"true"}) src=xbmc.translatePath("special://xbmc/system/library/video/").decode('utf-8'),
WINDOW.setProperty("Emby.nodes.%s.inprogress.title" %str(windowPropId),label) dst=xbmc.translatePath("special://profile/library/video/").decode('utf-8'))
path = "library://video/Emby - %s/%s_progress.xml"%(tagname_normalized,tagname_normalized) xbmcvfs.exists(path)
WINDOW.setProperty("Emby.nodes.%s.inprogress.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.inprogress.content" %str(windowPropId),path) # Create the node directory
if not xbmcvfs.exists(nodepath):
# We need to copy over the default items
xbmcvfs.mkdirs(nodepath)
else:
if delete:
dirs, files = xbmcvfs.listdir(nodepath)
for file in files:
xbmcvfs.delete(nodepath + file)
self.logMsg("Sucessfully removed videonode: %s." % tagname, 1)
return
# Create index entry
nodeXML = "%sindex.xml" % nodepath
# Set windows property
path = "library://video/Emby - %s/" % dirname
for i in range(1, indexnumber):
# Verify to make sure we don't create duplicates
if utils.window('Emby.nodes.%s.index' % i) == path:
return
utils.window('Emby.nodes.%s.index' % indexnumber, value=path)
# Root
root = self.commonRoot(order=0, label=dirname, tagname=tagname, roottype=0)
try: try:
etree.ElementTree(root).write(nodefile, xml_declaration=True) utils.indent(root)
except: except: pass
etree.ElementTree(root).write(nodefile) etree.ElementTree(root).write(nodeXML)
#some movies-only nodes
if type == "movies":
#unwatched movies nodetypes = {
nodefile = os.path.join(libraryPath, tagname_normalized + "_unwatched.xml")
root = etree.Element("node", {"order":"4", "type":"filter"})
label = language(30189)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "movies"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
Rule = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule, "value").text = "0"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule2, "value").text = "0"
WINDOW.setProperty("Emby.nodes.%s.unwatched.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_unwatched.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.unwatched.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.unwatched.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#sets '1': "all",
nodefile = os.path.join(libraryPath, tagname_normalized + "_sets.xml") '2': "recent",
root = etree.Element("node", {"order":"9", "type":"filter"}) '3': "recentepisodes",
label = xbmc.getLocalizedString(20434) '4': "inprogress",
etree.SubElement(root, "label").text = label '5': "inprogressepisodes",
etree.SubElement(root, "match").text = "all" '6': "unwatched",
etree.SubElement(root, "group").text = "sets" '7': "nextupepisodes",
etree.SubElement(root, "content").text = type '8': "sets",
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png" '9': "genres",
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle" '10': "random",
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"}) '11': "recommended"
etree.SubElement(Rule, "value").text = tagname }
WINDOW.setProperty("Emby.nodes.%s.sets.title" %str(windowPropId),label) mediatypes = {
path = "library://video/Emby - %s/%s_sets.xml"%(tagname_normalized,tagname_normalized) # label according to nodetype per mediatype
WINDOW.setProperty("Emby.nodes.%s.sets.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path) 'movies': {
WINDOW.setProperty("Emby.nodes.%s.sets.content" %str(windowPropId),path) '1': tagname,
try: '2': 30174,
etree.ElementTree(root).write(nodefile, xml_declaration=True) '4': 30177,
except: '6': 30189,
etree.ElementTree(root).write(nodefile) '8': 20434,
'9': 135,
'10': 30229,
'11': 30230},
#create tag node - genres 'tvshows': {
nodefile = os.path.join(libraryPath, tagname_normalized + "_genres.xml") '1': tagname,
root = etree.Element("node", {"order":"9", "type":"filter"}) '2': 30170,
label = xbmc.getLocalizedString(135) '3': 30175,
etree.SubElement(root, "label").text = label '4': 30171,
etree.SubElement(root, "match").text = "all" '5': 30178,
etree.SubElement(root, "group").text = "genres" '7': 30179,
etree.SubElement(root, "content").text = type '9': 135,
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png" '10': 30229,
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle" '11': 30230},
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"}) }
etree.SubElement(Rule, "value").text = tagname
WINDOW.setProperty("Emby.nodes.%s.genres.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_genres.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.genres.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.genres.content" %str(windowPropId),path)
try: nodes = mediatypes[mediatype]
etree.ElementTree(root).write(nodefile, xml_declaration=True) for node in nodes:
except:
etree.ElementTree(root).write(nodefile)
#create tag node - random items nodetype = nodetypes[node]
nodefile = os.path.join(libraryPath, tagname_normalized + "_random.xml") nodeXML = "%s%s_%s.xml" % (nodepath, cleantagname, nodetype)
root = etree.Element("node", {"order":"10", "type":"filter"}) # Get label
label = language(30229) stringid = nodes[node]
etree.SubElement(root, "label").text = label if node != '1':
etree.SubElement(root, "match").text = "all" label = utils.language(stringid)
etree.SubElement(root, "content").text = type if not label:
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png" label = xbmc.getLocalizedString(stringid)
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"}) else:
etree.SubElement(Rule, "value").text = tagname label = stringid
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "random"
WINDOW.setProperty("Emby.nodes.%s.random.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_random.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.random.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.random.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - recommended items # Set window properties
nodefile = os.path.join(libraryPath, tagname_normalized + "_recommended.xml") if nodetype == "nextupepisodes":
root = etree.Element("node", {"order":"10", "type":"filter"}) # Custom query
label = language(30230)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = type
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule2, "value").text = "0"
Rule3 = etree.SubElement(root, "rule", {"field":"rating","operator":"greaterthan"})
etree.SubElement(Rule3, "value").text = "7"
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
etree.SubElement(root, "order", {"direction":"descending"}).text = "rating"
WINDOW.setProperty("Emby.nodes.%s.random.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_recommended.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recommended.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.recommended.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#### TAGS ONLY FOR TV SHOWS COLLECTIONS ####
if type == "tvshows":
#as from kodi isengard you can use tags for episodes to filter
#for below isengard we still use the plugin's entrypoint to build a listing
if kodiVersion == 15:
#create tag node - recent episodes
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent_episodes.xml")
root = etree.Element("node", {"order":"3", "type":"filter"})
label = language(30175)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"descending"}).text = "dateadded"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
#exclude watched items --> currently hardcoded --> TODO: add a setting for this ?
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule2, "value").text = "0"
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_recent_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - inprogress episodes
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress_episodes.xml")
root = etree.Element("node", {"order":"4", "type":"filter"})
label = language(30178)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
Rule2 = etree.SubElement(root, "rule", {"field":"inprogress","operator":"true"})
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_progress_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
if kodiVersion == 14:
#create tag node - recent episodes
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent_episodes.xml")
root = etree.Element("node", {"order":"4", "type":"folder"})
label = language(30175)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" %tagname
etree.SubElement(root, "path").text = path
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_recent_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - inprogress items
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress_episodes.xml")
root = etree.Element("node", {"order":"5", "type":"folder"})
label = language(30178)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25" %tagname
etree.SubElement(root, "path").text = path
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_progress_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - nextup items
#for nextup we always use the dynamic content approach with the plugin's entrypoint because it involves a custom query
nodefile = os.path.join(libraryPath, tagname_normalized + "_nextup_episodes.xml")
root = etree.Element("node", {"order":"6", "type":"folder"})
label = language(30179)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "episodes"
path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname
etree.SubElement(root, "path").text = path elif kodiversion == 14 and nodetype == "recentepisodes":
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png" # Custom query
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.title" %str(windowPropId),label) path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname
path = "library://video/Emby - %s/%s_nextup_episodes.xml"%(tagname_normalized,tagname_normalized) elif kodiversion == 14 and nodetype == "inprogressepisodes":
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path) # Custom query
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.content" %str(windowPropId),path) path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname
try: else:
etree.ElementTree(root).write(nodefile, xml_declaration=True) path = "library://video/Emby - %s/%s_%s.xml" % (dirname, cleantagname, nodetype)
except: windowpath = "ActivateWindow(Video, %s, return)" % path
etree.ElementTree(root).write(nodefile)
def buildVideoNodesListing(self): if nodetype == "all":
if viewtype == "mixed":
templabel = dirname
else:
templabel = label
embynode = "Emby.nodes.%s" % indexnumber
utils.window('%s.title' % embynode, value=templabel)
utils.window('%s.path' % embynode, value=windowpath)
utils.window('%s.content' % embynode, value=path)
utils.window('%s.type' % embynode, value=mediatype)
else:
embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
utils.window('%s.title' % embynode, value=label)
utils.window('%s.path' % embynode, value=windowpath)
utils.window('%s.content' % embynode, value=path)
if xbmcvfs.exists(nodeXML):
# Don't recreate xml if already exists
continue
# Create the root
if nodetype == "nextupepisodes" or (kodiversion == 14 and
nodetype in ('recentepisodes', 'inprogressepisodes')):
# Folder type with plugin path
root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2)
etree.SubElement(root, 'path').text = path
etree.SubElement(root, 'content').text = "episodes"
else:
root = self.commonRoot(order=node, label=label, tagname=tagname)
if nodetype in ('recentepisodes', 'inprogressepisodes'):
etree.SubElement(root, 'content').text = "episodes"
else:
etree.SubElement(root, 'content').text = mediatype
limit = "25"
# Elements per nodetype
if nodetype == "all":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
elif nodetype == "recent":
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogress":
etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
etree.SubElement(root, 'limit').text = limit
elif nodetype == "genres":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root, 'group').text = "genres"
elif nodetype == "unwatched":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "sets":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root, 'group').text = "sets"
elif nodetype == "random":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random"
etree.SubElement(root, 'limit').text = limit
elif nodetype == "recommended":
etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating"
etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
rule2 = etree.SubElement(root, 'rule',
attrib={'field': "rating", 'operator': "greaterthan"})
etree.SubElement(rule2, 'value').text = "7"
elif nodetype == "recentepisodes":
# Kodi Isengard, Jarvis
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogressepisodes":
# Kodi Isengard, Jarvis
etree.SubElement(root, 'limit').text = "25"
rule = etree.SubElement(root, 'rule',
attrib={'field': "inprogress", 'operator':"true"})
try: try:
utils.indent(root)
except: pass
etree.ElementTree(root).write(nodeXML)
# the library path doesn't exist on all systems def singleNode(self, indexnumber, tagname, mediatype, itemtype):
if not xbmcvfs.exists("special://profile/library/"):
xbmcvfs.mkdir("special://profile/library")
if not xbmcvfs.exists("special://profile/library/video/"):
#we need to copy over the default items
shutil.copytree(xbmc.translatePath("special://xbmc/system/library/video"), xbmc.translatePath("special://profile/library/video"))
#always cleanup existing Emby video nodes first because we don't want old stuff to stay in there tagname = tagname.encode('utf-8')
path = "special://profile/library/video/" cleantagname = utils.normalize_nodes(tagname)
if xbmcvfs.exists(path): nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
allDirs, allFiles = xbmcvfs.listdir(path) nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
for dir in allDirs: path = "library://video/emby_%s.xml" % (cleantagname)
if dir.startswith("Emby "): windowpath = "ActivateWindow(Video, %s, return)" % path
shutil.rmtree(xbmc.translatePath("special://profile/library/video/" + dir))
for file in allFiles:
if file.startswith("emby"):
xbmcvfs.delete(path + file)
#we build up a listing and set window props for all nodes we created # Create the video node directory
#the window props will be used by the main entry point to quickly build up the listing and can be used in skins (like titan) too for quick reference if not xbmcvfs.exists(nodepath):
#comment marcelveldt: please leave the window props as-is because I will be referencing them in titan skin... # We need to copy over the default items
totalNodesCount = 0 shutil.copytree(
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
xbmcvfs.exists(path)
#build the listing for all views labels = {
views_movies = ReadEmbyDB().getCollections("movies")
if views_movies:
for view in views_movies:
title = view.get('title')
content = view.get('content')
if content == "mixed":
title = "%s - Movies" % title
self.buildVideoNodeForView(title, "movies", totalNodesCount)
totalNodesCount +=1
views_shows = ReadEmbyDB().getCollections("tvshows") 'Favorite movies': 30180,
if views_shows: 'Favorite tvshows': 30181,
for view in views_shows: 'channels': 30173
title = view.get('title') }
content = view.get('content') label = utils.language(labels[tagname])
if content == "mixed": embynode = "Emby.nodes.%s" % indexnumber
title = "%s - TV Shows" % title utils.window('%s.title' % embynode, value=label)
self.buildVideoNodeForView(title, "tvshows", totalNodesCount) utils.window('%s.path' % embynode, value=windowpath)
totalNodesCount +=1 utils.window('%s.content' % embynode, value=path)
utils.window('%s.type' % embynode, value=itemtype)
if xbmcvfs.exists(nodeXML):
# Don't recreate xml if already exists
return
if itemtype == "channels":
root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels"
else:
root = self.commonRoot(order=1, label=label, tagname=tagname)
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root, 'content').text = mediatype
#create tag node for emby channels
nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"), "emby_channels.xml")
root = etree.Element("node", {"order":"1", "type":"folder"})
label = language(30173)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "movies"
etree.SubElement(root, "path").text = "plugin://plugin.video.emby/?id=0&mode=channels"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
WINDOW.setProperty("Emby.nodes.%s.title" %str(totalNodesCount),label)
WINDOW.setProperty("Emby.nodes.%s.type" %str(totalNodesCount),"channels")
path = "library://video/emby_channels.xml"
WINDOW.setProperty("Emby.nodes.%s.path" %str(totalNodesCount),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.content" %str(totalNodesCount),path)
totalNodesCount +=1
try: try:
etree.ElementTree(root).write(nodefile, xml_declaration=True) utils.indent(root)
except: except: pass
etree.ElementTree(root).write(nodefile) etree.ElementTree(root).write(nodeXML)
#create tag node - favorite shows def clearProperties(self):
nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"),"emby_favorite_shows.xml")
root = etree.Element("node", {"order":"1", "type":"filter"})
label = language(30181)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "tvshows"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = "Favorite tvshows" #do not localize the tagname itself
WINDOW.setProperty("Emby.nodes.%s.title" %str(totalNodesCount),label)
WINDOW.setProperty("Emby.nodes.%s.type" %str(totalNodesCount),"favourites")
path = "library://video/emby_favorite_shows.xml"
WINDOW.setProperty("Emby.nodes.%s.path" %str(totalNodesCount),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.content" %str(totalNodesCount),path)
totalNodesCount +=1
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - favorite movies self.logMsg("Clearing nodes properties.", 1)
nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"),"emby_favorite_movies.xml") embyprops = utils.window('Emby.nodes.total')
root = etree.Element("node", {"order":"1", "type":"filter"}) propnames = [
label = language(30180)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "movies"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = "Favorite movies" #do not localize the tagname itself
WINDOW.setProperty("Emby.nodes.%s.title" %str(totalNodesCount),label)
WINDOW.setProperty("Emby.nodes.%s.type" %str(totalNodesCount),"favourites")
path = "library://video/emby_favorite_movies.xml"
WINDOW.setProperty("Emby.nodes.%s.path" %str(totalNodesCount),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.content" %str(totalNodesCount),path)
totalNodesCount +=1
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
WINDOW.setProperty("Emby.nodes.total", str(totalNodesCount)) "index","path","title","content",
"inprogress.content","inprogress.title",
"inprogress.content","inprogress.path",
"nextepisodes.title","nextepisodes.content",
"nextepisodes.path","unwatched.title",
"unwatched.content","unwatched.path",
"recent.title","recent.content","recent.path",
"recentepisodes.title","recentepisodes.content",
"recentepisodes.path","inprogressepisodes.title",
"inprogressepisodes.content","inprogressepisodes.path"
]
if embyprops:
except Exception as e: totalnodes = int(embyprops)
utils.logMsg("Emby addon","Error while creating videonodes listings, restart required ?") for i in range(totalnodes):
print e for prop in propnames:
utils.window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)