Background sync using websockets

This commit is contained in:
tomkat83 2016-03-27 16:57:35 +02:00
parent 6a2094d444
commit 1e49e9dea9
3 changed files with 99 additions and 25 deletions

View file

@ -1482,7 +1482,9 @@ class API():
def getChecksum(self): 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! # Include a letter to prohibit saving as an int!
checksum = "K%s%s" % (self.getRatingKey(), checksum = "K%s%s" % (self.getRatingKey(),
@ -1553,7 +1555,6 @@ class API():
item = self.item.attrib item = self.item.attrib
# Default # Default
favorite = False favorite = False
playcount = None
played = False played = False
lastPlayedDate = None lastPlayedDate = None
resume = 0 resume = 0
@ -1562,7 +1563,7 @@ class API():
try: try:
playcount = int(item['viewCount']) playcount = int(item['viewCount'])
except: except:
playcount = None playcount = 0
if playcount: if playcount:
played = True played = True

View file

@ -258,10 +258,24 @@ class Items(object):
userdata['LastPlayedDate']) userdata['LastPlayedDate'])
def updatePlaystate(self, item): def updatePlaystate(self, item):
if item['duration'] is None: """
item['duration'] = self.kodi_db.getVideoRuntime(item['kodi_id'], Use with websockets, not xml
item['kodi_type']) """
self.logMsg('Updating item with: %s' % item, 0) # 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'], self.kodi_db.addPlaystate(item['file_id'],
item['viewOffset'], item['viewOffset'],
item['duration'], item['duration'],
@ -352,10 +366,6 @@ class Movies(Items):
update_item = False update_item = False
self.logMsg("movieid: %s missing from Kodi, repairing the entry." % movieid, 1) 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 # fileId information
checksum = API.getChecksum() checksum = API.getChecksum()
dateadded = API.getDateCreated() dateadded = API.getDateCreated()

View file

