From 8912a0b6010f27045f95c0025c24e72643a86f85 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 29 Jan 2016 20:07:21 +0100 Subject: [PATCH] Overhaul Part 1 --- default.py | 3 +- resources/lib/PlexAPI.py | 218 +++++++++----------- resources/lib/PlexFunctions.py | 134 ++++++++---- resources/lib/entrypoint.py | 87 +++++--- resources/lib/initialsetup.py | 36 ++-- resources/lib/itemtypes.py | 100 +++++---- resources/lib/librarysync.py | 227 ++++++++++++--------- resources/lib/playbackutils.py | 156 +++++++++++--- resources/lib/playutils.py | 106 +++++----- resources/lib/plexbmchelper/functions.py | 6 +- resources/lib/plexbmchelper/listener.py | 25 ++- resources/lib/plexbmchelper/plexgdm.py | 2 +- resources/lib/plexbmchelper/subscribers.py | 27 ++- resources/lib/userclient.py | 1 - resources/lib/utils.py | 55 +++-- resources/settings.xml | 1 + 16 files changed, 719 insertions(+), 465 deletions(-) diff --git a/default.py b/default.py index 36f3d041..b9c2cc52 100644 --- a/default.py +++ b/default.py @@ -91,8 +91,7 @@ class Main: folderid = params['folderid'][0] modes[mode](itemid, folderid) elif mode == "companion": - resume = params.get('resume', '')[0] - modes[mode](itemid, resume=resume) + modes[mode](itemid, params=sys.argv[2]) else: modes[mode]() else: diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 818a9146..1aae5053 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -55,6 +55,8 @@ import re import json from urllib import urlencode, quote_plus +from PlexFunctions import PlexToKodiTimefactor + try: import xml.etree.cElementTree as etree except ImportError: @@ -198,9 +200,9 @@ class PlexAPI(): 'avatar': avatar, 'token': token } - utils.settings('plexLogin', value=username) - utils.settings('plexToken', value=token) - utils.settings('plexhome', value=home) + utils.settings('plexLogin', username) + utils.settings('plexToken', token) + utils.settings('plexhome', home) return result def CheckPlexTvSignin(self, identifier): @@ -339,8 +341,8 @@ class PlexAPI(): verify=sslverify, timeout=timeout) except requests.exceptions.ConnectionError as e: - self.logMsg("Server is offline or cannot be reached. Url: %s." - "Header: %s. Error message: %s" + self.logMsg("Server is offline or cannot be reached. Url: %s " + "Header: %s Error message: %s" % (url, header, e), -1) return False except requests.exceptions.ReadTimeout: @@ -781,8 +783,8 @@ class PlexAPI(): """ # Get addon infos xargs = { - "Content-type": "application/x-www-form-urlencoded", - "Access-Control-Allow-Origin": "*", + "Content-Type": "application/x-www-form-urlencoded", + # "Access-Control-Allow-Origin": "*", 'X-Plex-Language': 'en', 'X-Plex-Device': self.addonName, 'X-Plex-Client-Platform': self.platform, @@ -1371,8 +1373,6 @@ class API(): def __init__(self, item): self.item = item - # which child in the XML response shall we look at? - self.child = 0 # which media part in the XML response shall we look at? self.part = 0 self.clientinfo = clientinfo.ClientInfo() @@ -1383,18 +1383,6 @@ class API(): self.jumpback = int(utils.settings('resumeJumpBack')) - def setChildNumber(self, number=0): - """ - Which child in the XML response shall we look at and work with? - """ - self.child = int(number) - - def getChildNumber(self): - """ - Returns the child in the XML response that we're currently looking at - """ - return self.child - def setPartNumber(self, number=0): """ Sets the part number to work with (used to deal with Movie with several @@ -1435,12 +1423,9 @@ class API(): def getType(self): """ - Returns the type of media, e.g. 'movie' + Returns the type of media, e.g. 'movie' or 'clip' for trailers """ - item = self.item - item = item[self.child].attrib - itemtype = item['type'] - return itemtype + return self.item['type'] def getChecksum(self): """ @@ -1450,47 +1435,58 @@ class API(): item = self.item # XML try: - item = item[self.child].attrib + item = item[0].attrib # JSON - except KeyError: + except (AttributeError, KeyError): pass # Include a letter to prohibit saving as an int! - checksum = "K%s%s" % (self.getKey(), + checksum = "K%s%s" % (self.getRatingKey(), item.get('updatedAt', '')) return checksum - def getKey(self): + def getRatingKey(self): """ Can be used on both XML and JSON - Returns the Plex unique movie id as a str, not int + Returns the Plex key such as '246922' as a string """ item = self.item # XML try: - item = item[self.child].attrib + item = item[0].attrib # JSON - except KeyError: + except (AttributeError, KeyError): pass key = item['ratingKey'] return str(key) + def getKey(self): + """ + Can be used on both XML and JSON + Returns the Plex key such as '/library/metadata/246922' + """ + item = self.item + # XML + try: + item = item[0].attrib + # JSON + except (AttributeError, KeyError): + pass + key = item['key'] + return str(key) + def getIndex(self): """ Returns the 'index' of an PMS XML reply. Depicts e.g. season number. """ - item = self.item[self.child].attrib + item = self.item[0].attrib index = item['index'] return str(index) def getDateCreated(self): """ Returns the date when this library item was created - - Input: - index child number as int; normally =0 """ - item = self.item - item = item[self.child].attrib + item = self.item[0].attrib dateadded = item['addedAt'] dateadded = self.convert_date(dateadded) return dateadded @@ -1517,7 +1513,13 @@ class API(): resume = 0 rating = 0 - item = item[self.child].attrib + # XML + try: + item = item[0].attrib + # JSON + except (AttributeError, KeyError): + pass + try: playcount = int(item['viewCount']) except KeyError: @@ -1558,7 +1560,7 @@ class API(): writer = [] cast = [] producer = [] - for child in item[self.child]: + for child in item[0]: if child.tag == 'Director': director.append(child.attrib['tag']) elif child.tag == 'Writer': @@ -1594,7 +1596,7 @@ class API(): 'Role': 'Actor', 'Producer': 'Producer' } - for child in item[self.child]: + for child in item[0]: if child.tag in people_of_interest.keys(): name = child.attrib['tag'] name_id = child.attrib['id'] @@ -1626,7 +1628,7 @@ class API(): """ item = self.item genre = [] - for child in item[self.child]: + for child in item[0]: if child.tag == 'Genre': genre.append(child.attrib['tag']) return genre @@ -1638,7 +1640,7 @@ class API(): Return IMDB, e.g. "imdb://tt0903624?lang=en". Returns None if not found """ item = self.item - item = item[self.child].attrib + item = item[0].attrib try: item = item['guid'] except KeyError: @@ -1663,12 +1665,14 @@ class API(): sorttitle = title, if no sorttitle is found """ item = self.item + # XML try: - item = item[self.child].attrib + item = item[0].attrib # JSON - except KeyError: + except (AttributeError, KeyError): pass + try: title = item['title'] except: @@ -1684,7 +1688,7 @@ class API(): Returns the plot or None. """ item = self.item - item = item[self.child].attrib + item = item[0].attrib try: plot = item['summary'] except: @@ -1696,7 +1700,7 @@ class API(): Returns a shorter tagline or None """ item = self.item - item = item[self.child].attrib + item = item[0].attrib try: tagline = item['tagline'] except KeyError: @@ -1708,7 +1712,7 @@ class API(): Returns the audience rating or None """ item = self.item - item = item[self.child].attrib + item = item[0].attrib try: rating = item['audienceRating'] except KeyError: @@ -1720,7 +1724,7 @@ class API(): Returns the production(?) year ("year") or None """ item = self.item - item = item[self.child].attrib + item = item[0].attrib try: year = item['year'] except KeyError: @@ -1737,9 +1741,14 @@ class API(): Output: resume, runtime as floats. 0.0 if not found """ - time_factor = 1.0 / 1000.0 # millisecond -> seconds - item = self.item - item = item[self.child].attrib + time_factor = PlexToKodiTimefactor() + + # XML + try: + item = self.item[0].attrib + # JSON + except (AttributeError, KeyError): + pass try: runtime = float(item['duration']) @@ -1769,7 +1778,7 @@ class API(): """ # Convert more complex cases item = self.item - item = item[self.child].attrib + item = item[0].attrib try: mpaa = item['contentRating'] except KeyError: @@ -1785,7 +1794,7 @@ class API(): """ item = self.item country = [] - for child in item[self.child]: + for child in item[0]: if child.tag == 'Country': country.append(child.attrib['tag']) return country @@ -1795,7 +1804,7 @@ class API(): Returns the "originallyAvailableAt" or None """ item = self.item - item = item[self.child].attrib + item = item[0].attrib try: premiere = item['originallyAvailableAt'] except: @@ -1808,7 +1817,7 @@ class API(): """ item = self.item studio = [] - item = item[self.child].attrib + item = item[0].attrib try: studio.append(self.getStudio(item['studio'])) except KeyError: @@ -1849,67 +1858,36 @@ class API(): Episode number, Plex: 'index' ] """ - item = self.item[self.child].attrib + item = self.item[0].attrib key = item['grandparentRatingKey'] title = item['grandparentTitle'] season = item['parentIndex'] episode = item['index'] - return key, title, season, episode - - def getFilePath(self): - """ - returns the path to the Plex object, e.g. "/library/metadata/221803" - """ - item = self.item - item = item[self.child].attrib - try: - filepath = item['key'] - except KeyError: - filepath = "" - # Plex: do we need this? - else: - if "\\\\" in filepath: - # append smb protocol - filepath = filepath.replace("\\\\", "smb://") - filepath = filepath.replace("\\", "/") - - if item.get('VideoType'): - videotype = item['VideoType'] - # Specific format modification - if 'Dvd'in videotype: - filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath - elif 'Bluray' in videotype: - filepath = "%s/BDMV/index.bdmv" % filepath - - if "\\" in filepath: - # Local path scenario, with special videotype - filepath = filepath.replace("/", "\\") - - return filepath + return str(key), title, str(season), str(episode) def addPlexCredentialsToUrl(self, url, arguments={}): """ Takes an URL and optional arguments (also to be URL-encoded); returns an extended URL with e.g. the Plex token included. + + arguments overrule everything """ token = {'X-Plex-Token': self.token} xargs = PlexAPI().getXArgsDeviceInfo(options=token) xargs.update(arguments) - url = "%s?%s" % (url, urlencode(xargs)) + if '?' not in url: + url = "%s?%s" % (url, urlencode(xargs)) + else: + url = "%s&%s" % (url, urlencode(xargs)) return url - def getBitrate(self): + def GetPlayQueueItemID(self): """ - Returns the bitrate as an int. The Part bitrate is returned; if not - available in the Plex XML, the Media bitrate is returned + Returns current playQueueItemID for the item. + + If not found, empty str is returned """ - item = self.item - try: - bitrate = item[self.child][0][self.part].attrib['bitrate'] - except KeyError: - bitrate = item[self.child][0].attrib['bitrate'] - bitrate = int(bitrate) - return bitrate + return self.item.get('playQueueItemID') def getDataFromPartOrMedia(self, key): """ @@ -1918,8 +1896,8 @@ class API(): If all fails, None is returned. """ - media = self.item[self.child][0].attrib - part = self.item[self.child][0][self.part].attrib + media = self.item['_children'][0] + part = media['_children'][self.part] try: try: value = part[key] @@ -2025,12 +2003,12 @@ class API(): subtitlelanguages = [] aspectratio = None try: - aspectratio = item[self.child][0].attrib['aspectRatio'] + aspectratio = item[0][0].attrib['aspectRatio'] except KeyError: pass # TODO: what if several Media tags exist?!? # Loop over parts - for child in item[self.child][0]: + for child in item[0][0]: container = child.attrib['container'].lower() # Loop over Streams for grandchild in child: @@ -2105,6 +2083,13 @@ class API(): server = self.server item = self.item + # XML + try: + item = item[0].attrib + # JSON + except (AttributeError, KeyError): + pass + maxHeight = 10000 maxWidth = 10000 customquery = "" @@ -2126,7 +2111,6 @@ class API(): } # Process backdrops # Get background artwork URL - item = item[self.child].attrib try: background = item['art'] background = "%s%s" % (server, background) @@ -2259,7 +2243,7 @@ class API(): xargs = PlexAPI().getXArgsDeviceInfo(options=options) # For Direct Playing if action == "DirectPlay": - path = self.item[self.child][0][self.part].attrib['key'] + path = self.item['_children'][0]['_children'][self.partNumber]['key'] transcodePath = self.server + path # Be sure to have exactly ONE '?' in the path (might already have # been returned, e.g. trailers!) @@ -2273,15 +2257,7 @@ class API(): # For Direct Streaming or Transcoding transcodePath = self.server + \ '/video/:/transcode/universal/start.m3u8?' - partCount = 0 - for parts in self.item[self.child][0]: - partCount = partCount + 1 - # Movie consists of several parts; grap one part - if partCount > 1: - path = self.item[self.child][0][self.part].attrib['key'] - # Movie consists of only one part - else: - path = self.item[self.child].attrib['key'] + path = self.getDataFromPartOrMedia('key') args = { 'path': path, 'mediaIndex': 0, # Probably refering to XML reply sheme @@ -2337,9 +2313,9 @@ class API(): mapping = {} item = self.item - itemid = self.getKey() + itemid = self.getRatingKey() try: - mediastreams = item[self.child][0][0] + mediastreams = item[0][0][0] except (TypeError, KeyError, IndexError): return @@ -2372,14 +2348,14 @@ class API(): Returns raw API metadata XML dump for a playlist with e.g. trailers. """ item = self.item - key = self.getKey() + key = self.getRatingKey() try: uuid = item.attrib['librarySectionUUID'] # if not found: probably trying to start a trailer directly # Hence no playlist needed except KeyError: return None - mediatype = item[self.child].tag.lower() + mediatype = item[0].tag.lower() trailerNumber = utils.settings('trailerNumber') if not trailerNumber: trailerNumber = '3' @@ -2407,4 +2383,4 @@ class API(): """ Returns the parts of the specified video child in the XML response """ - return self.item[self.child][0] + return self.item[0][0] diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 8e8db570..a053a114 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- from urllib import urlencode +from ast import literal_eval +from urlparse import urlparse, parse_qs +import re from xbmcaddon import Addon -from downloadutils import DownloadUtils +import downloadutils from utils import logMsg @@ -11,6 +14,13 @@ addonName = Addon().getAddonInfo('name') title = "%s %s" % (addonName, __name__) +def PlexToKodiTimefactor(): + """ + Kodi measures time in seconds, but Plex in milliseconds + """ + return 1.0 / 1000.0 + + def GetItemClassFromType(itemType): classes = { 'movie': 'Movies', @@ -21,6 +31,42 @@ def GetItemClassFromType(itemType): return classes[itemType] +def GetPlexKeyNumber(plexKey): + """ + Deconstructs e.g. '/library/metadata/xxxx' to the tuple + + ('library/metadata', 'xxxx') + + Returns ('','') if nothing is found + """ + regex = re.compile(r'''/(.+)/(\d+)$''') + try: + result = regex.findall(plexKey)[0] + except IndexError: + result = ('', '') + return result + + +def ParseContainerKey(containerKey): + """ + Parses e.g. /playQueues/3045?own=1&repeat=0&window=200 to: + 'playQueues', '3045', {'window': ['200'], 'own': ['1'], 'repeat': ['0']} + + Output hence: library, key, query (query as a special dict) + """ + result = urlparse(containerKey) + library, key = GetPlexKeyNumber(result.path) + query = parse_qs(result.query) + return library, key, query + + +def LiteralEval(string): + """ + Turns a string e.g. in a dict, safely :-) + """ + return literal_eval(string) + + def GetMethodFromPlexType(plexType): methods = { 'movie': 'add_update', @@ -46,18 +92,25 @@ def EmbyItemtypes(): return ['Movie', 'Series', 'Season', 'Episode'] -def XbmcPhoto(): - return "photo" -def XbmcVideo(): - return "video" -def XbmcAudio(): - return "audio" -def PlexPhoto(): - return "photo" -def PlexVideo(): - return "video" -def PlexAudio(): - return "music" +def GetPlayQueue(playQueueID): + """ + Fetches the PMS playqueue with the playQueueID as a JSON + + Returns False if something went wrong + """ + url = "{server}/playQueues/%s" % playQueueID + headerOptions = {'Accept': 'application/json'} + json = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + try: + json = json.json() + except: + return False + try: + json['_children'] + json['playQueueID'] + except KeyError: + return False + return json def GetPlexMetadata(key): @@ -87,7 +140,7 @@ def GetPlexMetadata(key): } url = url + '?' + urlencode(arguments) headerOptions = {'Accept': 'application/xml'} - xml = DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions) # Did we receive a valid XML? try: xml.tag @@ -108,7 +161,7 @@ def GetAllPlexChildren(key): """ result = [] url = "{server}/library/metadata/%s/children" % key - jsondata = DownloadUtils().downloadUrl(url) + jsondata = downloadutils.DownloadUtils().downloadUrl(url) try: result = jsondata['_children'] except KeyError: @@ -125,7 +178,7 @@ def GetPlexSectionResults(viewId, headerOptions={}): """ result = [] url = "{server}/library/sections/%s/all" % viewId - jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + jsondata = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions) try: result = jsondata['_children'] except TypeError: @@ -146,37 +199,38 @@ def GetPlexSectionResults(viewId, headerOptions={}): return result -def GetPlexUpdatedItems(viewId, unixTime, headerOptions={}): - """ - Returns a list (raw JSON or XML API dump) of all Plex items in the Plex - section with key = viewId AFTER the unixTime - """ - result = [] - url = "{server}/library/sections/%s/allLeaves?updatedAt>=%s" \ - % (viewId, unixTime) - jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions) - try: - result = jsondata['_children'] - except KeyError: - logMsg(title, - "Error retrieving all items for Plex section %s and time %s" - % (viewId, unixTime), -1) - return result - - -def GetAllPlexLeaves(viewId, headerOptions={}): +def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None, + headerOptions={}): """ Returns a list (raw JSON or XML API dump) of all Plex subitems for the key. (e.g. /library/sections/2/allLeaves pointing to all TV shows) Input: - viewId Id of Plex library, e.g. '2' - headerOptions to override the download headers + viewId Id of Plex library, e.g. '2' + lastViewedAt Unix timestamp; only retrieves PMS items viewed + since that point of time until now. + updatedAt Unix timestamp; only retrieves PMS items updated + by the PMS since that point of time until now. + headerOptions to override any download headers + + If lastViewedAt and updatedAt=None, ALL PMS items are returned. + + Warning: lastViewedAt and updatedAt are combined with AND by the PMS! + + Relevant "master time": PMS server. I guess this COULD lead to problems, + e.g. when server and client are in different time zones. """ result = [] - url = "{server}/library/sections/%s/allLeaves" % viewId - jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + args = [] + url = "{server}/library/sections/%s/allLeaves?" % viewId + if lastViewedAt: + args.append('lastViewedAt>=%s' % lastViewedAt) + if updatedAt: + args.append('updatedAt>=%s' % updatedAt) + args = '&'.join(args) + jsondata = downloadutils.DownloadUtils().downloadUrl( + url+args, headerOptions=headerOptions) try: result = jsondata['_children'] except TypeError: @@ -213,7 +267,7 @@ def GetPlexCollections(mediatype): """ collections = [] url = "{server}/library/sections" - jsondata = DownloadUtils().downloadUrl(url) + jsondata = downloadutils.DownloadUtils().downloadUrl(url) try: result = jsondata['_children'] except KeyError: diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index fc426c39..21ff0a95 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -31,36 +31,65 @@ import embydb_functions ################################################################################################# +# For logging only +title = " %s %s" % (clientinfo.ClientInfo().getAddonName(), __name__) + + +def plexCompanion(fullurl, params=None): + params = PlexFunctions.LiteralEval(params[26:]) + utils.logMsg("entrypoint - plexCompanion", + "params is: %s" % params, -1) + # {'protocol': 'http', + # 'containerKey': '/playQueues/3045?own=1&repeat=0&window=200', + # 'offset': '0', + # 'commandID': '20', + # 'token': 'transient-0243a39f-4c7d-495f-a5c8-6991b622b5a6', + # 'key': '/library/metadata/470', + # 'address': '192.168.0.2', + # 'machineIdentifier': '3eb2fc28af89500e000db2e07f8e8234d159f2c4', + # 'type': 'video', + # 'port': '32400'} + + if (params.get('machineIdentifier') != + utils.window('plex_machineIdentifier')): + utils.logMsg( + title, + "Command was not for us, machineIdentifier controller: %s, " + "our machineIdentifier : %s" + % (params.get('machineIdentifier'), + utils.window('plex_machineIdentifier')), -1) + return + utils.window('plex_key', params.get('key')) + library, key, query = PlexFunctions(params.get('containerKey')) + # Construct a container key that works always (get rid of playlist args) + utils.window('plex_containerKey', '/'+library+'/'+key) + # Assume it's video when something goes wrong + playbackType = params.get('type', 'video') + + if 'playQueues' in library: + utils.logMsg(title, "Playing a playQueue. Query was: %s" % query, 1) + # Playing a playlist that we need to fetch from PMS + playQueue = PlexFunctions.GetPlayQueue(key) + if not playQueue: + utils.logMsg( + title, "Error getting PMS playlist for key %s" % key, -1) + return + + # Set window properties to make them available for other threads + utils.window('plex_playQueueID', playQueue['playQueueID']) + utils.window('plex_playQueueVersion', playQueue['playQueueVersion']) + utils.window('plex_playQueueShuffled', playQueue['playQueueShuffled']) + utils.window( + 'plex_playQueueSelectedItemID', + playQueue['playQueueSelectedItemID']) + utils.window( + 'plex_playQueueSelectedItemOffset', + playQueue['playQueueSelectedItemOffset']) + + pbutils.PlaybackUtils(playQueue['_children']).StartPlay( + resume=playQueue['playQueueSelectedItemOffset'], + resumeItem=playQueue['playQueueSelectedItemID']) -def plexCompanion(fullurl, resume=None): - regex = re.compile(r'''/(\d+)$''') - itemid = regex.findall(fullurl) - try: - itemid = itemid[0] - except IndexError: - # No matches found, url not like: - # http://192.168.0.2:32400/library/metadata/243480 - utils.logMsg("entrypoint - plexCompanion", - "Could not parse url: %s" % fullurl, -1) - return False - # Initialize embydb - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby = embydb_functions.Embydb_Functions(embycursor) - # Get dbid using itemid - # Works only for library items, not e.g. for trailers - try: - dbid = emby.getItem_byId(itemid)[0] - except TypeError: - # Trailers and the like - dbid = None - embyconn.close() - # Fix resume timing - if resume: - if resume == '0': - resume = None - else: - resume = round(float(resume) / 1000.0, 6) # Start playing item = PlexFunctions.GetPlexMetadata(itemid) pbutils.PlaybackUtils(item).play(itemid, dbid, seektime=resume) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 9dbe5608..ee7ea94b 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -42,6 +42,7 @@ class InitialSetup(): clientId = self.clientInfo.getDeviceId() serverid = self.userClient.getServerId() myplexlogin, plexhome, plexLogin, plexToken = self.plx.GetPlexLoginFromSettings() + dialog = xbmcgui.Dialog() # Optionally sign into plex.tv. Will not be called on very first run # as plexToken will be '' @@ -49,7 +50,6 @@ class InitialSetup(): chk = self.plx.CheckConnection('plex.tv', plexToken) # HTTP Error: unauthorized if chk == 401: - dialog = xbmcgui.Dialog() dialog.ok( self.addonName, 'Could not login to plex.tv.', @@ -60,7 +60,6 @@ class InitialSetup(): plexLogin = result['username'] plexToken = result['token'] elif chk is False or chk >= 400: - dialog = xbmcgui.Dialog() dialog.ok( self.addonName, 'Problems connecting to plex.tv.', @@ -81,12 +80,8 @@ class InitialSetup(): plexLogin = result['username'] plexToken = result['token'] # Get g_PMS list of servers (saved to plx.g_PMS) - serverNum = 1 - while serverNum > 0: - if plexToken: - tokenDict = {'MyPlexToken': plexToken} - else: - tokenDict = {} + while True: + tokenDict = {'MyPlexToken': plexToken} if plexToken else {} # Populate g_PMS variable with the found Plex servers self.plx.discoverPMS(clientId, None, @@ -100,8 +95,12 @@ class InitialSetup(): # Get a nicer list dialoglist = [] # Exit if no servers found - serverNum = len(serverlist) - if serverNum == 0: + if len(serverlist) == 0: + dialog.ok( + self.addonName, + 'Could not find any Plex server in the network.' + 'Aborting...' + ) break for server in serverlist: if server['local'] == '1': @@ -109,7 +108,6 @@ class InitialSetup(): dialoglist.append(str(server['name']) + ' (nearby)') else: dialoglist.append(str(server['name'])) - dialog = xbmcgui.Dialog() resp = dialog.select( 'Choose your Plex server', dialoglist) @@ -119,11 +117,11 @@ class InitialSetup(): server['port'] # Deactive SSL verification if the server is local! if server['local'] == '1': - self.addon.setSetting('sslverify', 'false') + utils.settings('sslverify', 'false') self.logMsg("Setting SSL verify to false, because server is " "local", 1) else: - self.addon.setSetting('sslverify', 'true') + utils.settings('sslverify', 'true') self.logMsg("Setting SSL verify to true, because server is " "not local", 1) chk = self.plx.CheckConnection(url, server['accesstoken']) @@ -142,7 +140,6 @@ class InitialSetup(): break # Problems connecting elif chk >= 400 or chk is False: - dialog = xbmcgui.Dialog() resp = dialog.yesno(self.addonName, 'Problems connecting to server.', 'Pick another server?') @@ -158,20 +155,19 @@ class InitialSetup(): xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId) return # Write to Kodi settings file - self.addon.setSetting('plex_machineIdentifier', activeServer) - self.addon.setSetting('ipaddress', server['ip']) - self.addon.setSetting('port', server['port']) + utils.settings('plex_machineIdentifier', activeServer) + utils.settings('ipaddress', server['ip']) + utils.settings('port', server['port']) if server['scheme'] == 'https': - self.addon.setSetting('https', 'true') + utils.settings('https', 'true') else: - self.addon.setSetting('https', 'false') + utils.settings('https', 'false') self.logMsg("Wrote to Kodi user settings file:", 0) self.logMsg("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s " % (activeServer, server['ip'], server['port'], server['scheme']), 0) ##### ADDITIONAL PROMPTS ##### - dialog = xbmcgui.Dialog() directPaths = dialog.yesno( heading="%s: Playback Mode" % self.addonName, line1=( diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 9f4334d6..1bcdaec2 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -263,7 +263,7 @@ class Movies(Items): API = PlexAPI.API(itemList) for itemNumber in range(len(itemList)): API.setChildNumber(itemNumber) - itemid = API.getKey() + itemid = API.getRatingKey() # Get key and db entry on the Kodi db side fileid = self.emby_db.getItem_byId(itemid)[1] # Grab the user's viewcount, resume points etc. from PMS' answer @@ -276,6 +276,7 @@ class Movies(Items): userdata['LastPlayedDate']) def add_update(self, item, viewtag=None, viewid=None): + self.logMsg("Entering add_update", 1) # Process single movie kodicursor = self.kodicursor emby_db = self.emby_db @@ -286,7 +287,7 @@ class Movies(Items): # If the item already exist in the local Kodi DB we'll perform a full item update # If the item doesn't exist, we'll add it to the database update_item = True - itemid = API.getKey() + itemid = API.getRatingKey() # Cannot parse XML, abort if not itemid: self.logMsg("Cannot parse XML data for movie", -1) @@ -346,22 +347,22 @@ class Movies(Items): # Find one trailer trailer = None extras = API.getExtras() - for item in extras: + for extra in extras: # Only get 1st trailer element - if item['extraType'] == '1': - trailer = item['key'] + if extra['extraType'] == '1': + trailer = extra['key'] trailer = "plugin://plugin.video.plexkodiconnect/trailer/?id=%s&mode=play" % trailer self.logMsg("Trailer for %s: %s" % (itemid, trailer), 2) break ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] + playurl = API.getKey() + filename = playurl + # if "\\" in playurl: + # # Local path + # filename = playurl.rsplit("\\", 1)[1] + # else: # Network share + # filename = playurl.rsplit("/", 1)[1] if self.directpath: # Direct paths is set the Kodi way @@ -386,13 +387,13 @@ class Movies(Items): path = "plugin://plugin.video.plexkodiconnect.movies/" params = { - 'filename': filename.encode('utf-8'), + #'filename': filename.encode('utf-8'), + 'filename': filename, 'id': itemid, 'dbid': movieid, 'mode': "play" } filename = "%s?%s" % (path, urllib.urlencode(params)) - ##### UPDATE THE MOVIE ##### if update_item: self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1) @@ -462,24 +463,33 @@ class Movies(Items): # Process cast people = API.getPeopleList() kodi_db.addPeople(movieid, people, "movie") + self.logMsg('People added', 2) # Process genres kodi_db.addGenres(movieid, genres, "movie") + self.logMsg('Genres added', 2) # Process artwork allartworks = API.getAllArtwork() + self.logMsg('Artwork processed', 2) artwork.addArtwork(allartworks, movieid, "movie", kodicursor) + self.logMsg('Artwork added', 2) # Process stream details streams = API.getMediaStreams() + self.logMsg('Streames processed', 2) kodi_db.addStreams(fileid, streams, runtime) + self.logMsg('Streames added', 2) # Process studios kodi_db.addStudios(movieid, studios, "movie") + self.logMsg('Studios added', 2) # Process tags: view, emby tags tags = [viewtag] # tags.extend(item['Tags']) # if userdata['Favorite']: # tags.append("Favorite movies") kodi_db.addTags(movieid, tags, "movie") + self.logMsg('Tags added', 2) # Process playstates kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) + self.logMsg('Done processing %s' % itemid, 2) def remove(self, itemid): # Remove movieid, fileid, emby reference @@ -598,7 +608,7 @@ class MusicVideos(Items): ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() + playurl = API.getKey() if "\\" in playurl: # Local path @@ -883,7 +893,7 @@ class TVShows(Items): API = PlexAPI.API(itemList) for itemNumber in range(len(itemList)): API.setChildNumber(itemNumber) - itemid = API.getKey() + itemid = API.getRatingKey() # Get key and db entry on the Kodi db side fileid = self.emby_db.getItem_byId(itemid)[1] # Grab the user's viewcount, resume points etc. from PMS' answer @@ -909,7 +919,7 @@ class TVShows(Items): # If the item already exist in the local Kodi DB we'll perform a full item update # If the item doesn't exist, we'll add it to the database update_item = True - itemid = API.getKey() + itemid = API.getRatingKey() if not itemid: self.logMsg("Cannot parse XML data for TV show", -1) return @@ -943,7 +953,7 @@ class TVShows(Items): studio = None ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() + playurl = API.getKey() if self.directpath: # Direct paths is set the Kodi way @@ -1070,7 +1080,7 @@ class TVShows(Items): def add_updateSeason(self, item, viewid=None, viewtag=None): API = PlexAPI.API(item) showid = viewid - itemid = API.getKey() + itemid = API.getRatingKey() kodicursor = self.kodicursor emby_db = self.emby_db kodi_db = self.kodi_db @@ -1116,7 +1126,7 @@ class TVShows(Items): # If the item already exist in the local Kodi DB we'll perform a full item update # If the item doesn't exist, we'll add it to the database update_item = True - itemid = API.getKey() + itemid = API.getRatingKey() emby_dbitem = emby_db.getItem_byId(itemid) self.logMsg("Processing episode with Plex Id: %s" % itemid, 2) try: @@ -1150,8 +1160,12 @@ class TVShows(Items): resume, runtime = API.getRuntime() premieredate = API.getPremiereDate() + self.logMsg("Retrieved metadata for %s" % itemid, 2) + # episode details seriesId, seriesName, season, episode = API.getEpisodeDetails() + self.logMsg("Got episode details: %s %s: s%se%s" + % (seriesId, seriesName, season, episode), 2) if season is None: if item.get('AbsoluteEpisodeNumber'): @@ -1180,27 +1194,30 @@ class TVShows(Items): try: showid = show[0] except TypeError: - # Show is missing from database - show = self.emby.getItem(seriesId) - self.add_update(show) - show = emby_db.getItem_byId(seriesId) - try: - showid = show[0] - except TypeError: - self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId), -1) - return False - + # self.logMsg("Show is missing from database, trying to add", 2) + # show = self.emby.getItem(seriesId) + # self.logMsg("Show now: %s. Trying to add new show" % show, 2) + # self.add_update(show) + # show = emby_db.getItem_byId(seriesId) + # try: + # showid = show[0] + # except TypeError: + # self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId), -1) + self.logMsg("Parent tvshow now found, skip item", 2) + return False + self.logMsg("showid: %s" % showid, 2) seasonid = kodi_db.addSeason(showid, season) + self.logMsg("seasonid: %s" % seasonid, 2) - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() + playurl = API.getKey() + filename = playurl - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] + # if "\\" in playurl: + # # Local path + # filename = playurl.rsplit("\\", 1)[1] + # else: # Network share + # filename = playurl.rsplit("/", 1)[1] if self.directpath: # Direct paths is set the Kodi way @@ -1225,7 +1242,8 @@ class TVShows(Items): path = "plugin://plugin.video.plexkodiconnect.tvshows/%s/" % seriesId params = { - 'filename': filename.encode('utf-8'), + #'filename': filename.encode('utf-8'), + 'filename': filename, 'id': itemid, 'dbid': episodeid, 'mode': "play" @@ -1234,7 +1252,7 @@ class TVShows(Items): ##### UPDATE THE EPISODE ##### if update_item: - self.logMsg("UPDATE episode itemid: %s - Title: %s" % (itemid, title), 1) + self.logMsg("UPDATE episode itemid: %s" % (itemid), 1) # Update the movie entry if kodiversion in (16, 17): @@ -1268,7 +1286,7 @@ class TVShows(Items): ##### OR ADD THE EPISODE ##### else: - self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1) + self.logMsg("ADD episode itemid: %s" % (itemid), 1) # Add path pathid = kodi_db.addPath(path) @@ -1850,7 +1868,7 @@ class Music(Items): path = "%s/emby/Audio/%s/" % (self.server, itemid) filename = "stream.mp3" else: - playurl = API.getFilePath() + playurl = API.getKey() if "\\" in playurl: # Local path diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 05027da9..cfc44d88 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -27,7 +27,7 @@ import PlexFunctions ################################################################################################## -@utils.ThreadMethodsStopsync +@utils.ThreadMethodsAdditionalStop('emby_shouldStop') @utils.ThreadMethods class ThreadedGetMetadata(threading.Thread): """ @@ -41,10 +41,11 @@ class ThreadedGetMetadata(threading.Thread): the downloaded metadata XMLs as etree objects lock threading.Lock(), used for counting where we are """ - def __init__(self, queue, out_queue, lock): + def __init__(self, queue, out_queue, lock, errorQueue): self.queue = queue self.out_queue = out_queue self.lock = lock + self.errorQueue = errorQueue threading.Thread.__init__(self) def run(self): @@ -54,35 +55,41 @@ class ThreadedGetMetadata(threading.Thread): lock = self.lock threadStopped = self.threadStopped global getMetadataCount - while threadStopped() is False: - # grabs Plex item from queue - try: - updateItem = queue.get(block=False) - # Empty queue - except Queue.Empty: - continue - # Download Metadata - try: + try: + while threadStopped() is False: + # grabs Plex item from queue + try: + updateItem = queue.get(block=False) + # Empty queue + except Queue.Empty: + continue + # Download Metadata plexXML = PlexFunctions.GetPlexMetadata(updateItem['itemId']) - except: - raise - # check whether valid XML - if plexXML: + try: + plexXML.tag + except: + # Did not receive a valid XML - skip that one for now + queue.task_done() + continue + # Get rid of first XML level: + updateItem['XML'] = plexXML # place item into out queue out_queue.put(updateItem) del plexXML - del updateItem - # If we don't have a valid XML, don't put that into the queue - # but skip this item for now - # Keep track of where we are at - with lock: - getMetadataCount += 1 - # signals to queue job is done - queue.task_done() + del updateItem + # If we don't have a valid XML, don't put that into the queue + # but skip this item for now + # Keep track of where we are at + with lock: + getMetadataCount += 1 + # signals to queue job is done + queue.task_done() + except: + self.errorQueue.put(sys.exc_info()) -@utils.ThreadMethodsStopsync +@utils.ThreadMethodsAdditionalStop('emby_shouldStop') @utils.ThreadMethods class ThreadedProcessMetadata(threading.Thread): """ @@ -96,10 +103,11 @@ class ThreadedProcessMetadata(threading.Thread): e.g. 'Movies' => itemtypes.Movies() lock: threading.Lock(), used for counting where we are """ - def __init__(self, queue, itemType, lock): + def __init__(self, queue, itemType, lock, errorQueue): self.queue = queue self.lock = lock self.itemType = itemType + self.errorQueue = errorQueue threading.Thread.__init__(self) def run(self): @@ -111,35 +119,40 @@ class ThreadedProcessMetadata(threading.Thread): threadStopped = self.threadStopped global processMetadataCount global processingViewName - with itemFkt() as item: - while threadStopped() is False: - # grabs item from queue - try: - updateItem = queue.get(block=False) - # Empty queue - except Queue.Empty: - continue - # Do the work; lock to be sure we've only got 1 Thread - plexitem = updateItem['XML'] - method = updateItem['method'] - viewName = updateItem['viewName'] - viewId = updateItem['viewId'] - title = updateItem['title'] - itemSubFkt = getattr(item, method) - with lock: - itemSubFkt(plexitem, - viewtag=viewName, - viewid=viewId) - # Keep track of where we are at - processMetadataCount += 1 - processingViewName = title - del plexitem - del updateItem - # signals to queue job is done - self.queue.task_done() + try: + with itemFkt() as item: + while threadStopped() is False: + # grabs item from queue + try: + updateItem = queue.get(block=False) + # Empty queue + except Queue.Empty: + continue + # Do the work; lock to be sure we've only got 1 Thread + plexitem = updateItem['XML'] + method = updateItem['method'] + viewName = updateItem['viewName'] + viewId = updateItem['viewId'] + title = updateItem['title'] + itemSubFkt = getattr(item, method) + with lock: + itemSubFkt(plexitem, + viewtag=viewName, + viewid=viewId) + # Keep track of where we are at + processMetadataCount += 1 + processingViewName = title + del plexitem + del updateItem + # signals to queue job is done + self.queue.task_done() + except: + xbmc.log('An error occured') + xbmc.log(sys.exc_info()) + self.errorQueue.put(sys.exc_info()) -@utils.ThreadMethodsStopsync +@utils.ThreadMethodsAdditionalStop('emby_shouldStop') @utils.ThreadMethods class ThreadedShowSyncInfo(threading.Thread): """ @@ -184,10 +197,15 @@ class ThreadedShowSyncInfo(threading.Thread): percentage = int(float(totalProgress) / float(total)*100.0) except ZeroDivisionError: percentage = 0 - dialog.update(percentage, - message="Downloaded: %s, Processed: %s: %s" - % (getMetadataProgress, - processMetadataProgress, viewName)) + try: + dialog.update( + percentage, + message="Downloaded: %s, Processed: %s: %s" + % (getMetadataProgress, processMetadataProgress, + viewName)) + except: + # Unicode formating of the string?!? + pass # Sleep for x milliseconds xbmc.sleep(500) dialog.close() @@ -195,7 +213,7 @@ class ThreadedShowSyncInfo(threading.Thread): @utils.logging @utils.ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@utils.ThreadMethodsStopsync +@utils.ThreadMethodsAdditionalStop('emby_shouldStop') @utils.ThreadMethods class LibrarySync(threading.Thread): @@ -213,6 +231,9 @@ class LibrarySync(threading.Thread): self.__dict__ = self._shared_state + # How long should we look into the past for fast syncing items (in s) + self.syncPast = 60 + self.clientInfo = clientinfo.ClientInfo() self.doUtils = downloadutils.DownloadUtils() self.user = userclient.UserClient() @@ -257,13 +278,14 @@ class LibrarySync(threading.Thread): """ self.compare = True # Get last sync time - lastSync = utils.window('LastIncrementalSync') + lastSync = self.lastSync - self.syncPast if not lastSync: # Original Emby format: # lastSync = "2016-01-01T00:00:00Z" # January 1, 2015 at midnight: - lastSync = '1420070400' - self.logMsg("Last sync run: %s" % lastSync, 1) + lastSync = 1420070400 + # Set new timestamp NOW because sync might take a while + self.saveLastSync() # Get all PMS items already saved in Kodi embyconn = utils.kodiSQL('emby') @@ -287,7 +309,8 @@ class LibrarySync(threading.Thread): if self.threadStopped(): return True # Get items per view - items = PlexFunctions.GetPlexUpdatedItems(view['id'], lastSync) + items = PlexFunctions.GetAllPlexLeaves( + view['id'], updatedAt=lastSync) if not items: continue # Get one itemtype, because they're the same in the PMS section @@ -311,18 +334,16 @@ class LibrarySync(threading.Thread): for view in self.views: self.PlexUpdateWatched( view['id'], - PlexFunctions.GetItemClassFromType(view['itemtype'])) + PlexFunctions.GetItemClassFromType(view['itemtype']), + lastViewedAt=lastSync) # Reset and return - self.saveLastSync() self.allKodiElementsId = {} self.allPlexElementsId = {} return True def saveLastSync(self): # Save last sync time - lastSync = str(utils.getUnixTimestamp()) - self.logMsg("New sync time: %s" % lastSync, 1) - utils.window('LastIncrementalSync', value=lastSync) + self.lastSync = utils.getUnixTimestamp() def initializeDBs(self): """ @@ -348,7 +369,7 @@ class LibrarySync(threading.Thread): def fullSync(self, manualrun=False, repair=False): # Only run once when first setting up. Can be run manually. - self.compare = manualrun + self.compare = manualrun or repair music_enabled = utils.settings('enableMusic') == "true" # Add sources @@ -361,7 +382,8 @@ class LibrarySync(threading.Thread): else: message = "Initial sync" utils.window('emby_initialScan', value="true") - + # Set new timestamp NOW because sync might take a while + self.saveLastSync() starttotal = datetime.now() # Ensure that DBs exist if called for very first time @@ -418,7 +440,6 @@ class LibrarySync(threading.Thread): # musiccursor.close() xbmc.executebuiltin('UpdateLibrary(video)') - self.saveLastSync() elapsedtotal = datetime.now() - starttotal utils.window('emby_initialScan', clear=True) @@ -572,7 +593,7 @@ class LibrarySync(threading.Thread): Output: self.updatelist, self.allPlexElementsId self.updatelist APPENDED(!!) list itemids (Plex Keys as - as received from API.getKey()) + as received from API.getRatingKey()) One item in this list is of the form: 'itemId': xxx, 'itemType': 'Movies','TVShows', ... @@ -594,7 +615,7 @@ class LibrarySync(threading.Thread): return False API = PlexAPI.API(item) plex_checksum = API.getChecksum() - itemId = API.getKey() + itemId = API.getRatingKey() title, sorttitle = API.getTitle() self.allPlexElementsId[itemId] = plex_checksum kodi_checksum = self.allKodiElementsId.get(itemId) @@ -616,7 +637,7 @@ class LibrarySync(threading.Thread): if self.threadStopped(): return False API = PlexAPI.API(item) - itemId = API.getKey() + itemId = API.getRatingKey() title, sorttitle = API.getTitle() plex_checksum = API.getChecksum() self.allPlexElementsId[itemId] = plex_checksum @@ -648,6 +669,7 @@ class LibrarySync(threading.Thread): self.logMsg("Starting sync threads", 1) getMetadataQueue = Queue.Queue() processMetadataQueue = Queue.Queue(maxsize=100) + errorQueue = Queue.Queue() getMetadataLock = threading.Lock() processMetadataLock = threading.Lock() # To keep track @@ -665,20 +687,12 @@ class LibrarySync(threading.Thread): for i in range(min(self.syncThreadNumber, itemNumber)): thread = ThreadedGetMetadata(getMetadataQueue, processMetadataQueue, - getMetadataLock) + getMetadataLock, + errorQueue) thread.setDaemon(True) thread.start() threads.append(thread) self.logMsg("Download threads spawned", 1) - # Spawn one more thread to process Metadata, once downloaded - thread = ThreadedProcessMetadata(processMetadataQueue, - itemType, - processMetadataLock) - thread.setDaemon(True) - thread.start() - threads.append(thread) - self.logMsg("Processing thread spawned", 1) - # Start one thread to show sync progress dialog = xbmcgui.DialogProgressBG() thread = ThreadedShowSyncInfo(dialog, @@ -689,9 +703,32 @@ class LibrarySync(threading.Thread): thread.start() threads.append(thread) self.logMsg("Kodi Infobox thread spawned", 1) + # Spawn one more thread to process Metadata, once downloaded + thread = ThreadedProcessMetadata(processMetadataQueue, + itemType, + processMetadataLock, + errorQueue) + thread.setDaemon(True) + thread.start() + threads.append(thread) + self.logMsg("Processing thread spawned", 1) + # Wait until finished - getMetadataQueue.join() - processMetadataQueue.join() + while True: + try: + exc = errorQueue.get(block=False) + except Queue.Empty: + pass + else: + exc_type, exc_obj, exc_trace = exc + # deal with the exception + self.logMsg("Error occured in thread", -1) + self.logMsg(str(exc_type) + str(exc_obj), -1) + self.logMsg(exc_trace, -1) + if getMetadataQueue.empty() and processMetadataQueue.empty(): + break + xbmc.sleep(500) + # Kill threads self.logMsg("Waiting to kill threads", 1) for thread in threads: @@ -770,24 +807,24 @@ class LibrarySync(threading.Thread): self.logMsg("%s sync is finished." % itemType, 1) return True - def PlexUpdateWatched(self, viewId, itemType): + def PlexUpdateWatched(self, viewId, itemType, + lastViewedAt=None, updatedAt=None): """ - Updates ALL plex elements' view status ('watched' or 'unwatched') and + Updates plex elements' view status ('watched' or 'unwatched') and also updates resume times. This is done by downloading one XML for ALL elements with viewId """ - starttotal = datetime.now() # Download XML, not JSON, because PMS JSON seems to be damaged headerOptions = {'Accept': 'application/xml'} plexItems = PlexFunctions.GetAllPlexLeaves( - viewId, headerOptions=headerOptions) - itemMth = getattr(itemtypes, itemType) - with itemMth() as method: - method.updateUserdata(plexItems) - - elapsedtotal = datetime.now() - starttotal - self.logMsg("Syncing userdata for itemtype %s and viewid %s took " - "%s seconds" % (itemType, viewId, elapsedtotal), 1) + viewId, + lastViewedAt=lastViewedAt, + updatedAt=updatedAt, + headerOptions=headerOptions) + if plexItems: + itemMth = getattr(itemtypes, itemType) + with itemMth() as method: + method.updateUserdata(plexItems) def musicvideos(self, embycursor, kodicursor, pdialog): # Get musicvideos from emby diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 396853d5..6e772b36 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -15,6 +15,7 @@ import playutils as putils import playlist import read_embyserver as embyserver import utils +import embydb_functions import PlexAPI @@ -23,12 +24,10 @@ import PlexAPI @utils.logging class PlaybackUtils(): - - + def __init__(self, item): self.item = item - self.API = PlexAPI.API(self.item) self.doUtils = downloadutils.DownloadUtils() @@ -40,21 +39,132 @@ class PlaybackUtils(): self.emby = embyserver.Read_EmbyServer() self.pl = playlist.Playlist() - def play(self, itemid, dbid=None, seektime=None): + def StartPlay(self, resume=None, resumeItem=None): + self.logMsg("StartPlay called with resume=%s, resumeItem=%s" + % (resume, resumeItem), 1) + # Setup Kodi playlist (e.g. make new one or append or even update) + # Why should we have different behaviour if user is on home screen?!? + # self.homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') + self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + # Clear playlist since we're always using PMS playQueues + self.playlist.clear() - self.logMsg("Play called with itemid: %s, dbid: %s, seektime: %s." - % (itemid, dbid, seektime), 1) + self.startPos = max(self.playlist.getposition(), 0) # Can return -1 + self.sizePlaylist = self.playlist.size() + self.currentPosition = self.startPos + self.logMsg("Playlist start position: %s" % self.startPos, 1) + self.logMsg("Playlist position we're starting with: %s" + % self.currentPosition, 1) + self.logMsg("Playlist size: %s" % self.sizePlaylist, 1) - doUtils = self.doUtils - item = self.item - API = self.API + self.plexResumeItemId = resumeItem + # Where should we ultimately start playback? + self.resumePost = self.startPos + + if resume: + if resume == '0': + resume = None + else: + resume = int(resume) + + # Run through the passed PMS playlist and construct playlist + for mediaItem in self.item: + self.AddMediaItemToPlaylist(mediaItem) + # Kick off playback + Player = xbmc.Player() + Player.play(self.playlist, startpos=self.resumePost) + if resume: + try: + Player.seekTime(resume) + except: + self.logMsg("Could not use resume: %s. Start from beginning." + % resume, 0) + + def AddMediaItemToPlaylist(self, item): + """ + Feed with ONE media item from PMS json response + (on level with e.g. key=/library/metadata/220493 present) + + An item may consist of several parts (e.g. movie in 2 pieces/files) + """ + API = PlexAPI.API(item) + playutils = putils.PlayUtils(item) + + # e.g. itemid='219155' + itemid = API.getRatingKey() + # Get DB id from Kodi by using plex id, if that works + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby = embydb_functions.Embydb_Functions(embycursor) + try: + dbid = emby.getItem_byId(itemid)[0] + except TypeError: + # Trailers and the like that are not in the kodi DB + dbid = None + embyconn.close() + + # Get playurls per part and process them + for playurl in playutils.getPlayUrl(): + # One new listitem per part + listitem = xbmcgui.ListItem() + # For items that are not (yet) synced to Kodi lib, e.g. trailers + if not dbid: + self.logMsg("Add item to playlist without Kodi DB id", 1) + # Add Plex credentials to url because Kodi will have no headers + playurl = API.addPlexCredentialsToUrl(playurl) + listitem.setPath(playurl) + self.setProperties(playurl, listitem) + # Set artwork already done in setProperties + self.playlist.add( + playurl, listitem, index=self.currentPosition) + self.currentPosition += 1 + else: + self.logMsg("Add item to playlist with existing Kodi DB id", 1) + self.pl.addtoPlaylist(dbid, API.getType()) + self.currentPosition += 1 + + # For transcoding only, ask for audio/subs pref + if utils.window('emby_%s.playmethod' % playurl) == "Transcode": + playurl = playutils.audioSubsPref(playurl, listitem) + utils.window('emby_%s.playmethod' % playurl, value="Transcode") + + playQueueItemID = API.GetPlayQueueItemID() + # Is this the position where we should start playback? + if playQueueItemID == self.plexResumeItemId: + self.logMsg( + "Figure we should start playback at position %s " + "with playQueueItemID %s" + % (self.currentPosition, playQueueItemID), 2) + self.resumePost = self.currentPosition + # We need to keep track of playQueueItemIDs for Plex Companion + utils.window( + 'plex_%s.playQueueItemID' % playurl, API.GetPlayQueueItemID()) + utils.window( + 'plex_%s.playlistPosition' % playurl, self.currentPosition) + + # Log the playlist that we end up with + self.pl.verifyPlaylist() + + def play(self, item): + + API = PlexAPI.API(item) listitem = xbmcgui.ListItem() playutils = putils.PlayUtils(item) - # Set child number to the very last one, because that's what we want - # to play ultimately - API.setChildNumber(-1) - playurl = playutils.getPlayUrl(child=-1) + # e.g. itemid='219155' + itemid = API.getRatingKey() + # Get DB id from Kodi by using plex id, if that works + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby = embydb_functions.Embydb_Functions(embycursor) + try: + dbid = emby.getItem_byId(itemid)[0] + except TypeError: + # Trailers and the like that are not in the kodi DB + dbid = None + embyconn.close() + + playurl = playutils.getPlayUrl() if not playurl: return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) @@ -82,10 +192,9 @@ class PlaybackUtils(): self.logMsg("Playlist size: %s" % sizePlaylist, 1) ############### RESUME POINT ################ - - if seektime is None: - userdata = API.getUserData() - seektime = userdata['Resume'] + + userdata = API.getUserData() + seektime = userdata['Resume'] # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. @@ -132,7 +241,7 @@ class PlaybackUtils(): self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) introsPlaylist = True currentPosition += 1 - self.logMsg("Key: %s" % API.getKey(), 1) + self.logMsg("Key: %s" % API.getRatingKey(), 1) self.logMsg("Successfally added trailer number %s" % i, 1) # Set "working point" to the movie (last one in playlist) API.setChildNumber(-1) @@ -220,7 +329,7 @@ class PlaybackUtils(): # Set all properties necessary for plugin path playback item = self.item # itemid = item['Id'] - itemid = self.API.getKey() + itemid = self.API.getRatingKey() # itemtype = item['Type'] itemtype = self.API.getType() resume, runtime = self.API.getRuntime() @@ -230,8 +339,9 @@ class PlaybackUtils(): utils.window('%s.type' % embyitem, value=itemtype) utils.window('%s.itemid' % embyitem, value=itemid) - if itemtype == "Episode": - utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId')) + if itemtype == "episode": + utils.window('%s.refreshid' % embyitem, + value=item.get('parentRatingKey')) else: utils.window('%s.refreshid' % embyitem, value=itemid) @@ -282,10 +392,6 @@ class PlaybackUtils(): return externalsubs def setArtwork(self, listItem): - # Set up item and item info - item = self.item - artwork = self.artwork - # allartwork = artwork.getAllArtwork(item, parentInfo=True) allartwork = self.API.getAllArtwork(parentInfo=True) # Set artwork for listitem diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 27125a8b..4943e9fe 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -21,6 +21,7 @@ class PlayUtils(): def __init__(self, item): self.item = item + self.API = PlexAPI.API(item) self.clientInfo = clientinfo.ClientInfo() @@ -28,54 +29,44 @@ class PlayUtils(): self.server = utils.window('emby_server%s' % self.userid) self.machineIdentifier = utils.window('plex_machineIdentifier') - self.API = PlexAPI.API(item) + def getPlayUrl(self): + """ + Returns a list of playurls, one per part in item + """ + playurls = [] + # TODO: multiple media parts for e.g. trailers: replace [0] here + partCount = len(self.item['_children'][0]['_children']) + for partNumber in range(partCount): + playurl = None + self.API.setPartNumber(partNumber) - def getPlayUrl(self, child=0, partIndex=None): - item = self.item - # NO, I am not very fond of this construct! - self.API.setChildNumber(child) - if partIndex is not None: - self.API.setPartNumber(partIndex) - playurl = None - - if item.get('Type') in ["Recording","TvChannel"] and item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http": - #Is this the right way to play a Live TV or recordings ? - self.logMsg("File protocol is http (livetv).", 1) - playurl = "%s/emby/Videos/%s/live.m3u8?static=true" % (self.server, item['Id']) - utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") + if self.isDirectPlay(): + self.logMsg("File is direct playing.", 1) + playurl = self.API.getTranscodeVideoPath('DirectPlay') + playurl = playurl.encode('utf-8') + # Set playmethod property + utils.window('emby_%s.playmethod' % playurl, "DirectPlay") - # if item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http": - # # Only play as http - # self.logMsg("File protocol is http.", 1) - # playurl = self.httpPlay() - # utils.window('emby_%s.playmethod' % playurl, value="DirectStream") + elif self.isDirectStream(): + self.logMsg("File is direct streaming.", 1) + playurl = self.API.getTranscodeVideoPath('DirectStream') + # Set playmethod property + utils.window('emby_%s.playmethod' % playurl, "DirectStream") - if self.isDirectPlay(): - self.logMsg("File is direct playing.", 1) - playurl = self.API.getTranscodeVideoPath('DirectPlay') - playurl = playurl.encode('utf-8') - # Set playmethod property - utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") + elif self.isTranscoding(): + self.logMsg("File is transcoding.", 1) + quality = { + 'maxVideoBitrate': self.getBitrate() + } + playurl = self.API.getTranscodeVideoPath('Transcode', + quality=quality) + # Set playmethod property + utils.window('emby_%s.playmethod' % playurl, value="Transcode") - elif self.isDirectStream(): - self.logMsg("File is direct streaming.", 1) - playurl = self.API.getTranscodeVideoPath('DirectStream') - # Set playmethod property - utils.window('emby_%s.playmethod' % playurl, value="DirectStream") + playurls.append(playurl) - elif self.isTranscoding(): - self.logMsg("File is transcoding.", 1) - quality = { - 'maxVideoBitrate': self.getBitrate() - } - playurl = self.API.getTranscodeVideoPath( - 'Transcode', - quality=quality - ) - # Set playmethod property - utils.window('emby_%s.playmethod' % playurl, value="Transcode") - self.logMsg("The playurl is: %s" % playurl, 1) - return playurl + self.logMsg("The playurls are: %s" % playurls, 1) + return playurls def httpPlay(self): # Audio, Video, Photo @@ -155,25 +146,24 @@ class PlayUtils(): videoCodec = self.API.getVideoCodec() codec = videoCodec['videocodec'] resolution = videoCodec['resolution'] - if ((utils.settings('transcodeH265') == "true") and - ("hevc" in codec) and - (resolution == "1080")): - # Avoid HEVC(H265) 1080p - self.logMsg("Option to transcode 1080P/HEVC enabled.", 0) + # 720p + if ((utils.settings('transcode720H265') == "true") and + ("h265" in codec) and + (resolution in "720 1080")): + self.logMsg("Option to transcode 720P/h265 enabled.", 0) return False - else: - return True + # 1080p + if ((utils.settings('transcodeH265') == "true") and + ("h265" in codec) and + (resolution == "1080")): + self.logMsg("Option to transcode 1080P/h265 enabled.", 0) + return False + return True def isDirectStream(self): if not self.h265enabled(): return False - elif (utils.settings('transcode720H265') == "true" and - item['MediaSources'][0]['Name'].startswith(("720P/HEVC","720P/H265"))): - # Avoid H265 720p - self.logMsg("Option to transcode 720P/H265 enabled.", 1) - return False - # Requirement: BitRate, supported encoding # canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] # Plex: always able?!? @@ -192,7 +182,7 @@ class PlayUtils(): server = self.server - itemid = self.API.getKey() + itemid = self.API.getRatingKey() type = self.API.getType() # if 'Path' in item and item['Path'].endswith('.strm'): @@ -211,7 +201,7 @@ class PlayUtils(): settings = self.getBitrate() - sourceBitrate = self.API.getBitrate() + sourceBitrate = int(self.API.getDataFromPartOrMedia()) self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s" % (settings, sourceBitrate), 1) if settings < sourceBitrate: return False diff --git a/resources/lib/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py index 90bfcb7d..0fb2a81c 100644 --- a/resources/lib/plexbmchelper/functions.py +++ b/resources/lib/plexbmchelper/functions.py @@ -71,11 +71,9 @@ def jsonrpc(action, arguments = {}): "id" : 1 , "method" : "JSONRPC.Ping" }) elif action.lower() == "playmedia": - fullurl=arguments[0] - resume=arguments[1] xbmc.Player().play("plugin://plugin.video.plexkodiconnect/" - "?mode=companion&resume=%s&id=%s" - % (resume, fullurl)) + "?mode=companion&arguments=%s" + % arguments) return True elif arguments: request=json.dumps({ "id" : 1, diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 471c99a2..58181986 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -111,16 +111,33 @@ class MyHandler(BaseHTTPRequestHandler): printDebug("adjusting the volume to %s%%" % volume) jsonrpc("Application.SetVolume", {"volume": volume}) elif "/playMedia" in request_path: + playQueueVersion = int(params.get('playQueueVersion', 1)) + if playQueueVersion < subMgr.playQueueVersion: + # playQueue was updated; ignore this command for now + return + if playQueueVersion > subMgr.playQueueVersion: + # TODO: we should probably update something else now :-) + subMgr.playQueueVersion = playQueueVersion s.response(getOKMsg(), getPlexHeaders()) - resume = params.get('viewOffset', params.get('offset', "0")) + offset = params.get('viewOffset', params.get('offset', "0")) protocol = params.get('protocol', "http") address = params.get('address', s.client_address[0]) server = getServerByHost(address) port = params.get('port', server.get('port', '32400')) - fullurl = protocol+"://"+address+":"+port+params['key'] - printDebug("playMedia command -> fullurl: %s" % fullurl) - jsonrpc("playmedia", [fullurl, resume]) + try: + containerKey = urlparse(params.get('containerKey')).path + except: + containerKey = '' + regex = re.compile(r'''/playQueues/(\d+)$''') + try: + playQueueID = regex.findall(containerKey)[0] + except IndexError: + playQueueID = '' + + jsonrpc("playmedia", params) subMgr.lastkey = params['key'] + subMgr.containerKey = containerKey + subMgr.playQueueID = playQueueID subMgr.server = server.get('server', 'localhost') subMgr.port = port subMgr.protocol = protocol diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index f355212c..06544f94 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -62,7 +62,7 @@ class plexgdm: print "PlexGDM: %s" % message def clientDetails(self, c_id, c_name, c_post, c_product, c_version): - self.client_data = "Content-Type: plex/media-player\r\nResource-Identifier: %s\r\nName: %s\r\nPort: %s\r\nProduct: %s\r\nVersion: %s\r\nProtocol: plex\r\nProtocol-Version: 1\r\nProtocol-Capabilities: navigation,playback,timeline\r\nDevice-Class: HTPC" % ( c_id, c_name, c_post, c_product, c_version ) + self.client_data = "Content-Type: plex/media-player\r\nResource-Identifier: %s\r\nName: %s\r\nPort: %s\r\nProduct: %s\r\nVersion: %s\r\nProtocol: plex\r\nProtocol-Version: 1\r\nProtocol-Capabilities: timeline,playback,navigation,mirror,playqueues\r\nDevice-Class: HTPC" % ( c_id, c_name, c_post, c_product, c_version ) self.client_id = c_id def getClientDetails(self): diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 3da2a600..fb52d1a9 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -12,6 +12,9 @@ class SubscriptionManager: self.subscribers = {} self.info = {} self.lastkey = "" + self.containerKey = "" + self.playQueueID = '' + self.playQueueVersion = 1 self.lastratingkey = "" self.volume = 0 self.guid = "" @@ -75,12 +78,15 @@ class SubscriptionManager: if keyid: self.lastkey = "/library/metadata/%s"%keyid self.lastratingkey = keyid - ret += ' containerKey="%s"' % (self.lastkey) + ret += ' containerKey="%s"' % (self.containerKey) ret += ' key="%s"' % (self.lastkey) ret += ' ratingKey="%s"' % (self.lastratingkey) if pbmc_server: (self.server, self.port) = pbmc_server.split(':') serv = getServerByHost(self.server) + if self.playQueueID: + ret += ' playQueueID="%s"' % self.playQueueID + ret += ' playQueueVersion="%s"' % self.playQueueVersion ret += ' duration="%s"' % info['duration'] ret += ' seekRange="0-%s"' % info['duration'] ret += ' controllable="%s"' % self.controllable() @@ -119,12 +125,15 @@ class SubscriptionManager: for p in players.values(): info = self.playerprops[p.get('playerid')] params = {} - params['containerKey'] = (self.lastkey or "/library/metadata/900000") + params['containerKey'] = (self.containerKey or "/library/metadata/900000") + if self.playQueueID: + params['playQueueID'] = self.playQueueID params['key'] = (self.lastkey or "/library/metadata/900000") params['ratingKey'] = (self.lastratingkey or "900000") params['state'] = info['state'] params['time'] = info['time'] params['duration'] = info['duration'] + params['playQueueVersion'] = self.playQueueVersion serv = getServerByHost(self.server) url = serv.get('protocol', 'http') + '://' \ + serv.get('server', 'localhost') + ':' \ @@ -134,11 +143,9 @@ class SubscriptionManager: printDebug("params: %s" % params) printDebug("players: %s" % players) printDebug("sent server notification with state = %s" % params['state']) - WINDOW = xbmcgui.Window(10000) - WINDOW.setProperty('plexbmc.nowplaying.sent', '1') def controllable(self): - return "playPause,play,stop,skipPrevious,skipNext,volume,stepBack,stepForward,seekTo" + return "volume,shuffle,repeat,audioStream,videoStream,subtitleStream,skipPrevious,skipNext,seekTo,stepBack,stepForward,stop,playPause" def addSubscriber(self, protocol, host, port, uuid, commandID): sub = Subscriber(protocol, host, port, uuid, commandID) @@ -211,7 +218,13 @@ class Subscriber: + "/:/timeline" # Override some headers headerOptions = { - 'Accept': '*/*' + 'Content-Range': 'bytes 0-/-1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17', + 'Accept': '*/*', + 'X-Plex-Username': 'croneter', + 'Connection': 'keep-alive', + 'X-Plex-Client-Capabilities': 'protocols=shoutcast,http-video;videoDecoders=h264{profile:high&resolution:1080&level:51};audioDecoders=mp3,aac,dts{bitrate:800000&channels:8},ac3{bitrate:800000&channels:8}', + 'X-Plex-Client-Profile-Extra': 'add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=*&audioCodec=dca,ac3)' } response = self.download.downloadUrl( url, @@ -220,6 +233,6 @@ class Subscriber: headerOptions=headerOptions) # if not requests.post(self.host, self.port, "/:/timeline", msg, getPlexHeaders(), self.protocol): # subMgr.removeSubscriber(self.uuid) - if response in [False, 401]: + if response in [False, None, 401]: subMgr.removeSubscriber(self.uuid) subMgr = SubscriptionManager() diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 19bffa45..c3d743c9 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -13,7 +13,6 @@ import utils import downloadutils import PlexAPI -import librarysync ################################################################################################## diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 72cc3558..edb22bcc 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -9,7 +9,7 @@ import sqlite3 import time import unicodedata import xml.etree.ElementTree as etree -from functools import wraps, update_wrapper +from functools import wraps from datetime import datetime, timedelta from calendar import timegm @@ -24,19 +24,35 @@ import xbmcvfs addonName = xbmcaddon.Addon().getAddonInfo('name') -def ThreadMethodsStopsync(cls): +def LogTime(func): """ - Decorator to replace stopThread method to include the Kodi window property - 'emby_shouldStop' + Decorator for functions and methods to log the time it took to run the code + """ + @wraps(func) + def wrapper(*args, **kwargs): + starttotal = datetime.now() + result = func(*args, **kwargs) + elapsedtotal = datetime.now() - starttotal + logMsg('%s %s' % (addonName, func.__name__), + 'It took %s to run the function.' % (elapsedtotal), 1) + return result + return wrapper - Use with any library sync threads. @ThreadMethods still required FIRST + +def ThreadMethodsAdditionalStop(windowAttribute): """ - def threadStopped(self): - return (self._threadStopped or - self._abortMonitor.abortRequested() or - window('emby_shouldStop') == "true") - cls.threadStopped = threadStopped - return cls + Decorator to replace stopThread method to include the Kodi windowAttribute + + Use with any sync threads. @ThreadMethods still required FIRST + """ + def wrapper(cls): + def threadStopped(self): + return (self._threadStopped or + self._abortMonitor.abortRequested() or + window(windowAttribute) == "true") + cls.threadStopped = threadStopped + return cls + return wrapper def ThreadMethodsAdditionalSuspend(windowAttribute): @@ -48,8 +64,8 @@ def ThreadMethodsAdditionalSuspend(windowAttribute): """ def wrapper(cls): def threadSuspended(self): - return (self._threadSuspended or True if - window(windowAttribute) == 'true' else False) + return (self._threadSuspended or + window(windowAttribute) == 'true') cls.threadSuspended = threadSuspended return cls return wrapper @@ -140,7 +156,6 @@ def getUnixTimestamp(secondsIntoTheFuture=None): def logMsg(title, msg, level=1): - # Get the logLevel set in UserClient try: logLevel = int(window('emby_logLevel')) @@ -155,13 +170,19 @@ def logMsg(title, msg, level=1): xbmc.log("%s -> %s : %s" % ( title, func.co_name, msg)) except UnicodeEncodeError: - xbmc.log("%s -> %s : %s" % ( - title, func.co_name, msg.encode('utf-8'))) + try: + xbmc.log("%s -> %s : %s" % ( + title, func.co_name, msg.encode('utf-8'))) + except: + xbmc.log("%s -> %s : %s" % (title, func.co_name, 'COULDNT LOG')) else: try: xbmc.log("%s -> %s" % (title, msg)) except UnicodeEncodeError: - xbmc.log("%s -> %s" % (title, msg.encode('utf-8'))) + try: + xbmc.log("%s -> %s" % (title, msg.encode('utf-8'))) + except: + xbmc.log("%s -> %s " % (title, 'COULDNT LOG')) def window(property, value=None, clear=False, windowid=10000): diff --git a/resources/settings.xml b/resources/settings.xml index c84a85bf..53b11ea5 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -28,6 +28,7 @@ +