diff --git a/resources/lib/DownloadUtils.py b/resources/lib/DownloadUtils.py index 5c47144d..04d2da85 100644 --- a/resources/lib/DownloadUtils.py +++ b/resources/lib/DownloadUtils.py @@ -1,38 +1,32 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import json -import requests -import logging - import xbmc import xbmcgui +import xbmcaddon -import utils -import clientinfo +import requests +import json +import logging -################################################################################################## +import Utils as utils +from ClientInformation import ClientInformation +from requests.packages.urllib3.exceptions import InsecureRequestWarning # Disable requests logging -from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) -#logging.getLogger('requests').setLevel(logging.WARNING) - -################################################################################################## - +#logging.getLogger("requests").setLevel(logging.WARNING) class DownloadUtils(): # Borg - multiple instances, shared state _shared_state = {} - clientInfo = clientinfo.ClientInfo() + clientInfo = ClientInformation() + addonName = clientInfo.getAddonName() + addon = xbmcaddon.Addon() + WINDOW = xbmcgui.Window(10000) # Requests session s = None - timeout = 30 - + timeout = 60 def __init__(self): @@ -40,44 +34,41 @@ class DownloadUtils(): def logMsg(self, msg, lvl=1): - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl)) def setUsername(self, username): - # Reserved for userclient only + # Reserved for UserClient only self.username = username self.logMsg("Set username: %s" % username, 2) def setUserId(self, userId): - # Reserved for userclient only + # Reserved for UserClient only self.userId = userId self.logMsg("Set userId: %s" % userId, 2) def setServer(self, server): - # Reserved for userclient only + # Reserved for UserClient only self.server = server self.logMsg("Set server: %s" % server, 2) def setToken(self, token): - # Reserved for userclient only + # Reserved for UserClient only self.token = token self.logMsg("Set token: %s" % token, 2) def setSSL(self, ssl, sslclient): - # Reserved for userclient only + # Reserved for UserClient only self.sslverify = ssl self.sslclient = sslclient self.logMsg("Verify SSL host certificate: %s" % ssl, 2) self.logMsg("SSL client side certificate: %s" % sslclient, 2) - def postCapabilities(self, deviceId): # Post settings to session - url = "{server}/emby/Sessions/Capabilities/Full?format=json" + url = "{server}/mediabrowser/Sessions/Capabilities/Full" data = { - 'PlayableMediaTypes': "Audio,Video", 'SupportsMediaControl': True, 'SupportedCommands': ( @@ -95,57 +86,49 @@ class DownloadUtils(): } self.logMsg("Capabilities URL: %s" % url, 2) - self.logMsg("Postdata: %s" % data, 2) + self.logMsg("PostData: %s" % data, 2) - self.downloadUrl(url, postBody=data, type="POST") - self.logMsg("Posted capabilities to %s" % self.server, 2) + try: + self.downloadUrl(url, postBody=data, type="POST") + self.logMsg("Posted capabilities to %s" % self.server, 1) + except: + self.logMsg("Posted capabilities failed.") # Attempt at getting sessionId - url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId - result = self.downloadUrl(url) + url = "{server}/mediabrowser/Sessions?DeviceId=%s&format=json" % deviceId + try: - sessionId = result[0]['Id'] - - except (KeyError, TypeError): - self.logMsg("Failed to retrieve sessionId.", 1) - - else: + result = self.downloadUrl(url) self.logMsg("Session: %s" % result, 2) - self.logMsg("SessionId: %s" % sessionId, 1) - utils.window('emby_sessionId', value=sessionId) + sessionId = result[0][u'Id'] + self.logMsg("SessionId: %s" % sessionId) + self.WINDOW.setProperty("sessionId%s" % self.username, sessionId) + except: + self.logMsg("Failed to retrieve sessionId.", 1) + else: # Post any permanent additional users - additionalUsers = utils.settings('additionalUsers') - 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 - url = "{server}/emby/Users?format=json" - result = self.downloadUrl(url) - - for additional in additionalUsers: - addUser = additional.decode('utf-8').lower() - - # Compare to server users to list of permanent additional users - for user in result: - username = user['Name'].lower() + additionalUsers = utils.settings('additionalUsers').split(',') + self.logMsg("List of permanent users that should be added to the session: %s" % str(additionalUsers), 1) + # Get the user list from server to get the userId + url = "{server}/mediabrowser/Users?format=json" + result = self.downloadUrl(url) + if result: + for user in result: + username = user['Name'].lower() + userId = user['Id'] + for additional in additionalUsers: + addUser = additional.decode('utf-8').lower() if username in addUser: - userId = user['Id'] - url = ( - "{server}/emby/Sessions/%s/Users/%s?format=json" - % (sessionId, userId) - ) - self.downloadUrl(url, postBody={}, type="POST") - + url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, userId) + postdata = {} + self.downloadUrl(url, postBody=postdata, type="POST") + #xbmcgui.Dialog().notification("Success!", "%s added to viewing session" % username, time=1000) def startSession(self): - self.deviceId = self.clientInfo.getDeviceId() + self.deviceId = self.clientInfo.getMachineId() # User is identified from this point # Attach authenticated header to the session @@ -169,7 +152,7 @@ class DownloadUtils(): self.s.mount("http://", 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, 1) + self.logMsg("Requests session started on: %s" % self.server) def stopSession(self): try: @@ -182,116 +165,93 @@ class DownloadUtils(): clientInfo = self.clientInfo deviceName = clientInfo.getDeviceName() - deviceId = clientInfo.getDeviceId() + deviceId = clientInfo.getMachineId() version = clientInfo.getVersion() if not authenticate: # If user is not authenticated - 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 - } + 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) + return header else: userId = self.userId token = self.token # Attached to the requests session - auth = ( - '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 - } + auth = '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) - - return header + return header - def downloadUrl(self, url, postBody=None, type="GET", parameters=None, authenticate=True): + def downloadUrl(self, url, postBody=None, type="GET", authenticate=True): self.logMsg("=== ENTER downloadUrl ===", 2) + WINDOW = self.WINDOW timeout = self.timeout default_link = "" try: + # If user is authenticated if (authenticate): # Get requests session try: s = self.s - # Replace for the real values - url = url.replace("{server}", self.server) - url = url.replace("{UserId}", self.userId) + # Replace for the real values and append api_key + url = url.replace("{server}", self.server, 1) + url = url.replace("{UserId}", self.userId, 1) + self.logMsg("URL: %s" % url, 2) # Prepare request if type == "GET": - r = s.get(url, json=postBody, params=parameters, timeout=timeout) + r = s.get(url, json=postBody, timeout=timeout) elif type == "POST": r = s.post(url, json=postBody, timeout=timeout) elif type == "DELETE": r = s.delete(url, json=postBody, timeout=timeout) except AttributeError: - # request session does not exists + # Get user information - self.userId = utils.window('emby_currUser') - self.server = utils.window('emby_server%s' % self.userId) - self.token = utils.window('emby_accessToken%s' % self.userId) + self.username = WINDOW.getProperty('currUser') + self.userId = WINDOW.getProperty('userId%s' % self.username) + self.server = WINDOW.getProperty('server%s' % self.username) + self.token = WINDOW.getProperty('accessToken%s' % self.username) header = self.getHeader() verifyssl = False cert = None # IF user enables ssl verification - if utils.settings('sslverify') == "true": - verifyssl = True - if utils.settings('sslcert') != "None": - cert = utils.settings('sslcert') + try: + if utils.settings('sslverify') == "true": + verifyssl = True + if utils.settings('sslcert') != "None": + cert = utils.settings('sslcert') + except: + self.logMsg("Could not load SSL settings.", 1) + pass - # Replace for the real values - url = url.replace("{server}", self.server) - url = url.replace("{UserId}", self.userId) + # Replace for the real values and append api_key + url = url.replace("{server}", self.server, 1) + url = url.replace("{UserId}", self.userId, 1) + self.logMsg("URL: %s" % url, 2) # Prepare request if type == "GET": - r = requests.get(url, - json=postBody, - params=parameters, - headers=header, - timeout=timeout, - cert=cert, - verify=verifyssl) - + r = requests.get(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl) 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": - 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 elif not authenticate: - + + self.logMsg("URL: %s" % url, 2) header = self.getHeader(authenticate=False) verifyssl = False @@ -303,49 +263,41 @@ class DownloadUtils(): # Prepare request if type == "GET": - r = requests.get(url, - json=postBody, - params=parameters, - headers=header, - timeout=timeout, - verify=verifyssl) - + r = requests.get(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl) elif type == "POST": - r = requests.post(url, - json=postBody, - headers=header, - timeout=timeout, - verify=verifyssl) + r = requests.post(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl) - ##### THE RESPONSE ##### - self.logMsg(r.url, 2) + # Process the response if r.status_code == 204: # No body in the response self.logMsg("====== 204 Success ======", 2) + return default_link elif r.status_code == requests.codes.ok: - try: # UTF-8 - JSON object r = r.json() self.logMsg("====== 200 Success ======", 2) self.logMsg("Response: %s" % r, 2) return r - 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) else: r.raise_for_status() - - ##### EXCEPTIONS ##### + return default_link + + # TO REVIEW EXCEPTIONS except requests.exceptions.ConnectionError as e: # Make the addon aware of status - if utils.window('emby_online') != "false": + if WINDOW.getProperty("Server_online") != "false": self.logMsg("Server unreachable at: %s" % url, 0) self.logMsg(e, 2) - utils.window('emby_online', value="false") + WINDOW.setProperty("Server_online", "false") + pass except requests.exceptions.ConnectTimeout as e: self.logMsg("Server timeout at: %s" % url, 0) @@ -355,35 +307,29 @@ class DownloadUtils(): if r.status_code == 401: # Unauthorized - status = utils.window('emby_serverStatus') + status = WINDOW.getProperty("Server_status") - if 'X-Application-Error-Code' in r.headers: - # Emby server errors + if 'x-application-error-code' in r.headers: if r.headers['X-Application-Error-Code'] == "ParentalControl": # Parental control - access restricted - utils.window('emby_serverStatus', value="restricted") - xbmcgui.Dialog().notification( - heading="Emby server", - message="Access restricted.", - icon=xbmcgui.NOTIFICATION_ERROR, - time=5000) + WINDOW.setProperty("Server_status", "restricted") + xbmcgui.Dialog().notification("Emby server", "Access restricted.", xbmcgui.NOTIFICATION_ERROR, time=5000) return False - elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException": - # User tried to do something his emby account doesn't allow + # User tried to do something his emby account doesn't allow - admin restricted in some way pass - elif status not in ("401", "Auth"): - # Tell userclient token has been revoked. - utils.window('emby_serverStatus', value="401") + elif (status == "401") or (status == "Auth"): + pass + + else: + # Tell UserClient token has been revoked. + WINDOW.setProperty("Server_status", "401") self.logMsg("HTTP Error: %s" % e, 0) - xbmcgui.Dialog().notification( - heading="Error connecting", - message="Unauthorized.", - icon=xbmcgui.NOTIFICATION_ERROR) + xbmcgui.Dialog().notification("Error connecting", "Unauthorized.", xbmcgui.NOTIFICATION_ERROR) return 401 - elif r.status_code in (301, 302): + elif (r.status_code == 301) or (r.status_code == 302): # Redirects pass elif r.status_code == 400: @@ -398,4 +344,4 @@ class DownloadUtils(): self.logMsg("Unknown error connecting to: %s" % url, 0) self.logMsg(e, 1) - return default_link \ No newline at end of file + return default_link diff --git a/resources/lib/Entrypoint.py b/resources/lib/Entrypoint.py index 588b8d3c..95a5a327 100644 --- a/resources/lib/Entrypoint.py +++ b/resources/lib/Entrypoint.py @@ -1,157 +1,100 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import os -import sys -import urlparse - -import xbmc import xbmcaddon +import xbmcplugin +import xbmc import xbmcgui import xbmcvfs -import xbmcplugin +import os, sys +import threading +import json +import urllib +import time -import artwork -import utils -import clientinfo -import downloadutils -import read_embyserver as embyserver -import embydb_functions as embydb -import playlist -import playbackutils as pbutils -import playutils -import api +WINDOW = xbmcgui.Window(10000) -################################################################################################# +import Utils as utils +from ClientInformation import ClientInformation +from PlaybackUtils import PlaybackUtils +from PlayUtils import PlayUtils +from DownloadUtils import DownloadUtils +from ReadEmbyDB import ReadEmbyDB +from API import API +from UserPreferences import UserPreferences -def doPlayback(itemid, dbid): +##### Play items via plugin://plugin.video.emby/ ##### +def doPlayback(id): + url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json&ImageTypeLimit=1" % id + result = DownloadUtils().downloadUrl(url) + item = PlaybackUtils().PLAY(result, setup="default") - emby = embyserver.Read_EmbyServer() - item = emby.getItem(itemid) - pbutils.PlaybackUtils(item).play(itemid, dbid) - -##### DO RESET AUTH ##### +#### DO RESET AUTH ##### def resetAuth(): # User tried login and failed too many times - resp = xbmcgui.Dialog().yesno( - heading="Warning", - line1=( - "Emby might lock your account if you fail to log in too many times. " - "Proceed anyway?")) + resp = xbmcgui.Dialog().yesno("Warning", "Emby might lock your account if you fail to log in too many times. Proceed anyway?") if resp == 1: - utils.logMsg("EMBY", "Reset login attempts.", 1) - utils.window('emby_serverStatus', value="Auth") + xbmc.log("Reset login attempts.") + WINDOW.setProperty("Server_status", "Auth") else: xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') -def addDirectoryItem(label, path, folder=True): - li = xbmcgui.ListItem(label, path=path) - li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png") - li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"}) - li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"}) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder) - -def doMainListing(): - - xbmcplugin.setContent(int(sys.argv[1]), 'files') - # Get emby nodes from the window props - embyprops = utils.window('Emby.nodes.total') - if embyprops: - totalnodes = int(embyprops) - for i in range(totalnodes): - path = utils.window('Emby.nodes.%s.index' % i) - if not path: - path = utils.window('Emby.nodes.%s.content' % i) - label = utils.window('Emby.nodes.%s.title' % i) - if path: - addDirectoryItem(label, path) - - # some extra entries for settings and stuff. TODO --> localize the labels - addDirectoryItem("Network credentials", "plugin://plugin.video.emby/?mode=passwords", False) - addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings", False) - addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser", False) - #addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.emby/?mode=texturecache") - addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync", False) - addDirectoryItem( - label="Repair local database (force update all content)", - path="plugin://plugin.video.emby/?mode=repair", - folder=False) - addDirectoryItem( - label="Perform local database reset (full resync)", - path="plugin://plugin.video.emby/?mode=reset", - folder=False) - addDirectoryItem( - label="Sync Emby Theme Media to Kodi", - path="plugin://plugin.video.emby/?mode=thememedia", - folder=False) - - xbmcplugin.endOfDirectory(int(sys.argv[1])) - -##### ADD ADDITIONAL USERS ##### +### ADD ADDITIONAL USERS ### def addUser(): - doUtils = downloadutils.DownloadUtils() - clientInfo = clientinfo.ClientInfo() - deviceId = clientInfo.getDeviceId() + doUtils = DownloadUtils() + clientInfo = ClientInformation() + currUser = WINDOW.getProperty("currUser") + deviceId = clientInfo.getMachineId() deviceName = clientInfo.getDeviceName() - userid = utils.window('emby_currUser') - dialog = xbmcgui.Dialog() # Get session - url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId + url = "{server}/mediabrowser/Sessions?DeviceId=%s" % deviceId result = doUtils.downloadUrl(url) try: - sessionId = result[0]['Id'] - additionalUsers = result[0]['AdditionalUsers'] + sessionId = result[0][u'Id'] + additionalUsers = result[0][u'AdditionalUsers'] # Add user to session userlist = {} users = [] - url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json" + url = "{server}/mediabrowser/Users?IsDisabled=false&IsHidden=false" result = doUtils.downloadUrl(url) # pull the list of users for user in result: - name = user['Name'] - userId = user['Id'] - if userid != userId: + name = user[u'Name'] + userId = user[u'Id'] + if currUser not in name: userlist[name] = userId users.append(name) # Display dialog if there's additional users if additionalUsers: - option = dialog.select("Add/Remove user from the session", ["Add user", "Remove user"]) + option = xbmcgui.Dialog().select("Add/Remove user from the session", ["Add user", "Remove user"]) # Users currently in the session additionalUserlist = {} additionalUsername = [] # Users currently in the session for user in additionalUsers: - name = user['UserName'] - userId = user['UserId'] + name = user[u'UserName'] + userId = user[u'UserId'] additionalUserlist[name] = userId additionalUsername.append(name) if option == 1: # User selected Remove user - resp = dialog.select("Remove user from the session", additionalUsername) + resp = xbmcgui.Dialog().select("Remove user from the session", additionalUsername) if resp > -1: selected = additionalUsername[resp] selected_userId = additionalUserlist[selected] - url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, type="DELETE") - dialog.notification( - heading="Success!", - message="%s removed from viewing session" % selected, - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000) + url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, selected_userId) + postdata = {} + doUtils.downloadUrl(url, postBody=postdata, type="DELETE") + xbmcgui.Dialog().notification("Success!", "%s removed from viewing session" % selected, time=1000) # clear picture - position = utils.window('EmbyAdditionalUserPosition.%s' % selected_userId) - utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) + position = WINDOW.getProperty('EmbyAdditionalUserPosition.' + selected_userId) + WINDOW.clearProperty('EmbyAdditionalUserImage.' + str(position)) return else: return @@ -168,143 +111,138 @@ def addUser(): return # Subtract any additional users - utils.logMsg("EMBY", "Displaying list of users: %s" % users) - resp = dialog.select("Add user to the session", users) + xbmc.log("Displaying list of users: %s" % users) + resp = xbmcgui.Dialog().select("Add user to the session", users) # post additional user if resp > -1: selected = users[resp] selected_userId = userlist[selected] - url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, type="POST") - dialog.notification( - heading="Success!", - message="%s added to viewing session" % selected, - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000) + url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, selected_userId) + postdata = {} + doUtils.downloadUrl(url, postBody=postdata, type="POST") + xbmcgui.Dialog().notification("Success!", "%s added to viewing session" % selected, time=1000) except: - utils.logMsg("EMBY", "Failed to add user to session.") - dialog.notification( - heading="Error", - message="Unable to add/remove user from the session.", - icon=xbmcgui.NOTIFICATION_ERROR) + xbmc.log("Failed to add user to session.") + xbmcgui.Dialog().notification("Error", "Unable to add/remove user from the session.", xbmcgui.NOTIFICATION_ERROR) - # Add additional user images - # always clear the individual items first - totalNodes = 10 - for i in range(totalNodes): - if not utils.window('EmbyAdditionalUserImage.%s' % i): - break - utils.window('EmbyAdditionalUserImage.%s' % i) + try: + # Add additional user images + #always clear the individual items first + totalNodes = 10 + for i in range(totalNodes): + if not WINDOW.getProperty('EmbyAdditionalUserImage.' + str(i)): + break + WINDOW.clearProperty('EmbyAdditionalUserImage.' + str(i)) - url = "{server}/emby/Sessions?DeviceId=%s" % deviceId - result = doUtils.downloadUrl(url) - additionalUsers = result[0]['AdditionalUsers'] - count = 0 - for additionaluser in additionalUsers: - url = "{server}/emby/Users/%s?format=json" % additionaluser['UserId'] + url = "{server}/mediabrowser/Sessions?DeviceId=%s" % deviceId result = doUtils.downloadUrl(url) - utils.window('EmbyAdditionalUserImage.%s' % count, - value=artwork.Artwork().getUserArtwork(result, 'Primary')) - utils.window('EmbyAdditionalUserPosition.%s' % additionaluser['UserId'], value=str(count)) - count +=1 + additionalUsers = result[0][u'AdditionalUsers'] + count = 0 + for additionaluser in additionalUsers: + url = "{server}/mediabrowser/Users/%s?format=json" % (additionaluser[u'UserId']) + result = doUtils.downloadUrl(url) + WINDOW.setProperty("EmbyAdditionalUserImage." + str(count),API().getUserArtwork(result,"Primary")) + WINDOW.setProperty("EmbyAdditionalUserPosition." + str(additionaluser[u'UserId']),str(count)) + count +=1 + except: + pass -##### THEME MUSIC/VIDEOS ##### +# THEME MUSIC/VIDEOS def getThemeMedia(): - doUtils = downloadutils.DownloadUtils() - dialog = xbmcgui.Dialog() + doUtils = DownloadUtils() + playUtils = PlayUtils() + + currUser = WINDOW.getProperty('currUser') + server = WINDOW.getProperty('server%s' % currUser) playback = None + library = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library/").decode('utf-8') + # Choose playback method - resp = dialog.select("Playback method for your themes", ["Direct Play", "Direct Stream"]) + resp = xbmcgui.Dialog().select("Choose playback method for your themes", ["Direct Play", "Direct Stream"]) if resp == 0: + # Direct Play playback = "DirectPlay" elif resp == 1: + # Direct Stream playback = "DirectStream" - else: - return - - library = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/").decode('utf-8') - # Create library directory - if not xbmcvfs.exists(library): - xbmcvfs.mkdir(library) + else:return # Set custom path for user - tvtunes_path = xbmc.translatePath( - "special://profile/addon_data/script.tvtunes/").decode('utf-8') + tvtunes_path = xbmc.translatePath("special://profile/addon_data/script.tvtunes/").decode('utf-8') if xbmcvfs.exists(tvtunes_path): tvtunes = xbmcaddon.Addon(id="script.tvtunes") tvtunes.setSetting('custom_path_enable', "true") tvtunes.setSetting('custom_path', library) - utils.logMsg("EMBY", "TV Tunes custom path is enabled and set.", 1) + xbmc.log("TV Tunes custom path is enabled and set.") else: - # if it does not exist this will not work so warn user - # often they need to edit the settings first for it to be created. - dialog.ok( - heading="Warning", - line1=( - "The settings file does not exist in tvtunes. ", - "Go to the tvtunes addon and change a setting, then come back and re-run.")) - xbmc.executebuiltin('Addon.OpenSettings(script.tvtunes)') + # if it does not exist this will not work so warn user, often they need to edit the settings first for it to be created. + dialog = xbmcgui.Dialog() + dialog.ok('Warning', 'The settings file does not exist in tvtunes. Go to the tvtunes addon and change a setting, then come back and re-run') return + + # Create library directory + if not xbmcvfs.exists(library): + xbmcvfs.mkdir(library) + # Get every user view Id - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - viewids = emby_db.getViews() - embycursor.close() + userViews = [] + url = "{server}/mediabrowser/Users/{UserId}/Items?format=json" + result = doUtils.downloadUrl(url) + + for view in result[u'Items']: + userviewId = view[u'Id'] + userViews.append(userviewId) + # Get Ids with Theme Videos itemIds = {} - for view in viewids: - url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view + for view in userViews: + url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view result = doUtils.downloadUrl(url) - if result['TotalRecordCount'] != 0: - for item in result['Items']: - itemId = item['Id'] - folderName = item['Name'] + if result[u'TotalRecordCount'] != 0: + for item in result[u'Items']: + itemId = item[u'Id'] + folderName = item[u'Name'] folderName = utils.normalize_string(folderName.encode('utf-8')) itemIds[itemId] = folderName # Get paths for theme videos for itemId in itemIds: - nfo_path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId]) + nfo_path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId]) # Create folders for each content if not xbmcvfs.exists(nfo_path): xbmcvfs.mkdir(nfo_path) # Where to put the nfos nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") - url = "{server}/emby/Items/%s/ThemeVideos?format=json" % itemId + url = "{server}/mediabrowser/Items/%s/ThemeVideos?format=json" % itemId result = doUtils.downloadUrl(url) # Create nfo and write themes to it nfo_file = open(nfo_path, 'w') pathstowrite = "" # May be more than one theme - for theme in result['Items']: - putils = playutils.PlayUtils(theme) + for theme in result[u'Items']: if playback == "DirectPlay": - playurl = putils.directPlay() + playurl = playUtils.directPlay(theme) else: - playurl = putils.directStream() + playurl = playUtils.directStream(result, server, theme[u'Id'], "ThemeVideo") pathstowrite += ('%s' % playurl.encode('utf-8')) # Check if the item has theme songs and add them - url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId + url = "{server}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId result = doUtils.downloadUrl(url) # May be more than one theme - for theme in result['Items']: - putils = playutils.PlayUtils(theme) + for theme in result[u'Items']: if playback == "DirectPlay": - playurl = putils.directPlay() + playurl = playUtils.directPlay(theme) else: - playurl = putils.directStream() + playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio") pathstowrite += ('%s' % playurl.encode('utf-8')) nfo_file.write( @@ -315,13 +253,13 @@ def getThemeMedia(): # Get Ids with Theme songs musicitemIds = {} - for view in viewids: - url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view + for view in userViews: + url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view result = doUtils.downloadUrl(url) - if result['TotalRecordCount'] != 0: - for item in result['Items']: - itemId = item['Id'] - folderName = item['Name'] + if result[u'TotalRecordCount'] != 0: + for item in result[u'Items']: + itemId = item[u'Id'] + folderName = item[u'Name'] folderName = utils.normalize_string(folderName.encode('utf-8')) musicitemIds[itemId] = folderName @@ -332,27 +270,25 @@ def getThemeMedia(): if itemId in itemIds: continue - nfo_path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId]) + nfo_path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId]) # Create folders for each content if not xbmcvfs.exists(nfo_path): xbmcvfs.mkdir(nfo_path) # Where to put the nfos nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") - url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId + url = "{server}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId result = doUtils.downloadUrl(url) # Create nfo and write themes to it nfo_file = open(nfo_path, 'w') pathstowrite = "" # May be more than one theme - for theme in result['Items']: - putils = playutils.PlayUtils(theme) + for theme in result[u'Items']: if playback == "DirectPlay": - playurl = putils.directPlay() + playurl = playUtils.directPlay(theme) else: - playurl = putils.directStream() + playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio") pathstowrite += ('%s' % playurl.encode('utf-8')) nfo_file.write( @@ -361,482 +297,398 @@ def getThemeMedia(): # Close nfo file nfo_file.close() - dialog.notification( - heading="Emby for Kodi", - message="Themes added!", - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) +def userPreferences(): + doUtils = DownloadUtils() + addonSettings = xbmcaddon.Addon(id='plugin.video.emby') + userPreferencesPage = UserPreferences("script-emby-kodi-UserPreferences.xml", addonSettings.getAddonInfo('path'), "default", "1080i") + url = "{server}/mediabrowser/Users/{UserId}" + result = doUtils.downloadUrl(url) + configuration = result[u'Configuration'] + userPreferencesPage.setConfiguration(configuration) + userPreferencesPage.setName(result[u'Name']) + userPreferencesPage.setImage(API().getUserArtwork(result,"Primary")) + + userPreferencesPage.doModal() + if userPreferencesPage.isSave(): + url = "{server}/mediabrowser/Users/{UserId}/Configuration" + postdata = userPreferencesPage.getConfiguration() + doUtils.downloadUrl(url, postBody=postdata, type="POST") ##### BROWSE EMBY CHANNELS ##### -def BrowseChannels(itemid, folderid=None): +def BrowseChannels(id, folderid=None): _addon_id = int(sys.argv[1]) _addon_url = sys.argv[0] - doUtils = downloadutils.DownloadUtils() - art = artwork.Artwork() - + xbmcplugin.setContent(int(sys.argv[1]), 'files') if folderid: - url = ( - "{server}/emby/Channels/%s/Items?userid={UserId}&folderid=%s&format=json" - % (itemid, folderid)) - elif itemid == "0": - # id 0 is the root channels folder - url = "{server}/emby/Channels?{UserId}&format=json" + url = "{server}/mediabrowser/Channels/" + id + "/Items?userid={UserId}&folderid=" + folderid + "&format=json" else: - url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid + if id == "0": # id 0 is the root channels folder + url = "{server}/mediabrowser/Channels?{UserId}&format=json" + else: + url = "{server}/mediabrowser/Channels/" + id + "/Items?userid={UserId}&format=json" - result = doUtils.downloadUrl(url) - try: - channels = result['Items'] - except TypeError: - pass - else: - for item in channels: + results = DownloadUtils().downloadUrl(url) + if results: + result = results.get("Items") + if(result == None): + result = [] - API = api.API(item) - itemid = item['Id'] - itemtype = item['Type'] - title = item.get('Name', "Missing Title") - li = xbmcgui.ListItem(title) - - if itemtype == "ChannelFolderItem": + item_count = len(result) + current_item = 1; + + for item in result: + id=str(item.get("Id")).encode('utf-8') + type=item.get("Type").encode('utf-8') + + + if(item.get("Name") != None): + tempTitle = item.get("Name") + tempTitle=tempTitle.encode('utf-8') + else: + tempTitle = "Missing Title" + + if type=="ChannelFolderItem": isFolder = True else: isFolder = False - - channelId = item.get('ChannelId', "") - channelName = item.get('ChannelName', "") - - premieredate = API.getPremiereDate() - # Process Genres - genre = API.getGenres() - # Process UserData - overlay = 0 - - userdata = API.getUserData() - seektime = userdata['Resume'] - played = userdata['Played'] - if played: - overlay = 7 - else: - overlay = 6 - - favorite = userdata['Favorite'] - if favorite: - overlay = 5 + item_type = str(type).encode('utf-8') - playcount = userdata['PlayCount'] - if playcount is None: - playcount = 0 - + if(item.get("ChannelId") != None): + channelId = str(item.get("ChannelId")).encode('utf-8') + + channelName = '' + if(item.get("ChannelName") != None): + channelName = item.get("ChannelName").encode('utf-8') + + if(item.get("PremiereDate") != None): + premieredatelist = (item.get("PremiereDate")).split("T") + premieredate = premieredatelist[0] + else: + premieredate = "" + + #mediaStreams=API().getMediaStreams(item, True) + + #people = API().getPeople(item) + + # Process Genres + genre = API().getGenre(item) + + # Process UserData + userData = item.get("UserData") + PlaybackPositionTicks = '100' + overlay = "0" + favorite = "False" + seekTime = 0 + if(userData != None): + if userData.get("Played") != True: + overlay = "7" + watched = "true" + else: + overlay = "6" + watched = "false" + if userData.get("IsFavorite") == True: + overlay = "5" + favorite = "True" + else: + favorite = "False" + if userData.get("PlaybackPositionTicks") != None: + PlaybackPositionTicks = str(userData.get("PlaybackPositionTicks")) + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + seekTime = reasonableTicks / 10000 + + playCount = 0 + if(userData != None and userData.get("Played") == True): + playCount = 1 # Populate the details list - details = { - - 'title': title, - 'channelname': channelName, - 'plot': API.getOverview(), - 'Overlay': str(overlay), - 'playcount': str(playcount) - } - - if itemtype == "ChannelVideoItem": + details={'title' : tempTitle, + 'channelname' : channelName, + 'plot' : item.get("Overview"), + 'Overlay' : overlay, + 'playcount' : str(playCount)} + + if item.get("Type") == "ChannelVideoItem": xbmcplugin.setContent(_addon_id, 'movies') - elif itemtype == "ChannelAudioItem": + elif item.get("Type") == "ChannelAudioItem": xbmcplugin.setContent(_addon_id, 'songs') - # Populate the extradata list and artwork - pbutils.PlaybackUtils(item).setArtwork(li) - extradata = { + # Populate the extraData list + extraData={'thumb' : API().getArtwork(item, "Primary") , + 'fanart_image' : API().getArtwork(item, "Backdrop") , + 'poster' : API().getArtwork(item, "poster") , + 'tvshow.poster': API().getArtwork(item, "tvshow.poster") , + 'banner' : API().getArtwork(item, "Banner") , + 'clearlogo' : API().getArtwork(item, "Logo") , + 'discart' : API().getArtwork(item, "Disc") , + 'clearart' : API().getArtwork(item, "Art") , + 'landscape' : API().getArtwork(item, "Thumb") , + 'id' : id , + 'rating' : item.get("CommunityRating"), + 'year' : item.get("ProductionYear"), + 'premieredate' : premieredate, + 'genre' : genre, + 'playcount' : str(playCount), + 'itemtype' : item_type} + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + liz = xbmcgui.ListItem(tempTitle) - 'id': itemid, - 'rating': item.get('CommunityRating'), - 'year': item.get('ProductionYear'), - 'premieredate': premieredate, - 'genre': genre, - 'playcount': str(playcount), - 'itemtype': itemtype - } - li.setInfo('video', infoLabels=extradata) - li.setThumbnailImage(art.getAllArtwork(item)['Primary']) - li.setIconImage('DefaultTVShows.png') - - if itemtype == "Channel": - path = "%s?id=%s&mode=channels" % (_addon_url, itemid) - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) + artTypes=['poster', 'tvshow.poster', 'fanart_image', 'clearlogo', 'discart', 'banner', 'clearart', 'landscape', 'small_poster', 'tiny_poster', 'medium_poster','small_fanartimage', 'medium_fanartimage', 'medium_landscape', 'fanart_noindicators'] - elif isFolder: - path = "%s?id=%s&mode=channelsfolder&folderid=%s" % (_addon_url, channelId, itemid) - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) + for artType in artTypes: + imagePath=str(extraData.get(artType,'')) + liz=PlaybackUtils().setArt(liz,artType, imagePath) + + liz.setThumbnailImage(API().getArtwork(item, "Primary")) + liz.setIconImage('DefaultTVShows.png') + #liz.setInfo( type="Video", infoLabels={ "Rating": item.get("CommunityRating") }) + #liz.setInfo( type="Video", infoLabels={ "Plot": item.get("Overview") }) + + if type=="Channel": + file = _addon_url + "?id=%s&mode=channels"%id + xbmcplugin.addDirectoryItem(handle=_addon_id, url=file, listitem=liz, isFolder=True) + + elif isFolder == True: + file = _addon_url + "?id=%s&mode=channelsfolder&folderid=%s" %(channelId, id) + xbmcplugin.addDirectoryItem(handle=_addon_id, url=file, listitem=liz, isFolder=True) else: - path = "%s?id=%s&mode=play" % (_addon_url, itemid) - li.setProperty('IsPlayable', 'true') - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li) + file = _addon_url + "?id=%s&mode=play"%id + liz.setProperty('IsPlayable', 'true') + xbmcplugin.addDirectoryItem(handle=_addon_id, url=file, listitem=liz) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) -##### LISTITEM SETUP FOR VIDEONODES ##### -def createListItem(item): - - title = item['title'] - li = xbmcgui.ListItem(title) - li.setProperty('IsPlayable', "true") - - metadata = { - - 'Title': title, - 'duration': str(item['runtime']/60), - 'Plot': item['plot'], - 'Playcount': item['playcount'] - } - - if "episode" in item: - episode = item['episode'] - metadata['Episode'] = episode - - if "season" in item: - season = item['season'] - metadata['Season'] = season - - if season and episode: - li.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) - - if "firstaired" in item: - metadata['Premiered'] = item['firstaired'] - - if "showtitle" in item: - metadata['TVshowTitle'] = item['showtitle'] - - if "rating" in item: - metadata['Rating'] = str(round(float(item['rating']),1)) - - if "director" in item: - metadata['Director'] = " / ".join(item['director']) - - if "writer" in item: - metadata['Writer'] = " / ".join(item['writer']) - - if "cast" in item: - cast = [] - castandrole = [] - for person in item['cast']: - name = person['name'] - cast.append(name) - castandrole.append((name, person['role'])) - metadata['Cast'] = cast - metadata['CastAndRole'] = castandrole - - li.setInfo(type="Video", infoLabels=metadata) - li.setProperty('resumetime', str(item['resume']['position'])) - li.setProperty('totaltime', str(item['resume']['total'])) - li.setArt(item['art']) - li.setThumbnailImage(item['art'].get('thumb','')) - li.setIconImage('DefaultTVShows.png') - li.setProperty('dbid', str(item['episodeid'])) - li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) - for key, value in item['streamdetails'].iteritems(): - for stream in value: - li.addStreamInfo(key, stream) - - return li - ##### GET NEXTUP EPISODES FOR TAGNAME ##### -def getNextUpEpisodes(tagname, limit): - - count = 0 - # if the addon is called with nextup parameter, - # we return the nextepisodes list of the given tagname - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - # First we get a list of all the TV shows - filtered by tag - query = { +def getNextUpEpisodes(tagname,limit): + count=0 - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "contains", 'field': "tag", 'value': "%s" % tagname} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - if utils.settings('ignoreSpecialsNextEpisodes') == "true": - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'and': [ - {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - {'operator': "greaterthan", 'field': "season", 'value': "0"} - ]}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - } - else: - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - } - - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=item['file'], - listitem=li) - count += 1 - - if count == limit: - break - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### GET INPROGRESS EPISODES FOR TAGNAME ##### -def getInProgressEpisodes(tagname, limit): - - count = 0 - # if the addon is called with inprogressepisodes parameter, - # we return the inprogressepisodes list of the given tagname + #if the addon is called with nextup parameter, we return the nextepisodes list of the given tagname xbmcplugin.setContent(int(sys.argv[1]), 'episodes') # First we get a list of all the in-progress TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "contains", 'field': "tag", 'value': "%s" % tagname} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) + json_query_string = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": { "sort": { "order": "descending", "method": "lastplayed" }, "filter": {"and": [{"operator":"true", "field":"inprogress", "value":""}, {"operator": "is", "field": "tag", "value": "%s"}]}, "properties": [ "title", "studio", "mpaa", "file", "art" ] }, "id": "libTvShows"}' %tagname) + + json_result = json.loads(json_query_string) # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - query = { + if json_result.has_key('result') and json_result['result'].has_key('tvshows'): + for item in json_result['result']['tvshows']: - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "true", 'field': "inprogress", 'value': ""}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", "plot", - "file", "rating", "resume", "tvshowid", "art", "cast", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass + # If Ignore Specials is true only choose episodes from seasons greater than 0. + if utils.settings("ignoreSpecialsNextEpisodes")=="true": + json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "tvshowid": %d, "sort": {"method":"episode"}, "filter": {"and": [ {"field": "playcount", "operator": "lessthan", "value":"1"}, {"field": "season", "operator": "greaterthan", "value": "0"} ]}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "streamdetails", "firstaired", "runtime", "writer", "dateadded", "lastplayed" ], "limits":{"end":1}}, "id": "1"}' %item['tvshowid']) else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=item['file'], - listitem=li) - count += 1 + json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "tvshowid": %d, "sort": {"method":"episode"}, "filter": {"field": "playcount", "operator": "lessthan", "value":"1"}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "streamdetails", "firstaired", "runtime", "writer", "dateadded", "lastplayed" ], "limits":{"end":1}}, "id": "1"}' %item['tvshowid']) + if json_query2: + json_query2 = json.loads(json_query2) + if json_query2.has_key('result') and json_query2['result'].has_key('episodes'): + for item in json_query2['result']['episodes']: + liz = createListItem(item) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item['file'], listitem=liz) + count +=1 if count == limit: break - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) -##### GET RECENT EPISODES FOR TAGNAME ##### -def getRecentEpisodes(tagname, limit): - +def getInProgressEpisodes(tagname,limit): count = 0 - # if the addon is called with recentepisodes parameter, - # we return the recentepisodes list of the given tagname + #if the addon is called with inprogressepisodes parameter, we return the inprogressepisodes list of the given tagname + xbmcplugin.setContent(int(sys.argv[1]), 'episodes') + # First we get a list of all the in-progress TV shows - filtered by tag + json_query_string = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": { "sort": { "order": "descending", "method": "lastplayed" }, "filter": {"and": [{"operator":"true", "field":"inprogress", "value":""}, {"operator": "contains", "field": "tag", "value": "%s"}]}, "properties": [ "title", "studio", "mpaa", "file", "art" ] }, "id": "libTvShows"}' %tagname) + json_result = json.loads(json_query_string) + # If we found any, find all in progress episodes for each one. + if json_result.has_key('result') and json_result['result'].has_key('tvshows'): + for item in json_result['result']['tvshows']: + json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "tvshowid": %d, "sort": {"method":"episode"}, "filter": {"field": "inprogress", "operator": "true", "value":""}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "cast", "streamdetails", "firstaired", "runtime", "writer", "dateadded", "lastplayed" ]}, "id": "1"}' %item['tvshowid']) + + if json_query2: + json_query2 = json.loads(json_query2) + if json_query2.has_key('result') and json_query2['result'].has_key('episodes'): + for item in json_query2['result']['episodes']: + liz = createListItem(item) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item['file'], listitem=liz) + count +=1 + if count == limit: + break + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + +def getRecentEpisodes(tagname,limit): + #if the addon is called with recentepisodes parameter, we return the recentepisodes list of the given tagname xbmcplugin.setContent(int(sys.argv[1]), 'episodes') # First we get a list of all the TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "dateadded"}, - 'filter': {'operator': "contains", 'field': "tag", 'value': "%s" % tagname}, - 'properties': ["title","sorttitle"] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - allshowsIds = set() - for item in items: - allshowsIds.add(item['tvshowid']) - - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'sort': {'order': "descending", 'method': "dateadded"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", "plot", - "file", "rating", "resume", "tvshowid", "art", "streamdetails", - "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed" - ], - "limits": {"end": limit} - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - if episode['tvshowid'] in allshowsIds: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=item['file'], - listitem=li) - count += 1 - - if count == limit: - break - + json_query_string = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": { "sort": { "order": "descending", "method": "dateadded" }, "properties": [ "title","sorttitle" ], "filter": {"operator": "contains", "field": "tag", "value": "%s"} }, "id": "libTvShows"}' %tagname) + json_result = json.loads(json_query_string) + + # If we found any, put all tv show id's in a list + if json_result.has_key('result') and json_result['result'].has_key('tvshows'): + alltvshowIds = list() + for tvshow in json_result['result']['tvshows']: + alltvshowIds.append(tvshow["tvshowid"]) + alltvshowIds = set(alltvshowIds) + + #get all recently added episodes + json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "sort": {"order": "descending", "method": "dateadded"}, "filter": {"field": "playcount", "operator": "lessthan", "value":"1"}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "streamdetails", "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed" ]}, "limits":{"end":%d}, "id": "1"}' %limit) + count = 0 + if json_query2: + json_query2 = json.loads(json_query2) + if json_query2.has_key('result') and json_query2['result'].has_key('episodes'): + for item in json_query2['result']['episodes']: + if item["tvshowid"] in alltvshowIds: + liz = createListItem(item) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item['file'], listitem=liz) + count += 1 + if count == limit: + break xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - + +def createListItem(item): + + liz = xbmcgui.ListItem(item['title']) + liz.setInfo( type="Video", infoLabels={ "Title": item['title'] }) + liz.setProperty('IsPlayable', 'true') + liz.setInfo( type="Video", infoLabels={ "duration": str(item['runtime']/60) }) + + if "episode" in item: + episode = "%.2d" % float(item['episode']) + liz.setInfo( type="Video", infoLabels={ "Episode": item['episode'] }) + + if "season" in item: + season = "%.2d" % float(item['season']) + liz.setInfo( type="Video", infoLabels={ "Season": item['season'] }) + + if season and episode: + episodeno = "s%se%s" %(season,episode) + liz.setProperty("episodeno", episodeno) + + if "firstaired" in item: + liz.setInfo( type="Video", infoLabels={ "Premiered": item['firstaired'] }) + + plot = item['plot'] + liz.setInfo( type="Video", infoLabels={ "Plot": plot }) + + if "showtitle" in item: + liz.setInfo( type="Video", infoLabels={ "TVshowTitle": item['showtitle'] }) + + if "rating" in item: + liz.setInfo( type="Video", infoLabels={ "Rating": str(round(float(item['rating']),1)) }) + liz.setInfo( type="Video", infoLabels={ "Playcount": item['playcount'] }) + if "director" in item: + liz.setInfo( type="Video", infoLabels={ "Director": " / ".join(item['director']) }) + if "writer" in item: + liz.setInfo( type="Video", infoLabels={ "Writer": " / ".join(item['writer']) }) + + if "cast" in item: + listCast = [] + listCastAndRole = [] + for castmember in item["cast"]: + listCast.append( castmember["name"] ) + listCastAndRole.append( (castmember["name"], castmember["role"]) ) + cast = [listCast, listCastAndRole] + liz.setInfo( type="Video", infoLabels={ "Cast": cast[0] }) + liz.setInfo( type="Video", infoLabels={ "CastAndRole": cast[1] }) + + liz.setProperty("resumetime", str(item['resume']['position'])) + liz.setProperty("totaltime", str(item['resume']['total'])) + liz.setArt(item['art']) + liz.setThumbnailImage(item['art'].get('thumb','')) + liz.setIconImage('DefaultTVShows.png') + liz.setProperty("dbid", str(item['episodeid'])) + liz.setProperty("fanart_image", item['art'].get('tvshow.fanart','')) + for key, value in item['streamdetails'].iteritems(): + for stream in value: + liz.addStreamInfo( key, stream ) + + return liz + ##### GET EXTRAFANART FOR LISTITEM ##### def getExtraFanArt(): + itemPath = "" + embyId = "" - emby = embyserver.Read_EmbyServer() - art = artwork.Artwork() - - # Get extrafanart for listitem - # this will only be used for skins that actually call the listitem's path + fanart dir... + #get extrafanart for listitem - this will only be used for skins that actually call the listitem's path + fanart dir... try: - # Only do this if the listitem has actually changed + #only do this if the listitem has actually changed itemPath = xbmc.getInfoLabel("ListItem.FileNameAndPath") if not itemPath: itemPath = xbmc.getInfoLabel("ListItem.Path") - if any([x in itemPath for x in ['tvshows', 'musicvideos', 'movies']]): - params = urlparse.parse_qs(itemPath) - embyId = params['id'][0] + if ("/tvshows/" in itemPath or "/musicvideos/" in itemPath or "/movies/" in itemPath): + embyId = itemPath.split("/")[-2] - utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % embyId, 1) + utils.logMsg("%s %s" % ("Emby addon", "getExtraFanArt"), "requesting extraFanArt for Id: " + embyId, 1) - # We need to store the images locally for this to work - # because of the caching system in xbmc - fanartDir = xbmc.translatePath("special://thumbnails/emby/%s/" % embyId).decode('utf-8') + #we need to store the images locally for this to work because of the caching system in xbmc + fanartDir = xbmc.translatePath("special://thumbnails/emby/" + embyId + "/") if not xbmcvfs.exists(fanartDir): - # Download the images to the cache directory - xbmcvfs.mkdirs(fanartDir) - item = emby.getItem(embyId) - if item: - backdrops = art.getAllArtwork(item)['Backdrop'] - tags = item['BackdropImageTags'] - count = 0 - for backdrop in backdrops: - # Same ordering as in artwork - tag = tags[count] - fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag) - li = xbmcgui.ListItem(tag, path=fanartFile) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=fanartFile, - listitem=li) - xbmcvfs.copy(backdrop, fanartFile) - count += 1 + #download the images to the cache directory + xbmcvfs.mkdir(fanartDir) + item = ReadEmbyDB().getFullItem(embyId) + if item != None: + if item.has_key("BackdropImageTags"): + if(len(item["BackdropImageTags"]) > 0): + WINDOW = xbmcgui.Window(10000) + username = WINDOW.getProperty('currUser') + server = WINDOW.getProperty('server%s' % username) + totalbackdrops = len(item["BackdropImageTags"]) + count = 0 + for backdrop in item["BackdropImageTags"]: + backgroundUrl = "%s/mediabrowser/Items/%s/Images/Backdrop/%s/?MaxWidth=10000&MaxHeight=10000&Format=original&Tag=%s&EnableImageEnhancers=false" % (server, embyId, str(count), backdrop) + count += 1 + fanartFile = os.path.join(fanartDir,"fanart" + backdrop + ".jpg") + li = xbmcgui.ListItem(backdrop, path=fanartFile) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=fanartFile, listitem=li) + xbmcvfs.copy(backgroundUrl,fanartFile) + else: - utils.logMsg("EMBY", "Found cached backdrop.", 2) - # Use existing cached images + #use existing cached images dirs, files = xbmcvfs.listdir(fanartDir) + count = 1 for file in files: - fanartFile = os.path.join(fanartDir, file) - li = xbmcgui.ListItem(file, path=fanartFile) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=fanartFile, - listitem=li) + count +=1 + li = xbmcgui.ListItem(file, path=os.path.join(fanartDir,file)) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=os.path.join(fanartDir,file), listitem=li) except Exception as e: - utils.logMsg("EMBY", "Error getting extrafanart: %s" % e, 1) + utils.logMsg("%s %s" % ("Emby addon", "Error in getExtraFanArt"), str(e), 1) + pass + + #always do endofdirectory to prevent errors in the logs + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def addDirectoryItem(label, path, folder=True): + li = xbmcgui.ListItem(label, path=path) + li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png") + li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"}) + li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"}) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder) + +# if the addon is called without parameters we show the listing... +def doMainListing(): + + xbmcplugin.setContent(int(sys.argv[1]), 'files') + #get emby nodes from the window props + embyProperty = WINDOW.getProperty("Emby.nodes.total") + if embyProperty: + totalNodes = int(embyProperty) + for i in range(totalNodes): + path = WINDOW.getProperty("Emby.nodes.%s.index" %str(i)) + if not path: + path = WINDOW.getProperty("Emby.nodes.%s.content" %str(i)) + label = WINDOW.getProperty("Emby.nodes.%s.title" %str(i)) + if path: + addDirectoryItem(label, path) + + # some extra entries for settings and stuff. TODO --> localize the labels + addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings") + addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync") + addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser") + addDirectoryItem("Configure user preferences", "plugin://plugin.video.emby/?mode=userprefs") + addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset") + addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.emby/?mode=texturecache") + addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia") - # Always do endofdirectory to prevent errors in the logs xbmcplugin.endOfDirectory(int(sys.argv[1])) \ No newline at end of file diff --git a/resources/lib/KodiMonitor.py b/resources/lib/KodiMonitor.py index 1b3f7862..2d4dc943 100644 --- a/resources/lib/KodiMonitor.py +++ b/resources/lib/KodiMonitor.py @@ -1,195 +1,150 @@ -# -*- coding: utf-8 -*- - ################################################################################################# - -import json +# Kodi Monitor +# Watched events that occur in Kodi, like setting media watched +################################################################################################# import xbmc import xbmcgui +import xbmcaddon +import json -import clientinfo -import downloadutils -import embydb_functions as embydb -import playbackutils as pbutils -import utils - -################################################################################################# +import Utils as utils +from WriteKodiVideoDB import WriteKodiVideoDB +from ReadKodiDB import ReadKodiDB +from PlayUtils import PlayUtils +from DownloadUtils import DownloadUtils +from PlaybackUtils import PlaybackUtils -class KodiMonitor(xbmc.Monitor): +class Kodi_Monitor( xbmc.Monitor ): + + WINDOW = xbmcgui.Window(10000) + def __init__(self, *args, **kwargs): + xbmc.Monitor.__init__(self) - def __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): - - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + def logMsg(self, msg, lvl = 1): + className = self.__class__.__name__ + utils.logMsg("%s %s" % ("EMBY", className), msg, int(lvl)) def onScanStarted(self, library): - self.logMsg("Kodi library scan %s running." % library, 2) - if library == "video": - utils.window('emby_kodiScan', value="true") - + utils.window('kodiScan', value="true") + self.logMsg("Kodi library scan running.", 2) + def onScanFinished(self, library): - self.logMsg("Kodi library scan %s finished." % library, 2) - if library == "video": - utils.window('emby_kodiScan', clear=True) + utils.window('kodiScan', clear=True) + self.logMsg("Kodi library scan finished.", 2) + + #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 + downloadUtils = DownloadUtils() + #player started playing an item - + if ("Playlist.OnAdd" in method or "Player.OnPlay" in method): - doUtils = self.doUtils - if method not in ("Playlist.OnAdd"): - self.logMsg("Method: %s Data: %s" % (method, data), 1) + jsondata = json.loads(data) + if jsondata: + 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": + connection = utils.KodiSQL('music') + cursor = connection.cursor() + embyid = ReadKodiDB().getEmbyIdByKodiId(id, type, connection, cursor) + cursor.close() + else: + embyid = ReadKodiDB().getEmbyIdByKodiId(id,type) + + if embyid: + + url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % embyid + result = downloadUtils.downloadUrl(url) + self.logMsg("Result: %s" % result, 2) + + playurl = None + count = 0 + while not playurl and count < 2: + try: + playurl = xbmc.Player().getPlayingFile() + except RuntimeError: + xbmc.sleep(200) + else: + listItem = xbmcgui.ListItem() + PlaybackUtils().setProperties(playurl, result, listItem) + + if type == "song" and utils.settings('directstreammusic') == "true": + utils.window('%splaymethod' % playurl, value="DirectStream") + else: + utils.window('%splaymethod' % playurl, value="DirectPlay") + + count += 1 + + if method == "VideoLibrary.OnUpdate": + # Triggers 4 times, the following is only for manually marking as watched/unwatched + jsondata = json.loads(data) - if data: - data = json.loads(data) - - - if method == "Player.OnPlay": - # Set up report progress for emby playback - item = data.get('item') try: - kodiid = item['id'] - type = item['type'] - except (KeyError, TypeError): - self.logMsg("Properties already set for item.", 1) + playcount = jsondata.get('playcount') + item = jsondata['item']['id'] + type = jsondata['item']['type'] + prop = utils.window('Played%s%s' % (type, item)) + except: + self.logMsg("Could not process VideoLibrary.OnUpdate data.", 1) else: - if ((utils.settings('useDirectPaths') == "1" and not type == "song") or - (type == "song" and utils.settings('disableMusic') == "false")): - # Set up properties for player - 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("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) + self.logMsg("VideoLibrary.OnUpdate: %s" % data, 2) + if prop != "true": + # Set property to prevent the multi triggering + utils.window('Played%s%s' % (type, item), "true") + WriteKodiVideoDB().updatePlayCountFromKodi(item, type, playcount) - playurl = None - count = 0 - while not playurl and count < 2: - try: - playurl = xbmc.Player().getPlayingFile() - except RuntimeError: - count += 1 - xbmc.sleep(200) - else: - listItem = xbmcgui.ListItem() - playback = pbutils.PlaybackUtils(result) + self.clearProperty(type, item) + + if method == "System.OnWake": + xbmc.sleep(10000) #Allow network to wake up + WINDOW.setProperty("OnWakeSync", "true") - if type == "song" and utils.settings('streamMusic') == "true": - utils.window('emby_%s.playmethod' % playurl, - value="DirectStream") - else: - utils.window('emby_%s.playmethod' % playurl, - value="DirectPlay") - # Set properties for player.py - playback.setProperties(playurl, listItem) - finally: - embycursor.close() + if method == "VideoLibrary.OnRemove": + xbmc.log('Intercepted remove from sender: ' + sender + ' method: ' + method + ' data: ' + data) + jsondata = json.loads(data) + id = ReadKodiDB().getEmbyIdByKodiId(jsondata.get("id"), jsondata.get("type")) + if id == None: + return + xbmc.log("Deleting Emby ID: " + id + " from database") + connection = utils.KodiSQL() + cursor = connection.cursor() + cursor.execute("DELETE FROM emby WHERE emby_id = ?", (id,)) + connection.commit() + cursor.close - - elif method == "VideoLibrary.OnUpdate": - # Manually marking as watched/unwatched - 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: - kodiid = data['id'] - type = data['type'] - except (KeyError, TypeError): - self.logMsg("Item is invalid for emby deletion.", 1) - else: - # 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 offerDelete: - 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: - 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 - url = "{server}/emby/Items/%s?format=json" % itemid - self.logMsg("Deleting request: %s" % itemid) - doUtils.downloadUrl(url, type="DELETE") - finally: - embycursor.close() - - - elif method == "System.OnWake": - # Allow network to wake up - xbmc.sleep(10000) - utils.window('emby_onWake', value="true") + if jsondata: + if jsondata.get("type") == "episode" or "movie": + url='{server}/mediabrowser/Items?Ids=' + id + '&format=json' + #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 + result = DownloadUtils().downloadUrl(url) + 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": - utils.window('emby_customPlaylist', clear=True, windowid=10101) - #xbmcgui.Window(10101).clearProperties() - self.logMsg("Clear playlist properties.") \ No newline at end of file + self.logMsg("Clear playback properties.", 2) + utils.window('propertiesPlayback', clear=True) + + 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") \ No newline at end of file diff --git a/resources/lib/LibrarySync.py b/resources/lib/LibrarySync.py index d6d15d8e..f50c88e2 100644 --- a/resources/lib/LibrarySync.py +++ b/resources/lib/LibrarySync.py @@ -1,181 +1,1023 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import sqlite3 -import threading -from datetime import datetime, timedelta, time +################################################################################################# +# LibrarySync +################################################################################################# import xbmc import xbmcgui +import xbmcaddon import xbmcvfs +import json +import sqlite3 +import inspect +import threading +import urllib +from datetime import datetime, timedelta, time +from itertools import chain +import urllib2 +import os -import api -import utils -import clientinfo -import downloadutils -import itemtypes -import embydb_functions as embydb -import kodidb_functions as kodidb -import read_embyserver as embyserver -import userclient -import videonodes +import KodiMonitor +from API import API +import Utils as utils +from ClientInformation import ClientInformation +from DownloadUtils import DownloadUtils +from ReadEmbyDB import ReadEmbyDB +from ReadKodiDB import ReadKodiDB +from WriteKodiVideoDB import WriteKodiVideoDB +from WriteKodiMusicDB import WriteKodiMusicDB +from VideoNodes import VideoNodes -################################################################################################## +addondir = xbmc.translatePath(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile')) +dataPath = os.path.join(addondir,"library") +movieLibrary = os.path.join(dataPath,'movies') +tvLibrary = os.path.join(dataPath,'tvshows') +WINDOW = xbmcgui.Window( 10000 ) class LibrarySync(threading.Thread): _shared_state = {} - stop_thread = False - suspend_thread = False + KodiMonitor = KodiMonitor.Kodi_Monitor() + clientInfo = ClientInformation() + + addonName = clientInfo.getAddonName() - # Track websocketclient updates - addedItems = [] updateItems = [] userdataItems = [] removeItems = [] - forceLibraryUpdate = False - refresh_views = False + forceUpdate = False - - def __init__(self): + def __init__(self, *args): self.__dict__ = self._shared_state - self.monitor = xbmc.Monitor() - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils() - self.user = userclient.UserClient() - self.emby = embyserver.Read_EmbyServer() - self.vnodes = videonodes.VideoNodes() - - threading.Thread.__init__(self) + threading.Thread.__init__(self, *args) def logMsg(self, msg, lvl=1): className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - def progressDialog(self, title, forced=False): - - dialog = None - - if utils.settings('dbSyncIndicator') == "true" or forced: - dialog = xbmcgui.DialogProgressBG() - dialog.create("Emby for Kodi", title) - self.logMsg("Show progress dialog: %s" % title, 2) - - return dialog - - def startSync(self): - # Run at start up - optional to use the server plugin - if utils.settings('SyncInstallRunDone') == "true": - - # Validate views - self.refreshViews() - completed = False - # Verify if server plugin is installed. - if utils.settings('serverSync') == "true": - # Try to use fast start up - url = "{server}/emby/Plugins?format=json" - result = self.doUtils.downloadUrl(url) - - for plugin in result: - if plugin['Name'] == "Emby.Kodi Sync Queue": - self.logMsg("Found server plugin.", 2) - completed = self.fastSync() - - if not completed: - # Fast sync failed or server plugin is not found - completed = self.fullSync(manualrun=True) - else: - # Install sync is not completed - completed = self.fullSync() + utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) - return completed + def FullLibrarySync(self,manualRun=False): + + startupDone = WINDOW.getProperty("startup") == "done" + syncInstallRunDone = utils.settings("SyncInstallRunDone") == "true" + performMusicSync = utils.settings("enableMusicSync") == "true" + dbSyncIndication = utils.settings("dbSyncIndication") == "true" - def fastSync(self): + ### BUILD VIDEO NODES LISTING ### + VideoNodes().buildVideoNodesListing() + ### CREATE SOURCES ### + if utils.settings("Sources") != "true": + # Only create sources once + self.logMsg("Sources.xml created.", 0) + utils.createSources() + utils.settings("Sources", "true") + + # just do a incremental sync if that is what is required + if(utils.settings("useIncSync") == "true" and utils.settings("SyncInstallRunDone") == "true") and manualRun == False: + utils.logMsg("Sync Database", "Using incremental sync instead of full sync useIncSync=True)", 0) + + du = DownloadUtils() + + lastSync = utils.settings("LastIncrenetalSync") + if(lastSync == None or len(lastSync) == 0): + lastSync = "2010-01-01T00:00:00Z" + utils.logMsg("Sync Database", "Incremental Sync Setting Last Run Time Loaded : " + lastSync, 0) - lastSync = utils.settings('LastIncrementalSync') - if not lastSync: - lastSync = "2010-01-01T00:00:00Z" - self.logMsg("Last sync run: %s" % lastSync, 1) - - url = "{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json" - params = {'LastUpdateDT': lastSync} - result = self.doUtils.downloadUrl(url, parameters=params) + lastSync = urllib2.quote(lastSync) + + url = "{server}/Emby.Kodi.SyncQueue/{UserId}/GetItems?LastUpdateDT=" + lastSync + "&format=json" + utils.logMsg("Sync Database", "Incremental Sync Get Items URL : " + url, 0) + + try: + results = du.downloadUrl(url) + changedItems = results["ItemsUpdated"] + results["ItemsAdded"] + removedItems = results["ItemsRemoved"] + userChanges = results["UserDataChanged"] + except: + utils.logMsg("Sync Database", "Incremental Sync Get Changes Failed", 0) + pass + else: + maxItems = int(utils.settings("incSyncMaxItems")) + utils.logMsg("Sync Database", "Incremental Sync Changes : " + str(results), 0) + if(len(changedItems) < maxItems and len(removedItems) < maxItems and len(userChanges) < maxItems): + + WINDOW.setProperty("startup", "done") + + LibrarySync().remove_items(removedItems) + LibrarySync().update_items(changedItems) + LibrarySync().user_data_update(userChanges) + + return True + else: + utils.logMsg("Sync Database", "Too Many For Incremental Sync (" + str(maxItems) + "), changedItems" + str(len(changedItems)) + " removedItems:" + str(len(removedItems)) + " userChanges:" + str(len(userChanges)), 0) + + #set some variable to check if this is the first run + WINDOW.setProperty("SyncDatabaseRunning", "true") + + #show the progress dialog + pDialog = None + if (syncInstallRunDone == False or dbSyncIndication or manualRun): + pDialog = xbmcgui.DialogProgressBG() + pDialog.create('Emby for Kodi', 'Performing full sync') + + if(WINDOW.getProperty("SyncDatabaseShouldStop") == "true"): + utils.logMsg("Sync Database", "Can not start SyncDatabaseShouldStop=True", 0) + return True try: - processlist = { - - 'added': result['ItemsAdded'], - 'update': result['ItemsUpdated'], - 'userdata': result['UserDataChanged'], - 'remove': result['ItemsRemoved'] - } + completed = True + + ### PROCESS VIDEO LIBRARY ### + + #create the sql connection to video db + connection = utils.KodiSQL("video") + cursor = connection.cursor() + + #Add the special emby table + cursor.execute("CREATE TABLE IF NOT EXISTS emby(emby_id TEXT, kodi_id INTEGER, media_type TEXT, checksum TEXT, parent_id INTEGER, kodi_file_id INTEGER)") + try: + cursor.execute("ALTER TABLE emby ADD COLUMN kodi_file_id INTEGER") + except: pass + self.dbCommit(connection) + + # sync movies + self.MoviesFullSync(connection,cursor,pDialog) + + if (self.ShouldStop()): + return False + + #sync Tvshows and episodes + self.TvShowsFullSync(connection,cursor,pDialog) + + if (self.ShouldStop()): + return False + + # sync musicvideos + self.MusicVideosFullSync(connection,cursor,pDialog) + + #close sql connection + cursor.close() + + ### PROCESS MUSIC LIBRARY ### + if performMusicSync: + #create the sql connection to music db + connection = utils.KodiSQL("music") + cursor = connection.cursor() + + #Add the special emby table + cursor.execute("CREATE TABLE IF NOT EXISTS emby(emby_id TEXT, kodi_id INTEGER, media_type TEXT, checksum TEXT, parent_id INTEGER, kodi_file_id INTEGER)") + try: + cursor.execute("ALTER TABLE emby ADD COLUMN kodi_file_id INTEGER") + except: pass + self.dbCommit(connection) + + self.MusicFullSync(connection,cursor,pDialog) + cursor.close() + + # set the install done setting + if(syncInstallRunDone == False and completed): + utils.settings("SyncInstallRunDone", "true") + utils.settings("dbCreatedWithVersion", self.clientInfo.getVersion()) + + # Commit all DB changes at once and Force refresh the library + #xbmc.executebuiltin("UpdateLibrary(video)") + #self.updateLibrary("video") + #xbmc.executebuiltin("UpdateLibrary(music)") + + # set prop to show we have run for the first time + WINDOW.setProperty("startup", "done") + + # tell any widgets to refresh because the content has changed + WINDOW.setProperty("widgetreload", datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + + self.SaveLastSync() - except (KeyError, TypeError): - self.logMsg("Failed to retrieve latest updates using fast sync.", 1) - return False - - else: - self.logMsg("Fast sync changes: %s" % result, 1) - for action in processlist: - self.triage_items(action, processlist[action]) - - return True - - def saveLastSync(self): - # Save last sync time - overlap = 2 - - url = "{server}/Emby.Kodi.SyncQueue/GetServerDateTime?format=json" - result = self.doUtils.downloadUrl(url) - try: # datetime fails when used more than once, TypeError - server_time = result['ServerDateTime'] - server_time = datetime.strptime(server_time, "%Y-%m-%dT%H:%M:%SZ") - - except Exception as e: - # If the server plugin is not installed or an error happened. - self.logMsg("An exception occurred: %s" % e, 1) - time_now = datetime.utcnow()-timedelta(minutes=overlap) - lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ') - self.logMsg("New sync time: client time -%s min: %s" % (overlap, lastSync), 1) - - else: - lastSync = (server_time - timedelta(minutes=overlap)).strftime('%Y-%m-%dT%H:%M:%SZ') - self.logMsg("New sync time: server time -%s min: %s" % (overlap, lastSync), 1) - finally: - utils.settings('LastIncrementalSync', value=lastSync) + WINDOW.setProperty("SyncDatabaseRunning", "false") + utils.logMsg("Sync DB", "syncDatabase Exiting", 0) - def shouldStop(self): - # Checkpoint during the syncing process - if self.monitor.abortRequested(): - return True - elif utils.window('emby_shouldStop') == "true": - return True - else: # Keep going - return False + if(pDialog != None): + pDialog.close() + + return True + + def SaveLastSync(self): + # save last sync time + + du = DownloadUtils() + url = "{server}/Emby.Kodi.SyncQueue/GetServerDateTime?format=json" + + try: + results = du.downloadUrl(url) + lastSync = results["ServerDateTime"] + self.logMsg("Sync Database, Incremental Sync Using Server Time: %s" % lastSync, 0) + lastSync = datetime.strptime(lastSync, "%Y-%m-%dT%H:%M:%SZ") + lastSync = (lastSync - timedelta(minutes=5)).strftime('%Y-%m-%dT%H:%M:%SZ') + self.logMsg("Sync Database, Incremental Sync Using Server Time -5 min: %s" % lastSync, 0) + except: + lastSync = (datetime.utcnow() - timedelta(minutes=5)).strftime('%Y-%m-%dT%H:%M:%SZ') + self.logMsg("Sync Database, Incremental Sync Using Client Time -5 min: %s" % lastSync, 0) + + self.logMsg("Sync Database, Incremental Sync Setting Last Run Time Saved: %s" % lastSync, 0) + utils.settings("LastIncrenetalSync", lastSync) + + def MoviesFullSync(self,connection, cursor, pDialog): + + views = ReadEmbyDB().getCollections("movies") + + allKodiMovieIds = list() + allEmbyMovieIds = list() + + for view in views: + + allEmbyMovies = ReadEmbyDB().getMovies(view.get('id')) + allKodiMovies = ReadKodiDB().getKodiMovies(connection, cursor) + + for kodimovie in allKodiMovies: + allKodiMovieIds.append(kodimovie[1]) + + title = view.get('title') + content = view.get('content') + + if content == "mixed": + title = "%s - Movies" % title + + for kodimovie in allKodiMovies: + allKodiMovieIds.append(kodimovie[1]) + + total = len(allEmbyMovies) + 1 + count = 1 + + #### PROCESS ADDS AND UPDATES ### + for item in allEmbyMovies: + + if (self.ShouldStop()): + return False + + if not item.get('IsFolder'): + allEmbyMovieIds.append(item["Id"]) + + if(pDialog != None): + progressTitle = "Processing " + view.get('title') + " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Running Sync", progressTitle) + count += 1 + + kodiMovie = None + for kodimovie in allKodiMovies: + if kodimovie[1] == item["Id"]: + kodiMovie = kodimovie + + if kodiMovie == None: + WriteKodiVideoDB().addOrUpdateMovieToKodiLibrary(item["Id"],connection, cursor, title) + else: + if kodiMovie[2] != API().getChecksum(item): + WriteKodiVideoDB().addOrUpdateMovieToKodiLibrary(item["Id"],connection, cursor, title) + + + + #### PROCESS BOX SETS ##### + utils.logMsg("Sync Movies", "BoxSet Sync Started", 1) + boxsets = ReadEmbyDB().getBoxSets() + + total = len(boxsets) + 1 + count = 1 + for boxset in boxsets: + if(pDialog != None): + progressTitle = "Processing BoxSets" + " (" + str(count) + " of " + str(total-1) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Running Sync", progressTitle) + count += 1 + if(self.ShouldStop()): + return False + boxsetMovies = ReadEmbyDB().getMoviesInBoxSet(boxset["Id"]) + WriteKodiVideoDB().addBoxsetToKodiLibrary(boxset, connection, cursor) + + WriteKodiVideoDB().removeMoviesFromBoxset(boxset, connection, cursor) + for boxsetMovie in boxsetMovies: + if(self.ShouldStop()): + return False + WriteKodiVideoDB().updateBoxsetToKodiLibrary(boxsetMovie,boxset, connection, cursor) + + utils.logMsg("Sync Movies", "BoxSet Sync Finished", 1) + + #### PROCESS DELETES ##### + allEmbyMovieIds = set(allEmbyMovieIds) + for kodiId in allKodiMovieIds: + if not kodiId in allEmbyMovieIds: + WINDOW.setProperty(kodiId,"deleted") + WriteKodiVideoDB().deleteItemFromKodiLibrary(kodiId, connection, cursor) + + ### commit all changes to database ### + self.dbCommit(connection) + + def MusicVideosFullSync(self,connection,cursor, pDialog): + + allKodiMusicvideoIds = list() + allEmbyMusicvideoIds = list() + + allEmbyMusicvideos = ReadEmbyDB().getMusicVideos() + allKodiMusicvideos = ReadKodiDB().getKodiMusicVideos(connection, cursor) + + for kodivideo in allKodiMusicvideos: + allKodiMusicvideoIds.append(kodivideo[1]) + + total = len(allEmbyMusicvideos) + 1 + count = 1 + + #### PROCESS ADDS AND UPDATES ### + for item in allEmbyMusicvideos: + + if (self.ShouldStop()): + return False + + if not item.get('IsFolder'): + allEmbyMusicvideoIds.append(item["Id"]) + + if(pDialog != None): + progressTitle = "Processing MusicVideos (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Running Sync", progressTitle) + count += 1 + + kodiVideo = None + for kodivideo in allKodiMusicvideos: + if kodivideo[1] == item["Id"]: + kodiVideo = kodivideo + + if kodiVideo == None: + WriteKodiVideoDB().addOrUpdateMusicVideoToKodiLibrary(item["Id"],connection, cursor) + else: + if kodiVideo[2] != API().getChecksum(item): + WriteKodiVideoDB().addOrUpdateMusicVideoToKodiLibrary(item["Id"],connection, cursor) + + #### PROCESS DELETES ##### + allEmbyMusicvideoIds = set(allEmbyMusicvideoIds) + for kodiId in allKodiMusicvideoIds: + if not kodiId in allEmbyMusicvideoIds: + WINDOW.setProperty(kodiId,"deleted") + WriteKodiVideoDB().deleteItemFromKodiLibrary(kodiId, connection, cursor) + + ### commit all changes to database ### + self.dbCommit(connection) + + def TvShowsFullSync(self,connection,cursor,pDialog): + + views = ReadEmbyDB().getCollections("tvshows") + + allKodiTvShowIds = list() + allEmbyTvShowIds = list() + + for view in views: + + allEmbyTvShows = ReadEmbyDB().getTvShows(view.get('id')) + allKodiTvShows = ReadKodiDB().getKodiTvShows(connection, cursor) + + title = view.get('title') + content = view.get('content') + + if content == "mixed": + title = "%s - TV Shows" % title + + total = len(allEmbyTvShows) + 1 + count = 1 + + for kodishow in allKodiTvShows: + allKodiTvShowIds.append(kodishow[1]) + + #### TVSHOW: PROCESS ADDS AND UPDATES ### + for item in allEmbyTvShows: + + if (self.ShouldStop()): + return False + + if(pDialog != None): + progressTitle = "Processing " + view.get('title') + " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Running Sync", progressTitle) + count += 1 + + if utils.settings('syncEmptyShows') == "true" or (item.get('IsFolder') and item.get('RecursiveItemCount') != 0): + allEmbyTvShowIds.append(item["Id"]) + + #build a list with all Id's and get the existing entry (if exists) in Kodi DB + kodiShow = None + for kodishow in allKodiTvShows: + if kodishow[1] == item["Id"]: + kodiShow = kodishow + + if kodiShow == None: + # Tv show doesn't exist in Kodi yet so proceed and add it + WriteKodiVideoDB().addOrUpdateTvShowToKodiLibrary(item["Id"],connection, cursor, title) + else: + # If there are changes to the item, perform a full sync of the item + if kodiShow[2] != API().getChecksum(item): + WriteKodiVideoDB().addOrUpdateTvShowToKodiLibrary(item["Id"],connection, cursor, title) + + #### PROCESS EPISODES ###### + self.EpisodesFullSync(connection,cursor,item["Id"]) + + #### TVSHOW: PROCESS DELETES ##### + allEmbyTvShowIds = set(allEmbyTvShowIds) + for kodiId in allKodiTvShowIds: + if not kodiId in allEmbyTvShowIds: + WINDOW.setProperty(kodiId,"deleted") + WriteKodiVideoDB().deleteItemFromKodiLibrary(kodiId, connection, cursor) + + ### commit all changes to database ### + self.dbCommit(connection) + + def EpisodesFullSync(self,connection,cursor,showId): + + WINDOW = xbmcgui.Window( 10000 ) + + allKodiEpisodeIds = list() + allEmbyEpisodeIds = list() + + # Get the kodi parent id + cursor.execute("SELECT kodi_id FROM emby WHERE emby_id=?",(showId,)) + try: + kodiShowId = cursor.fetchone()[0] + except: + self.logMsg("Unable to find show itemId:%s" % showId, 1) + return + + allEmbyEpisodes = ReadEmbyDB().getEpisodes(showId) + allKodiEpisodes = ReadKodiDB().getKodiEpisodes(connection, cursor, kodiShowId) + + for kodiepisode in allKodiEpisodes: + allKodiEpisodeIds.append(kodiepisode[1]) + + #### EPISODES: PROCESS ADDS AND UPDATES ### + for item in allEmbyEpisodes: + + if (self.ShouldStop()): + return False + + allEmbyEpisodeIds.append(item["Id"]) + + #get the existing entry (if exists) in Kodi DB + kodiEpisode = None + for kodiepisode in allKodiEpisodes: + if kodiepisode[1] == item["Id"]: + kodiEpisode = kodiepisode + + if kodiEpisode == None: + # Episode doesn't exist in Kodi yet so proceed and add it + WriteKodiVideoDB().addOrUpdateEpisodeToKodiLibrary(item["Id"], kodiShowId, connection, cursor) + else: + # If there are changes to the item, perform a full sync of the item + if kodiEpisode[2] != API().getChecksum(item): + WriteKodiVideoDB().addOrUpdateEpisodeToKodiLibrary(item["Id"], kodiShowId, connection, cursor) + + #### EPISODES: PROCESS DELETES ##### + allEmbyEpisodeIds = set(allEmbyEpisodeIds) + for kodiId in allKodiEpisodeIds: + if (not kodiId in allEmbyEpisodeIds): + WINDOW.setProperty(kodiId,"deleted") + WriteKodiVideoDB().deleteItemFromKodiLibrary(kodiId, connection, cursor) + + def MusicFullSync(self, connection,cursor, pDialog): + + self.ProcessMusicArtists(connection,cursor,pDialog) + self.dbCommit(connection) + self.ProcessMusicAlbums(connection,cursor,pDialog) + self.dbCommit(connection) + self.ProcessMusicSongs(connection,cursor,pDialog) + + ### commit all changes to database ### + self.dbCommit(connection) + + def ProcessMusicSongs(self,connection,cursor,pDialog): + + allKodiSongIds = list() + allEmbySongIds = list() + + allEmbySongs = ReadEmbyDB().getMusicSongsTotal() + allKodiSongs = ReadKodiDB().getKodiMusicSongs(connection, cursor) + + for kodisong in allKodiSongs: + allKodiSongIds.append(kodisong[1]) + + total = len(allEmbySongs) + 1 + count = 1 + + #### PROCESS SONGS ADDS AND UPDATES ### + for item in allEmbySongs: + + if (self.ShouldStop()): + return False + + allEmbySongIds.append(item["Id"]) + + if(pDialog != None): + progressTitle = "Processing Music Songs (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Running Sync", progressTitle) + count += 1 + + kodiSong = None + for kodisong in allKodiSongs: + if kodisong[1] == item["Id"]: + kodiSong = kodisong + + if kodiSong == None: + WriteKodiMusicDB().addOrUpdateSongToKodiLibrary(item,connection, cursor) + else: + if kodiSong[2] != API().getChecksum(item): + WriteKodiMusicDB().addOrUpdateSongToKodiLibrary(item,connection, cursor) + + #### PROCESS DELETES ##### + allEmbySongIds = set(allEmbySongIds) + for kodiId in allKodiSongIds: + if not kodiId in allEmbySongIds: + WINDOW.setProperty(kodiId,"deleted") + WriteKodiMusicDB().deleteItemFromKodiLibrary(kodiId, connection, cursor) + + def ProcessMusicArtists(self,connection,cursor,pDialog): + + allKodiArtistIds = list() + allEmbyArtistIds = list() + + allEmbyArtists = ReadEmbyDB().getMusicArtistsTotal() + allKodiArtists = ReadKodiDB().getKodiMusicArtists(connection, cursor) + + for kodiartist in allKodiArtists: + allKodiArtistIds.append(kodiartist[1]) + + total = len(allEmbyArtists) + 1 + count = 1 + + #### PROCESS ARTIST ADDS AND UPDATES ### + for item in allEmbyArtists: + + if (self.ShouldStop()): + return False + + allEmbyArtistIds.append(item["Id"]) + + if(pDialog != None): + progressTitle = "Processing Music Artists (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Running Sync", progressTitle) + count += 1 + + kodiArtist = None + for kodiartist in allKodiArtists: + if kodiartist[1] == item["Id"]: + kodiArtist = kodiartist + + if kodiArtist == None: + WriteKodiMusicDB().addOrUpdateArtistToKodiLibrary(item,connection, cursor) + else: + if kodiArtist[2] != API().getChecksum(item): + WriteKodiMusicDB().addOrUpdateArtistToKodiLibrary(item,connection, cursor) + + #### PROCESS DELETES ##### + allEmbyArtistIds = set(allEmbyArtistIds) + for kodiId in allKodiArtistIds: + if not kodiId in allEmbyArtistIds: + WINDOW.setProperty(kodiId,"deleted") + WriteKodiMusicDB().deleteItemFromKodiLibrary(kodiId, connection, cursor) + + def ProcessMusicAlbums(self,connection,cursor,pDialog): + + allKodiAlbumIds = list() + allEmbyAlbumIds = list() + + allEmbyAlbums = ReadEmbyDB().getMusicAlbumsTotal() + allKodiAlbums = ReadKodiDB().getKodiMusicAlbums(connection, cursor) + + for kodialbum in allKodiAlbums: + allKodiAlbumIds.append(kodialbum[1]) + + total = len(allEmbyAlbums) + 1 + count = 1 + + #### PROCESS SONGS ADDS AND UPDATES ### + for item in allEmbyAlbums: + + if (self.ShouldStop()): + return False + + allEmbyAlbumIds.append(item["Id"]) + + if(pDialog != None): + progressTitle = "Processing Music Albums (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Running Sync", progressTitle) + count += 1 + + kodiAlbum = None + for kodialbum in allKodiAlbums: + if kodialbum[1] == item["Id"]: + kodiAlbum = kodialbum + + if kodiAlbum == None: + WriteKodiMusicDB().addOrUpdateAlbumToKodiLibrary(item,connection, cursor) + else: + if kodiAlbum[2] != API().getChecksum(item): + WriteKodiMusicDB().addOrUpdateAlbumToKodiLibrary(item,connection, cursor) + + #### PROCESS DELETES ##### + allEmbyAlbumIds = set(allEmbyAlbumIds) + for kodiId in allKodiAlbumIds: + if not kodiId in allEmbyAlbumIds: + WINDOW.setProperty(kodiId,"deleted") + WriteKodiMusicDB().deleteItemFromKodiLibrary(kodiId, connection, cursor) + + def IncrementalSync(self, itemList): + + startupDone = WINDOW.getProperty("startup") == "done" + + #only perform incremental scan when full scan is completed + if startupDone: + + #this will only perform sync for items received by the websocket + dbSyncIndication = utils.settings("dbSyncIndication") == "true" + performMusicSync = utils.settings("enableMusicSync") == "true" + WINDOW.setProperty("SyncDatabaseRunning", "true") + + #show the progress dialog + pDialog = None + if (dbSyncIndication and xbmc.Player().isPlaying() == False): + pDialog = xbmcgui.DialogProgressBG() + pDialog.create('Emby for Kodi', 'Incremental Sync') + self.logMsg("Doing LibraryChanged : Show Progress IncrementalSync()", 0); + + connection = utils.KodiSQL("video") + cursor = connection.cursor() + + try: + #### PROCESS MOVIES #### + views = ReadEmbyDB().getCollections("movies") + for view in views: + allEmbyMovies = ReadEmbyDB().getMovies(view.get('id'), itemList) + count = 1 + total = len(allEmbyMovies) + 1 + for item in allEmbyMovies: + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync Movies", progressTitle) + count = count + 1 + if not item.get('IsFolder'): + WriteKodiVideoDB().addOrUpdateMovieToKodiLibrary(item["Id"],connection, cursor, view.get('title')) + + #### PROCESS BOX SETS ##### + boxsets = ReadEmbyDB().getBoxSets() + count = 1 + total = len(boxsets) + 1 + for boxset in boxsets: + if(boxset["Id"] in itemList): + utils.logMsg("IncrementalSync", "Updating box Set : " + str(boxset["Name"]), 1) + boxsetMovies = ReadEmbyDB().getMoviesInBoxSet(boxset["Id"]) + WriteKodiVideoDB().addBoxsetToKodiLibrary(boxset, connection, cursor) + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync BoxSet", progressTitle) + count = count + 1 + WriteKodiVideoDB().removeMoviesFromBoxset(boxset, connection, cursor) + for boxsetMovie in boxsetMovies: + WriteKodiVideoDB().updateBoxsetToKodiLibrary(boxsetMovie, boxset, connection, cursor) + else: + utils.logMsg("IncrementalSync", "Skipping Box Set : " + boxset["Name"], 1) + + #### PROCESS TV SHOWS #### + views = ReadEmbyDB().getCollections("tvshows") + for view in views: + allEmbyTvShows = ReadEmbyDB().getTvShows(view.get('id'),itemList) + count = 1 + total = len(allEmbyTvShows) + 1 + for item in allEmbyTvShows: + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync Tv", progressTitle) + count = count + 1 + if utils.settings('syncEmptyShows') == "true" or (item.get('IsFolder') and item.get('RecursiveItemCount') != 0): + kodiId = WriteKodiVideoDB().addOrUpdateTvShowToKodiLibrary(item["Id"],connection, cursor, view.get('title')) + + + #### PROCESS OTHERS BY THE ITEMLIST ###### + count = 1 + total = len(itemList) + 1 + for item in itemList: + + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync Items", progressTitle) + count = count + 1 + + MBitem = ReadEmbyDB().getItem(item) + itemType = MBitem.get('Type', "") + + #### PROCESS EPISODES ###### + if "Episode" in itemType: + + #get the tv show + cursor.execute("SELECT kodi_id FROM emby WHERE media_type='tvshow' AND emby_id=?", (MBitem.get("SeriesId"),)) + result = cursor.fetchone() + if result: + kodi_show_id = result[0] + else: + kodi_show_id = None + + if kodi_show_id: + WriteKodiVideoDB().addOrUpdateEpisodeToKodiLibrary(MBitem["Id"], kodi_show_id, connection, cursor) + else: + #tv show doesn't exist + #perform full tvshow sync instead so both the show and episodes get added + self.TvShowsFullSync(connection,cursor,None) + + elif "Season" in itemType: + + #get the tv show + cursor.execute("SELECT kodi_id FROM emby WHERE media_type='tvshow' AND emby_id=?", (MBitem.get("SeriesId"),)) + result = cursor.fetchone() + if result: + kodi_show_id = result[0] + # update season + WriteKodiVideoDB().updateSeasons(MBitem["SeriesId"], kodi_show_id, connection, cursor) + + #### PROCESS BOXSETS ###### + elif "BoxSet" in itemType: + boxsetMovies = ReadEmbyDB().getMoviesInBoxSet(boxset["Id"]) + WriteKodiVideoDB().addBoxsetToKodiLibrary(boxset,connection, cursor) + + for boxsetMovie in boxsetMovies: + WriteKodiVideoDB().updateBoxsetToKodiLibrary(boxsetMovie,boxset, connection, cursor) + + #### PROCESS MUSICVIDEOS #### + elif "MusicVideo" in itemType: + if not MBitem.get('IsFolder'): + WriteKodiVideoDB().addOrUpdateMusicVideoToKodiLibrary(MBitem["Id"],connection, cursor) + + ### commit all changes to database ### + self.dbCommit(connection) + cursor.close() + + ### PROCESS MUSIC LIBRARY ### + if performMusicSync: + connection = utils.KodiSQL("music") + cursor = connection.cursor() + for item in itemList: + MBitem = ReadEmbyDB().getItem(item) + itemType = MBitem.get('Type', "") + + if "MusicArtist" in itemType: + WriteKodiMusicDB().addOrUpdateArtistToKodiLibrary(MBitem, connection, cursor) + if "MusicAlbum" in itemType: + WriteKodiMusicDB().addOrUpdateAlbumToKodiLibrary(MBitem, connection, cursor) + if "Audio" in itemType: + WriteKodiMusicDB().addOrUpdateSongToKodiLibrary(MBitem, connection, cursor) + self.dbCommit(connection) + cursor.close() + + finally: + if(pDialog != None): + pDialog.close() + + #self.updateLibrary("video") + WINDOW.setProperty("SyncDatabaseRunning", "false") + # tell any widgets to refresh because the content has changed + WINDOW.setProperty("widgetreload", datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + + def removefromDB(self, itemList, deleteEmbyItem = False): + + dbSyncIndication = utils.settings("dbSyncIndication") == "true" + + #show the progress dialog + pDialog = None + if (dbSyncIndication and xbmc.Player().isPlaying() == False): + pDialog = xbmcgui.DialogProgressBG() + pDialog.create('Emby for Kodi', 'Incremental Sync') + self.logMsg("Doing LibraryChanged : Show Progress removefromDB()", 0); + + # Delete from Kodi before Emby + # To be able to get mediaType + doUtils = DownloadUtils() + video = {} + music = [] + + # Database connection to myVideosXX.db + connectionvideo = utils.KodiSQL() + cursorvideo = connectionvideo.cursor() + # Database connection to myMusicXX.db + connectionmusic = utils.KodiSQL("music") + cursormusic = connectionmusic.cursor() + + count = 1 + total = len(itemList) + 1 + for item in itemList: + + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync Delete ", progressTitle) + count = count + 1 + + # Sort by type for database deletion + try: # Search video database + self.logMsg("Check video database.", 1) + cursorvideo.execute("SELECT media_type FROM emby WHERE emby_id = ?", (item,)) + mediatype = cursorvideo.fetchone()[0] + video[item] = mediatype + #video.append(itemtype) + except: + self.logMsg("Check music database.", 1) + try: # Search music database + cursormusic.execute("SELECT media_type FROM emby WHERE emby_id = ?", (item,)) + cursormusic.fetchone()[0] + music.append(item) + except: self.logMsg("Item %s is not found in Kodi database." % item, 1) + + if len(video) > 0: + connection = connectionvideo + cursor = cursorvideo + # Process video library + count = 1 + total = len(video) + 1 + for item in video: + + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync Delete ", progressTitle) + count = count + 1 + + type = video[item] + self.logMsg("Doing LibraryChanged: Items Removed: Calling deleteItemFromKodiLibrary: %s" % item, 1) + + if "episode" in type: + # Get the TV Show Id for reference later + showId = ReadKodiDB().getShowIdByEmbyId(item, connection, cursor) + self.logMsg("ShowId: %s" % showId, 1) + WriteKodiVideoDB().deleteItemFromKodiLibrary(item, connection, cursor) + # Verification + if "episode" in type: + showTotalCount = ReadKodiDB().getShowTotalCount(showId, connection, cursor) + self.logMsg("ShowTotalCount: %s" % showTotalCount, 1) + # If there are no episodes left + if showTotalCount == 0 or showTotalCount == None: + # Delete show + embyId = ReadKodiDB().getEmbyIdByKodiId(showId, "tvshow", connection, cursor) + self.logMsg("Message: Doing LibraryChanged: Deleting show: %s" % embyId, 1) + WriteKodiVideoDB().deleteItemFromKodiLibrary(embyId, connection, cursor) + + self.dbCommit(connection) + # Close connection + cursorvideo.close() + + if len(music) > 0: + connection = connectionmusic + cursor = cursormusic + #Process music library + if utils.settings('enableMusicSync') == "true": + + for item in music: + self.logMsg("Message : Doing LibraryChanged : Items Removed : Calling deleteItemFromKodiLibrary (musiclibrary): " + item, 0) + WriteKodiMusicDB().deleteItemFromKodiLibrary(item, connection, cursor) + + self.dbCommit(connection) + # Close connection + cursormusic.close() + + if deleteEmbyItem: + for item in itemList: + url = "{server}/mediabrowser/Items/%s" % item + self.logMsg('Deleting via URL: %s' % url) + doUtils.downloadUrl(url, type = "DELETE") + xbmc.executebuiltin("Container.Refresh") + + if(pDialog != None): + pDialog.close() + + def setUserdata(self, listItems): + + dbSyncIndication = utils.settings("dbSyncIndication") == "true" + musicenabled = utils.settings('enableMusicSync') == "true" + + #show the progress dialog + pDialog = None + if (dbSyncIndication and xbmc.Player().isPlaying() == False): + pDialog = xbmcgui.DialogProgressBG() + pDialog.create('Emby for Kodi', 'Incremental Sync') + self.logMsg("Doing LibraryChanged : Show Progress setUserdata()", 0); + + # We need to sort between video and music database + video = [] + music = [] + # Database connection to myVideosXX.db + connectionvideo = utils.KodiSQL() + cursorvideo = connectionvideo.cursor() + # Database connection to myMusicXX.db + connectionmusic = utils.KodiSQL('music') + cursormusic = connectionmusic.cursor() + + count = 1 + total = len(listItems) + 1 + for userdata in listItems: + # Sort between video and music + itemId = userdata['ItemId'] + + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync User Data ", progressTitle) + count = count + 1 + + cursorvideo.execute("SELECT media_type FROM emby WHERE emby_id = ?", (itemId,)) + try: # Search video database + self.logMsg("Check video database.", 2) + mediatype = cursorvideo.fetchone()[0] + video.append(userdata) + except: + if musicenabled: + cursormusic.execute("SELECT media_type FROM emby WHERE emby_id = ?", (itemId,)) + try: # Search music database + self.logMsg("Check the music database.", 2) + mediatype = cursormusic.fetchone()[0] + music.append(userdata) + except: self.logMsg("Item %s is not found in Kodi database." % itemId, 1) + else: + self.logMsg("Item %s is not found in Kodi database." % itemId, 1) + + if len(video) > 0: + connection = connectionvideo + cursor = cursorvideo + # Process the userdata update for video library + count = 1 + total = len(video) + 1 + for userdata in video: + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync User Data ", progressTitle) + count = count + 1 + WriteKodiVideoDB().updateUserdata(userdata, connection, cursor) + + self.dbCommit(connection) + #self.updateLibrary("video") + # Close connection + cursorvideo.close() + + if len(music) > 0: + connection = connectionmusic + cursor = cursormusic + #Process music library + count = 1 + total = len(video) + 1 + # Process the userdata update for music library + if musicenabled: + for userdata in music: + if(pDialog != None): + progressTitle = "Incremental Sync "+ " (" + str(count) + " of " + str(total) + ")" + percentage = int(((float(count) / float(total)) * 100)) + pDialog.update(percentage, "Emby for Kodi - Incremental Sync User Data ", progressTitle) + count = count + 1 + WriteKodiMusicDB().updateUserdata(userdata, connection, cursor) + + self.dbCommit(connection) + #xbmc.executebuiltin("UpdateLibrary(music)") + # Close connection + cursormusic.close() + + if(pDialog != None): + pDialog.close() + + def remove_items(self, itemsRemoved): + # websocket client + if(len(itemsRemoved) > 0): + self.logMsg("Doing LibraryChanged : Processing Deleted : " + str(itemsRemoved), 0) + self.removeItems.extend(itemsRemoved) + + def update_items(self, itemsToUpdate): + # websocket client + if(len(itemsToUpdate) > 0): + self.logMsg("Doing LibraryChanged : Processing Added and Updated : " + str(itemsToUpdate), 0) + self.updateItems.extend(itemsToUpdate) + + def user_data_update(self, userDataList): + # websocket client + if(len(userDataList) > 0): + self.logMsg("Doing LibraryChanged : Processing User Data Changed : " + str(userDataList), 0) + self.userdataItems.extend(userDataList) def dbCommit(self, connection): - # Central commit, verifies if Kodi database update is running - kodidb_scan = utils.window('emby_kodiScan') == "true" + # Central commit, will verify if Kodi database + kodidb_scan = utils.window('kodiScan') == "true" while kodidb_scan: + + self.logMsg("Kodi scan running. Waiting...", 1) + kodidb_scan = utils.window('kodiScan') == "true" - self.logMsg("Kodi scan is running. Waiting...", 1) - kodidb_scan = utils.window('emby_kodiScan') == "true" - - if self.shouldStop(): - self.logMsg("Commit unsuccessful. Sync terminated.", 1) - break - - if self.monitor.waitForAbort(1): + if self.KodiMonitor.waitForAbort(1): # Abort was requested while waiting. We should exit self.logMsg("Commit unsuccessful.", 1) break @@ -183,1027 +1025,32 @@ class LibrarySync(threading.Thread): connection.commit() self.logMsg("Commit successful.", 1) - def fullSync(self, manualrun=False, repair=False): - # Only run once when first setting up. Can be run manually. - emby = self.emby - music_enabled = utils.settings('enableMusic') == "true" + def updateLibrary(self, type): - utils.window('emby_dbScan', value="true") - # Add sources - utils.sourcesXML() + self.logMsg("Updating %s library." % type, 1) + utils.window('kodiScan', value="true") + xbmc.executebuiltin('UpdateLibrary(%s)' % type) - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - # Create the tables for the emby database - # emby, view, version - embycursor.execute( - """CREATE TABLE IF NOT EXISTS emby( - emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, - kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""") - embycursor.execute( - """CREATE TABLE IF NOT EXISTS view( - view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") - embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") - embyconn.commit() - - # content sync: movies, tvshows, musicvideos, music - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - - if manualrun: - message = "Manual sync" - elif repair: - message = "Repair sync" - else: - message = "Initial sync" - - pDialog = self.progressDialog("%s" % message, forced=True) - starttotal = datetime.now() - - # Set views - self.maintainViews(embycursor, kodicursor) - embyconn.commit() - - # Sync video library - process = { - - 'movies': self.movies, - 'musicvideos': self.musicvideos, - 'tvshows': self.tvshows, - 'homevideos': self.homevideos - } - for itemtype in process: - startTime = datetime.now() - completed = process[itemtype](embycursor, kodicursor, pDialog, compare=manualrun) - if not completed: - - utils.window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - embycursor.close() - kodicursor.close() - return False - else: - self.dbCommit(kodiconn) - embyconn.commit() - elapsedTime = datetime.now() - startTime - self.logMsg( - "SyncDatabase (finished %s in: %s)" - % (itemtype, str(elapsedTime).split('.')[0]), 1) - - # sync music - if music_enabled: + def ShouldStop(self): - musicconn = utils.kodiSQL('music') - musiccursor = musicconn.cursor() - - startTime = datetime.now() - completed = self.music(embycursor, musiccursor, pDialog, compare=manualrun) - if not completed: + if(xbmc.abortRequested): + return True - utils.window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() + if(WINDOW.getProperty("SyncDatabaseShouldStop") == "true"): + return True - embycursor.close() - musiccursor.close() - return False - else: - musicconn.commit() - embyconn.commit() - elapsedTime = datetime.now() - startTime - self.logMsg( - "SyncDatabase (finished music in: %s)" - % (str(elapsedTime).split('.')[0]), 1) - musiccursor.close() - - if pDialog: - pDialog.close() + return False - embycursor.close() - kodicursor.close() - - utils.settings('SyncInstallRunDone', value="true") - utils.settings("dbCreatedWithVersion", self.clientInfo.getVersion()) - self.saveLastSync() - # tell any widgets to refresh because the content has changed - utils.window('widgetreload', value=datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - xbmc.executebuiltin('UpdateLibrary(video)') - elapsedtotal = datetime.now() - starttotal - - utils.window('emby_dbScan', clear=True) - xbmcgui.Dialog().notification( - heading="Emby for Kodi", - message="%s completed in: %s!" % - (message, str(elapsedtotal).split('.')[0]), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - return True - - - def refreshViews(self): - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - - # Compare views, assign correct tags to items - self.maintainViews(embycursor, kodicursor) - - self.dbCommit(kodiconn) - kodicursor.close() - - embyconn.commit() - embycursor.close() - - def maintainViews(self, embycursor, kodicursor): - # Compare the views to emby - emby_db = embydb.Embydb_Functions(embycursor) - kodi_db = kodidb.Kodidb_Functions(kodicursor) - doUtils = self.doUtils - vnodes = self.vnodes - - # Get views - url = "{server}/emby/Users/{UserId}/Views?format=json" - result = doUtils.downloadUrl(url) - grouped_views = result['Items'] - - try: - groupedFolders = self.user.userSettings['Configuration']['GroupedFolders'] - except TypeError: - url = "{server}/emby/Users/{UserId}?format=json" - result = doUtils.downloadUrl(url) - groupedFolders = result['Configuration']['GroupedFolders'] - - # total nodes for window properties - vnodes.clearProperties() - totalnodes = 0 - - # Set views for supported media type - mediatypes = ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music'] - for mediatype in mediatypes: - - # Get media folders from server - folders = self.emby.getViews(mediatype, root=True) - for folder in folders: - - folderid = folder['id'] - foldername = folder['name'] - viewtype = folder['type'] - - if folderid in groupedFolders: - # Media folders are grouped into userview - for grouped_view in grouped_views: - if (grouped_view['Type'] == "UserView" and - grouped_view['CollectionType'] == mediatype): - # Take the name of the userview - foldername = grouped_view['Name'] - break - - # Get current media folders from emby database - view = emby_db.getView_byId(folderid) - try: - current_viewname = view[0] - current_viewtype = view[1] - current_tagid = view[2] - - except TypeError: - self.logMsg("Creating viewid: %s in Emby database." % folderid, 1) - tagid = kodi_db.createTag(foldername) - # Create playlist for the video library - if mediatype != "music": - utils.playlistXSP(mediatype, foldername, viewtype) - # Create the video node - if mediatype != "musicvideos": - vnodes.viewNode(totalnodes, foldername, mediatype, viewtype) - totalnodes += 1 - # Add view to emby database - emby_db.addView(folderid, foldername, viewtype, tagid) - - else: - self.logMsg(' '.join(( - - "Found viewid: %s" % folderid, - "viewname: %s" % current_viewname, - "viewtype: %s" % current_viewtype, - "tagid: %s" % current_tagid)), 2) - - # View was modified, update with latest info - if current_viewname != foldername: - self.logMsg("viewid: %s new viewname: %s" % (folderid, foldername), 1) - tagid = kodi_db.createTag(foldername) - - # Update view with new info - emby_db.updateView(foldername, tagid, folderid) - - if mediatype != "music": - if emby_db.getView_byName(current_viewname) is None: - # The tag could be a combined view. Ensure there's no other tags - # with the same name before deleting playlist. - utils.playlistXSP( - mediatype, current_viewname, current_viewtype, True) - # Delete video node - if mediatype != "musicvideos": - vnodes.viewNode( - indexnumber=totalnodes, - tagname=current_viewname, - mediatype=mediatype, - viewtype=current_viewtype, - delete=True) - # Added new playlist - utils.playlistXSP(mediatype, foldername, viewtype) - # Add new video node - if mediatype != "musicvideos": - vnodes.viewNode(totalnodes, foldername, mediatype, viewtype) - totalnodes += 1 - - # Update items with new tag - items = emby_db.getItem_byView(folderid) - for item in items: - # Remove the "s" from viewtype for tags - kodi_db.updateTag( - current_tagid, tagid, item[0], current_viewtype[:-1]) - else: - if mediatype != "music": - # Validate the playlist exists or recreate it - utils.playlistXSP(mediatype, foldername, viewtype) - # Create the video node if not already exists - if mediatype != "musicvideos": - vnodes.viewNode(totalnodes, foldername, mediatype, viewtype) - totalnodes += 1 - else: - # Add video nodes listings - vnodes.singleNode(totalnodes, "Favorite movies", "movies", "favourites") - totalnodes += 1 - vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites") - totalnodes += 1 - vnodes.singleNode(totalnodes, "channels", "movies", "channels") - totalnodes += 1 - # Save total - utils.window('Emby.nodes.total', str(totalnodes)) - - - def movies(self, embycursor, kodicursor, pdialog, compare=False): - # Get movies from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - movies = itemtypes.Movies(embycursor, kodicursor) - - views = emby_db.getView_byType('movies') - views += emby_db.getView_byType('mixed') - self.logMsg("Media folders: %s" % views, 1) - - if compare: - # Pull the list of movies and boxsets in Kodi - try: - all_kodimovies = dict(emby_db.getChecksum('Movie')) - except ValueError: - all_kodimovies = {} - - try: - all_kodisets = dict(emby_db.getChecksum('BoxSet')) - except ValueError: - all_kodisets = {} - - all_embymoviesIds = set() - all_embyboxsetsIds = set() - updatelist = [] - - ##### PROCESS MOVIES ##### - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Gathering movies from view: %s..." % viewName) - - if compare: - # Manual sync - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Comparing movies from view: %s..." % viewName) - - all_embymovies = emby.getMovies(viewId, basic=True) - for embymovie in all_embymovies['Items']: - - if self.shouldStop(): - return False - - API = api.API(embymovie) - itemid = embymovie['Id'] - all_embymoviesIds.add(itemid) - - - if all_kodimovies.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - self.logMsg("Movies to update for %s: %s" % (viewName, updatelist), 1) - embymovies = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - else: - # Initial or repair sync - all_embymovies = emby.getMovies(viewId) - total = all_embymovies['TotalRecordCount'] - embymovies = all_embymovies['Items'] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymovie in embymovies: - # Process individual movies - if self.shouldStop(): - return False - - title = embymovie['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_update(embymovie, viewName, viewId) - else: - self.logMsg("Movies finished.", 2) - - - ##### PROCESS BOXSETS ##### - if pdialog: - pdialog.update(heading="Emby for Kodi", message="Gathering boxsets from server...") - - boxsets = emby.getBoxset() - - if compare: - # Manual sync - embyboxsets = [] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Comparing boxsets...") - - for boxset in boxsets['Items']: - - if self.shouldStop(): - return False - - # Boxset has no real userdata, so using etag to compare - checksum = boxset['Etag'] - itemid = boxset['Id'] - all_embyboxsetsIds.add(itemid) - - if all_kodisets.get(itemid) != checksum: - # Only update if boxset is not in Kodi or checksum is different - updatelist.append(itemid) - embyboxsets.append(boxset) - - self.logMsg("Boxsets to update: %s" % updatelist, 1) - total = len(updatelist) - else: - total = boxsets['TotalRecordCount'] - embyboxsets = boxsets['Items'] - - - if pdialog: - pdialog.update(heading="Processing Boxsets / %s items" % total) - - count = 0 - for boxset in embyboxsets: - # Process individual boxset - if self.shouldStop(): - return False - - title = boxset['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_updateBoxset(boxset) - else: - self.logMsg("Boxsets finished.", 2) - - - ##### PROCESS DELETES ##### - if compare: - # Manual sync, process deletes - for kodimovie in all_kodimovies: - if kodimovie not in all_embymoviesIds: - movies.remove(kodimovie) - else: - self.logMsg("Movies compare finished.", 1) - - for boxset in all_kodisets: - if boxset not in all_embyboxsetsIds: - movies.remove(boxset) - else: - self.logMsg("Boxsets compare finished.", 1) - - return True - - def musicvideos(self, embycursor, kodicursor, pdialog, compare=False): - # Get musicvideos from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - mvideos = itemtypes.MusicVideos(embycursor, kodicursor) - - views = emby_db.getView_byType('musicvideos') - self.logMsg("Media folders: %s" % views, 1) - - if compare: - # Pull the list of musicvideos in Kodi - try: - all_kodimvideos = dict(emby_db.getChecksum('MusicVideo')) - except ValueError: - all_kodimvideos = {} - - all_embymvideosIds = set() - updatelist = [] - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Gathering musicvideos from view: %s..." % viewName) - - if compare: - # Manual sync - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Comparing musicvideos from view: %s..." % viewName) - - all_embymvideos = emby.getMusicVideos(viewId, basic=True) - for embymvideo in all_embymvideos['Items']: - - if self.shouldStop(): - return False - - API = api.API(embymvideo) - itemid = embymvideo['Id'] - all_embymvideosIds.add(itemid) - - - if all_kodimvideos.get(itemid) != API.getChecksum(): - # Only update if musicvideo is not in Kodi or checksum is different - updatelist.append(itemid) - - self.logMsg("MusicVideos to update for %s: %s" % (viewName, updatelist), 1) - embymvideos = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - else: - # Initial or repair sync - all_embymvideos = emby.getMusicVideos(viewId) - total = all_embymvideos['TotalRecordCount'] - embymvideos = all_embymvideos['Items'] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymvideo in embymvideos: - # Process individual musicvideo - if self.shouldStop(): - return False - - title = embymvideo['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - mvideos.add_update(embymvideo, viewName, viewId) - else: - self.logMsg("MusicVideos finished.", 2) - - ##### PROCESS DELETES ##### - if compare: - # Manual sync, process deletes - for kodimvideo in all_kodimvideos: - if kodimvideo not in all_embymvideosIds: - mvideos.remove(kodimvideo) - else: - self.logMsg("MusicVideos compare finished.", 1) - - return True - - def homevideos(self, embycursor, kodicursor, pdialog, compare=False): - # Get homevideos from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - hvideos = itemtypes.HomeVideos(embycursor, kodicursor) - - views = emby_db.getView_byType('homevideos') - self.logMsg("Media folders: %s" % views, 1) - - if compare: - # Pull the list of homevideos in Kodi - try: - all_kodihvideos = dict(emby_db.getChecksum('Video')) - except ValueError: - all_kodihvideos = {} - - all_embyhvideosIds = set() - updatelist = [] - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Gathering homevideos from view: %s..." % viewName) - - all_embyhvideos = emby.getHomeVideos(viewId) - - if compare: - # Manual sync - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Comparing homevideos from view: %s..." % viewName) - - for embyhvideo in all_embyhvideos['Items']: - - if self.shouldStop(): - return False - - API = api.API(embyhvideo) - itemid = embyhvideo['Id'] - all_embyhvideosIds.add(itemid) - - - if all_kodihvideos.get(itemid) != API.getChecksum(): - # Only update if homemovie is not in Kodi or checksum is different - updatelist.append(itemid) - - self.logMsg("HomeVideos to update for %s: %s" % (viewName, updatelist), 1) - embyhvideos = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - else: - total = all_embyhvideos['TotalRecordCount'] - embyhvideos = all_embyhvideos['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embyhvideo in embyhvideos: - # Process individual homemovies - if self.shouldStop(): - return False - - title = embyhvideo['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - hvideos.add_update(embyhvideo, viewName, viewId) - else: - self.logMsg("HomeVideos finished.", 2) - - ##### PROCESS DELETES ##### - if compare: - # Manual sync, process deletes - for kodihvideo in all_kodihvideos: - if kodihvideo not in all_embyhvideosIds: - hvideos.remove(kodihvideo) - else: - self.logMsg("HomeVideos compare finished.", 1) - - return True - - def tvshows(self, embycursor, kodicursor, pdialog, compare=False): - # Get shows from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - tvshows = itemtypes.TVShows(embycursor, kodicursor) - - views = emby_db.getView_byType('tvshows') - views += emby_db.getView_byType('mixed') - self.logMsg("Media folders: %s" % views, 1) - - if compare: - # Pull the list of movies and boxsets in Kodi - try: - all_koditvshows = dict(emby_db.getChecksum('Series')) - except ValueError: - all_koditvshows = {} - - try: - all_kodiepisodes = dict(emby_db.getChecksum('Episode')) - except ValueError: - all_kodiepisodes = {} - - all_embytvshowsIds = set() - all_embyepisodesIds = set() - updatelist = [] - - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Gathering tvshows from view: %s..." % viewName) - - if compare: - # Manual sync - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Comparing tvshows from view: %s..." % viewName) - - all_embytvshows = emby.getShows(viewId, basic=True) - for embytvshow in all_embytvshows['Items']: - - if self.shouldStop(): - return False - - API = api.API(embytvshow) - itemid = embytvshow['Id'] - all_embytvshowsIds.add(itemid) - - - if all_koditvshows.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - self.logMsg("TVShows to update for %s: %s" % (viewName, updatelist), 1) - embytvshows = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - else: - all_embytvshows = emby.getShows(viewId) - total = all_embytvshows['TotalRecordCount'] - embytvshows = all_embytvshows['Items'] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embytvshow in embytvshows: - # Process individual show - if self.shouldStop(): - return False - - itemid = embytvshow['Id'] - title = embytvshow['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - tvshows.add_update(embytvshow, viewName, viewId) - - if not compare: - # Process episodes - all_episodes = emby.getEpisodesbyShow(itemid) - for episode in all_episodes['Items']: - - # Process individual show - if self.shouldStop(): - return False - - episodetitle = episode['Name'] - if pdialog: - pdialog.update(percentage, message="%s - %s" % (title, episodetitle)) - tvshows.add_updateEpisode(episode) - else: - if compare: - # Get all episodes in view - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Comparing episodes from view: %s..." % viewName) - - all_embyepisodes = emby.getEpisodes(viewId, basic=True) - for embyepisode in all_embyepisodes['Items']: - - if self.shouldStop(): - return False - - API = api.API(embyepisode) - itemid = embyepisode['Id'] - all_embyepisodesIds.add(itemid) - - if all_kodiepisodes.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - self.logMsg("Episodes to update for %s: %s" % (viewName, updatelist), 1) - embyepisodes = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - for episode in embyepisodes: - - # Process individual episode - if self.shouldStop(): - return False - - title = episode['SeriesName'] - episodetitle = episode['Name'] - if pdialog: - pdialog.update(percentage, message="%s - %s" % (title, episodetitle)) - tvshows.add_updateEpisode(episode) - else: - self.logMsg("TVShows finished.", 2) - - ##### PROCESS DELETES ##### - if compare: - # Manual sync, process deletes - for koditvshow in all_koditvshows: - if koditvshow not in all_embytvshowsIds: - tvshows.remove(koditvshow) - else: - self.logMsg("TVShows compare finished.", 1) - - for kodiepisode in all_kodiepisodes: - if kodiepisode not in all_embyepisodesIds: - tvshows.remove(kodiepisode) - else: - self.logMsg("Episodes compare finished.", 1) - - return True - - def music(self, embycursor, kodicursor, pdialog, compare=False): - # Get music from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - music = itemtypes.Music(embycursor, kodicursor) - - if compare: - # Pull the list of movies and boxsets in Kodi - try: - all_kodiartists = dict(emby_db.getChecksum('MusicArtist')) - except ValueError: - all_kodiartists = {} - - try: - all_kodialbums = dict(emby_db.getChecksum('MusicAlbum')) - except ValueError: - all_kodialbums = {} - - try: - all_kodisongs = dict(emby_db.getChecksum('Audio')) - except ValueError: - all_kodisongs = {} - - all_embyartistsIds = set() - all_embyalbumsIds = set() - all_embysongsIds = set() - updatelist = [] - - process = { - - 'artists': [emby.getArtists, music.add_updateArtist], - 'albums': [emby.getAlbums, music.add_updateAlbum], - 'songs': [emby.getSongs, music.add_updateSong] - } - types = ['artists', 'albums', 'songs'] - for type in types: - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Gathering %s..." % type) - - if compare: - # Manual Sync - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="Comparing %s..." % type) - - if type != "artists": - all_embyitems = process[type][0](basic=True) - else: - all_embyitems = process[type][0]() - for embyitem in all_embyitems['Items']: - - if self.shouldStop(): - return False - - API = api.API(embyitem) - itemid = embyitem['Id'] - if type == "artists": - all_embyartistsIds.add(itemid) - if all_kodiartists.get(itemid) != API.getChecksum(): - # Only update if artist is not in Kodi or checksum is different - updatelist.append(itemid) - elif type == "albums": - all_embyalbumsIds.add(itemid) - if all_kodialbums.get(itemid) != API.getChecksum(): - # Only update if album is not in Kodi or checksum is different - updatelist.append(itemid) - else: - all_embysongsIds.add(itemid) - if all_kodisongs.get(itemid) != API.getChecksum(): - # Only update if songs is not in Kodi or checksum is different - updatelist.append(itemid) - - self.logMsg("%s to update: %s" % (type, updatelist), 1) - embyitems = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - else: - all_embyitems = process[type][0]() - total = all_embyitems['TotalRecordCount'] - embyitems = all_embyitems['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (type, total)) - - count = 0 - for embyitem in embyitems: - # Process individual item - if self.shouldStop(): - return False - - title = embyitem['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - - process[type][1](embyitem) - else: - self.logMsg("%s finished." % type, 2) - - ##### PROCESS DELETES ##### - if compare: - # Manual sync, process deletes - for kodiartist in all_kodiartists: - if kodiartist not in all_embyartistsIds and all_kodiartists[kodiartist] is not None: - music.remove(kodiartist) - else: - self.logMsg("Artist compare finished.", 1) - - for kodialbum in all_kodialbums: - if kodialbum not in all_embyalbumsIds: - music.remove(kodialbum) - else: - self.logMsg("Albums compare finished.", 1) - - for kodisong in all_kodisongs: - if kodisong not in all_embysongsIds: - music.remove(kodisong) - else: - self.logMsg("Songs compare finished.", 1) - - return True - - # Reserved for websocket_client.py and fast start - def triage_items(self, process, items): - - processlist = { - - 'added': self.addedItems, - 'update': self.updateItems, - 'userdata': self.userdataItems, - 'remove': self.removeItems - } - if items: - if process == "userdata": - itemids = [] - for item in items: - itemids.append(item['ItemId']) - items = itemids - - self.logMsg("Queue %s: %s" % (process, items), 1) - processlist[process].extend(items) - - def incrementalSync(self): - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - pDialog = None - - if self.refresh_views: - # Received userconfig update - self.refresh_views = False - self.maintainViews(embycursor, kodicursor) - self.forceLibraryUpdate = True - - if self.addedItems or self.updateItems or self.userdataItems or self.removeItems: - # Only present dialog if we are going to process items - pDialog = self.progressDialog('Incremental sync') - - - process = { - - 'added': self.addedItems, - 'update': self.updateItems, - 'userdata': self.userdataItems, - 'remove': self.removeItems - } - types = ['added', 'update', 'userdata', 'remove'] - for type in types: - - if process[type] and utils.window('emby_kodiScan') != "true": - - listItems = list(process[type]) - del process[type][:] # Reset class list - - items_process = itemtypes.Items(embycursor, kodicursor) - update = False - - # Prepare items according to process type - if type == "added": - items = emby.sortby_mediatype(listItems) - - elif type in ("userdata", "remove"): - items = emby_db.sortby_mediaType(listItems, unsorted=False) - - else: - items = emby_db.sortby_mediaType(listItems) - if items.get('Unsorted'): - sorted_items = emby.sortby_mediatype(items['Unsorted']) - doupdate = items_process.itemsbyId(sorted_items, "added", pDialog) - if doupdate: - update = True - del items['Unsorted'] - - doupdate = items_process.itemsbyId(items, type, pDialog) - if doupdate: - update = True - - if update: - self.forceLibraryUpdate = True - - - if self.forceLibraryUpdate: - # Force update the Kodi library - self.forceLibraryUpdate = False - self.dbCommit(kodiconn) - embyconn.commit() - self.saveLastSync() - - # tell any widgets to refresh because the content has changed - utils.window('widgetreload', value=datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - - self.logMsg("Updating video library.", 1) - utils.window('emby_kodiScan', value="true") - xbmc.executebuiltin('UpdateLibrary(video)') - - if pDialog: - pDialog.close() - - kodicursor.close() - embycursor.close() - - - def compareDBVersion(self, current, minimum): - # It returns True is database is up to date. False otherwise. - self.logMsg("current: %s minimum: %s" % (current, minimum), 1) - currMajor, currMinor, currPatch = current.split(".") - minMajor, minMinor, minPatch = minimum.split(".") - + def checkDBVersion(self, currVersion, minVersion): + currMajor, currMinor, currPatch = currVersion.split(".") + minMajor, minMinor, minPatch = minVersion.split(".") if currMajor > minMajor: return True - elif currMajor == minMajor and (currMinor > minMinor or - (currMinor == minMinor and currPatch >= minPatch)): + elif currMajor == minMajor and currMinor > minMinor: + return True + elif currMajor == minMajor and currMinor == minMinor and currPatch >= minPatch: return True else: - # Database out of date. return False def run(self): @@ -1211,128 +1058,134 @@ class LibrarySync(threading.Thread): try: self.run_internal() except Exception as e: - xbmcgui.Dialog().ok( - heading="Emby for Kodi", - line1=( - "Library sync thread has exited! " - "You should restart Kodi now. " - "Please report this on the forum.")) + xbmcgui.Dialog().ok("Emby for Kodi", "Library sync thread has crashed!", "You will need to restart Kodi.", "Please report this on the forum, we will need your log.") raise def run_internal(self): startupComplete = False - monitor = self.monitor + kodiProfile = xbmc.translatePath("special://profile") - self.logMsg("---===### Starting LibrarySync ###===---", 0) + self.logMsg("--- Starting Library Sync Thread ---", 0) - while not monitor.abortRequested(): + while not self.KodiMonitor.abortRequested(): - # In the event the server goes offline - while self.suspend_thread: - # Set in service.py - if monitor.waitForAbort(5): + # In the event the server goes offline after + # the thread has already been started. + while self.suspendClient == True: + # The service.py will change self.suspendClient to False + if self.KodiMonitor.waitForAbort(5): # Abort was requested while waiting. We should exit break - if (utils.window('emby_dbCheck') != "true" and - utils.settings('SyncInstallRunDone') == "true"): - - # Verify the validity of the database - currentVersion = utils.settings('dbCreatedWithVersion') - minVersion = utils.window('emby_minDBVersion') - uptoDate = self.compareDBVersion(currentVersion, minVersion) - - if not uptoDate: - self.logMsg( - "Db version out of date: %s minimum version required: %s" - % (currentVersion, minVersion), 0) - - resp = xbmcgui.Dialog().yesno( - heading="Db Version", - line1=( - "Detected the database needs to be " - "recreated for this version of Emby for Kodi. " - "Proceed?")) - if not resp: - self.logMsg("Db version out of date! USER IGNORED!", 0) - xbmcgui.Dialog().ok( - heading="Emby for Kodi", - line1=( - "Emby for Kodi may not work correctly " - "until the database is reset.")) - else: - utils.reset() - - utils.window('emby_dbCheck', value="true") - - + # Check if the version of Emby for Kodi the DB was created with is recent enough - controled by Window property set at top of service _INIT_ + + # START TEMPORARY CODE + # Only get in here for a while, can be removed later + if utils.settings("dbCreatedWithVersion")=="" and utils.settings("SyncInstallRunDone") == "true": + self.logMsg("Unknown DB version", 0) + return_value = xbmcgui.Dialog().yesno("DB Version", "Can't detect version of Emby for Kodi the DB was created with.\nWas it at least version " + utils.window('minDBVersion') + "?") + if return_value == 0: + utils.settings("dbCreatedWithVersion","0.0.0") + self.logMsg("DB version out of date according to user", 0) + else: + utils.settings("dbCreatedWithVersion", utils.window('minDBVersion')) + self.logMsg("DB version okay according to user", 0) + # END TEMPORARY CODE + + if (utils.settings("SyncInstallRunDone") == "true" and self.checkDBVersion(utils.settings("dbCreatedWithVersion"), utils.window('minDBVersion'))==False and utils.window('minDBVersionCheck') != "true"): + self.logMsg("DB version out of date according to check", 0) + return_value = xbmcgui.Dialog().yesno("DB Version", "Detected the DB needs to be recreated for\nthis version of Emby for Kodi.\nProceed?") + if return_value == 0: + self.logMsg("DB version out of date !!! USER IGNORED !!!", 0) + xbmcgui.Dialog().ok("Emby for Kodi","Emby for Kodi may not work\ncorrectly until the database is reset.\n") + utils.window('minDBVersionCheck', value="true") + else: + utils.reset() + + # Library sync if not startupComplete: - # Verify the video database can be found - videoDb = utils.getKodiVideoDBPath() - if not xbmcvfs.exists(videoDb): - # Database does not exists - self.logMsg( - "The current Kodi version is incompatible " - "with the Emby for Kodi add-on. Please visit " - "https://github.com/MediaBrowser/Emby.Kodi/wiki " - "to know which Kodi versions are supported.", 0) - - xbmcgui.Dialog().ok( - heading="Emby Warning", - line1=( - "Cancelling the database syncing process. " - "Current Kodi versoin: %s is unsupported. " - "Please verify your logs for more info." - % xbmc.getInfoLabel('System.BuildVersion'))) + + # Verify the database for videos + videodb = utils.getKodiVideoDBPath() + if not xbmcvfs.exists(videodb): + # Database does not exists. + self.logMsg("The current Kodi version is incompatible with the Emby for Kodi add-on. Please visit here, to see currently supported Kodi versions: https://github.com/MediaBrowser/Emby.Kodi/wiki", 0) + xbmcgui.Dialog().ok("Emby Warning", "Cancelling the database syncing process. Current Kodi version: %s is unsupported. Please verify your logs for more info." % xbmc.getInfoLabel('System.BuildVersion')) break - # Run start up sync - self.logMsg("Db version: %s" % utils.settings('dbCreatedWithVersion'), 0) - self.logMsg("SyncDatabase (started)", 1) + # Run full sync + self.logMsg("DB Version: " + utils.settings("dbCreatedWithVersion"), 0) + self.logMsg("Doing_Db_Sync: syncDatabase (Started)", 1) startTime = datetime.now() - librarySync = self.startSync() + libSync = self.FullLibrarySync() elapsedTime = datetime.now() - startTime - self.logMsg( - "SyncDatabase (finished in: %s) %s" - % (str(elapsedTime).split('.')[0], librarySync), 1) - # Only try the initial sync once per kodi session regardless - # This will prevent an infinite loop in case something goes wrong. - startupComplete = True + self.logMsg("Doing_Db_Sync: syncDatabase (Finished in: %s) %s" % (str(elapsedTime).split('.')[0], libSync), 1) - # Process updates - if utils.window('emby_dbScan') != "true": - self.incrementalSync() + if libSync: + startupComplete = True - if (utils.window('emby_onWake') == "true" and - utils.window('emby_online') == "true"): - # Kodi is waking up - # Set in kodimonitor.py - utils.window('emby_onWake', clear=True) - if utils.window('emby_syncRunning') != "true": - self.logMsg("SyncDatabase onWake (started)", 0) - librarySync = self.startSync() - self.logMsg("SyncDatabase onWake (finished) %s", librarySync, 0) + # Set via Kodi Monitor event + if utils.window('OnWakeSync') == "true" and utils.window('Server_online') == "true": + utils.window("OnWakeSync", clear=True) + if utils.window("SyncDatabaseRunning") != "true": + self.logMsg("Doing_Db_Sync Post Resume: syncDatabase (Started)", 0) + libSync = self.FullLibrarySync() + self.logMsg("Doing_Db_Sync Post Resume: syncDatabase (Finished) " + str(libSync), 0) - if self.stop_thread: - # Set in service.py - self.logMsg("Service terminated thread.", 2) + + doSaveLastSync = False + + if len(self.updateItems) > 0 and utils.window('kodiScan') != "true": + # Add or update items + self.logMsg("Processing items: %s" % (str(self.updateItems)), 1) + listItems = self.updateItems + self.updateItems = [] + self.IncrementalSync(listItems) + self.forceUpdate = True + doSaveLastSync = True + + if len(self.userdataItems) > 0 and utils.window('kodiScan') != "true": + # Process userdata changes only + self.logMsg("Processing items: %s" % (str(self.userdataItems)), 1) + listItems = self.userdataItems + self.userdataItems = [] + self.setUserdata(listItems) + self.forceUpdate = True + doSaveLastSync = True + + if len(self.removeItems) > 0 and utils.window('kodiScan') != "true": + # Remove item from Kodi library + self.logMsg("Removing items: %s" % self.removeItems, 1) + listItems = self.removeItems + self.removeItems = [] + self.removefromDB(listItems) + self.forceUpdate = True + doSaveLastSync = True + + if doSaveLastSync == True: + self.SaveLastSync() + + if self.forceUpdate and not self.updateItems and not self.userdataItems and not self.removeItems: + # Force update Kodi library + self.forceUpdate = False + self.updateLibrary("video") + + if utils.window("kodiProfile_emby") != kodiProfile: + # Profile change happened, terminate this thread + self.logMsg("Kodi profile was: %s and changed to: %s. Terminating Library thread." % (kodiProfile, utils.window("kodiProfile_emby")), 1) break - if monitor.waitForAbort(1): + if self.KodiMonitor.waitForAbort(1): # Abort was requested while waiting. We should exit break - self.logMsg("###===--- LibrarySync Stopped ---===###", 0) + self.logMsg("--- Library Sync Thread stopped ---", 0) - def stopThread(self): - self.stop_thread = True - self.logMsg("Ending thread...", 2) + def suspendClient(self): + self.suspendClient = True + self.logMsg("--- Library Sync Thread paused ---", 0) - def suspendThread(self): - self.suspend_thread = True - self.logMsg("Pausing thread...", 0) - - def resumeThread(self): - self.suspend_thread = False - self.logMsg("Resuming thread...", 0) \ No newline at end of file + def resumeClient(self): + self.suspendClient = False + self.logMsg("--- Library Sync Thread resumed ---", 0) \ No newline at end of file diff --git a/resources/lib/PlayUtils.py b/resources/lib/PlayUtils.py index 0a74690b..e8b9be58 100644 --- a/resources/lib/PlayUtils.py +++ b/resources/lib/PlayUtils.py @@ -6,279 +6,227 @@ import xbmc import xbmcgui import xbmcvfs -import clientinfo -import utils +from ClientInformation import ClientInformation +import Utils as utils ################################################################################################# - class PlayUtils(): - - - 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) + clientInfo = ClientInformation() + addonName = clientInfo.getAddonName() def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - + def getPlayUrl(self, server, id, result): - def getPlayUrl(self): + if self.isDirectPlay(result,True): + # Try direct play + playurl = self.directPlay(result) + if playurl: + self.logMsg("File is direct playing.", 1) + utils.window("%splaymethod" % playurl.encode('utf-8'), value="DirectPlay") - item = self.item - playurl = None + elif self.isDirectStream(result): + # Try direct stream + playurl = self.directStream(result, server, id) + if playurl: + self.logMsg("File is direct streaming.", 1) + utils.window("%splaymethod" % playurl, value="DirectStream") - 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.isTranscoding(result): + # Try transcoding + playurl = self.transcoding(result, server, id) + if playurl: + self.logMsg("File is transcoding.", 1) + utils.window("%splaymethod" % playurl, value="Transcode") + + else: # Error + utils.window("playurlFalse", value="true") + return - elif self.isDirectPlay(): + return playurl.encode('utf-8') - self.logMsg("File is direct playing.", 1) - playurl = self.directPlay() - playurl = playurl.encode('utf-8') - # Set playmethod property - utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") - elif self.isDirectStream(): - - self.logMsg("File is direct streaming.", 1) - playurl = self.directStream() - # Set playmethod property - utils.window('emby_%s.playmethod' % playurl, value="DirectStream") - - elif self.isTranscoding(): - - self.logMsg("File is transcoding.", 1) - playurl = self.transcoding() - # Set playmethod property - utils.window('emby_%s.playmethod' % playurl, value="Transcode") - - return playurl - - def httpPlay(self): - # Audio, Video, Photo - item = self.item - server = self.server - - itemid = item['Id'] - mediatype = item['MediaType'] - - if type == "Audio": - playurl = "%s/emby/Audio/%s/stream" % (server, itemid) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) - - return playurl - - def isDirectPlay(self): - - item = self.item - - # Requirement: Filesystem, Accessible path + def isDirectPlay(self, result, dialog = False): + # Requirements for Direct play: + # FileSystem, Accessible path if utils.settings('playFromStream') == "true": - # User forcing to play via HTTP - self.logMsg("Can't direct play, play from HTTP enabled.", 1) + # User forcing to play via HTTP instead of SMB + self.logMsg("Can't direct play: Play from HTTP is enabled.", 1) return False + # Avoid H265 1080p if (utils.settings('transcodeH265') == "true" and - result['MediaSources'][0]['Name'].startswith("1080P/H265")): - # Avoid H265 1080p + result['MediaSources'][0]['Name'].startswith("1080P/H265")): self.logMsg("Option to transcode 1080P/H265 enabled.", 1) return False - canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay'] - # Make sure direct play is supported by the server + canDirectPlay = result['MediaSources'][0]['SupportsDirectPlay'] + # Make sure it's supported by server if not canDirectPlay: - self.logMsg("Can't direct play, server doesn't allow/support it.", 1) + self.logMsg("Can't direct play: Server does not allow or support it.", 1) return False - location = item['LocationType'] - if location == "FileSystem": - # Verify the path - if not self.fileExists(): - self.logMsg("Unable to direct play.") - try: - count = int(utils.settings('failCount')) - except ValueError: - count = 0 - self.logMsg("Direct play failed: %s times." % count, 1) + location = result['LocationType'] + # File needs to be "FileSystem" + if 'FileSystem' in location: + # Verify if path is accessible + if self.fileExists(result): + return True + else: + 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) + if dialog: + + failCount = int(utils.settings('directSteamFailedCount')) + self.logMsg("Direct Play failCount: %s." % failCount, 1) + + if failCount < 2: + # Let user know that direct play failed + utils.settings('directSteamFailedCount', value=str(failCount + 1)) + 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) + elif utils.settings('playFromStream') != "true": + # Permanently set direct stream as 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) - if count < 2: - # Let the user know that direct play failed - utils.settings('failCount', value=str(count+1)) - xbmcgui.Dialog().notification( - heading="Emby server", - message="Unable to direct play.", - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - elif utils.settings('playFromStream') != "true": - # Permanently set direct stream as true - utils.settings('playFromStream', value="true") - 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 True - - def directPlay(self): - - item = self.item + def directPlay(self, result): try: - playurl = item['MediaSources'][0]['Path'] - except (IndexError, KeyError): - playurl = item['Path'] + playurl = result['MediaSources'][0]['Path'] + except KeyError: + playurl = result['Path'] - if item.get('VideoType'): + if 'VideoType' in result: # Specific format modification - type = item['VideoType'] - - if type == "Dvd": + if 'Dvd' in result['VideoType']: playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif type == "Bluray": + elif 'BluRay' in result['VideoType']: playurl = "%s/BDMV/index.bdmv" % playurl - # Assign network protocol - if playurl.startswith('\\\\'): - playurl = playurl.replace("\\\\", "smb://") + # Network - SMB protocol + if "\\\\" in playurl: + 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("\\", "/") - + if "apple.com" in playurl: USER_AGENT = "QuickTime/7.7.4" playurl += "?|User-Agent=%s" % USER_AGENT return playurl - def fileExists(self): - if 'Path' not in self.item: - # File has no path defined in server - 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 + def isDirectStream(self, result): + # Requirements for Direct stream: + # FileSystem or Remote, BitRate, supported encoding + # Avoid H265 1080p if (utils.settings('transcodeH265') == "true" and - result['MediaSources'][0]['Name'].startswith("1080P/H265")): - # Avoid H265 1080p + result['MediaSources'][0]['Name'].startswith("1080P/H265")): self.logMsg("Option to transcode 1080P/H265 enabled.", 1) return False - # Requirement: BitRate, supported encoding - canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] - # Make sure the server supports it + canDirectStream = result['MediaSources'][0]['SupportsDirectStream'] + # Make sure it's supported by server if not canDirectStream: return False - # Verify the bitrate - if not self.isNetworkSufficient(): - self.logMsg("The network speed is insufficient to direct stream file.", 1) + location = result['LocationType'] + # File can be FileSystem or Remote, not Virtual + if 'Virtual' in location: + 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 True + + def directStream(self, result, server, id, type = "Video"): - def directStream(self): - - item = self.item - server = self.server - - itemid = item['Id'] - type = item['Type'] - - if 'Path' in item and item['Path'].endswith('.strm'): + if result['Path'].endswith('.strm'): # Allow strm loading when direct streaming - playurl = self.directPlay() - elif type == "Audio": - playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) + playurl = self.directPlay(result) + return playurl + + if "ThemeVideo" in type: + playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) + 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 - def isNetworkSufficient(self): - settings = self.getBitrate()*1000 - - try: - sourceBitrate = int(self.item['MediaSources'][0]['Bitrate']) - 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 + def isTranscoding(self, result): + # Last resort, no requirements + # BitRate + canTranscode = result['MediaSources'][0]['SupportsTranscoding'] + # Make sure it's supported by server if not canTranscode: return False + location = result['LocationType'] + # File can be FileSystem or Remote, not Virtual + if 'Virtual' in location: + return False + return True - def transcoding(self): + def transcoding(self, result, server, id): - item = self.item - - if 'Path' in item and item['Path'].endswith('.strm'): + if result['Path'].endswith('.strm'): # Allow strm loading when transcoding - playurl = self.directPlay() - else: - itemid = item['Id'] - deviceId = self.clientInfo.getDeviceId() - 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)) + playurl = self.directPlay(result) + 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 getBitrate(self): + 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: + self.logMsg("The video quality selected is: %s, the video bitrate required to direct stream is: %s." % (settingsVideoBitRate, sourceBitRate), 1) + if settingsVideoBitRate < sourceBitRate: + return False + + return True + + def getVideoBitRate(self): # get the addon video quality - videoQuality = utils.settings('videoBitrate') + videoQuality = utils.settings('videoBitRate') bitrate = { '0': 664, @@ -304,8 +252,35 @@ class PlayUtils(): # max bit rate supported by server (max signed 32bit integer) return bitrate.get(videoQuality, 2147483) + + def fileExists(self, result): + + if 'Path' not in result: + # File has no path in server + return False - def audioSubsPref(self, url): + # 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 # Present the list of audio to select from audioStreamsList = {} @@ -317,21 +292,15 @@ class PlayUtils(): selectSubsIndex = "" playurlprefs = "%s" % url - item = self.item - try: - mediasources = item['MediaSources'][0] - mediastreams = mediasources['MediaStreams'] - except (TypeError, KeyError, IndexError): - return - - for stream in mediastreams: + mediaStream = mediaSources[0].get('MediaStreams') + for stream in mediaStream: # Since Emby returns all possible tracks together, have to sort them. index = stream['Index'] type = stream['Type'] if 'Audio' in type: codec = stream['Codec'] - channelLayout = stream.get('ChannelLayout', "") + channelLayout = stream['ChannelLayout'] try: track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) @@ -343,8 +312,6 @@ class PlayUtils(): audioStreams.append(track) elif 'Subtitle' in type: - if stream['IsExternal']: - continue try: track = "%s - %s" % (index, stream['Language']) except: @@ -369,7 +336,7 @@ class PlayUtils(): selectAudioIndex = audioStreamsList[selected] playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex else: # User backed out of selection - playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] + playurlprefs += "&AudioStreamIndex=%s" % mediaSources[0]['DefaultAudioStreamIndex'] else: # There's only one audiotrack. selectAudioIndex = audioStreamsList[audioStreams[0]] playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex @@ -385,7 +352,7 @@ class PlayUtils(): selectSubsIndex = subtitleStreamsList[selected] playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex else: # User backed out of selection - playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") + playurlprefs += "&SubtitleStreamIndex=%s" % mediaSources[0].get('DefaultSubtitleStreamIndex', "") # Get number of channels for selected audio track audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) diff --git a/resources/lib/PlaybackUtils.py b/resources/lib/PlaybackUtils.py index affa2b81..1172e52b 100644 --- a/resources/lib/PlaybackUtils.py +++ b/resources/lib/PlaybackUtils.py @@ -2,69 +2,71 @@ ################################################################################################# -import json +import datetime +import json as json import sys import xbmc -import xbmcgui +import xbmcaddon import xbmcplugin +import xbmcgui -import api -import artwork -import clientinfo -import downloadutils -import playutils as putils -import playlist -import read_embyserver as embyserver -import utils +from API import API +from DownloadUtils import DownloadUtils +from PlayUtils import PlayUtils +from ClientInformation import ClientInformation +import Utils as utils ################################################################################################# - class PlaybackUtils(): - - def __init__(self, item): + clientInfo = ClientInformation() + doUtils = DownloadUtils() + api = API() - 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() + addon = xbmcaddon.Addon() + language = addon.getLocalizedString + addonName = clientInfo.getAddonName() def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) - self.className = self.__class__.__name__ - 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 - item = self.item - API = self.API - listitem = xbmcgui.ListItem() - playutils = putils.PlayUtils(item) + username = utils.window('currUser') + server = utils.window('server%s' % username) - playurl = playutils.getPlayUrl() - if not playurl: - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) + id = result['Id'] + userdata = result['UserData'] + # Get the playurl - direct play, direct stream or transcoding + playurl = PlayUtils().getPlayUrl(server, id, result) + listItem = xbmcgui.ListItem() - 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) + 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) + ############### ORGANIZE CURRENT PLAYLIST ################ @@ -72,45 +74,58 @@ class PlaybackUtils(): playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) startPos = max(playlist.getposition(), 0) # Can return -1 sizePlaylist = playlist.size() - currentPosition = startPos - propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true" + propertiesPlayback = utils.window('propertiesPlayback') == "true" introsPlaylist = False dummyPlaylist = False + currentPosition = startPos + + self.logMsg("Playlist start position: %s" % startPos, 2) + self.logMsg("Playlist plugin position: %s" % currentPosition, 2) + self.logMsg("Playlist size: %s" % sizePlaylist, 2) - 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 ################ - userdata = API.getUserData() - seektime = API.adjustResume(userdata['Resume']) + # Resume point for widget only + timeInfo = api.getTimeInfo(result) + 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. # Otherwise we get a loop. if not propertiesPlayback: - utils.window('emby_playbackProps', value="true", windowid=10101) - 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 + utils.window('propertiesPlayback', value="true") + self.logMsg("Setting up properties in playlist.") ############### -- CHECK FOR INTROS ################ - if utils.settings('enableCinema') == "true" and not seektime: + if utils.settings('disableCinema') == "false" and not seekTime: # if we have any play them when the movie/show is not being resumed - url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid + url = "{server}/mediabrowser/Users/{UserId}/Items/%s/Intros?format=json&ImageTypeLimit=1&Fields=Etag" % id intros = doUtils.downloadUrl(url) if intros['TotalRecordCount'] != 0: @@ -126,15 +141,17 @@ class PlaybackUtils(): if getTrailers: for intro in intros['Items']: # The server randomly returns intros, process them. + introId = intro['Id'] + + introPlayurl = PlayUtils().getPlayUrl(server, introId, intro) introListItem = xbmcgui.ListItem() - introPlayurl = putils.PlayUtils(intro).getPlayUrl() self.logMsg("Adding Intro: %s" % introPlayurl, 1) # Set listitem and properties for intros - pbutils = PlaybackUtils(intro) - pbutils.setProperties(introPlayurl, introListItem) - - self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) + self.setProperties(introPlayurl, intro, introListItem) + self.setListItemProps(server, introId, introListItem, intro) + + playlist.add(introPlayurl, introListItem, index=currentPosition) introsPlaylist = True currentPosition += 1 @@ -142,126 +159,109 @@ class PlaybackUtils(): ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### 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.pl.addtoPlaylist(dbid, item['Type'].lower()) - + self.setListItemProps(server, id, listItem, result) + playlist.add(playurl, listItem, index=currentPosition) + # Ensure that additional parts are played after the main item currentPosition += 1 + ############### -- CHECK FOR ADDITIONAL PARTS ################ - if item.get('PartCount'): + if result.get('PartCount'): # Only add to the playlist after intros have played - partcount = item['PartCount'] - url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid + partcount = result['PartCount'] + url = "{server}/mediabrowser/Videos/%s/AdditionalParts" % id parts = doUtils.downloadUrl(url) + for part in parts['Items']: + partId = part['Id'] + additionalPlayurl = PlayUtils().getPlayUrl(server, partId, part) additionalListItem = xbmcgui.ListItem() - additionalPlayurl = putils.PlayUtils(part).getPlayUrl() self.logMsg("Adding additional part: %s" % partcount, 1) # Set listitem and properties for each additional parts - pbutils = PlaybackUtils(part) - pbutils.setProperties(additionalPlayurl, additionalListItem) - pbutils.setArtwork(additionalListItem) + self.setProperties(additionalPlayurl, part, additionalListItem) + self.setListItemProps(server, partId, additionalListItem, part) playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) - self.pl.verifyPlaylist() currentPosition += 1 - if dummyPlaylist: - # Added a dummy file to the playlist, - # because the first item is going to fail automatically. - self.logMsg("Processed as a playlist. First item is skipped.", 1) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) + + ############### ADD DUMMY TO PLAYLIST ################# + + if (not homeScreen and introsPlaylist) or (homeScreen and sizePlaylist > 0): + # Playlist will fail on the current position. Adding dummy url + 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. elif propertiesPlayback: self.logMsg("Resetting properties playback flag.", 2) - utils.window('emby_playbackProps', clear=True, windowid=10101) + utils.window('propertiesPlayback', clear=True) - #self.pl.verifyPlaylist() - ########## SETUP MAIN ITEM ########## - # 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) + self.verifyPlaylist() ############### PLAYBACK ################ + + if not homeScreen and not introsPlaylist: + + self.logMsg("Processed as a single item.", 1) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem) - if homeScreen and seektime: - self.logMsg("Play as a widget item.", 1) - self.setListItem(listitem) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - elif ((introsPlaylist and utils.window('emby_customPlaylist', windowid=10101) == "true") or - (homeScreen and not sizePlaylist)): - # Playlist was created just now, play it. - self.logMsg("Play playlist.", 1) - xbmc.Player().play(playlist, startpos=startPos) + elif dummyPlaylist: + # Added a dummy file to the playlist because the first item is going to fail automatically. + self.logMsg("Processed as a playlist. First item is skipped.", 1) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem) else: self.logMsg("Play as a regular item.", 1) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + xbmc.Player().play(playlist, startpos=startPos) - 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): + + playlistitems = '{"jsonrpc": "2.0", "method": "Playlist.GetItems", "params": { "playlistid": 1 }, "id": 1}' + items = xbmc.executeJSONRPC(playlistitems) + self.logMsg(items, 2) - 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) + def removeFromPlaylist(self, pos): - if itemtype == "Episode": - utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId')) - else: - utils.window('%s.refreshid' % embyitem, value=itemid) + playlistremove = '{"jsonrpc": "2.0", "method": "Playlist.Remove", "params": { "playlistid": 1, "position": %d }, "id": 1}' % pos + result = xbmc.executeJSONRPC(playlistremove) + self.logMsg(result, 1) - # 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) - self.setArtwork(listitem) - - def externalSubs(self, playurl): + def externalSubs(self, id, playurl, mediaSources): + username = utils.window('currUser') + server = utils.window('server%s' % username) externalsubs = [] mapping = {} - item = self.item - itemid = item['Id'] - try: - mediastreams = item['MediaSources'][0]['MediaStreams'] - except (TypeError, KeyError, IndexError): - return - + mediaStream = mediaSources[0].get('MediaStreams') kodiindex = 0 - for stream in mediastreams: - + for stream in mediaStream: + index = stream['Index'] # Since Emby returns all possible tracks together, have to pull only external subtitles. # IsTextSubtitleStream if true, is available to download from emby. - if (stream['Type'] == "Subtitle" and - stream['IsExternal'] and stream['IsTextSubtitleStream']): + if "Subtitle" in stream['Type'] and stream['IsExternal'] and stream['IsTextSubtitleStream']: + + playmethod = utils.window("%splaymethod" % playurl) - # Direct stream - url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" - % (self.server, itemid, itemid, index)) + if "DirectPlay" in playmethod: + # 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 mapping[kodiindex] = index @@ -269,79 +269,69 @@ class PlaybackUtils(): kodiindex += 1 mapping = json.dumps(mapping) - utils.window('emby_%s.indexMapping' % playurl, value=mapping) + utils.window('%sIndexMapping' % playurl, value=mapping) return externalsubs - def setArtwork(self, listItem): - # Set up item and item info - item = self.item - artwork = self.artwork - allartwork = artwork.getAllArtwork(item, parentInfo=True) - # Set artwork for listitem - arttypes = { + def setProperties(self, playurl, result, listItem): + # Set runtimeticks, type, refresh_id and item_id + id = result.get('Id') + type = result.get('Type', "") - '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: + utils.window("%sruntimeticks" % playurl, value=str(result.get('RunTimeTicks'))) + utils.window("%stype" % playurl, value=type) + utils.window("%sitem_id" % playurl, value=id) - 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) + if type == "Episode": + utils.window("%srefresh_id" % playurl, value=result.get('SeriesId')) else: - listItem.setArt({arttype: path}) + utils.window("%srefresh_id" % playurl, value=id) - def setListItem(self, listItem): + 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) - item = self.item - type = item['Type'] - API = self.API - people = API.getPeople() - studios = API.getStudios() + 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 + api = self.api + + type = result.get('Type') + people = api.getPeople(result) + studios = api.getStudios(result) metadata = { - 'title': item.get('Name', "Missing name"), - 'year': item.get('ProductionYear'), - 'plot': API.getOverview(), + 'title': result.get('Name', "Missing name"), + 'year': result.get('ProductionYear'), + 'plot': api.getOverview(result), 'director': people.get('Director'), 'writer': people.get('Writer'), - 'mpaa': API.getMpaa(), - 'genre': " / ".join(item['Genres']), + 'mpaa': api.getMpaa(result), + 'genre': api.getGenre(result), 'studio': " / ".join(studios), - 'aired': API.getPremiereDate(), - 'rating': item.get('CommunityRating'), - 'votes': item.get('VoteCount') + 'aired': api.getPremiereDate(result), + 'rating': result.get('CommunityRating'), + 'votes': result.get('VoteCount') } if "Episode" in type: # Only for tv shows - thumbId = item.get('SeriesId') - season = item.get('ParentIndexNumber', -1) - episode = item.get('IndexNumber', -1) - show = item.get('SeriesName', "") + thumbId = result.get('SeriesId') + season = result.get('ParentIndexNumber', -1) + episode = result.get('IndexNumber', -1) + show = result.get('SeriesName', "") metadata['TVShowTitle'] = show metadata['season'] = season @@ -350,4 +340,123 @@ class PlaybackUtils(): listItem.setProperty('IsPlayable', 'true') listItem.setProperty('IsFolder', 'false') listItem.setLabel(metadata['title']) - listItem.setInfo('video', infoLabels=metadata) \ No newline at end of file + 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)''' \ No newline at end of file diff --git a/resources/lib/Player.py b/resources/lib/Player.py index b6d25e19..0b760a88 100644 --- a/resources/lib/Player.py +++ b/resources/lib/Player.py @@ -2,47 +2,45 @@ ################################################################################################# -import json +import json as json import xbmc import xbmcgui -import utils -import clientinfo -import downloadutils -import kodidb_functions as kodidb -import websocket_client as wsc +from DownloadUtils import DownloadUtils +from WebSocketClient import WebSocketThread +from ClientInformation import ClientInformation +from LibrarySync import LibrarySync +import Utils as utils ################################################################################################# - -class Player(xbmc.Player): +class Player( xbmc.Player ): # Borg - multiple instances, shared state _shared_state = {} - played_info = {} + xbmcplayer = xbmc.Player() + doUtils = DownloadUtils() + clientInfo = ClientInformation() + ws = WebSocketThread() + librarySync = LibrarySync() + + addonName = clientInfo.getAddonName() + + played_information = {} playStats = {} currentFile = None - - def __init__(self): + def __init__(self, *args): 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) def logMsg(self, msg, lvl=1): self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - + utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl)) def GetPlayStats(self): return self.playStats @@ -76,50 +74,39 @@ class Player(xbmc.Player): self.currentFile = currentFile # We may need to wait for info to be set in kodi monitor - itemId = utils.window("emby_%s.itemid" % currentFile) + itemId = utils.window("%sitem_id" % currentFile) tryCount = 0 while not itemId: xbmc.sleep(200) - itemId = utils.window("emby_%s.itemid" % currentFile) + itemId = utils.window("%sitem_id" % currentFile) if tryCount == 20: # try 20 times or about 10 seconds self.logMsg("Could not find itemId, cancelling playback report...", 1) break else: tryCount += 1 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. - embyitem = "emby_%s" % currentFile - runtime = utils.window("%s.runtime" % embyitem) - refresh_id = utils.window("%s.refreshid" % embyitem) - playMethod = utils.window("%s.playmethod" % embyitem) - itemType = utils.window("%s.type" % embyitem) - utils.window('emby_skipWatched%s' % itemId, value="true") - + runtime = utils.window("%sruntimeticks" % currentFile) + refresh_id = utils.window("%srefresh_id" % currentFile) + playMethod = utils.window("%splaymethod" % currentFile) + itemType = utils.window("%stype" % currentFile) seekTime = xbmcplayer.getTime() + # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) + volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' + result = xbmc.executeJSONRPC(volume_query) result = json.loads(result) result = result.get('result') - + volume = result.get('volume') muted = result.get('muted') # Postdata structure to send to Emby server - url = "{server}/emby/Sessions/Playing" + url = "{server}/mediabrowser/Sessions/Playing" postdata = { 'QueueableMediaTypes': "Video", @@ -136,22 +123,12 @@ class Player(xbmc.Player): if playMethod == "Transcode": # property set in PlayUtils.py postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile) - postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" - % currentFile) + postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" % currentFile) + else: # Get the current kodi audio and subtitles and convert to Emby equivalent - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) + track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' + result = xbmc.executeJSONRPC(track_query) result = json.loads(result) result = result.get('result') @@ -178,9 +155,9 @@ class Player(xbmc.Player): # Number of audiotracks to help get Emby Index audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = utils.window("%s.indexMapping" % embyitem) + mapping = utils.window("%sIndexMapping" % currentFile) - if mapping: # Set in playbackutils.py + if mapping: # Set in PlaybackUtils.py self.logMsg("Mapping for external subtitles index: %s" % mapping, 2) externalIndex = json.loads(mapping) @@ -190,8 +167,7 @@ class Player(xbmc.Player): postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] else: # Internal subtitle currently selected - subindex = indexSubs - len(externalIndex) + audioTracks + 1 - postdata['SubtitleStreamIndex'] = subindex + postdata['SubtitleStreamIndex'] = indexSubs - len(externalIndex) + audioTracks + 1 else: # Direct paths enabled scenario or no external subtitles set postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 @@ -208,7 +184,7 @@ class Player(xbmc.Player): runtime = int(runtime) except ValueError: runtime = xbmcplayer.getTotalTime() - self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1) + self.logMsg("Runtime is missing, grabbing runtime from Kodi player: %s" % runtime, 1) # Save data map for updates and position calls data = { @@ -224,8 +200,8 @@ class Player(xbmc.Player): 'currentPosition': int(seekTime) } - self.played_info[currentFile] = data - self.logMsg("ADDING_FILE: %s" % self.played_info, 1) + self.played_information[currentFile] = data + self.logMsg("ADDING_FILE: %s" % self.played_information, 1) # log some playback stats '''if(itemType != None): @@ -249,7 +225,7 @@ class Player(xbmc.Player): # Get current file currentFile = self.currentFile - data = self.played_info.get(currentFile) + data = self.played_information.get(currentFile) # only report playback if emby has initiated the playback (item_id has value) if data: @@ -263,23 +239,15 @@ class Player(xbmc.Player): # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) + volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' + result = xbmc.executeJSONRPC(volume_query) result = json.loads(result) result = result.get('result') volume = result.get('volume') muted = result.get('muted') + # Postdata for the websocketclient report postdata = { @@ -301,18 +269,8 @@ class Player(xbmc.Player): else: # Get current audio and subtitles track - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) + track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid":1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' + result = xbmc.executeJSONRPC(track_query) result = json.loads(result) result = result.get('result') @@ -339,7 +297,7 @@ class Player(xbmc.Player): # Number of audiotracks to help get Emby Index audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = utils.window("emby_%s.indexMapping" % currentFile) + mapping = utils.window("%sIndexMapping" % currentFile) if mapping: # Set in PlaybackUtils.py @@ -348,16 +306,13 @@ class Player(xbmc.Player): if externalIndex.get(str(indexSubs)): # If the current subtitle is in the mapping - subindex = [externalIndex[str(indexSubs)]] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [externalIndex[str(indexSubs)]] * 2 else: # Internal subtitle currently selected - subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 else: # Direct paths enabled scenario or no external subtitles set - subindex = [indexSubs + audioTracks + 1] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs + audioTracks + 1] * 2 else: data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2 @@ -371,8 +326,8 @@ class Player(xbmc.Player): currentFile = self.currentFile self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2) - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = True + if self.played_information.get(currentFile): + self.played_information[currentFile]['paused'] = True self.reportPlayback() @@ -381,8 +336,8 @@ class Player(xbmc.Player): currentFile = self.currentFile self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2) - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = False + if self.played_information.get(currentFile): + self.played_information[currentFile]['paused'] = False self.reportPlayback() @@ -391,17 +346,15 @@ class Player(xbmc.Player): currentFile = self.currentFile self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2) - if self.played_info.get(currentFile): + if self.played_information.get(currentFile): position = self.xbmcplayer.getTime() - self.played_info[currentFile]['currentPosition'] = position + self.played_information[currentFile]['currentPosition'] = position self.reportPlayback() def onPlayBackStopped( self ): # Will be called when user stops xbmc playing a file self.logMsg("ONPLAYBACK_STOPPED", 2) - xbmcgui.Window(10101).clearProperties() - self.logMsg("Clear playlist properties.") self.stopAll() def onPlayBackEnded( self ): @@ -411,16 +364,14 @@ class Player(xbmc.Player): def stopAll(self): - doUtils = self.doUtils - - if not self.played_info: + if not self.played_information: return - self.logMsg("Played_information: %s" % self.played_info, 1) + self.logMsg("Played_information: %s" % self.played_information, 1) # Process each items - for item in self.played_info: + for item in self.played_information: - data = self.played_info.get(item) + data = self.played_information.get(item) if data: self.logMsg("Item path: %s" % item, 2) @@ -428,61 +379,47 @@ class Player(xbmc.Player): runtime = data['runtime'] currentPosition = data['currentPosition'] - itemid = data['item_id'] + itemId = data['item_id'] refresh_id = data['refresh_id'] currentFile = data['currentfile'] type = data['Type'] playMethod = data['playmethod'] if currentPosition and runtime: - try: - percentComplete = (currentPosition * 10000000) / int(runtime) - except ZeroDivisionError: - # Runtime is 0. - percentComplete = 0 - + percentComplete = (currentPosition * 10000000) / int(runtime) markPlayedAt = float(utils.settings('markPlayed')) / 100 - self.logMsg( - "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.logMsg("Percent complete: %s Mark played at: %s" % (percentComplete, markPlayedAt), 1) + # Prevent manually mark as watched in Kodi monitor > WriteKodiVideoDB().UpdatePlaycountFromKodi() + utils.window('SkipWatched%s' % itemId, "true") self.stopPlayback(data) - # Stop transcoding - if playMethod == "Transcode": - self.logMsg("Transcoding for %s terminated." % itemid, 1) - deviceId = self.clientInfo.getDeviceId() - url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId - doUtils.downloadUrl(url, type="DELETE") + offerDelete = utils.settings('offerDelete') == "true" + offerTypeDelete = False - # Send the delete action to the server. - offerDelete = False + if type == "Episode" and utils.settings('offerDeleteTV') == "true": + offerTypeDelete = True - if type == "Episode" and utils.settings('deleteTV') == "true": - offerDelete = True - elif type == "Movie" and utils.settings('deleteMovies') == "true": - offerDelete = True + elif type == "Movie" and utils.settings('offerDeleteMovies') == "true": + offerTypeDelete = 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") + 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 + if playMethod == "Transcode": + self.logMsg("Transcoding for %s terminated." % itemId, 1) + deviceId = self.clientInfo.getMachineId() + url = "{server}/mediabrowser/Videos/ActiveEncodings?DeviceId=%s" % deviceId + self.doUtils.downloadUrl(url, type="DELETE") - self.played_info.clear() + self.played_information.clear() def stopPlayback(self, data): @@ -492,11 +429,12 @@ class Player(xbmc.Player): currentPosition = data['currentPosition'] positionTicks = int(currentPosition * 10000000) - url = "{server}/emby/Sessions/Playing/Stopped" + url = "{server}/mediabrowser/Sessions/Playing/Stopped" postdata = { 'ItemId': itemId, 'MediaSourceId': itemId, 'PositionTicks': positionTicks } + self.doUtils.downloadUrl(url, postBody=postdata, type="POST") \ No newline at end of file diff --git a/resources/lib/UserClient.py b/resources/lib/UserClient.py index a6562a53..f95f9291 100644 --- a/resources/lib/UserClient.py +++ b/resources/lib/UserClient.py @@ -1,21 +1,22 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import hashlib -import threading +################################################################################################# +# UserClient thread +################################################################################################# import xbmc import xbmcgui import xbmcaddon import xbmcvfs -import artwork -import utils -import clientinfo -import downloadutils +import threading +import hashlib +import json as json -################################################################################################## +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): @@ -23,7 +24,16 @@ class UserClient(threading.Thread): # Borg - multiple instances, shared state _shared_state = {} + clientInfo = ClientInformation() + doUtils = DownloadUtils() + KodiMonitor = KodiMonitor.Kodi_Monitor() + + addonName = clientInfo.getAddonName() + addon = xbmcaddon.Addon() + WINDOW = xbmcgui.Window(10000) + stopClient = False + logLevel = int(addon.getSetting('logLevel')) auth = True retry = 0 @@ -34,25 +44,25 @@ class UserClient(threading.Thread): HasAccess = True AdditionalUser = [] - userSettings = None - - - def __init__(self): + def __init__(self, *args): self.__dict__ = self._shared_state - self.addon = xbmcaddon.Addon() - - self.addonName = clientinfo.ClientInfo().getAddonName() - self.doUtils = downloadutils.DownloadUtils() - self.logLevel = int(utils.settings('logLevel')) - - threading.Thread.__init__(self) + threading.Thread.__init__(self, *args) def logMsg(self, msg, lvl=1): className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + utils.logMsg("%s %s" % (self.addonName, className), str(msg), int(lvl)) + def getUsername(self): + + username = utils.settings('username') + + if (username == ""): + self.logMsg("No username saved.", 2) + return "" + + return username def getAdditionalUsers(self): @@ -61,21 +71,11 @@ class UserClient(threading.Thread): if additionalUsers: 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): try: logLevel = int(utils.settings('logLevel')) - except ValueError: + except: logLevel = 0 return logLevel @@ -83,84 +83,71 @@ class UserClient(threading.Thread): def getUserId(self): username = self.getUsername() - w_userId = utils.window('emby_userId%s' % username) + w_userId = self.WINDOW.getProperty('userId%s' % username) s_userId = utils.settings('userId%s' % username) # Verify the window property - if w_userId: - 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) + if (w_userId != ""): + self.logMsg("Returning userId from WINDOW for username: %s UserId: %s" % (username, w_userId), 2) return w_userId # Verify the settings - elif s_userId: - self.logMsg( - "Returning userId from SETTINGS for username: %s userId: %s" - % (username, s_userId), 2) + elif (s_userId != ""): + self.logMsg("Returning userId from SETTINGS for username: %s userId: %s" % (username, s_userId), 2) return s_userId # No userId found else: - self.logMsg("No userId saved for username: %s." % username, 1) + self.logMsg("No userId saved for username: %s." % username) + return def getServer(self, prefix=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: - # Alternate host - HTTPS = utils.settings('secondhttps') == "true" + HTTPS = utils.settings('secondhttps') host = utils.settings('secondipaddress') port = utils.settings('secondport') - else: - # Original host - HTTPS = utils.settings('https') == "true" - host = utils.settings('ipaddress') - port = utils.settings('port') - + server = host + ":" + port - if not host: + if host == "": self.logMsg("No server information saved.", 2) - return False + return "" # If https is true - if prefix and HTTPS: + if prefix and (HTTPS == "true"): server = "https://%s" % server return server # If https is false - elif prefix and not HTTPS: + elif prefix and (HTTPS == "false"): server = "http://%s" % server return server # If only the host:port is required - elif not prefix: + elif (prefix == False): return server def getToken(self): username = self.getUsername() - w_token = utils.window('emby_accessToken%s' % username) + w_token = self.WINDOW.getProperty('accessToken%s' % username) s_token = utils.settings('accessToken') # Verify the window property - if w_token: - 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) + if (w_token != ""): + self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s" % (username, w_token), 2) return w_token # Verify the settings - elif s_token: - self.logMsg( - "Returning accessToken from SETTINGS for username: %s accessToken: %s" - % (username, s_token), 2) - utils.window('emby_accessToken%s' % username, value=s_token) + elif (s_token != ""): + self.logMsg("Returning accessToken from SETTINGS for username: %s accessToken: %s" % (username, s_token), 2) + self.WINDOW.setProperty('accessToken%s' % username, s_token) return s_token else: - self.logMsg("No token found.", 1) + self.logMsg("No token found.") return "" def getSSLverify(self): @@ -187,63 +174,71 @@ class UserClient(threading.Thread): def setUserPref(self): - doUtils = self.doUtils + player = Player() + 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 - if result.get('PrimaryImageTag'): - utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result, 'Primary')) + self.WINDOW.setProperty("EmbyUserImage",API().getUserArtwork(result,"Primary")) - # Set resume point max - url = "{server}/emby/System/Configuration?format=json" - result = doUtils.downloadUrl(url) + # Load the resume point from Emby and set as setting + url = "{server}/mediabrowser/System/Configuration?format=json" + result = self.doUtils.downloadUrl(url) utils.settings('markPlayed', value=str(result['MaxResumePct'])) + return True + def getPublicUsers(self): server = self.getServer() # Get public Users - url = "%s/emby/Users/Public?format=json" % server + url = "%s/mediabrowser/Users/Public?format=json" % server result = self.doUtils.downloadUrl(url, authenticate=False) - if result != "": - return result + users = [] + + if (result != ""): + users = result else: # Server connection failed return False + return users + def hasAccess(self): - # hasAccess is verified in service.py - url = "{server}/emby/Users?format=json" + + url = "{server}/mediabrowser/Users" result = self.doUtils.downloadUrl(url) - if result == False: - # Access is restricted, set in downloadutils.py via exception - self.logMsg("Access is restricted.", 1) + if result is False: + # Access is restricted + self.logMsg("Access is restricted.") self.HasAccess = False - - elif utils.window('emby_online') != "true": + return + elif self.WINDOW.getProperty('Server_online') != "true": # Server connection failed - pass + return - elif utils.window('emby_serverStatus') == "restricted": - self.logMsg("Access is granted.", 1) + if self.WINDOW.getProperty("Server_status") == "restricted": + self.logMsg("Access is granted.") self.HasAccess = True - utils.window('emby_serverStatus', clear=True) + self.WINDOW.setProperty("Server_status", "") xbmcgui.Dialog().notification("Emby server", "Access is enabled.") + return def loadCurrUser(self, authenticated=False): + WINDOW = self.WINDOW doUtils = self.doUtils username = self.getUsername() - userId = self.getUserId() - + # Only to be used if token exists - self.currUserId = userId + self.currUserId = self.getUserId() self.currServer = self.getServer() self.currToken = self.getToken() self.ssl = self.getSSLverify() @@ -251,21 +246,21 @@ class UserClient(threading.Thread): # Test the validity of current token if authenticated == False: - url = "%s/emby/Users/%s?format=json" % (self.currServer, userId) - utils.window('emby_currUser', value=userId) - utils.window('emby_accessToken%s' % userId, value=self.currToken) + url = "%s/mediabrowser/Users/%s" % (self.currServer, self.currUserId) + WINDOW.setProperty("currUser", username) + WINDOW.setProperty("accessToken%s" % username, self.currToken) result = doUtils.downloadUrl(url) - if result == 401: # Token is no longer valid self.resetClient() return False # Set to windows property - utils.window('emby_currUser', value=userId) - utils.window('emby_accessToken%s' % userId, value=self.currToken) - utils.window('emby_server%s' % userId, value=self.currServer) - utils.window('emby_server_%s' % userId, value=self.getServer(prefix=False)) + WINDOW.setProperty("currUser", username) + WINDOW.setProperty("accessToken%s" % username, self.currToken) + WINDOW.setProperty("server%s" % username, self.currServer) + WINDOW.setProperty("server_%s" % username, self.getServer(prefix=False)) + WINDOW.setProperty("userId%s" % username, self.currUserId) # Set DownloadUtils values doUtils.setUsername(username) @@ -278,194 +273,188 @@ class UserClient(threading.Thread): # Start DownloadUtils session doUtils.startSession() self.getAdditionalUsers() - # Set user preferences in settings + self.currUser = username + # Set user preferences in settings self.setUserPref() - def authenticate(self): - # Get /profile/addon_data - addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') - hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) + + WINDOW = self.WINDOW + addon = self.addon username = self.getUsername() server = self.getServer() + addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') + hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) # If there's no settings.xml - if not hasSettings: - self.logMsg("No settings.xml found.", 1) + if (hasSettings == 0): + self.logMsg("No settings.xml found.") self.auth = False return # If no user information - elif not server or not username: - self.logMsg("Missing server information.", 1) + if (server == "") or (username == ""): + self.logMsg("Missing server information.") self.auth = False return - # If there's a token, load the user - elif self.getToken(): + # If there's a token + if (self.getToken() != ""): result = self.loadCurrUser() if result == False: pass else: - self.logMsg("Current user: %s" % self.currUser, 1) - self.logMsg("Current userId: %s" % self.currUserId, 1) - self.logMsg("Current accessToken: %s" % self.currToken, 2) + self.logMsg("Current user: %s" % self.currUser, 0) + self.logMsg("Current userId: %s" % self.currUserId, 0) + self.logMsg("Current accessToken: %s" % self.currToken, 0) return - ##### AUTHENTICATE USER ##### - users = self.getPublicUsers() password = "" # Find user in list for user in users: - name = user['Name'] + name = user[u'Name'] + userHasPassword = False - if username.decode('utf-8') in name: + if (unicode(username, 'utf-8') in name): + # Verify if user has a password + if (user.get("HasPassword") == True): + userHasPassword = True # If user has password - if user['HasPassword'] == True: - password = xbmcgui.Dialog().input( - heading="Enter password for user: %s" % username, - option=xbmcgui.ALPHANUM_HIDE_INPUT) + if (userHasPassword): + password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) # If password dialog is cancelled - if not password: + if (password == ""): self.logMsg("No password entered.", 0) - utils.window('emby_serverStatus', value="Stop") + self.WINDOW.setProperty("Server_status", "Stop") self.auth = False return break else: # Manual login, user is hidden - password = xbmcgui.Dialog().input( - heading="Enter password for user: %s" % username, - option=xbmcgui.ALPHANUM_HIDE_INPUT) + password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) + sha1 = hashlib.sha1(password) sha1 = sha1.hexdigest() # Authenticate username and password - url = "%s/emby/Users/AuthenticateByName?format=json" % server + url = "%s/mediabrowser/Users/AuthenticateByName?format=json" % server data = {'username': username, 'password': sha1} self.logMsg(data, 2) result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False) + accessToken = None try: - self.logMsg("Auth response: %s" % result, 1) - accessToken = result['AccessToken'] - - except (KeyError, TypeError): - self.logMsg("Failed to retrieve the api key.", 1) - accessToken = None + self.logMsg("Auth_Reponse: %s" % result, 1) + accessToken = result[u'AccessToken'] + except: + pass - if accessToken is not None: + if (result != None and accessToken != None): self.currUser = username xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser) - userId = result['User']['Id'] - utils.settings('accessToken', value=accessToken) - utils.settings('userId%s' % username, value=userId) - self.logMsg("User Authenticated: %s" % accessToken, 1) + userId = result[u'User'][u'Id'] + utils.settings("accessToken", accessToken) + utils.settings("userId%s" % username, userId) + self.logMsg("User Authenticated: %s" % accessToken) self.loadCurrUser(authenticated=True) - utils.window('emby_serverStatus', clear=True) + self.WINDOW.setProperty("Server_status", "") self.retry = 0 + return else: - self.logMsg("User authentication failed.", 1) - utils.settings('accessToken', value="") - utils.settings('userId%s' % username, value="") + self.logMsg("User authentication failed.") + utils.settings("accessToken", "") + utils.settings("userId%s" % username, "") xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.") # Give two attempts at entering password - if self.retry == 2: - self.logMsg( - """Too many retries. You can retry by resetting - 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 + if self.retry == 2: + self.logMsg("Too many retries. You can retry by selecting the option in the addon settings.") + self.WINDOW.setProperty("Server_status", "Stop") + xbmcgui.Dialog().ok("Error connecting", "Failed to authenticate too many times. You can retry by selecting the option in the addon settings.") + self.auth = False + return def resetClient(self): - self.logMsg("Reset UserClient authentication.", 1) username = self.getUsername() - - if self.currToken is not None: + self.logMsg("Reset UserClient authentication.", 1) + if (self.currToken != None): # In case of 401, removed saved token - utils.settings('accessToken', value="") - utils.window('emby_accessToken%s' % username, clear=True) + utils.settings("accessToken", "") + self.WINDOW.setProperty("accessToken%s" % username, "") self.currToken = None self.logMsg("User token has been removed.", 1) self.auth = True self.currUser = None + return + def run(self): - monitor = xbmc.Monitor() - self.logMsg("----===## Starting UserClient ##===----", 0) + self.logMsg("|---- Starting UserClient ----|", 0) - while not monitor.abortRequested(): + while not self.KodiMonitor.abortRequested(): # Verify the log level currLogLevel = self.getLogLevel() if self.logLevel != currLogLevel: # Set new log level self.logLevel = currLogLevel - utils.window('emby_logLevel', value=str(currLogLevel)) self.logMsg("New Log Level: %s" % currLogLevel, 0) + self.WINDOW.setProperty('getLogLevel', str(currLogLevel)) - - status = utils.window('emby_serverStatus') - if status: - # Verify the connection status to server + if (self.WINDOW.getProperty("Server_status") != ""): + status = self.WINDOW.getProperty("Server_status") + if status == "restricted": # Parental control is restricting access self.HasAccess = False elif status == "401": - # Unauthorized access, revoke token - utils.window('emby_serverStatus', value="Auth") + self.WINDOW.setProperty("Server_status", "Auth") + # Revoked token self.resetClient() - if self.auth and (self.currUser is None): - # Try to authenticate user - status = utils.window('emby_serverStatus') - if not status or status == "Auth": - # Set auth flag because we no longer need - # to authenticate the user + if self.auth and (self.currUser == None): + status = self.WINDOW.getProperty("Server_status") + + if (status == "") or (status == "Auth"): self.auth = False self.authenticate() - - if not self.auth and (self.currUser is None): - # If authenticate failed. + if (self.auth == False) and (self.currUser == None): + # Only if there's information found to login server = self.getServer() username = self.getUsername() - status = utils.window('emby_serverStatus') + status = self.WINDOW.getProperty("Server_status") + + # If user didn't enter a password when prompted + if status == "Stop": + pass - # The status Stop is for when user cancelled password dialog. - if server and username and status != "Stop": - # Only if there's information found to login - self.logMsg("Server found: %s" % server, 2) - self.logMsg("Username found: %s" % username, 2) + elif (server != "") and (username != ""): + self.logMsg("Server found: %s" % server) + self.logMsg("Username found: %s" % username) self.auth = True - + # If stopping the client didn't work if self.stopClient == True: - # If stopping the client didn't work break - if monitor.waitForAbort(1): + if self.KodiMonitor.waitForAbort(1): # Abort was requested while waiting. We should exit break self.doUtils.stopSession() - self.logMsg("##===---- UserClient Stopped ----===##", 0) + self.logMsg("|---- UserClient Stopped ----|", 0) def stopClient(self): - # When emby for kodi terminates + # As last resort self.stopClient = True \ No newline at end of file diff --git a/resources/lib/Utils.py b/resources/lib/Utils.py index 83e73e1d..73c49c95 100644 --- a/resources/lib/Utils.py +++ b/resources/lib/Utils.py @@ -1,76 +1,59 @@ -# -*- coding: utf-8 -*- - +################################################################################################# +# utils ################################################################################################# +import xbmc +import xbmcgui +import xbmcaddon +import xbmcvfs +import json +import os import cProfile -import inspect import pstats -import sqlite3 import time +import inspect +import sqlite3 +import string import unicodedata import xml.etree.ElementTree as etree -import xbmc -import xbmcaddon -import xbmcgui -import xbmcvfs +from API import API +from PlayUtils import PlayUtils +from DownloadUtils import DownloadUtils -################################################################################################# +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 - try: - logLevel = int(window('emby_logLevel')) - except ValueError: - logLevel = 0 + logLevel = int(WINDOW.getProperty('getLogLevel')) - if logLevel >= level: - - if logLevel == 2: # inspect.stack() is expensive + if(logLevel >= level): + if(logLevel == 2): # inspect.stack() is expensive try: - xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg)) + xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg)) except UnicodeEncodeError: - xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8'))) + xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) else: try: - xbmc.log("%s -> %s" % (title, msg)) + xbmc.log(title + " -> " + str(msg)) except UnicodeEncodeError: - xbmc.log("%s -> %s" % (title, msg.encode('utf-8'))) + xbmc.log(title + " -> " + str(msg.encode('utf-8'))) -def window(property, value=None, clear=False, windowid=10000): - # Get or set window property - WINDOW = xbmcgui.Window(windowid) +def convertEncoding(data): + #nasty hack to make sure we have a unicode string + try: + 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) - -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": + if type == "music": dbPath = getKodiMusicDBPath() elif type == "texture": dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') @@ -111,140 +94,219 @@ def getKodiMusicDBPath(): "special://database/MyMusic%s.db" % dbVersion.get(kodibuild, "")).decode('utf-8') return dbPath - -def reset(): - - dialog = xbmcgui.Dialog() - - resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") - if resp == 0: - return - - # first stop any db sync - window('emby_shouldStop', value="true") - count = 10 - while window('emby_dbScan') == "true": - logMsg("EMBY", "Sync is running, will retry: %s..." % count) - count -= 1 - if count == 0: - dialog.ok("Warning", "Could not stop the database from running. Try again.") - return - xbmc.sleep(1000) - - # Clean up the playlists - 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 prettifyXml(elem): + rough_string = etree.tostring(elem, "utf-8") + reparsed = minidom.parseString(rough_string) + return reparsed.toprettyxml(indent="\t") def startProfiling(): - pr = cProfile.Profile() pr.enable() - return pr - -def stopProfiling(pr, profileName): +def stopProfiling(pr, profileName): pr.disable() ps = pstats.Stats(pr) - profiles = xbmc.translatePath("%sprofiles/" - % xbmcaddon.Addon().getAddonInfo('profile')).decode('utf-8') - - if not xbmcvfs.exists(profiles): - # Create the profiles folder - xbmcvfs.mkdir(profiles) - - timestamp = time.strftime("%Y-%m-%d %H-%M-%S") - profile = "%s%s_profile_(%s).tab" % (profiles, profileName, timestamp) + addondir = xbmc.translatePath(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile')) - f = open(profile, 'wb') + fileTimeStamp = time.strftime("%Y-%m-%d %H-%M-%S") + tabFileNamepath = os.path.join(addondir, "profiles") + tabFileName = os.path.join(addondir, "profiles" , profileName + "_profile_(" + fileTimeStamp + ").tab") + + if not xbmcvfs.exists(tabFileNamepath): + xbmcvfs.mkdir(tabFileNamepath) + + f = open(tabFileName, 'wb') f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n") for (key, value) in ps.stats.items(): (filename, count, func_name) = key (ccalls, ncalls, total_time, cumulative_time, callers) = value try: - f.write( - "%s\t%s\t%s\t%s\t%s\r\n" - % (ncalls, "{:10.4f}".format(total_time), - "{:10.4f}".format(cumulative_time), func_name, filename)) + f.write(str(ncalls) + "\t" + "{:10.4f}".format(total_time) + "\t" + "{:10.4f}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") except ValueError: - f.write( - "%s\t%s\t%s\t%s\t%s\r\n" - % (ncalls, "{0}".format(total_time), - "{0}".format(cumulative_time), func_name, filename)) + f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") 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): # For video nodes text = text.replace(":", "") @@ -265,223 +327,99 @@ def normalize_nodes(text): return text -def normalize_string(text): - # For theme media, do not modify unless - # modified in TV Tunes - text = text.replace(":", "") - text = text.replace("/", "-") - text = text.replace("\\", "-") - text = text.replace("<", "") - text = text.replace(">", "") - text = text.replace("*", "") - text = text.replace("?", "") - text = text.replace('|', "") - text = text.strip() - # Remove dots from the last character as windows can not have directories - # with dots at the end - text = text.rstrip('.') - text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore') +def reloadProfile(): + # Useful to reload the add-on without restarting Kodi. + profile = xbmc.getInfoLabel('System.ProfileName') + xbmc.executebuiltin("LoadProfile(%s)" % profile) + - return text +def reset(): -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 + WINDOW = xbmcgui.Window( 10000 ) + return_value = xbmcgui.Dialog().yesno("Warning", "Are you sure you want to reset your local Kodi database?") -def sourcesXML(): - # To make Master lock compatible - path = xbmc.translatePath("special://profile/").decode('utf-8') - xmlpath = "%ssources.xml" % path + if return_value == 0: + return - try: - xmlparse = etree.parse(xmlpath) - except: # Document is blank or missing - root = etree.Element('sources') - else: - root = xmlparse.getroot() + # Because the settings dialog could be open + # it seems to override settings so we need to close it before we reset settings. + xbmc.executebuiltin("Dialog.Close(all,true)") + + #cleanup video nodes + 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") + + # Ask if user information should be deleted too. + return_user = xbmcgui.Dialog().yesno("Warning", "Reset all Emby Addon settings?") + if return_user == 1: + WINDOW.setProperty('deletesettings', "true") + 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) + + # 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() - - video = root.find('video') - if video is None: - video = etree.SubElement(root, 'video') - etree.SubElement(video, 'default', attrib={'pathversion': "1"}) - - # Add elements - for i in range(1, 3): - - for source in root.findall('.//path'): - 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 = ( - - "smb://embydummy/dummypath%s/" % i - ) - etree.SubElement(source, 'allowsharing').text = "true" - # Prettify and write to file - try: - indent(root) - except: pass - etree.ElementTree(root).write(xmlpath) - -def passwordsXML(): - - # To add network credentials - path = xbmc.translatePath("special://userdata/").decode('utf-8') - xmlpath = "%spasswords.xml" % path - - try: - xmlparse = etree.parse(xmlpath) - except: # Document is blank or missing - root = etree.Element('passwords') - else: - root = xmlparse.getroot() + + # reset the install run flag + #settings('SyncInstallRunDone', "false") + #WINDOW.setProperty("SyncInstallRunDone", "false") dialog = xbmcgui.Dialog() - credentials = settings('networkCreds') - if credentials: - # Present user with options - option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"]) - - if option < 0: - # User cancelled dialog - return - - elif option == 1: - # User selected remove - iterator = root.getiterator('passwords') - - for paths in iterator: - for path in paths: - if path.find('.//from').text == "smb://%s/" % credentials: - paths.remove(path) - logMsg("EMBY", "Successfully removed credentials for: %s" - % credentials, 1) - etree.ElementTree(root).write(xmlpath) - break - else: - logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1) - - settings('networkCreds', value="") - xbmcgui.Dialog().notification( - 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( - '\n' - '\n\t' - 'Emby %s\n\t' - 'all\n\t' - '\n\t\t' - '%s\n\t' - '' - % (itemtypes.get(mediatype, mediatype), plname, tagname)) - f.close() - logMsg("EMBY", "Successfully added playlist: %s" % tagname) \ No newline at end of file + # Reload would work instead of restart since the add-on is a service. + #dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.') + #WINDOW.clearProperty("SyncDatabaseShouldStop") + #reloadProfile() + dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.') + xbmc.executebuiltin("RestartApp") \ No newline at end of file diff --git a/resources/lib/VideoNodes.py b/resources/lib/VideoNodes.py index 1dc9d5a6..056ee13f 100644 --- a/resources/lib/VideoNodes.py +++ b/resources/lib/VideoNodes.py @@ -1,344 +1,466 @@ -# -*- coding: utf-8 -*- - +################################################################################################# +# VideoNodes - utils to create video nodes listings in kodi for the emby addon ################################################################################################# -import shutil -import xml.etree.ElementTree as etree import xbmc +import xbmcgui import xbmcaddon import xbmcvfs +import json +import os +import shutil +#import common elementree because cElementree has issues with kodi +import xml.etree.ElementTree as etree -import clientinfo -import utils +import Utils as utils -################################################################################################# +from ReadEmbyDB import ReadEmbyDB +WINDOW = xbmcgui.Window(10000) +addonSettings = xbmcaddon.Addon() +language = addonSettings.getLocalizedString -class VideoNodes(object): - - - def __init__(self): - - clientInfo = clientinfo.ClientInfo() - self.addonName = clientInfo.getAddonName() - - self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) - - def logMsg(self, msg, lvl=1): - - 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: - # Folder - root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"}) - - etree.SubElement(root, 'label').text = label - etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png" - - return root - - 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: - dirname = cleantagname +class VideoNodes(): + + + def buildVideoNodeForView(self, tagname, type, windowPropId): + #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')) - path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - nodepath = xbmc.translatePath( - "special://profile/library/video/Emby - %s/" % dirname).decode('utf-8') - - # Verify the video directory - if not xbmcvfs.exists(path): - 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) - - # 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) + 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 + 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: - utils.indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) - - - nodetypes = { - - '1': "all", - '2': "recent", - '3': "recentepisodes", - '4': "inprogress", - '5': "inprogressepisodes", - '6': "unwatched", - '7': "nextupepisodes", - '8': "sets", - '9': "genres", - '10': "random", - '11': "recommended" - } - mediatypes = { - # label according to nodetype per mediatype - 'movies': { - '1': tagname, - '2': 30174, - '4': 30177, - '6': 30189, - '8': 20434, - '9': 135, - '10': 30229, - '11': 30230}, - - 'tvshows': { - '1': tagname, - '2': 30170, - '3': 30175, - '4': 30171, - '5': 30178, - '7': 30179, - '9': 135, - '10': 30229, - '11': 30230}, - } - - nodes = mediatypes[mediatype] - for node in nodes: - - nodetype = nodetypes[node] - nodeXML = "%s%s_%s.xml" % (nodepath, cleantagname, nodetype) - # Get label - stringid = nodes[node] - if node != '1': - label = utils.language(stringid) - if not label: - label = xbmc.getLocalizedString(stringid) - else: - label = stringid - - # Set window properties - if nodetype == "nextupepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname - elif kodiversion == 14 and nodetype == "recentepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname - elif kodiversion == 14 and nodetype == "inprogressepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname - else: - path = "library://video/Emby - %s/%s_%s.xml" % (dirname, cleantagname, nodetype) - windowpath = "ActivateWindow(Video, %s, return)" % path + etree.ElementTree(root).write(nodefile, xml_declaration=True) + except: + etree.ElementTree(root).write(nodefile) + + #create tag node - all items + nodefile = os.path.join(libraryPath, tagname_normalized + "_all.xml") + 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 + nodefile = os.path.join(libraryPath, tagname_normalized + "_recent.xml") + root = etree.Element("node", {"order":"2", "type":"filter"}) + if type == "tvshows": + label = language(30170) + else: + label = language(30174) + 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 + 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 + nodefile = os.path.join(libraryPath, tagname_normalized + "_progress.xml") + root = etree.Element("node", {"order":"3", "type":"filter"}) + if type == "tvshows": + label = language(30171) + else: + label = language(30177) + 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 + #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.inprogress.title" %str(windowPropId),label) + path = "library://video/Emby - %s/%s_progress.xml"%(tagname_normalized,tagname_normalized) + WINDOW.setProperty("Emby.nodes.%s.inprogress.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path) + WINDOW.setProperty("Emby.nodes.%s.inprogress.content" %str(windowPropId),path) + try: + etree.ElementTree(root).write(nodefile, xml_declaration=True) + except: + etree.ElementTree(root).write(nodefile) + + #some movies-only nodes + if type == "movies": - 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"}) - + #unwatched movies + 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: - utils.indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) + etree.ElementTree(root).write(nodefile, xml_declaration=True) + except: + etree.ElementTree(root).write(nodefile) + + #sets + nodefile = os.path.join(libraryPath, tagname_normalized + "_sets.xml") + root = etree.Element("node", {"order":"9", "type":"filter"}) + label = xbmc.getLocalizedString(20434) + etree.SubElement(root, "label").text = label + etree.SubElement(root, "match").text = "all" + etree.SubElement(root, "group").text = "sets" + 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"}) + etree.SubElement(Rule, "value").text = tagname + WINDOW.setProperty("Emby.nodes.%s.sets.title" %str(windowPropId),label) + path = "library://video/Emby - %s/%s_sets.xml"%(tagname_normalized,tagname_normalized) + WINDOW.setProperty("Emby.nodes.%s.sets.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path) + WINDOW.setProperty("Emby.nodes.%s.sets.content" %str(windowPropId),path) + try: + etree.ElementTree(root).write(nodefile, xml_declaration=True) + except: + etree.ElementTree(root).write(nodefile) - def singleNode(self, indexnumber, tagname, mediatype, itemtype): - - tagname = tagname.encode('utf-8') - cleantagname = utils.normalize_nodes(tagname) - nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - nodeXML = "%semby_%s.xml" % (nodepath, cleantagname) - path = "library://video/emby_%s.xml" % (cleantagname) - windowpath = "ActivateWindow(Video, %s, return)" % path + #create tag node - genres + nodefile = os.path.join(libraryPath, tagname_normalized + "_genres.xml") + root = etree.Element("node", {"order":"9", "type":"filter"}) + label = xbmc.getLocalizedString(135) + etree.SubElement(root, "label").text = label + etree.SubElement(root, "match").text = "all" + etree.SubElement(root, "group").text = "genres" + 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"}) + 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) - # Create the video node directory - if not xbmcvfs.exists(nodepath): - # We need to copy over the default items - 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) - - labels = { - - 'Favorite movies': 30180, - 'Favorite tvshows': 30181, - 'channels': 30173 - } - label = utils.language(labels[tagname]) - embynode = "Emby.nodes.%s" % indexnumber - utils.window('%s.title' % embynode, value=label) - utils.window('%s.path' % embynode, value=windowpath) - 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 - try: - utils.indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) - - def clearProperties(self): - - self.logMsg("Clearing nodes properties.", 1) - embyprops = utils.window('Emby.nodes.total') - propnames = [ + etree.ElementTree(root).write(nodefile, xml_declaration=True) + except: + etree.ElementTree(root).write(nodefile) - "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" - ] + #create tag node - random items + nodefile = os.path.join(libraryPath, tagname_normalized + "_random.xml") + root = etree.Element("node", {"order":"10", "type":"filter"}) + label = language(30229) + 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 + #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 + nodefile = os.path.join(libraryPath, tagname_normalized + "_recommended.xml") + root = etree.Element("node", {"order":"10", "type":"filter"}) + 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 + etree.SubElement(root, "path").text = path + etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png" + WINDOW.setProperty("Emby.nodes.%s.nextepisodes.title" %str(windowPropId),label) + path = "library://video/Emby - %s/%s_nextup_episodes.xml"%(tagname_normalized,tagname_normalized) + WINDOW.setProperty("Emby.nodes.%s.nextepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path) + WINDOW.setProperty("Emby.nodes.%s.nextepisodes.content" %str(windowPropId),path) + try: + etree.ElementTree(root).write(nodefile, xml_declaration=True) + except: + etree.ElementTree(root).write(nodefile) + + def buildVideoNodesListing(self): + + try: + + # the library path doesn't exist on all systems + 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 + 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) + + #we build up a listing and set window props for all nodes we created + #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 + #comment marcelveldt: please leave the window props as-is because I will be referencing them in titan skin... + totalNodesCount = 0 + + #build the listing for all views + 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") + if views_shows: + for view in views_shows: + title = view.get('title') + content = view.get('content') + if content == "mixed": + title = "%s - TV Shows" % title + self.buildVideoNodeForView(title, "tvshows", totalNodesCount) + totalNodesCount +=1 - if embyprops: - totalnodes = int(embyprops) - for i in range(totalnodes): - for prop in propnames: - utils.window('Emby.nodes.%s.%s' % (str(i), prop), clear=True) \ No newline at end of file + #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: + etree.ElementTree(root).write(nodefile, xml_declaration=True) + except: + etree.ElementTree(root).write(nodefile) + + #create tag node - favorite shows + 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 + nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"),"emby_favorite_movies.xml") + root = etree.Element("node", {"order":"1", "type":"filter"}) + 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)) + + + except Exception as e: + utils.logMsg("Emby addon","Error while creating videonodes listings, restart required ?") + print e \ No newline at end of file