@ -22,6 +22,7 @@ import userclient
import videonodes import videonodes
import PlexFunctions as PF import PlexFunctions as PF
import PlexAPI
############################################################################### ###############################################################################
@ -46,6 +47,8 @@ class ThreadedGetMetadata(Thread):
self.out_queue = out_queue self.out_queue = out_queue
self.lock = lock self.lock = lock
self.processlock = processlock self.processlock = processlock
# Just in case a time sync goes wrong
utils.window('kodiplextimeoffset', value='0')
Thread.__init__(self) Thread.__init__(self)
def run(self): def run(self):
@ -228,7 +231,9 @@ class LibrarySync(Thread):
# Communication with websockets # Communication with websockets
self.queue = queue self.queue = queue
self.itemsToProcess = [] 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.clientInfo = clientinfo.ClientInfo()
self.user = userclient.UserClient() 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- PMS does not provide a means to get a server timestamp. This is a work-
around. 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) self.logMsg('Synching time with PMS server', 0)
# Find a PMS item where we can toggle the view state to enforce a # 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 # Calculate time offset Kodi-PMS
self.timeoffset = int(koditime) - int(plextime) 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) % str(self.timeoffset), 0)
def getPMSfromKodiTime(self, koditime): def getPMSfromKodiTime(self, koditime):
@ -1420,17 +1430,16 @@ class LibrarySync(Thread):
if typus is None: if typus is None:
self.logMsg('No type, dropping message: %s' % message, -1) self.logMsg('No type, dropping message: %s' % message, -1)
return return
self.logMsg('Message received from websocket: %s' % message, 2)
if typus == 'playing': if typus == 'playing':
self.process_playing(message['_children']) self.process_playing(message['_children'])
elif typus == 'timeline': elif typus == 'timeline':
self.process_timeline(message['_children']) self.process_timeline(message['_children'])
else:
self.logMsg('Dropping message: %s' % message, -1)
def multi_delete(self, liste, deleteListe): def multi_delete(self, liste, deleteListe):
""" """
Deletes the list items of liste at the positions in deleteListe Deletes the list items of liste at the positions in deleteListe
(arbitrary order)
""" """
indexes = sorted(deleteListe, reverse=True) indexes = sorted(deleteListe, reverse=True)
for index in indexes: for index in indexes:
@ -1450,7 +1459,7 @@ class LibrarySync(Thread):
for i, item in enumerate(self.itemsToProcess): for i, item in enumerate(self.itemsToProcess):
ratingKey = item['ratingKey'] ratingKey = item['ratingKey']
timestamp = item['timestamp'] timestamp = item['timestamp']
if now - timestamp < self.safteyMargin: if now - timestamp < self.saftyMargin:
# We haven't waited long enough for the PMS to finish # We haven't waited long enough for the PMS to finish
# processing the item # processing the item
continue continue
@ -1521,7 +1530,8 @@ class LibrarySync(Thread):
elif item.get('state') == 5 and item.get('type') in (1, 4): elif item.get('state') == 5 and item.get('type') in (1, 4):
# Item added or changed # Item added or changed
# Need to process later because PMS needs to be done first # 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({ self.itemsToProcess.append({
'ratingKey': item.get('itemID'), 'ratingKey': item.get('itemID'),
'timestamp': utils.getUnixTimestamp() 'timestamp': utils.getUnixTimestamp()
@ -1533,18 +1543,66 @@ class LibrarySync(Thread):
xbmc.executebuiltin('UpdateLibrary(video)') xbmc.executebuiltin('UpdateLibrary(video)')
def process_playing(self, data): def process_playing(self, data):
"""
Someone (not necessarily the user signed in) is playing something some-
where
"""
items = [] items = []
with embydb.GetEmbyDB() as emby_db: with embydb.GetEmbyDB() as emby_db:
for item in data: for item in data:
# Drop buffering messages # Drop buffering messages immediately
state = item.get('state') state = item.get('state')
if state == 'buffering': if state == 'buffering':
continue continue
ratingKey = item.get('ratingKey') 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) kodiInfo = emby_db.getItem_byId(ratingKey)
if kodiInfo is None: if kodiInfo is None:
# Item not (yet) in Kodi library # Item not (yet) in Kodi library
continue 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({ items.append({
'ratingKey': ratingKey, 'ratingKey': ratingKey,
'kodi_id': kodiInfo[0], 'kodi_id': kodiInfo[0],
@ -1553,11 +1611,14 @@ class LibrarySync(Thread):
'viewOffset': PF.ConvertPlexToKodiTime( 'viewOffset': PF.ConvertPlexToKodiTime(
item.get('viewOffset')), item.get('viewOffset')),
'state': state, 'state': state,
'duration': PF.ConvertPlexToKodiTime( 'duration': currSess['duration'],
item.get('duration')), 'viewCount': currSess['viewCount'],
'viewCount': item.get('viewCount'),
'lastViewedAt': utils.DateToKodi(utils.getUnixTimestamp()) '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: for item in items:
itemFkt = getattr(itemtypes, itemFkt = getattr(itemtypes,
PF.GetItemClassFromType(item['kodi_type'])) PF.GetItemClassFromType(item['kodi_type']))
@ -1618,9 +1679,8 @@ class LibrarySync(Thread):
# Verify the validity of the database # Verify the validity of the database
currentVersion = settings('dbCreatedWithVersion') currentVersion = settings('dbCreatedWithVersion')
minVersion = window('emby_minDBVersion') 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: " log("Db version out of date: %s minimum version required: "
"%s" % (currentVersion, minVersion), 0) "%s" % (currentVersion, minVersion), 0)
# DB out of date. Proceed to recreate? # DB out of date. Proceed to recreate?
@ -1655,10 +1715,10 @@ class LibrarySync(Thread):
# Run start up sync # Run start up sync
window('emby_dbScan', value="true") window('emby_dbScan', value="true")
log("Db version: %s" % settings('dbCreatedWithVersion'), 0) log("Db version: %s" % settings('dbCreatedWithVersion'), 0)
self.syncPMStime()
log("Initial start-up full sync starting", 0) log("Initial start-up full sync starting", 0)
librarySync = fullSync(manualrun=True) librarySync = fullSync(manualrun=True)
# Initialize time offset Kodi - PMS # Initialize time offset Kodi - PMS
self.syncPMStime()
window('emby_dbScan', clear=True) window('emby_dbScan', clear=True)
if librarySync: if librarySync:
log("Initial start-up full sync successful", 0) log("Initial start-up full sync successful", 0)
@ -1730,7 +1790,9 @@ class LibrarySync(Thread):
self.showKodiNote(string(39407), forced=False) self.showKodiNote(string(39407), forced=False)
elif count % 300 == 0: elif count % 300 == 0:
count += 1 count += 1
window('emby_dbScan', value="true")
self.process_newitems() self.process_newitems()
window('emby_dbScan', clear=True)
else: else:
count += 1 count += 1
# See if there is a PMS message we need to handle # See if there is a PMS message we need to handle
@ -1744,6 +1806,7 @@ class LibrarySync(Thread):
else: else:
window('emby_dbScan', value="true") window('emby_dbScan', value="true")
processMessage(message) processMessage(message)
queue.task_done()
window('emby_dbScan', clear=True) window('emby_dbScan', clear=True)
# NO sleep! # NO sleep!
continue continue