Background sync using websockets
This commit is contained in:
parent
6a2094d444
commit
1e49e9dea9
3 changed files with 99 additions and 25 deletions
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue