diff --git a/resources/lib/DownloadUtils.py b/resources/lib/DownloadUtils.py index 04d2da85..5c47144d 100644 --- a/resources/lib/DownloadUtils.py +++ b/resources/lib/DownloadUtils.py @@ -1,32 +1,38 @@ -import xbmc -import xbmcgui -import xbmcaddon +# -*- coding: utf-8 -*- + +################################################################################################## -import requests import json +import requests import logging -import Utils as utils -from ClientInformation import ClientInformation -from requests.packages.urllib3.exceptions import InsecureRequestWarning +import xbmc +import xbmcgui + +import utils +import clientinfo + +################################################################################################## # 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 = ClientInformation() - + clientInfo = clientinfo.ClientInfo() addonName = clientInfo.getAddonName() - addon = xbmcaddon.Addon() - WINDOW = xbmcgui.Window(10000) # Requests session s = None - timeout = 60 + timeout = 30 + def __init__(self): @@ -34,41 +40,44 @@ class DownloadUtils(): def logMsg(self, msg, lvl=1): - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl)) + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, 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}/mediabrowser/Sessions/Capabilities/Full" + url = "{server}/emby/Sessions/Capabilities/Full?format=json" data = { + 'PlayableMediaTypes': "Audio,Video", 'SupportsMediaControl': True, 'SupportedCommands': ( @@ -86,49 +95,57 @@ class DownloadUtils(): } self.logMsg("Capabilities URL: %s" % url, 2) - self.logMsg("PostData: %s" % data, 2) + self.logMsg("Postdata: %s" % data, 2) - try: - self.downloadUrl(url, postBody=data, type="POST") - self.logMsg("Posted capabilities to %s" % self.server, 1) - except: - self.logMsg("Posted capabilities failed.") + self.downloadUrl(url, postBody=data, type="POST") + self.logMsg("Posted capabilities to %s" % self.server, 2) # Attempt at getting sessionId - url = "{server}/mediabrowser/Sessions?DeviceId=%s&format=json" % deviceId - + url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId + result = self.downloadUrl(url) try: - result = self.downloadUrl(url) - self.logMsg("Session: %s" % result, 2) - - sessionId = result[0][u'Id'] - self.logMsg("SessionId: %s" % sessionId) - self.WINDOW.setProperty("sessionId%s" % self.username, sessionId) - except: + sessionId = result[0]['Id'] + + except (KeyError, TypeError): self.logMsg("Failed to retrieve sessionId.", 1) + else: + self.logMsg("Session: %s" % result, 2) + self.logMsg("SessionId: %s" % sessionId, 1) + utils.window('emby_sessionId', value=sessionId) + # Post any permanent additional users - 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) + 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() - 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: - 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) + userId = user['Id'] + url = ( + "{server}/emby/Sessions/%s/Users/%s?format=json" + % (sessionId, userId) + ) + self.downloadUrl(url, postBody={}, type="POST") + def startSession(self): - self.deviceId = self.clientInfo.getMachineId() + self.deviceId = self.clientInfo.getDeviceId() # User is identified from this point # Attach authenticated header to the session @@ -152,7 +169,7 @@ class DownloadUtils(): self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) - self.logMsg("Requests session started on: %s" % self.server) + self.logMsg("Requests session started on: %s" % self.server, 1) def stopSession(self): try: @@ -165,93 +182,116 @@ class DownloadUtils(): clientInfo = self.clientInfo deviceName = clientInfo.getDeviceName() - deviceId = clientInfo.getMachineId() + deviceId = clientInfo.getDeviceId() 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} - - self.logMsg("Header: %s" % header, 2) - return header + auth = ( + 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' + % (userId, deviceName, deviceId, version)) + header = { - def downloadUrl(self, url, postBody=None, type="GET", authenticate=True): + '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 + + def downloadUrl(self, url, postBody=None, type="GET", parameters=None, 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 and append api_key - url = url.replace("{server}", self.server, 1) - url = url.replace("{UserId}", self.userId, 1) + # Replace for the real values + url = url.replace("{server}", self.server) + url = url.replace("{UserId}", self.userId) - self.logMsg("URL: %s" % url, 2) # Prepare request if type == "GET": - r = s.get(url, json=postBody, timeout=timeout) + r = s.get(url, json=postBody, params=parameters, 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.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) + self.userId = utils.window('emby_currUser') + self.server = utils.window('emby_server%s' % self.userId) + self.token = utils.window('emby_accessToken%s' % self.userId) header = self.getHeader() verifyssl = False cert = None # IF user enables ssl verification - 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 + if utils.settings('sslverify') == "true": + verifyssl = True + if utils.settings('sslcert') != "None": + cert = utils.settings('sslcert') - # Replace for the real values and append api_key - url = url.replace("{server}", self.server, 1) - url = url.replace("{UserId}", self.userId, 1) + # Replace for the real values + url = url.replace("{server}", self.server) + url = url.replace("{UserId}", self.userId) - self.logMsg("URL: %s" % url, 2) # Prepare request if type == "GET": - r = requests.get(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl) + r = requests.get(url, + json=postBody, + params=parameters, + headers=header, + timeout=timeout, + cert=cert, + verify=verifyssl) + elif type == "POST": - 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 @@ -263,41 +303,49 @@ class DownloadUtils(): # Prepare request if type == "GET": - r = requests.get(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl) + r = requests.get(url, + json=postBody, + params=parameters, + 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) - # Process the response + ##### THE RESPONSE ##### + self.logMsg(r.url, 2) 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": - pass - else: + if r.headers.get('content-type') != "text/html": self.logMsg("Unable to convert the response for: %s" % url, 1) else: r.raise_for_status() - - return default_link - # TO REVIEW EXCEPTIONS + ##### EXCEPTIONS ##### + except requests.exceptions.ConnectionError as e: # Make the addon aware of status - if WINDOW.getProperty("Server_online") != "false": + if utils.window('emby_online') != "false": self.logMsg("Server unreachable at: %s" % url, 0) self.logMsg(e, 2) - WINDOW.setProperty("Server_online", "false") - pass + utils.window('emby_online', value="false") except requests.exceptions.ConnectTimeout as e: self.logMsg("Server timeout at: %s" % url, 0) @@ -307,29 +355,35 @@ class DownloadUtils(): if r.status_code == 401: # Unauthorized - status = WINDOW.getProperty("Server_status") + status = utils.window('emby_serverStatus') - if 'x-application-error-code' in r.headers: + if 'X-Application-Error-Code' in r.headers: + # Emby server errors if r.headers['X-Application-Error-Code'] == "ParentalControl": # Parental control - access restricted - WINDOW.setProperty("Server_status", "restricted") - xbmcgui.Dialog().notification("Emby server", "Access restricted.", xbmcgui.NOTIFICATION_ERROR, time=5000) + utils.window('emby_serverStatus', value="restricted") + xbmcgui.Dialog().notification( + heading="Emby server", + message="Access restricted.", + icon=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 - admin restricted in some way + # User tried to do something his emby account doesn't allow pass - elif (status == "401") or (status == "Auth"): - pass - - else: - # Tell UserClient token has been revoked. - WINDOW.setProperty("Server_status", "401") + elif status not in ("401", "Auth"): + # Tell userclient token has been revoked. + utils.window('emby_serverStatus', value="401") self.logMsg("HTTP Error: %s" % e, 0) - xbmcgui.Dialog().notification("Error connecting", "Unauthorized.", xbmcgui.NOTIFICATION_ERROR) + xbmcgui.Dialog().notification( + heading="Error connecting", + message="Unauthorized.", + icon=xbmcgui.NOTIFICATION_ERROR) return 401 - elif (r.status_code == 301) or (r.status_code == 302): + elif r.status_code in (301, 302): # Redirects pass elif r.status_code == 400: @@ -344,4 +398,4 @@ class DownloadUtils(): self.logMsg("Unknown error connecting to: %s" % url, 0) self.logMsg(e, 1) - return default_link + return default_link \ No newline at end of file diff --git a/resources/lib/Entrypoint.py b/resources/lib/Entrypoint.py index 95a5a327..588b8d3c 100644 --- a/resources/lib/Entrypoint.py +++ b/resources/lib/Entrypoint.py @@ -1,100 +1,157 @@ -import xbmcaddon -import xbmcplugin +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import os +import sys +import urlparse + import xbmc +import xbmcaddon import xbmcgui import xbmcvfs -import os, sys -import threading -import json -import urllib -import time +import xbmcplugin -WINDOW = xbmcgui.Window(10000) +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 -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 +################################################################################################# -##### 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") +def doPlayback(itemid, dbid): -#### DO RESET AUTH ##### + emby = embyserver.Read_EmbyServer() + item = emby.getItem(itemid) + pbutils.PlaybackUtils(item).play(itemid, dbid) + +##### DO RESET AUTH ##### def resetAuth(): # User tried login and failed too many times - resp = xbmcgui.Dialog().yesno("Warning", "Emby might lock your account if you fail to log in too many times. Proceed anyway?") + resp = xbmcgui.Dialog().yesno( + heading="Warning", + line1=( + "Emby might lock your account if you fail to log in too many times. " + "Proceed anyway?")) if resp == 1: - xbmc.log("Reset login attempts.") - WINDOW.setProperty("Server_status", "Auth") + utils.logMsg("EMBY", "Reset login attempts.", 1) + utils.window('emby_serverStatus', value="Auth") else: xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') -### ADD ADDITIONAL USERS ### +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 ##### def addUser(): - doUtils = DownloadUtils() - clientInfo = ClientInformation() - currUser = WINDOW.getProperty("currUser") - deviceId = clientInfo.getMachineId() + doUtils = downloadutils.DownloadUtils() + clientInfo = clientinfo.ClientInfo() + deviceId = clientInfo.getDeviceId() deviceName = clientInfo.getDeviceName() + userid = utils.window('emby_currUser') + dialog = xbmcgui.Dialog() # Get session - url = "{server}/mediabrowser/Sessions?DeviceId=%s" % deviceId + url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId result = doUtils.downloadUrl(url) try: - sessionId = result[0][u'Id'] - additionalUsers = result[0][u'AdditionalUsers'] + sessionId = result[0]['Id'] + additionalUsers = result[0]['AdditionalUsers'] # Add user to session userlist = {} users = [] - url = "{server}/mediabrowser/Users?IsDisabled=false&IsHidden=false" + url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json" result = doUtils.downloadUrl(url) # pull the list of users for user in result: - name = user[u'Name'] - userId = user[u'Id'] - if currUser not in name: + name = user['Name'] + userId = user['Id'] + if userid != userId: userlist[name] = userId users.append(name) # Display dialog if there's additional users if additionalUsers: - option = xbmcgui.Dialog().select("Add/Remove user from the session", ["Add user", "Remove user"]) + option = 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[u'UserName'] - userId = user[u'UserId'] + name = user['UserName'] + userId = user['UserId'] additionalUserlist[name] = userId additionalUsername.append(name) if option == 1: # User selected Remove user - resp = xbmcgui.Dialog().select("Remove user from the session", additionalUsername) + resp = dialog.select("Remove user from the session", additionalUsername) if resp > -1: selected = additionalUsername[resp] selected_userId = additionalUserlist[selected] - 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) + 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) # clear picture - position = WINDOW.getProperty('EmbyAdditionalUserPosition.' + selected_userId) - WINDOW.clearProperty('EmbyAdditionalUserImage.' + str(position)) + position = utils.window('EmbyAdditionalUserPosition.%s' % selected_userId) + utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) return else: return @@ -111,138 +168,143 @@ def addUser(): return # Subtract any additional users - xbmc.log("Displaying list of users: %s" % users) - resp = xbmcgui.Dialog().select("Add user to the session", users) + utils.logMsg("EMBY", "Displaying list of users: %s" % users) + resp = dialog.select("Add user to the session", users) # post additional user if resp > -1: selected = users[resp] selected_userId = userlist[selected] - 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) + 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) except: - xbmc.log("Failed to add user to session.") - xbmcgui.Dialog().notification("Error", "Unable to add/remove user from the session.", xbmcgui.NOTIFICATION_ERROR) + 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) - 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)) + # 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) - url = "{server}/mediabrowser/Sessions?DeviceId=%s" % deviceId + 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'] result = doUtils.downloadUrl(url) - 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 + utils.window('EmbyAdditionalUserImage.%s' % count, + value=artwork.Artwork().getUserArtwork(result, 'Primary')) + utils.window('EmbyAdditionalUserPosition.%s' % additionaluser['UserId'], value=str(count)) + count +=1 -# THEME MUSIC/VIDEOS +##### THEME MUSIC/VIDEOS ##### def getThemeMedia(): - doUtils = DownloadUtils() - playUtils = PlayUtils() - - currUser = WINDOW.getProperty('currUser') - server = WINDOW.getProperty('server%s' % currUser) + doUtils = downloadutils.DownloadUtils() + dialog = xbmcgui.Dialog() playback = None - library = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library/").decode('utf-8') - # Choose playback method - resp = xbmcgui.Dialog().select("Choose playback method for your themes", ["Direct Play", "Direct Stream"]) + resp = dialog.select("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 - - # Set custom path for user - 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) - 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 = 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 - + 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) + # Set custom path for user + 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) + 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)') + return + # Get every user view Id - 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) - + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + viewids = emby_db.getViews() + embycursor.close() # Get Ids with Theme Videos itemIds = {} - for view in userViews: - url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view + for view in viewids: + url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view result = doUtils.downloadUrl(url) - if result[u'TotalRecordCount'] != 0: - for item in result[u'Items']: - itemId = item[u'Id'] - folderName = item[u'Name'] + if result['TotalRecordCount'] != 0: + for item in result['Items']: + itemId = item['Id'] + folderName = item['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}/mediabrowser/Items/%s/ThemeVideos?format=json" % itemId + url = "{server}/emby/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[u'Items']: + for theme in result['Items']: + putils = playutils.PlayUtils(theme) if playback == "DirectPlay": - playurl = playUtils.directPlay(theme) + playurl = putils.directPlay() else: - playurl = playUtils.directStream(result, server, theme[u'Id'], "ThemeVideo") + playurl = putils.directStream() pathstowrite += ('%s' % playurl.encode('utf-8')) # Check if the item has theme songs and add them - url = "{server}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId + url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId result = doUtils.downloadUrl(url) # May be more than one theme - for theme in result[u'Items']: + for theme in result['Items']: + putils = playutils.PlayUtils(theme) if playback == "DirectPlay": - playurl = playUtils.directPlay(theme) + playurl = putils.directPlay() else: - playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio") + playurl = putils.directStream() pathstowrite += ('%s' % playurl.encode('utf-8')) nfo_file.write( @@ -253,13 +315,13 @@ def getThemeMedia(): # Get Ids with Theme songs musicitemIds = {} - for view in userViews: - url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view + for view in viewids: + url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view result = doUtils.downloadUrl(url) - if result[u'TotalRecordCount'] != 0: - for item in result[u'Items']: - itemId = item[u'Id'] - folderName = item[u'Name'] + if result['TotalRecordCount'] != 0: + for item in result['Items']: + itemId = item['Id'] + folderName = item['Name'] folderName = utils.normalize_string(folderName.encode('utf-8')) musicitemIds[itemId] = folderName @@ -270,25 +332,27 @@ 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}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId + url = "{server}/emby/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[u'Items']: + for theme in result['Items']: + putils = playutils.PlayUtils(theme) if playback == "DirectPlay": - playurl = playUtils.directPlay(theme) + playurl = putils.directPlay() else: - playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio") + playurl = putils.directStream() pathstowrite += ('%s' % playurl.encode('utf-8')) nfo_file.write( @@ -297,398 +361,482 @@ def getThemeMedia(): # Close nfo file nfo_file.close() -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") + dialog.notification( + heading="Emby for Kodi", + message="Themes added!", + icon="special://home/addons/plugin.video.emby/icon.png", + time=1000, + sound=False) ##### BROWSE EMBY CHANNELS ##### -def BrowseChannels(id, folderid=None): +def BrowseChannels(itemid, 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}/mediabrowser/Channels/" + id + "/Items?userid={UserId}&folderid=" + folderid + "&format=json" + 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" else: - 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" + url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid - results = DownloadUtils().downloadUrl(url) - if results: - result = results.get("Items") - if(result == None): - result = [] + result = doUtils.downloadUrl(url) + try: + channels = result['Items'] + except TypeError: + pass + else: + for item in channels: - 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": + API = api.API(item) + itemid = item['Id'] + itemtype = item['Type'] + title = item.get('Name', "Missing Title") + li = xbmcgui.ListItem(title) + + if itemtype == "ChannelFolderItem": isFolder = True else: isFolder = False - item_type = str(type).encode('utf-8') - - 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) - + + channelId = item.get('ChannelId', "") + channelName = item.get('ChannelName', "") + + premieredate = API.getPremiereDate() # Process Genres - genre = API().getGenre(item) - + genre = API.getGenres() # 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 + 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 - playCount = 0 - if(userData != None and userData.get("Played") == True): - playCount = 1 + playcount = userdata['PlayCount'] + if playcount is None: + playcount = 0 + # Populate the details list - details={'title' : tempTitle, - 'channelname' : channelName, - 'plot' : item.get("Overview"), - 'Overlay' : overlay, - 'playcount' : str(playCount)} - - if item.get("Type") == "ChannelVideoItem": + details = { + + 'title': title, + 'channelname': channelName, + 'plot': API.getOverview(), + 'Overlay': str(overlay), + 'playcount': str(playcount) + } + + if itemtype == "ChannelVideoItem": xbmcplugin.setContent(_addon_id, 'movies') - elif item.get("Type") == "ChannelAudioItem": + elif itemtype == "ChannelAudioItem": xbmcplugin.setContent(_addon_id, 'songs') - # 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) + # Populate the extradata list and artwork + pbutils.PlaybackUtils(item).setArtwork(li) + extradata = { - 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'] + '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) - 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) + 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) else: - file = _addon_url + "?id=%s&mode=play"%id - liz.setProperty('IsPlayable', 'true') - xbmcplugin.addDirectoryItem(handle=_addon_id, url=file, listitem=liz) + path = "%s?id=%s&mode=play" % (_addon_url, itemid) + li.setProperty('IsPlayable', 'true') + xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) -##### 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 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": "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. - if json_result.has_key('result') and json_result['result'].has_key('tvshows'): - for item in json_result['result']['tvshows']: - - # 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: - 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])) - -def getInProgressEpisodes(tagname,limit): - count = 0 - #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 - 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])) - +##### LISTITEM SETUP FOR VIDEONODES ##### 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) }) + + 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 = "%.2d" % float(item['episode']) - liz.setInfo( type="Video", infoLabels={ "Episode": item['episode'] }) - + episode = item['episode'] + metadata['Episode'] = episode + if "season" in item: - season = "%.2d" % float(item['season']) - liz.setInfo( type="Video", infoLabels={ "Season": item['season'] }) - + season = item['season'] + metadata['Season'] = season + if season and episode: - episodeno = "s%se%s" %(season,episode) - liz.setProperty("episodeno", episodeno) - + li.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) + if "firstaired" in item: - liz.setInfo( type="Video", infoLabels={ "Premiered": item['firstaired'] }) - - plot = item['plot'] - liz.setInfo( type="Video", infoLabels={ "Plot": plot }) - + metadata['Premiered'] = item['firstaired'] + if "showtitle" in item: - liz.setInfo( type="Video", infoLabels={ "TVshowTitle": item['showtitle'] }) - + metadata['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'] }) + metadata['Rating'] = str(round(float(item['rating']),1)) + if "director" in item: - liz.setInfo( type="Video", infoLabels={ "Director": " / ".join(item['director']) }) + metadata['Director'] = " / ".join(item['director']) + if "writer" in item: - liz.setInfo( type="Video", infoLabels={ "Writer": " / ".join(item['writer']) }) - + metadata['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','')) + 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: - liz.addStreamInfo( key, stream ) + li.addStreamInfo(key, stream) - return liz + 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 = { + + '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 + 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) + # 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 = { + + '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 + 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 RECENT EPISODES FOR TAGNAME ##### +def getRecentEpisodes(tagname, limit): + + count = 0 + # 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 + + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + ##### GET EXTRAFANART FOR LISTITEM ##### def getExtraFanArt(): - itemPath = "" - embyId = "" - #get extrafanart for listitem - this will only be used for skins that actually call the listitem's path + fanart dir... + 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... 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 ("/tvshows/" in itemPath or "/musicvideos/" in itemPath or "/movies/" in itemPath): - embyId = itemPath.split("/")[-2] + if any([x in itemPath for x in ['tvshows', 'musicvideos', 'movies']]): + params = urlparse.parse_qs(itemPath) + embyId = params['id'][0] - utils.logMsg("%s %s" % ("Emby addon", "getExtraFanArt"), "requesting extraFanArt for Id: " + embyId, 1) + utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % 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/" + embyId + "/") + # 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') if not xbmcvfs.exists(fanartDir): - #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) - + # 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 else: - #use existing cached images + utils.logMsg("EMBY", "Found cached backdrop.", 2) + # Use existing cached images dirs, files = xbmcvfs.listdir(fanartDir) - count = 1 for file in files: - 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) + fanartFile = os.path.join(fanartDir, file) + li = xbmcgui.ListItem(file, path=fanartFile) + xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), + url=fanartFile, + listitem=li) except Exception as e: - 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") + utils.logMsg("EMBY", "Error getting extrafanart: %s" % e, 1) + # 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 2d4dc943..1b3f7862 100644 --- a/resources/lib/KodiMonitor.py +++ b/resources/lib/KodiMonitor.py @@ -1,150 +1,195 @@ +# -*- coding: utf-8 -*- + ################################################################################################# -# Kodi Monitor -# Watched events that occur in Kodi, like setting media watched -################################################################################################# + +import json import xbmc import xbmcgui -import xbmcaddon -import json -import Utils as utils -from WriteKodiVideoDB import WriteKodiVideoDB -from ReadKodiDB import ReadKodiDB -from PlayUtils import PlayUtils -from DownloadUtils import DownloadUtils -from PlaybackUtils import PlaybackUtils +import clientinfo +import downloadutils +import embydb_functions as embydb +import playbackutils as pbutils +import utils + +################################################################################################# -class Kodi_Monitor( xbmc.Monitor ): - - WINDOW = xbmcgui.Window(10000) +class KodiMonitor(xbmc.Monitor): - def __init__(self, *args, **kwargs): - xbmc.Monitor.__init__(self) - def logMsg(self, msg, lvl = 1): + 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) - className = self.__class__.__name__ - utils.logMsg("%s %s" % ("EMBY", className), msg, int(lvl)) def onScanStarted(self, library): - utils.window('kodiScan', value="true") - self.logMsg("Kodi library scan running.", 2) - + self.logMsg("Kodi library scan %s running." % library, 2) + if library == "video": + utils.window('emby_kodiScan', value="true") + def onScanFinished(self, library): - 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): + self.logMsg("Kodi library scan %s finished." % library, 2) + if library == "video": + utils.window('emby_kodiScan', clear=True) - WINDOW = self.WINDOW - downloadUtils = DownloadUtils() - #player started playing an item - - if ("Playlist.OnAdd" in method or "Player.OnPlay" in method): + def onNotification(self, sender, method, data): - 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) + doUtils = self.doUtils + if method not in ("Playlist.OnAdd"): + self.logMsg("Method: %s Data: %s" % (method, data), 1) + if data: + data = json.loads(data) + + + if method == "Player.OnPlay": + # Set up report progress for emby playback + item = data.get('item') try: - 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) + kodiid = item['id'] + type = item['type'] + except (KeyError, TypeError): + self.logMsg("Properties already set for item.", 1) else: - 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) + 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.clearProperty(type, item) - - if method == "System.OnWake": - xbmc.sleep(10000) #Allow network to wake up - WINDOW.setProperty("OnWakeSync", "true") + 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) - 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 + 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 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 == "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") elif method == "Playlist.OnClear": - 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 + utils.window('emby_customPlaylist', clear=True, windowid=10101) + #xbmcgui.Window(10101).clearProperties() + self.logMsg("Clear playlist properties.") \ No newline at end of file diff --git a/resources/lib/LibrarySync.py b/resources/lib/LibrarySync.py index f50c88e2..d6d15d8e 100644 --- a/resources/lib/LibrarySync.py +++ b/resources/lib/LibrarySync.py @@ -1,1023 +1,181 @@ -################################################################################################# -# LibrarySync -################################################################################################# +# -*- coding: utf-8 -*- + +################################################################################################## + +import sqlite3 +import threading +from datetime import datetime, timedelta, time 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 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 +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 -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 = {} - KodiMonitor = KodiMonitor.Kodi_Monitor() - clientInfo = ClientInformation() - - addonName = clientInfo.getAddonName() + stop_thread = False + suspend_thread = False + # Track websocketclient updates + addedItems = [] updateItems = [] userdataItems = [] removeItems = [] - forceUpdate = False + forceLibraryUpdate = False + refresh_views = False - def __init__(self, *args): + + def __init__(self): self.__dict__ = self._shared_state - threading.Thread.__init__(self, *args) + 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) def logMsg(self, msg, lvl=1): className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) - - 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" + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - ### 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 = urllib2.quote(lastSync) + 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": - url = "{server}/Emby.Kodi.SyncQueue/{UserId}/GetItems?LastUpdateDT=" + lastSync + "&format=json" - utils.logMsg("Sync Database", "Incremental Sync Get Items URL : " + url, 0) + # 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() - 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): + 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() + + return completed + + def fastSync(self): + + 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) + + try: + processlist = { - 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) + 'added': result['ItemsAdded'], + 'update': result['ItemsUpdated'], + 'userdata': result['UserDataChanged'], + 'remove': result['ItemsRemoved'] + } + + except (KeyError, TypeError): + self.logMsg("Failed to retrieve latest updates using fast sync.", 1) + return False - #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) + else: + self.logMsg("Fast sync changes: %s" % result, 1) + for action in processlist: + self.triage_items(action, processlist[action]) + return True - try: - 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() - - finally: - WINDOW.setProperty("SyncDatabaseRunning", "false") - utils.logMsg("Sync DB", "syncDatabase Exiting", 0) + def saveLastSync(self): + # Save last sync time + overlap = 2 - 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") + 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") - 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') + 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) - 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) + 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) - 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') + finally: + utils.settings('LastIncrementalSync', value=lastSync) - 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 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 def dbCommit(self, connection): - # Central commit, will verify if Kodi database - kodidb_scan = utils.window('kodiScan') == "true" + # Central commit, verifies if Kodi database update is running + kodidb_scan = utils.window('emby_kodiScan') == "true" while kodidb_scan: - - self.logMsg("Kodi scan running. Waiting...", 1) - kodidb_scan = utils.window('kodiScan') == "true" - if self.KodiMonitor.waitForAbort(1): + 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): # Abort was requested while waiting. We should exit self.logMsg("Commit unsuccessful.", 1) break @@ -1025,32 +183,1027 @@ class LibrarySync(threading.Thread): connection.commit() self.logMsg("Commit successful.", 1) - def updateLibrary(self, type): + 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" - self.logMsg("Updating %s library." % type, 1) - utils.window('kodiScan', value="true") - xbmc.executebuiltin('UpdateLibrary(%s)' % type) + utils.window('emby_dbScan', value="true") + # Add sources + utils.sourcesXML() - def ShouldStop(self): - - if(xbmc.abortRequested): - return True - - if(WINDOW.getProperty("SyncDatabaseShouldStop") == "true"): - return True - - return False + 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() - def checkDBVersion(self, currVersion, minVersion): - currMajor, currMinor, currPatch = currVersion.split(".") - minMajor, minMinor, minPatch = minVersion.split(".") + # 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: + + musicconn = utils.kodiSQL('music') + musiccursor = musicconn.cursor() + + startTime = datetime.now() + completed = self.music(embycursor, musiccursor, pDialog, compare=manualrun) + if not completed: + + utils.window('emby_dbScan', clear=True) + if pDialog: + pDialog.close() + + 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() + + 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(".") + if currMajor > minMajor: return True - elif currMajor == minMajor and currMinor > minMinor: - return True - elif currMajor == minMajor and currMinor == minMinor and currPatch >= minPatch: + elif currMajor == minMajor and (currMinor > minMinor or + (currMinor == minMinor and currPatch >= minPatch)): return True else: + # Database out of date. return False def run(self): @@ -1058,134 +1211,128 @@ class LibrarySync(threading.Thread): try: self.run_internal() except Exception as e: - 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.") + xbmcgui.Dialog().ok( + heading="Emby for Kodi", + line1=( + "Library sync thread has exited! " + "You should restart Kodi now. " + "Please report this on the forum.")) raise def run_internal(self): startupComplete = False - kodiProfile = xbmc.translatePath("special://profile") + monitor = self.monitor - self.logMsg("--- Starting Library Sync Thread ---", 0) + self.logMsg("---===### Starting LibrarySync ###===---", 0) - while not self.KodiMonitor.abortRequested(): + while not monitor.abortRequested(): - # 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): + # In the event the server goes offline + while self.suspend_thread: + # Set in service.py + if monitor.waitForAbort(5): # Abort was requested while waiting. We should exit break - # 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: + if (utils.window('emby_dbCheck') != "true" and + utils.settings('SyncInstallRunDone') == "true"): - # 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')) + # 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") + + + 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'))) break - # Run full sync - self.logMsg("DB Version: " + utils.settings("dbCreatedWithVersion"), 0) - self.logMsg("Doing_Db_Sync: syncDatabase (Started)", 1) + # Run start up sync + self.logMsg("Db version: %s" % utils.settings('dbCreatedWithVersion'), 0) + self.logMsg("SyncDatabase (started)", 1) startTime = datetime.now() - libSync = self.FullLibrarySync() + librarySync = self.startSync() elapsedTime = datetime.now() - startTime - self.logMsg("Doing_Db_Sync: syncDatabase (Finished in: %s) %s" % (str(elapsedTime).split('.')[0], libSync), 1) + 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 - if libSync: - startupComplete = True + # Process updates + if utils.window('emby_dbScan') != "true": + self.incrementalSync() - # 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 (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) - - 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) + if self.stop_thread: + # Set in service.py + self.logMsg("Service terminated thread.", 2) break - if self.KodiMonitor.waitForAbort(1): + if monitor.waitForAbort(1): # Abort was requested while waiting. We should exit break - self.logMsg("--- Library Sync Thread stopped ---", 0) + self.logMsg("###===--- LibrarySync Stopped ---===###", 0) - def suspendClient(self): - self.suspendClient = True - self.logMsg("--- Library Sync Thread paused ---", 0) + def stopThread(self): + self.stop_thread = True + self.logMsg("Ending thread...", 2) - def resumeClient(self): - self.suspendClient = False - self.logMsg("--- Library Sync Thread resumed ---", 0) \ No newline at end of file + 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 diff --git a/resources/lib/PlayUtils.py b/resources/lib/PlayUtils.py index e8b9be58..0a74690b 100644 --- a/resources/lib/PlayUtils.py +++ b/resources/lib/PlayUtils.py @@ -6,227 +6,279 @@ import xbmc import xbmcgui import xbmcvfs -from ClientInformation import ClientInformation -import Utils as utils +import clientinfo +import utils ################################################################################################# -class PlayUtils(): - clientInfo = ClientInformation() - addonName = clientInfo.getAddonName() +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) def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) - def getPlayUrl(self, server, id, result): + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + - 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") + def getPlayUrl(self): - 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") + item = self.item + playurl = None - 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 + 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") - return playurl.encode('utf-8') + elif self.isDirectPlay(): + 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") - def isDirectPlay(self, result, dialog = False): - # Requirements for Direct play: - # FileSystem, Accessible path + 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 if utils.settings('playFromStream') == "true": - # User forcing to play via HTTP instead of SMB - self.logMsg("Can't direct play: Play from HTTP is enabled.", 1) + # User forcing to play via HTTP + self.logMsg("Can't direct play, play from HTTP enabled.", 1) return False - # Avoid H265 1080p if (utils.settings('transcodeH265') == "true" and - result['MediaSources'][0]['Name'].startswith("1080P/H265")): + result['MediaSources'][0]['Name'].startswith("1080P/H265")): + # Avoid H265 1080p self.logMsg("Option to transcode 1080P/H265 enabled.", 1) return False - canDirectPlay = result['MediaSources'][0]['SupportsDirectPlay'] - # Make sure it's supported by server + canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay'] + # Make sure direct play is supported by the server if not canDirectPlay: - self.logMsg("Can't direct play: Server does not allow or support it.", 1) + self.logMsg("Can't direct play, server doesn't allow/support it.", 1) return False - 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) + 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) + 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 - def directPlay(self, result): + return True + + def directPlay(self): + + item = self.item try: - playurl = result['MediaSources'][0]['Path'] - except KeyError: - playurl = result['Path'] + playurl = item['MediaSources'][0]['Path'] + except (IndexError, KeyError): + playurl = item['Path'] - if 'VideoType' in result: + if item.get('VideoType'): # Specific format modification - if 'Dvd' in result['VideoType']: + type = item['VideoType'] + + if type == "Dvd": playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif 'BluRay' in result['VideoType']: + elif type == "Bluray": playurl = "%s/BDMV/index.bdmv" % playurl - # 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://") + # Assign network protocol + if playurl.startswith('\\\\'): + 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): - def isDirectStream(self, result): - # Requirements for Direct stream: - # FileSystem or Remote, BitRate, supported encoding + 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 - # Avoid H265 1080p if (utils.settings('transcodeH265') == "true" and - result['MediaSources'][0]['Name'].startswith("1080P/H265")): + result['MediaSources'][0]['Name'].startswith("1080P/H265")): + # Avoid H265 1080p self.logMsg("Option to transcode 1080P/H265 enabled.", 1) return False - canDirectStream = result['MediaSources'][0]['SupportsDirectStream'] - # Make sure it's supported by server + # Requirement: BitRate, supported encoding + canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] + # Make sure the server supports it if not canDirectStream: return False - 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) + # Verify the bitrate + if not self.isNetworkSufficient(): + self.logMsg("The network speed is insufficient to direct stream file.", 1) return False return True - - def directStream(self, result, server, id, type = "Video"): - if result['Path'].endswith('.strm'): + def directStream(self): + + item = self.item + server = self.server + + itemid = item['Id'] + type = item['Type'] + + if 'Path' in item and item['Path'].endswith('.strm'): # Allow strm loading when direct streaming - playurl = self.directPlay(result) - return playurl - - if "ThemeVideo" in type: - playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) + 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) - 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): - def isTranscoding(self, result): - # Last resort, no requirements - # BitRate - canTranscode = result['MediaSources'][0]['SupportsTranscoding'] - # Make sure it's supported by server + 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 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, result, server, id): + def transcoding(self): - if result['Path'].endswith('.strm'): + item = self.item + + if 'Path' in item and item['Path'].endswith('.strm'): # Allow strm loading when transcoding - 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 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) + playurl = self.directPlay() 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): + 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)) + + return playurl + + def getBitrate(self): + # get the addon video quality - videoQuality = utils.settings('videoBitRate') + videoQuality = utils.settings('videoBitrate') bitrate = { '0': 664, @@ -252,35 +304,8 @@ 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 - # 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): + def audioSubsPref(self, url): # For transcoding only # Present the list of audio to select from audioStreamsList = {} @@ -292,15 +317,21 @@ class PlayUtils(): selectSubsIndex = "" playurlprefs = "%s" % url - mediaStream = mediaSources[0].get('MediaStreams') - for stream in mediaStream: + item = self.item + try: + mediasources = item['MediaSources'][0] + mediastreams = mediasources['MediaStreams'] + except (TypeError, KeyError, IndexError): + return + + for stream in mediastreams: # Since Emby returns all possible tracks together, have to sort them. index = stream['Index'] type = stream['Type'] if 'Audio' in type: codec = stream['Codec'] - channelLayout = stream['ChannelLayout'] + channelLayout = stream.get('ChannelLayout', "") try: track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) @@ -312,6 +343,8 @@ class PlayUtils(): audioStreams.append(track) elif 'Subtitle' in type: + if stream['IsExternal']: + continue try: track = "%s - %s" % (index, stream['Language']) except: @@ -336,7 +369,7 @@ class PlayUtils(): selectAudioIndex = audioStreamsList[selected] playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex else: # User backed out of selection - playurlprefs += "&AudioStreamIndex=%s" % mediaSources[0]['DefaultAudioStreamIndex'] + playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] else: # There's only one audiotrack. selectAudioIndex = audioStreamsList[audioStreams[0]] playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex @@ -352,7 +385,7 @@ class PlayUtils(): selectSubsIndex = subtitleStreamsList[selected] playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex else: # User backed out of selection - playurlprefs += "&SubtitleStreamIndex=%s" % mediaSources[0].get('DefaultSubtitleStreamIndex', "") + playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") # Get number of channels for selected audio track audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) diff --git a/resources/lib/PlaybackUtils.py b/resources/lib/PlaybackUtils.py index 1172e52b..affa2b81 100644 --- a/resources/lib/PlaybackUtils.py +++ b/resources/lib/PlaybackUtils.py @@ -2,71 +2,69 @@ ################################################################################################# -import datetime -import json as json +import json import sys import xbmc -import xbmcaddon -import xbmcplugin import xbmcgui +import xbmcplugin -from API import API -from DownloadUtils import DownloadUtils -from PlayUtils import PlayUtils -from ClientInformation import ClientInformation -import Utils as utils +import api +import artwork +import clientinfo +import downloadutils +import playutils as putils +import playlist +import read_embyserver as embyserver +import utils ################################################################################################# + class PlaybackUtils(): - clientInfo = ClientInformation() - doUtils = DownloadUtils() - api = API() + + def __init__(self, item): - addon = xbmcaddon.Addon() - language = addon.getLocalizedString - addonName = clientInfo.getAddonName() + self.item = item + self.API = api.API(self.item) + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.doUtils = downloadutils.DownloadUtils() + + self.userid = utils.window('emby_currUser') + self.server = utils.window('emby_server%s' % self.userid) + + self.artwork = artwork.Artwork() + self.emby = embyserver.Read_EmbyServer() + self.pl = playlist.Playlist() def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) - def PLAY(self, result, setup = "service"): + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - self.logMsg("PLAY Called", 1) - api = self.api + def play(self, itemid, dbid=None): + + self.logMsg("Play called.", 1) + doUtils = self.doUtils - username = utils.window('currUser') - server = utils.window('server%s' % username) + item = self.item + API = self.API + listitem = xbmcgui.ListItem() + playutils = putils.PlayUtils(item) - id = result['Id'] - userdata = result['UserData'] - # Get the playurl - direct play, direct stream or transcoding - playurl = PlayUtils().getPlayUrl(server, id, result) - listItem = xbmcgui.ListItem() + playurl = playutils.getPlayUrl() + if not playurl: + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) - if utils.window('playurlFalse') == "true": - # Playurl failed - set in PlayUtils.py - utils.window('playurlFalse', clear=True) - self.logMsg("Failed to retrieve the playback path/url or dialog was cancelled.", 1) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem) - - - ############### -- SETUP MAIN ITEM ################ - - # Set listitem and properties for main item - self.logMsg("Returned playurl: %s" % playurl, 1) - listItem.setPath(playurl) - self.setProperties(playurl, result, listItem) - - mainArt = API().getArtwork(result, "Primary") - listItem.setThumbnailImage(mainArt) - listItem.setIconImage(mainArt) - + if dbid is None: + # Item is not in Kodi database + listitem.setPath(playurl) + self.setProperties(playurl, listitem) + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) ############### ORGANIZE CURRENT PLAYLIST ################ @@ -74,58 +72,45 @@ class PlaybackUtils(): playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) startPos = max(playlist.getposition(), 0) # Can return -1 sizePlaylist = playlist.size() - - 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) + propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true" + introsPlaylist = False + dummyPlaylist = False + self.logMsg("Playlist start position: %s" % startPos, 1) + self.logMsg("Playlist plugin position: %s" % currentPosition, 1) + self.logMsg("Playlist size: %s" % sizePlaylist, 1) ############### RESUME POINT ################ - # Resume point 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) + userdata = API.getUserData() + seektime = API.adjustResume(userdata['Resume']) # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. if not propertiesPlayback: - utils.window('propertiesPlayback', value="true") - self.logMsg("Setting up properties in playlist.") + 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 ############### -- CHECK FOR INTROS ################ - if utils.settings('disableCinema') == "false" and not seekTime: + if utils.settings('enableCinema') == "true" and not seektime: # if we have any play them when the movie/show is not being resumed - url = "{server}/mediabrowser/Users/{UserId}/Items/%s/Intros?format=json&ImageTypeLimit=1&Fields=Etag" % id + url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid intros = doUtils.downloadUrl(url) if intros['TotalRecordCount'] != 0: @@ -141,17 +126,15 @@ 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 - self.setProperties(introPlayurl, intro, introListItem) - self.setListItemProps(server, introId, introListItem, intro) - - playlist.add(introPlayurl, introListItem, index=currentPosition) + pbutils = PlaybackUtils(intro) + pbutils.setProperties(introPlayurl, introListItem) + + self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) introsPlaylist = True currentPosition += 1 @@ -159,109 +142,126 @@ 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.setListItemProps(server, id, listItem, result) - playlist.add(playurl, listItem, index=currentPosition) - + self.pl.addtoPlaylist(dbid, item['Type'].lower()) + # Ensure that additional parts are played after the main item currentPosition += 1 - ############### -- CHECK FOR ADDITIONAL PARTS ################ - if result.get('PartCount'): + if item.get('PartCount'): # Only add to the playlist after intros have played - partcount = result['PartCount'] - url = "{server}/mediabrowser/Videos/%s/AdditionalParts" % id + partcount = item['PartCount'] + url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid 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 - self.setProperties(additionalPlayurl, part, additionalListItem) - self.setListItemProps(server, partId, additionalListItem, part) + pbutils = PlaybackUtils(part) + pbutils.setProperties(additionalPlayurl, additionalListItem) + pbutils.setArtwork(additionalListItem) playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) + self.pl.verifyPlaylist() currentPosition += 1 - - ############### 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 + 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) # We just skipped adding properties. Reset flag for next time. elif propertiesPlayback: self.logMsg("Resetting properties playback flag.", 2) - utils.window('propertiesPlayback', clear=True) + utils.window('emby_playbackProps', clear=True, windowid=10101) + #self.pl.verifyPlaylist() + ########## SETUP MAIN ITEM ########## - self.verifyPlaylist() + # For transcoding only, ask for audio/subs pref + if utils.window('emby_%s.playmethod' % playurl) == "Transcode": + playurl = playutils.audioSubsPref(playurl) + utils.window('emby_%s.playmethod' % playurl, value="Transcode") + + listitem.setPath(playurl) + self.setProperties(playurl, listitem) ############### PLAYBACK ################ - - if not homeScreen and not introsPlaylist: - - self.logMsg("Processed as a single item.", 1) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem) - 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) + 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) else: self.logMsg("Play as a regular item.", 1) - xbmc.Player().play(playlist, startpos=startPos) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - def verifyPlaylist(self): - - playlistitems = '{"jsonrpc": "2.0", "method": "Playlist.GetItems", "params": { "playlistid": 1 }, "id": 1}' - items = xbmc.executeJSONRPC(playlistitems) - self.logMsg(items, 2) + def setProperties(self, playurl, listitem): + # Set all properties necessary for plugin path playback + item = self.item + itemid = item['Id'] + itemtype = item['Type'] - def removeFromPlaylist(self, pos): + 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) - playlistremove = '{"jsonrpc": "2.0", "method": "Playlist.Remove", "params": { "playlistid": 1, "position": %d }, "id": 1}' % pos - result = xbmc.executeJSONRPC(playlistremove) - self.logMsg(result, 1) + if itemtype == "Episode": + utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId')) + else: + utils.window('%s.refreshid' % embyitem, value=itemid) + # 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) - def externalSubs(self, id, playurl, mediaSources): + self.setArtwork(listitem) + + def externalSubs(self, playurl): - username = utils.window('currUser') - server = utils.window('server%s' % username) externalsubs = [] mapping = {} - mediaStream = mediaSources[0].get('MediaStreams') + item = self.item + itemid = item['Id'] + try: + mediastreams = item['MediaSources'][0]['MediaStreams'] + except (TypeError, KeyError, IndexError): + return + kodiindex = 0 - for stream in mediaStream: - + for stream in mediastreams: + 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 "Subtitle" in stream['Type'] and stream['IsExternal'] and stream['IsTextSubtitleStream']: - - playmethod = utils.window("%splaymethod" % playurl) + if (stream['Type'] == "Subtitle" and + stream['IsExternal'] and stream['IsTextSubtitleStream']): - 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) + # Direct stream + url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" + % (self.server, itemid, itemid, index)) # map external subtitles for mapping mapping[kodiindex] = index @@ -269,69 +269,79 @@ class PlaybackUtils(): kodiindex += 1 mapping = json.dumps(mapping) - utils.window('%sIndexMapping' % playurl, value=mapping) + utils.window('emby_%s.indexMapping' % playurl, value=mapping) return externalsubs - - def setProperties(self, playurl, result, listItem): - # Set runtimeticks, type, refresh_id and item_id - id = result.get('Id') - type = result.get('Type', "") - - utils.window("%sruntimeticks" % playurl, value=str(result.get('RunTimeTicks'))) - utils.window("%stype" % playurl, value=type) - utils.window("%sitem_id" % playurl, value=id) - - if type == "Episode": - utils.window("%srefresh_id" % playurl, value=result.get('SeriesId')) - else: - utils.window("%srefresh_id" % playurl, value=id) - - if utils.window("%splaymethod" % playurl) != "Transcode": - # Only for direct play and direct stream - # Append external subtitles to stream - subtitleList = self.externalSubs(id, playurl, result['MediaSources']) - listItem.setSubtitles(subtitleList) - - def setArt(self, list, name, path): - - if name in ("thumb", "fanart_image", "small_poster", "tiny_poster", "medium_landscape", "medium_poster", "small_fanartimage", "medium_fanartimage", "fanart_noindicators"): - list.setProperty(name, path) - else: - list.setArt({name:path}) - - return list - - def setListItemProps(self, server, id, listItem, result): + def setArtwork(self, listItem): # Set up item and item info - api = self.api + item = self.item + artwork = self.artwork - type = result.get('Type') - people = api.getPeople(result) - studios = api.getStudios(result) + allartwork = artwork.getAllArtwork(item, parentInfo=True) + # Set artwork for listitem + arttypes = { + + 'poster': "Primary", + 'tvshow.poster': "Primary", + 'clearart': "Art", + 'tvshow.clearart': "Art", + 'clearlogo': "Logo", + 'tvshow.clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb" + } + for arttype in arttypes: + + art = arttypes[arttype] + if art == "Backdrop": + try: # Backdrop is a list, grab the first backdrop + self.setArtProp(listItem, arttype, allartwork[art][0]) + except: pass + else: + self.setArtProp(listItem, arttype, allartwork[art]) + + def setArtProp(self, listItem, arttype, path): + + if arttype in ( + 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', + 'medium_landscape', 'medium_poster', 'small_fanartimage', + 'medium_fanartimage', 'fanart_noindicators'): + + listItem.setProperty(arttype, path) + else: + listItem.setArt({arttype: path}) + + def setListItem(self, listItem): + + item = self.item + type = item['Type'] + API = self.API + people = API.getPeople() + studios = API.getStudios() metadata = { - 'title': result.get('Name', "Missing name"), - 'year': result.get('ProductionYear'), - 'plot': api.getOverview(result), + 'title': item.get('Name', "Missing name"), + 'year': item.get('ProductionYear'), + 'plot': API.getOverview(), 'director': people.get('Director'), 'writer': people.get('Writer'), - 'mpaa': api.getMpaa(result), - 'genre': api.getGenre(result), + 'mpaa': API.getMpaa(), + 'genre': " / ".join(item['Genres']), 'studio': " / ".join(studios), - 'aired': api.getPremiereDate(result), - 'rating': result.get('CommunityRating'), - 'votes': result.get('VoteCount') + 'aired': API.getPremiereDate(), + 'rating': item.get('CommunityRating'), + 'votes': item.get('VoteCount') } if "Episode" in type: # Only for tv shows - thumbId = result.get('SeriesId') - season = result.get('ParentIndexNumber', -1) - episode = result.get('IndexNumber', -1) - show = result.get('SeriesName', "") + thumbId = item.get('SeriesId') + season = item.get('ParentIndexNumber', -1) + episode = item.get('IndexNumber', -1) + show = item.get('SeriesName', "") metadata['TVShowTitle'] = show metadata['season'] = season @@ -340,123 +350,4 @@ class PlaybackUtils(): listItem.setProperty('IsPlayable', 'true') listItem.setProperty('IsFolder', 'false') listItem.setLabel(metadata['title']) - 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 + listItem.setInfo('video', infoLabels=metadata) \ No newline at end of file diff --git a/resources/lib/Player.py b/resources/lib/Player.py index 0b760a88..b6d25e19 100644 --- a/resources/lib/Player.py +++ b/resources/lib/Player.py @@ -2,45 +2,47 @@ ################################################################################################# -import json as json +import json import xbmc import xbmcgui -from DownloadUtils import DownloadUtils -from WebSocketClient import WebSocketThread -from ClientInformation import ClientInformation -from LibrarySync import LibrarySync -import Utils as utils +import utils +import clientinfo +import downloadutils +import kodidb_functions as kodidb +import websocket_client as wsc ################################################################################################# -class Player( xbmc.Player ): + +class Player(xbmc.Player): # Borg - multiple instances, shared state _shared_state = {} - xbmcplayer = xbmc.Player() - doUtils = DownloadUtils() - clientInfo = ClientInformation() - ws = WebSocketThread() - librarySync = LibrarySync() - - addonName = clientInfo.getAddonName() - - played_information = {} + played_info = {} playStats = {} currentFile = None - def __init__(self, *args): + + def __init__(self): 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, int(lvl)) + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + def GetPlayStats(self): return self.playStats @@ -74,39 +76,50 @@ class Player( xbmc.Player ): self.currentFile = currentFile # We may need to wait for info to be set in kodi monitor - itemId = utils.window("%sitem_id" % currentFile) + itemId = utils.window("emby_%s.itemid" % currentFile) tryCount = 0 while not itemId: xbmc.sleep(200) - itemId = utils.window("%sitem_id" % currentFile) + itemId = utils.window("emby_%s.itemid" % currentFile) if tryCount == 20: # try 20 times or about 10 seconds 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. - runtime = utils.window("%sruntimeticks" % currentFile) - refresh_id = utils.window("%srefresh_id" % currentFile) - playMethod = utils.window("%splaymethod" % currentFile) - itemType = utils.window("%stype" % currentFile) + 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") + seekTime = xbmcplayer.getTime() - # Get playback volume - volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' - result = xbmc.executeJSONRPC(volume_query) + volume_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Application.GetProperties", + "params": { + + "properties": ["volume", "muted"] + } + } + result = xbmc.executeJSONRPC(json.dumps(volume_query)) result = json.loads(result) result = result.get('result') - + volume = result.get('volume') muted = result.get('muted') # Postdata structure to send to Emby server - url = "{server}/mediabrowser/Sessions/Playing" + url = "{server}/emby/Sessions/Playing" postdata = { 'QueueableMediaTypes': "Video", @@ -123,12 +136,22 @@ 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 - track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' - result = xbmc.executeJSONRPC(track_query) + tracks_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Player.GetProperties", + "params": { + + "playerid": 1, + "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] + } + } + result = xbmc.executeJSONRPC(json.dumps(tracks_query)) result = json.loads(result) result = result.get('result') @@ -155,9 +178,9 @@ class Player( xbmc.Player ): # Number of audiotracks to help get Emby Index audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = utils.window("%sIndexMapping" % currentFile) + mapping = utils.window("%s.indexMapping" % embyitem) - if mapping: # Set in PlaybackUtils.py + if mapping: # Set in playbackutils.py self.logMsg("Mapping for external subtitles index: %s" % mapping, 2) externalIndex = json.loads(mapping) @@ -167,7 +190,8 @@ class Player( xbmc.Player ): postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] else: # Internal subtitle currently selected - postdata['SubtitleStreamIndex'] = indexSubs - len(externalIndex) + audioTracks + 1 + subindex = indexSubs - len(externalIndex) + audioTracks + 1 + postdata['SubtitleStreamIndex'] = subindex else: # Direct paths enabled scenario or no external subtitles set postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 @@ -184,7 +208,7 @@ class Player( xbmc.Player ): runtime = int(runtime) except ValueError: runtime = xbmcplayer.getTotalTime() - self.logMsg("Runtime is missing, grabbing runtime from Kodi player: %s" % runtime, 1) + self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1) # Save data map for updates and position calls data = { @@ -200,8 +224,8 @@ class Player( xbmc.Player ): 'currentPosition': int(seekTime) } - self.played_information[currentFile] = data - self.logMsg("ADDING_FILE: %s" % self.played_information, 1) + self.played_info[currentFile] = data + self.logMsg("ADDING_FILE: %s" % self.played_info, 1) # log some playback stats '''if(itemType != None): @@ -225,7 +249,7 @@ class Player( xbmc.Player ): # Get current file currentFile = self.currentFile - data = self.played_information.get(currentFile) + data = self.played_info.get(currentFile) # only report playback if emby has initiated the playback (item_id has value) if data: @@ -239,15 +263,23 @@ class Player( xbmc.Player ): # Get playback volume - volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' - result = xbmc.executeJSONRPC(volume_query) + volume_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Application.GetProperties", + "params": { + + "properties": ["volume", "muted"] + } + } + result = xbmc.executeJSONRPC(json.dumps(volume_query)) result = json.loads(result) result = result.get('result') volume = result.get('volume') muted = result.get('muted') - # Postdata for the websocketclient report postdata = { @@ -269,8 +301,18 @@ class Player( xbmc.Player ): else: # Get current audio and subtitles track - track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid":1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' - result = xbmc.executeJSONRPC(track_query) + tracks_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Player.GetProperties", + "params": { + + "playerid": 1, + "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] + } + } + result = xbmc.executeJSONRPC(json.dumps(tracks_query)) result = json.loads(result) result = result.get('result') @@ -297,7 +339,7 @@ class Player( xbmc.Player ): # Number of audiotracks to help get Emby Index audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = utils.window("%sIndexMapping" % currentFile) + mapping = utils.window("emby_%s.indexMapping" % currentFile) if mapping: # Set in PlaybackUtils.py @@ -306,13 +348,16 @@ class Player( xbmc.Player ): if externalIndex.get(str(indexSubs)): # If the current subtitle is in the mapping - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [externalIndex[str(indexSubs)]] * 2 + subindex = [externalIndex[str(indexSubs)]] * 2 + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex else: # Internal subtitle currently selected - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 + subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex else: # Direct paths enabled scenario or no external subtitles set - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs + audioTracks + 1] * 2 + subindex = [indexSubs + audioTracks + 1] * 2 + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex else: data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2 @@ -326,8 +371,8 @@ class Player( xbmc.Player ): currentFile = self.currentFile self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2) - if self.played_information.get(currentFile): - self.played_information[currentFile]['paused'] = True + if self.played_info.get(currentFile): + self.played_info[currentFile]['paused'] = True self.reportPlayback() @@ -336,8 +381,8 @@ class Player( xbmc.Player ): currentFile = self.currentFile self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2) - if self.played_information.get(currentFile): - self.played_information[currentFile]['paused'] = False + if self.played_info.get(currentFile): + self.played_info[currentFile]['paused'] = False self.reportPlayback() @@ -346,15 +391,17 @@ class Player( xbmc.Player ): currentFile = self.currentFile self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2) - if self.played_information.get(currentFile): + if self.played_info.get(currentFile): position = self.xbmcplayer.getTime() - self.played_information[currentFile]['currentPosition'] = position + self.played_info[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 ): @@ -364,14 +411,16 @@ class Player( xbmc.Player ): def stopAll(self): - if not self.played_information: + doUtils = self.doUtils + + if not self.played_info: return - self.logMsg("Played_information: %s" % self.played_information, 1) + self.logMsg("Played_information: %s" % self.played_info, 1) # Process each items - for item in self.played_information: + for item in self.played_info: - data = self.played_information.get(item) + data = self.played_info.get(item) if data: self.logMsg("Item path: %s" % item, 2) @@ -379,47 +428,61 @@ 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: - percentComplete = (currentPosition * 10000000) / int(runtime) + try: + percentComplete = (currentPosition * 10000000) / int(runtime) + except ZeroDivisionError: + # Runtime is 0. + percentComplete = 0 + markPlayedAt = float(utils.settings('markPlayed')) / 100 + self.logMsg( + "Percent complete: %s Mark played at: %s" + % (percentComplete, markPlayedAt), 1) - 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") + # Prevent manually mark as watched in Kodi monitor + utils.window('emby_skipWatched%s' % itemid, value="true") self.stopPlayback(data) - offerDelete = utils.settings('offerDelete') == "true" - offerTypeDelete = False + # 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") - if type == "Episode" and utils.settings('offerDeleteTV') == "true": - offerTypeDelete = True + # Send the delete action to the server. + offerDelete = False - elif type == "Movie" and utils.settings('offerDeleteMovies') == "true": - offerTypeDelete = True + if type == "Episode" and utils.settings('deleteTV') == "true": + offerDelete = True + elif type == "Movie" and utils.settings('deleteMovies') == "true": + offerDelete = True - if percentComplete >= markPlayedAt and offerDelete and offerTypeDelete: - # Make the bigger setting be able to disable option easily. - self.logMsg("Offering deletion for: %s." % itemId, 1) - return_value = xbmcgui.Dialog().yesno("Offer Delete", "Delete %s" % currentFile.split("/")[-1], "on Emby Server?") - if return_value: - # Delete Kodi entry before Emby - listItem = [itemId] - LibrarySync().removefromDB(listItem, True) - - # Stop transcoding - 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") + if utils.settings('offerDelete') != "true": + # Delete could be disabled, even if the subsetting is enabled. + offerDelete = False + + if percentComplete >= markPlayedAt and offerDelete: + if utils.settings('skipConfirmDelete') != "true": + resp = xbmcgui.Dialog().yesno( + heading="Confirm delete", + line1="Delete file on Emby Server?") + if not resp: + self.logMsg("User skipped deletion.", 1) + continue + + url = "{server}/emby/Items/%s?format=json" % itemid + self.logMsg("Deleting request: %s" % itemid) + doUtils.downloadUrl(url, type="DELETE") - self.played_information.clear() + self.played_info.clear() def stopPlayback(self, data): @@ -429,12 +492,11 @@ class Player( xbmc.Player ): currentPosition = data['currentPosition'] positionTicks = int(currentPosition * 10000000) - url = "{server}/mediabrowser/Sessions/Playing/Stopped" + url = "{server}/emby/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 f95f9291..a6562a53 100644 --- a/resources/lib/UserClient.py +++ b/resources/lib/UserClient.py @@ -1,22 +1,21 @@ -################################################################################################# -# UserClient thread -################################################################################################# +# -*- coding: utf-8 -*- + +################################################################################################## + +import hashlib +import threading import xbmc import xbmcgui import xbmcaddon import xbmcvfs -import threading -import hashlib -import json as json +import artwork +import utils +import clientinfo +import downloadutils -import KodiMonitor -import Utils as utils -from ClientInformation import ClientInformation -from DownloadUtils import DownloadUtils -from Player import Player -from API import API +################################################################################################## class UserClient(threading.Thread): @@ -24,16 +23,7 @@ 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 @@ -44,25 +34,25 @@ class UserClient(threading.Thread): HasAccess = True AdditionalUser = [] - def __init__(self, *args): + userSettings = None + + + def __init__(self): self.__dict__ = self._shared_state - threading.Thread.__init__(self, *args) + self.addon = xbmcaddon.Addon() + + self.addonName = clientinfo.ClientInfo().getAddonName() + self.doUtils = downloadutils.DownloadUtils() + self.logLevel = int(utils.settings('logLevel')) + + threading.Thread.__init__(self) def logMsg(self, msg, lvl=1): className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), str(msg), int(lvl)) + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - def getUsername(self): - - username = utils.settings('username') - - if (username == ""): - self.logMsg("No username saved.", 2) - return "" - - return username def getAdditionalUsers(self): @@ -71,11 +61,21 @@ 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: + except ValueError: logLevel = 0 return logLevel @@ -83,71 +83,84 @@ class UserClient(threading.Thread): def getUserId(self): username = self.getUsername() - w_userId = self.WINDOW.getProperty('userId%s' % username) + w_userId = utils.window('emby_userId%s' % username) s_userId = utils.settings('userId%s' % username) # Verify the window property - if (w_userId != ""): - self.logMsg("Returning userId from WINDOW for username: %s UserId: %s" % (username, w_userId), 2) + 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) 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) - return + self.logMsg("No userId saved for username: %s." % username, 1) 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: - HTTPS = utils.settings('secondhttps') + # Alternate host + HTTPS = utils.settings('secondhttps') == "true" 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 host == "": + if not host: self.logMsg("No server information saved.", 2) - return "" + return False # If https is true - if prefix and (HTTPS == "true"): + if prefix and HTTPS: server = "https://%s" % server return server # If https is false - elif prefix and (HTTPS == "false"): + elif prefix and not HTTPS: server = "http://%s" % server return server # If only the host:port is required - elif (prefix == False): + elif not prefix: return server def getToken(self): username = self.getUsername() - w_token = self.WINDOW.getProperty('accessToken%s' % username) + w_token = utils.window('emby_accessToken%s' % username) s_token = utils.settings('accessToken') # Verify the window property - if (w_token != ""): - self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s" % (username, w_token), 2) + 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) return w_token # Verify the settings - 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) + 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) return s_token else: - self.logMsg("No token found.") + self.logMsg("No token found.", 1) return "" def getSSLverify(self): @@ -174,71 +187,63 @@ class UserClient(threading.Thread): def setUserPref(self): - player = Player() - server = self.getServer() - userId = self.getUserId() - - url = "{server}/mediabrowser/Users/{UserId}?format=json" - result = self.doUtils.downloadUrl(url) + doUtils = self.doUtils + url = "{server}/emby/Users/{UserId}?format=json" + result = doUtils.downloadUrl(url) + self.userSettings = result # Set user image for skin display - self.WINDOW.setProperty("EmbyUserImage",API().getUserArtwork(result,"Primary")) + if result.get('PrimaryImageTag'): + utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result, 'Primary')) - # Load the resume point from Emby and set as setting - url = "{server}/mediabrowser/System/Configuration?format=json" - result = self.doUtils.downloadUrl(url) + # Set resume point max + url = "{server}/emby/System/Configuration?format=json" + result = doUtils.downloadUrl(url) utils.settings('markPlayed', value=str(result['MaxResumePct'])) - return True - def getPublicUsers(self): server = self.getServer() # Get public Users - url = "%s/mediabrowser/Users/Public?format=json" % server + url = "%s/emby/Users/Public?format=json" % server result = self.doUtils.downloadUrl(url, authenticate=False) - users = [] - - if (result != ""): - users = result + if result != "": + return result else: # Server connection failed return False - return users - def hasAccess(self): - - url = "{server}/mediabrowser/Users" + # hasAccess is verified in service.py + url = "{server}/emby/Users?format=json" result = self.doUtils.downloadUrl(url) - if result is False: - # Access is restricted - self.logMsg("Access is restricted.") + if result == False: + # Access is restricted, set in downloadutils.py via exception + self.logMsg("Access is restricted.", 1) self.HasAccess = False - return - elif self.WINDOW.getProperty('Server_online') != "true": + + elif utils.window('emby_online') != "true": # Server connection failed - return + pass - if self.WINDOW.getProperty("Server_status") == "restricted": - self.logMsg("Access is granted.") + elif utils.window('emby_serverStatus') == "restricted": + self.logMsg("Access is granted.", 1) self.HasAccess = True - self.WINDOW.setProperty("Server_status", "") + utils.window('emby_serverStatus', clear=True) 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 = self.getUserId() + self.currUserId = userId self.currServer = self.getServer() self.currToken = self.getToken() self.ssl = self.getSSLverify() @@ -246,21 +251,21 @@ class UserClient(threading.Thread): # Test the validity of current token if authenticated == False: - url = "%s/mediabrowser/Users/%s" % (self.currServer, self.currUserId) - WINDOW.setProperty("currUser", username) - WINDOW.setProperty("accessToken%s" % username, self.currToken) + 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) result = doUtils.downloadUrl(url) + if result == 401: # Token is no longer valid self.resetClient() return False # Set to windows property - 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) + 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)) # Set DownloadUtils values doUtils.setUsername(username) @@ -273,188 +278,194 @@ class UserClient(threading.Thread): # Start DownloadUtils session doUtils.startSession() self.getAdditionalUsers() - - self.currUser = username # Set user preferences in settings + self.currUser = username self.setUserPref() + def authenticate(self): - - WINDOW = self.WINDOW - addon = self.addon + # Get /profile/addon_data + addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') + hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) 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 (hasSettings == 0): - self.logMsg("No settings.xml found.") + if not hasSettings: + self.logMsg("No settings.xml found.", 1) self.auth = False return # If no user information - if (server == "") or (username == ""): - self.logMsg("Missing server information.") + elif not server or not username: + self.logMsg("Missing server information.", 1) self.auth = False return - # If there's a token - if (self.getToken() != ""): + # If there's a token, load the user + elif self.getToken(): result = self.loadCurrUser() if result == False: pass else: - self.logMsg("Current user: %s" % self.currUser, 0) - self.logMsg("Current userId: %s" % self.currUserId, 0) - self.logMsg("Current accessToken: %s" % self.currToken, 0) + self.logMsg("Current user: %s" % self.currUser, 1) + self.logMsg("Current userId: %s" % self.currUserId, 1) + self.logMsg("Current accessToken: %s" % self.currToken, 2) return + ##### AUTHENTICATE USER ##### + users = self.getPublicUsers() password = "" # Find user in list for user in users: - name = user[u'Name'] - userHasPassword = False + name = user['Name'] - if (unicode(username, 'utf-8') in name): - # Verify if user has a password - if (user.get("HasPassword") == True): - userHasPassword = True + if username.decode('utf-8') in name: # If user has password - if (userHasPassword): - password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) + if user['HasPassword'] == True: + password = xbmcgui.Dialog().input( + heading="Enter password for user: %s" % username, + option=xbmcgui.ALPHANUM_HIDE_INPUT) # If password dialog is cancelled - if (password == ""): + if not password: self.logMsg("No password entered.", 0) - self.WINDOW.setProperty("Server_status", "Stop") + utils.window('emby_serverStatus', value="Stop") self.auth = False return break else: # Manual login, user is hidden - password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT) - + password = xbmcgui.Dialog().input( + heading="Enter password for user: %s" % username, + option=xbmcgui.ALPHANUM_HIDE_INPUT) sha1 = hashlib.sha1(password) sha1 = sha1.hexdigest() # Authenticate username and password - url = "%s/mediabrowser/Users/AuthenticateByName?format=json" % server + url = "%s/emby/Users/AuthenticateByName?format=json" % server data = {'username': username, 'password': sha1} self.logMsg(data, 2) result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False) - accessToken = None try: - self.logMsg("Auth_Reponse: %s" % result, 1) - accessToken = result[u'AccessToken'] - except: - pass + self.logMsg("Auth response: %s" % result, 1) + accessToken = result['AccessToken'] + + except (KeyError, TypeError): + self.logMsg("Failed to retrieve the api key.", 1) + accessToken = None - if (result != None and accessToken != None): + if accessToken is not None: self.currUser = username xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser) - userId = result[u'User'][u'Id'] - utils.settings("accessToken", accessToken) - utils.settings("userId%s" % username, userId) - self.logMsg("User Authenticated: %s" % accessToken) + userId = result['User']['Id'] + utils.settings('accessToken', value=accessToken) + utils.settings('userId%s' % username, value=userId) + self.logMsg("User Authenticated: %s" % accessToken, 1) self.loadCurrUser(authenticated=True) - self.WINDOW.setProperty("Server_status", "") + utils.window('emby_serverStatus', clear=True) self.retry = 0 - return else: - self.logMsg("User authentication failed.") - utils.settings("accessToken", "") - utils.settings("userId%s" % username, "") + self.logMsg("User authentication failed.", 1) + utils.settings('accessToken', value="") + utils.settings('userId%s' % username, value="") xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.") # Give two attempts at entering password - 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.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 self.auth = False - return def resetClient(self): - username = self.getUsername() self.logMsg("Reset UserClient authentication.", 1) - if (self.currToken != None): + username = self.getUsername() + + if self.currToken is not None: # In case of 401, removed saved token - utils.settings("accessToken", "") - self.WINDOW.setProperty("accessToken%s" % username, "") + utils.settings('accessToken', value="") + utils.window('emby_accessToken%s' % username, clear=True) self.currToken = None self.logMsg("User token has been removed.", 1) self.auth = True self.currUser = None - return - def run(self): - self.logMsg("|---- Starting UserClient ----|", 0) + monitor = xbmc.Monitor() + self.logMsg("----===## Starting UserClient ##===----", 0) - while not self.KodiMonitor.abortRequested(): + while not monitor.abortRequested(): # Verify the log level 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)) - if (self.WINDOW.getProperty("Server_status") != ""): - status = self.WINDOW.getProperty("Server_status") - + + status = utils.window('emby_serverStatus') + if status: + # Verify the connection status to server if status == "restricted": # Parental control is restricting access self.HasAccess = False elif status == "401": - self.WINDOW.setProperty("Server_status", "Auth") - # Revoked token + # Unauthorized access, revoke token + utils.window('emby_serverStatus', value="Auth") self.resetClient() - if self.auth and (self.currUser == None): - status = self.WINDOW.getProperty("Server_status") - - if (status == "") or (status == "Auth"): + 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 self.auth = False self.authenticate() - if (self.auth == False) and (self.currUser == None): - # Only if there's information found to login + + if not self.auth and (self.currUser is None): + # If authenticate failed. server = self.getServer() username = self.getUsername() - status = self.WINDOW.getProperty("Server_status") - - # If user didn't enter a password when prompted - if status == "Stop": - pass + status = utils.window('emby_serverStatus') - elif (server != "") and (username != ""): - self.logMsg("Server found: %s" % server) - self.logMsg("Username found: %s" % username) + # 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) self.auth = True - # If stopping the client didn't work + if self.stopClient == True: + # If stopping the client didn't work break - if self.KodiMonitor.waitForAbort(1): + if monitor.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): - # As last resort + # When emby for kodi terminates self.stopClient = True \ No newline at end of file diff --git a/resources/lib/Utils.py b/resources/lib/Utils.py index 73c49c95..83e73e1d 100644 --- a/resources/lib/Utils.py +++ b/resources/lib/Utils.py @@ -1,59 +1,76 @@ -################################################################################################# -# utils +# -*- coding: utf-8 -*- + ################################################################################################# -import xbmc -import xbmcgui -import xbmcaddon -import xbmcvfs -import json -import os import cProfile -import pstats -import time import inspect +import pstats import sqlite3 -import string +import time import unicodedata import xml.etree.ElementTree as etree -from API import API -from PlayUtils import PlayUtils -from DownloadUtils import DownloadUtils +import xbmc +import xbmcaddon +import xbmcgui +import xbmcvfs -downloadUtils = DownloadUtils() -addon = xbmcaddon.Addon() -language = addon.getLocalizedString +################################################################################################# - -def logMsg(title, msg, level = 1): + +def logMsg(title, msg, level=1): - WINDOW = xbmcgui.Window(10000) # Get the logLevel set in UserClient - logLevel = int(WINDOW.getProperty('getLogLevel')) + try: + logLevel = int(window('emby_logLevel')) + except ValueError: + logLevel = 0 - if(logLevel >= level): - if(logLevel == 2): # inspect.stack() is expensive + if logLevel >= level: + + if logLevel == 2: # inspect.stack() is expensive try: - xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg)) + xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg)) except UnicodeEncodeError: - xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) + xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8'))) else: try: - xbmc.log(title + " -> " + str(msg)) + xbmc.log("%s -> %s" % (title, msg)) except UnicodeEncodeError: - xbmc.log(title + " -> " + str(msg.encode('utf-8'))) + xbmc.log("%s -> %s" % (title, msg.encode('utf-8'))) -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"): +def window(property, value=None, clear=False, windowid=10000): + # Get or set window property + WINDOW = xbmcgui.Window(windowid) - if type == "music": + 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": dbPath = getKodiMusicDBPath() elif type == "texture": dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') @@ -94,219 +111,140 @@ 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() -def prettifyXml(elem): - rough_string = etree.tostring(elem, "utf-8") - reparsed = minidom.parseString(rough_string) - return reparsed.toprettyxml(indent="\t") + # reset the install run flag + settings('SyncInstallRunDone', value="false") + + # Remove emby info + resp = dialog.yesno("Warning", "Reset all Emby Addon settings?") + if resp == 1: + # Delete the settings + addon = xbmcaddon.Addon() + addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') + dataPath = "%ssettings.xml" % addondir + xbmcvfs.delete(dataPath) + logMsg("EMBY", "Deleting: settings.xml", 1) + + dialog.ok( + heading="Emby for Kodi", + line1="Database reset has completed, Kodi will now restart to apply the changes.") + xbmc.executebuiltin('RestartApp') def startProfiling(): + pr = cProfile.Profile() pr.enable() - return pr + return pr + def stopProfiling(pr, profileName): + pr.disable() ps = pstats.Stats(pr) - addondir = xbmc.translatePath(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile')) + profiles = xbmc.translatePath("%sprofiles/" + % xbmcaddon.Addon().getAddonInfo('profile')).decode('utf-8') + + 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) - 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 = open(profile, '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(str(ncalls) + "\t" + "{:10.4f}".format(total_time) + "\t" + "{:10.4f}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") + f.write( + "%s\t%s\t%s\t%s\t%s\r\n" + % (ncalls, "{:10.4f}".format(total_time), + "{:10.4f}".format(cumulative_time), func_name, filename)) except ValueError: - f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") + f.write( + "%s\t%s\t%s\t%s\t%s\r\n" + % (ncalls, "{0}".format(total_time), + "{0}".format(cumulative_time), func_name, filename)) f.close() -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(":", "") @@ -327,99 +265,223 @@ def normalize_nodes(text): return text -def reloadProfile(): - # Useful to reload the add-on without restarting Kodi. - profile = xbmc.getInfoLabel('System.ProfileName') - xbmc.executebuiltin("LoadProfile(%s)" % profile) - +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 reset(): + return text - WINDOW = xbmcgui.Window( 10000 ) - return_value = xbmcgui.Dialog().yesno("Warning", "Are you sure you want to reset your local Kodi database?") +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 - if return_value == 0: - return +def sourcesXML(): + # To make Master lock compatible + path = xbmc.translatePath("special://profile/").decode('utf-8') + xmlpath = "%ssources.xml" % path - # 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() + try: + xmlparse = etree.parse(xmlpath) + except: # Document is blank or missing + root = etree.Element('sources') + else: + root = xmlparse.getroot() - - # reset the install run flag - #settings('SyncInstallRunDone', "false") - #WINDOW.setProperty("SyncInstallRunDone", "false") + + 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() dialog = xbmcgui.Dialog() - # 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 + 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 diff --git a/resources/lib/VideoNodes.py b/resources/lib/VideoNodes.py index 056ee13f..1dc9d5a6 100644 --- a/resources/lib/VideoNodes.py +++ b/resources/lib/VideoNodes.py @@ -1,466 +1,344 @@ -################################################################################################# -# VideoNodes - utils to create video nodes listings in kodi for the emby addon +# -*- coding: utf-8 -*- + ################################################################################################# - -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 Utils as utils +import xbmc +import xbmcaddon +import xbmcvfs -from ReadEmbyDB import ReadEmbyDB -WINDOW = xbmcgui.Window(10000) +import clientinfo +import utils -addonSettings = xbmcaddon.Addon() -language = addonSettings.getLocalizedString +################################################################################################# -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')) - - 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: - 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) + +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: - 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) + # 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: - 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) + dirname = cleantagname - #some movies-only nodes - if type == "movies": - - #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: - 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) + path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') + nodepath = xbmc.translatePath( + "special://profile/library/video/Emby - %s/" % dirname).decode('utf-8') - #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) - - try: - etree.ElementTree(root).write(nodefile, xml_declaration=True) - except: - etree.ElementTree(root).write(nodefile) - - #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 + # 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 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)) - + # 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) - except Exception as e: - utils.logMsg("Emby addon","Error while creating videonodes listings, restart required ?") - print e \ No newline at end of file + self.logMsg("Sucessfully removed videonode: %s." % tagname, 1) + return + + # Create index entry + nodeXML = "%sindex.xml" % nodepath + # Set windows property + path = "library://video/Emby - %s/" % dirname + for i in range(1, indexnumber): + # Verify to make sure we don't create duplicates + if utils.window('Emby.nodes.%s.index' % i) == path: + return + + utils.window('Emby.nodes.%s.index' % indexnumber, value=path) + # Root + root = self.commonRoot(order=0, label=dirname, tagname=tagname, roottype=0) + try: + 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 + + if nodetype == "all": + + if viewtype == "mixed": + templabel = dirname + else: + templabel = label + + embynode = "Emby.nodes.%s" % indexnumber + utils.window('%s.title' % embynode, value=templabel) + utils.window('%s.path' % embynode, value=windowpath) + utils.window('%s.content' % embynode, value=path) + utils.window('%s.type' % embynode, value=mediatype) + else: + embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype) + utils.window('%s.title' % embynode, value=label) + utils.window('%s.path' % embynode, value=windowpath) + utils.window('%s.content' % embynode, value=path) + + if xbmcvfs.exists(nodeXML): + # Don't recreate xml if already exists + continue + + + # Create the root + if nodetype == "nextupepisodes" or (kodiversion == 14 and + nodetype in ('recentepisodes', 'inprogressepisodes')): + # Folder type with plugin path + root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2) + etree.SubElement(root, 'path').text = path + etree.SubElement(root, 'content').text = "episodes" + else: + root = self.commonRoot(order=node, label=label, tagname=tagname) + if nodetype in ('recentepisodes', 'inprogressepisodes'): + etree.SubElement(root, 'content').text = "episodes" + else: + etree.SubElement(root, 'content').text = mediatype + + limit = "25" + # Elements per nodetype + if nodetype == "all": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + elif nodetype == "recent": + etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, 'limit').text = limit + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + elif nodetype == "inprogress": + etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) + etree.SubElement(root, 'limit').text = limit + + elif nodetype == "genres": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, 'group').text = "genres" + + elif nodetype == "unwatched": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + elif nodetype == "sets": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, 'group').text = "sets" + + elif nodetype == "random": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" + etree.SubElement(root, 'limit').text = limit + + elif nodetype == "recommended": + etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" + etree.SubElement(root, 'limit').text = limit + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + rule2 = etree.SubElement(root, 'rule', + attrib={'field': "rating", 'operator': "greaterthan"}) + etree.SubElement(rule2, 'value').text = "7" + + elif nodetype == "recentepisodes": + # Kodi Isengard, Jarvis + etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, 'limit').text = limit + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + elif nodetype == "inprogressepisodes": + # Kodi Isengard, Jarvis + etree.SubElement(root, 'limit').text = "25" + rule = etree.SubElement(root, 'rule', + attrib={'field': "inprogress", 'operator':"true"}) + + try: + utils.indent(root) + except: pass + etree.ElementTree(root).write(nodeXML) + + 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 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 = [ + + "index","path","title","content", + "inprogress.content","inprogress.title", + "inprogress.content","inprogress.path", + "nextepisodes.title","nextepisodes.content", + "nextepisodes.path","unwatched.title", + "unwatched.content","unwatched.path", + "recent.title","recent.content","recent.path", + "recentepisodes.title","recentepisodes.content", + "recentepisodes.path","inprogressepisodes.title", + "inprogressepisodes.content","inprogressepisodes.path" + ] + + if embyprops: + 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