From 1e49e9dea931929656fefbece5f6970cb831fe26 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 27 Mar 2016 16:57:35 +0200 Subject: [PATCH] Background sync using websockets --- resources/lib/PlexAPI.py | 7 +-- resources/lib/itemtypes.py | 26 +++++++---- resources/lib/librarysync.py | 91 ++++++++++++++++++++++++++++++------ 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 81df5a42..aad838b0 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1482,7 +1482,9 @@ class API(): def getChecksum(self): """ - Returns a string, not int + Returns a string, not int. + + WATCH OUT - time in Plex, not Kodi ;-) """ # Include a letter to prohibit saving as an int! checksum = "K%s%s" % (self.getRatingKey(), @@ -1553,7 +1555,6 @@ class API(): item = self.item.attrib # Default favorite = False - playcount = None played = False lastPlayedDate = None resume = 0 @@ -1562,7 +1563,7 @@ class API(): try: playcount = int(item['viewCount']) except: - playcount = None + playcount = 0 if playcount: played = True diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index d400b707..4a5699dc 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -258,10 +258,24 @@ class Items(object): userdata['LastPlayedDate']) def updatePlaystate(self, item): - if item['duration'] is None: - item['duration'] = self.kodi_db.getVideoRuntime(item['kodi_id'], - item['kodi_type']) - self.logMsg('Updating item with: %s' % item, 0) + """ + Use with websockets, not xml + """ + # If the playback was stopped, check whether we need to increment the + # playcount. PMS won't tell us the playcount via websockets + if item['state'] in ('stopped', 'ended'): + complete = float(item['viewOffset']) / float(item['duration']) + complete = complete * 100 + self.logMsg('Item %s stopped with completion rate %s percent.' + 'Mark item played at %s percent.' + % (item['ratingKey'], + str(complete), + utils.settings('markPlayed')), 1) + if complete >= float(utils.settings('markPlayed')): + self.logMsg('Marking as completely watched in Kodi', 1) + item['viewCount'] += 1 + item['viewOffset'] = 0 + # Do the actual update self.kodi_db.addPlaystate(item['file_id'], item['viewOffset'], item['duration'], @@ -352,10 +366,6 @@ class Movies(Items): update_item = False self.logMsg("movieid: %s missing from Kodi, repairing the entry." % movieid, 1) - # if not viewtag or not viewid: - # # Get view tag from emby - # viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) - # fileId information checksum = API.getChecksum() dateadded = API.getDateCreated() diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index b7aa524d..1ee2ec38 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -22,6 +22,7 @@ import userclient import videonodes import PlexFunctions as PF +import PlexAPI ############################################################################### @@ -46,6 +47,8 @@ class ThreadedGetMetadata(Thread): self.out_queue = out_queue self.lock = lock self.processlock = processlock + # Just in case a time sync goes wrong + utils.window('kodiplextimeoffset', value='0') Thread.__init__(self) def run(self): @@ -228,7 +231,9 @@ class LibrarySync(Thread): # Communication with websockets self.queue = queue self.itemsToProcess = [] - self.safteyMargin = 30 + self.sessionKeys = [] + # How long should we wait at least to process new/changed PMS items? + self.saftyMargin = int(utils.settings('saftyMargin')) self.clientInfo = clientinfo.ClientInfo() self.user = userclient.UserClient() @@ -286,6 +291,10 @@ class LibrarySync(Thread): """ PMS does not provide a means to get a server timestamp. This is a work- around. + + In general, everything saved to Kodi shall be in Kodi time. + + Any info with a PMS timestamp is in Plex time, naturally """ self.logMsg('Synching time with PMS server', 0) # Find a PMS item where we can toggle the view state to enforce a @@ -372,7 +381,8 @@ class LibrarySync(Thread): # Calculate time offset Kodi-PMS self.timeoffset = int(koditime) - int(plextime) - self.logMsg("Time offset Koditime - PMStime in seconds: %s" + utils.window('kodiplextimeoffset', value=str(self.timeoffset)) + self.logMsg("Time offset Koditime - Plextime in seconds: %s" % str(self.timeoffset), 0) def getPMSfromKodiTime(self, koditime): @@ -1420,17 +1430,16 @@ class LibrarySync(Thread): if typus is None: self.logMsg('No type, dropping message: %s' % message, -1) return - + self.logMsg('Message received from websocket: %s' % message, 2) if typus == 'playing': self.process_playing(message['_children']) elif typus == 'timeline': self.process_timeline(message['_children']) - else: - self.logMsg('Dropping message: %s' % message, -1) def multi_delete(self, liste, deleteListe): """ Deletes the list items of liste at the positions in deleteListe + (arbitrary order) """ indexes = sorted(deleteListe, reverse=True) for index in indexes: @@ -1450,7 +1459,7 @@ class LibrarySync(Thread): for i, item in enumerate(self.itemsToProcess): ratingKey = item['ratingKey'] timestamp = item['timestamp'] - if now - timestamp < self.safteyMargin: + if now - timestamp < self.saftyMargin: # We haven't waited long enough for the PMS to finish # processing the item continue @@ -1521,7 +1530,8 @@ class LibrarySync(Thread): elif item.get('state') == 5 and item.get('type') in (1, 4): # Item added or changed # Need to process later because PMS needs to be done first - self.logMsg('New/changed PMS item: %s' % item.get('itemID'), 1) + self.logMsg('New/changed PMS item detected: %s' + % item.get('itemID'), 1) self.itemsToProcess.append({ 'ratingKey': item.get('itemID'), 'timestamp': utils.getUnixTimestamp() @@ -1533,18 +1543,66 @@ class LibrarySync(Thread): xbmc.executebuiltin('UpdateLibrary(video)') def process_playing(self, data): + """ + Someone (not necessarily the user signed in) is playing something some- + where + """ items = [] with embydb.GetEmbyDB() as emby_db: for item in data: - # Drop buffering messages + # Drop buffering messages immediately state = item.get('state') if state == 'buffering': continue ratingKey = item.get('ratingKey') + sessionKey = item.get('sessionKey') + # Do we already have a sessionKey stored? + if sessionKey not in self.sessionKeys: + # No - update our list of all current sessions + self.sessionKeys = PF.GetPMSStatus( + utils.window('plex_token')) + self.logMsg('Updated current sessions. They are: %s' + % self.sessionKeys, 2) + if sessionKey not in self.sessionKeys: + self.logMsg('Session key %s still unknown! Skip item' + % sessionKey, 1) + continue + + currSess = self.sessionKeys[sessionKey] + # Identify the user - same one as signed on with PKC? + # Skip update if neither session's username nor userid match + # (Owner sometime's returns id '1', not always) + if not (currSess['userId'] == utils.window('currUserId') + or + currSess['username'] == utils.window('plex_username')): + self.logMsg('Our username %s, userid %s did not match the ' + 'session username %s with userid %s' + % (utils.window('plex_username'), + utils.window('currUserId'), + currSess['username'], + currSess['userId']), 2) + continue + kodiInfo = emby_db.getItem_byId(ratingKey) if kodiInfo is None: # Item not (yet) in Kodi library continue + + # Get an up-to-date XML from the PMS + # because PMS will NOT directly tell us: + # duration of item + # viewCount + if currSess.get('duration') is None: + xml = PF.GetPlexMetadata(ratingKey) + if xml is None: + self.logMsg('Could not get up-to-date xml for item %s' + % ratingKey, -1) + continue + API = PlexAPI.API(xml[0]) + userdata = API.getUserData() + currSess['duration'] = userdata['Runtime'] + currSess['viewCount'] = userdata['PlayCount'] + # Append to list that we need to process items.append({ 'ratingKey': ratingKey, 'kodi_id': kodiInfo[0], @@ -1553,11 +1611,14 @@ class LibrarySync(Thread): 'viewOffset': PF.ConvertPlexToKodiTime( item.get('viewOffset')), 'state': state, - 'duration': PF.ConvertPlexToKodiTime( - item.get('duration')), - 'viewCount': item.get('viewCount'), + 'duration': currSess['duration'], + 'viewCount': currSess['viewCount'], 'lastViewedAt': utils.DateToKodi(utils.getUnixTimestamp()) }) + self.logMsg('Update playstate for user %s with id %s: %s' + % (utils.window('plex_username'), + utils.window('currUserId'), + items[-1]), 2) for item in items: itemFkt = getattr(itemtypes, PF.GetItemClassFromType(item['kodi_type'])) @@ -1618,9 +1679,8 @@ class LibrarySync(Thread): # Verify the validity of the database currentVersion = settings('dbCreatedWithVersion') minVersion = window('emby_minDBVersion') - uptoDate = self.compareDBVersion(currentVersion, minVersion) - if not uptoDate: + if not self.compareDBVersion(currentVersion, minVersion): log("Db version out of date: %s minimum version required: " "%s" % (currentVersion, minVersion), 0) # DB out of date. Proceed to recreate? @@ -1655,10 +1715,10 @@ class LibrarySync(Thread): # Run start up sync window('emby_dbScan', value="true") log("Db version: %s" % settings('dbCreatedWithVersion'), 0) + self.syncPMStime() log("Initial start-up full sync starting", 0) librarySync = fullSync(manualrun=True) # Initialize time offset Kodi - PMS - self.syncPMStime() window('emby_dbScan', clear=True) if librarySync: log("Initial start-up full sync successful", 0) @@ -1730,7 +1790,9 @@ class LibrarySync(Thread): self.showKodiNote(string(39407), forced=False) elif count % 300 == 0: count += 1 + window('emby_dbScan', value="true") self.process_newitems() + window('emby_dbScan', clear=True) else: count += 1 # See if there is a PMS message we need to handle @@ -1744,6 +1806,7 @@ class LibrarySync(Thread): else: window('emby_dbScan', value="true") processMessage(message) + queue.task_done() window('emby_dbScan', clear=True) # NO sleep! continue