Rework websocket playstate updates from the PMS

- Should fix #362
This commit is contained in:
croneter 2018-02-05 17:48:50 +01:00
parent dea8e6d5f5
commit 6cb69ada3f
3 changed files with 92 additions and 97 deletions

View file

@ -149,37 +149,26 @@ class Items(object):
db_item[4], db_item[4],
userdata['UserRating']) userdata['UserRating'])
def updatePlaystate(self, item): def updatePlaystate(self, mark_played, view_count, resume, duration,
file_id, lastViewedAt):
""" """
Use with websockets, not xml Use with websockets, not xml
""" """
# If the playback was stopped, check whether we need to increment the # If the playback was stopped, check whether we need to increment the
# playcount. PMS won't tell us the playcount via websockets # playcount. PMS won't tell us the playcount via websockets
if item['state'] in ('stopped', 'ended'): if mark_played:
# If offset exceeds duration skip update
if item['viewOffset'] > item['duration']:
log.error("Error while updating play state, viewOffset "
"exceeded duration")
return
complete = float(item['viewOffset']) / float(item['duration'])
log.info('Item %s stopped with completion rate %s percent.'
'Mark item played at %s percent.'
% (item['ratingKey'], str(complete), v.MARK_PLAYED_AT), 1)
if complete >= v.MARK_PLAYED_AT:
log.info('Marking as completely watched in Kodi') log.info('Marking as completely watched in Kodi')
try: try:
item['viewCount'] += 1 view_count += 1
except TypeError: except TypeError:
item['viewCount'] = 1 view_count = 1
item['viewOffset'] = 0 resume = 0
# Do the actual update # Do the actual update
self.kodi_db.addPlaystate(item['file_id'], self.kodi_db.addPlaystate(file_id,
item['viewOffset'], resume,
item['duration'], duration,
item['viewCount'], view_count,
item['lastViewedAt']) lastViewedAt)
class Movies(Items): class Movies(Items):

View file

@ -45,7 +45,7 @@ class LibrarySync(Thread):
""" """
def __init__(self): def __init__(self):
self.itemsToProcess = [] self.itemsToProcess = []
self.sessionKeys = [] self.sessionKeys = {}
self.fanartqueue = Queue.Queue() self.fanartqueue = Queue.Queue()
if settings('FanartTV') == 'true': if settings('FanartTV') == 'true':
self.fanartthread = Process_Fanart_Thread(self.fanartqueue) self.fanartthread = Process_Fanart_Thread(self.fanartqueue)
@ -223,6 +223,8 @@ class LibrarySync(Thread):
""" """
repair=True: force sync EVERY item repair=True: force sync EVERY item
""" """
# Reset our keys
self.sessionKeys = {}
# self.compare == False: we're syncing EVERY item # self.compare == False: we're syncing EVERY item
# True: we're syncing only the delta, e.g. different checksum # True: we're syncing only the delta, e.g. different checksum
self.compare = not repair self.compare = not repair
@ -1283,110 +1285,108 @@ class LibrarySync(Thread):
Someone (not necessarily the user signed in) is playing something some- Someone (not necessarily the user signed in) is playing something some-
where where
""" """
items = []
for item in data: for item in data:
# Drop buffering messages immediately
status = item['state'] status = item['state']
if status == 'buffering': if status == 'buffering':
# Drop buffering messages immediately
continue continue
ratingKey = str(item['ratingKey']) plex_id = str(item['ratingKey'])
for pid in (0, 1, 2): for pid in (0, 1, 2):
if ratingKey == state.PLAYER_STATES[pid]['plex_id']: if plex_id == state.PLAYER_STATES[pid]['plex_id']:
# Kodi is playing this item - no need to set the playstate # Kodi is playing this item - no need to set the playstate
continue continue
with plexdb.Get_Plex_DB() as plex_db:
kodi_info = plex_db.getItem_byId(ratingKey)
if kodi_info is None:
# Item not (yet) in Kodi library
continue
sessionKey = item['sessionKey'] sessionKey = item['sessionKey']
# Do we already have a sessionKey stored? # Do we already have a sessionKey stored?
if sessionKey not in self.sessionKeys: if sessionKey not in self.sessionKeys:
with plexdb.Get_Plex_DB() as plex_db:
kodi_info = plex_db.getItem_byId(plex_id)
if kodi_info is None:
# Item not (yet) in Kodi library
continue
if settings('plex_serverowned') == 'false': if settings('plex_serverowned') == 'false':
# Not our PMS, we are not authorized to get the # Not our PMS, we are not authorized to get the sessions
# sessions
# On the bright side, it must be us playing :-) # On the bright side, it must be us playing :-)
self.sessionKeys = { self.sessionKeys[sessionKey] = {}
sessionKey: {}
}
else: else:
# PMS is ours - get all current sessions # PMS is ours - get all current sessions
self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN) self.sessionKeys.update(GetPMSStatus(state.PLEX_TOKEN))
log.debug('Updated current sessions. They are: %s' log.debug('Updated current sessions. They are: %s',
% self.sessionKeys) self.sessionKeys)
if sessionKey not in self.sessionKeys: if sessionKey not in self.sessionKeys:
log.warn('Session key %s still unknown! Skip ' log.info('Session key %s still unknown! Skip '
'item' % sessionKey) 'playstate update', sessionKey)
continue continue
# Attach Kodi info to the session
currSess = self.sessionKeys[sessionKey] self.sessionKeys[sessionKey]['kodi_id'] = kodi_info[0]
self.sessionKeys[sessionKey]['file_id'] = kodi_info[1]
self.sessionKeys[sessionKey]['kodi_type'] = kodi_info[4]
session = self.sessionKeys[sessionKey]
if settings('plex_serverowned') != 'false': if settings('plex_serverowned') != 'false':
# Identify the user - same one as signed on with PKC? Skip # Identify the user - same one as signed on with PKC? Skip
# update if neither session's username nor userid match # update if neither session's username nor userid match
# (Owner sometime's returns id '1', not always) # (Owner sometime's returns id '1', not always)
if (not state.PLEX_TOKEN and currSess['userId'] == '1'): if not state.PLEX_TOKEN and session['userId'] == '1':
# PKC not signed in to plex.tv. Plus owner of PMS is # PKC not signed in to plex.tv. Plus owner of PMS is
# playing (the '1'). # playing (the '1').
# Hence must be us (since several users require plex.tv # Hence must be us (since several users require plex.tv
# token for PKC) # token for PKC)
pass pass
elif not (currSess['userId'] == state.PLEX_USER_ID elif not (session['userId'] == state.PLEX_USER_ID or
or session['username'] == state.PLEX_USERNAME):
currSess['username'] == state.PLEX_USERNAME):
log.debug('Our username %s, userid %s did not match ' log.debug('Our username %s, userid %s did not match '
'the session username %s with userid %s' 'the session username %s with userid %s',
% (state.PLEX_USERNAME, state.PLEX_USERNAME,
state.PLEX_USER_ID, state.PLEX_USER_ID,
currSess['username'], session['username'],
currSess['userId'])) session['userId'])
continue continue
# Get an up-to-date XML from the PMS because PMS will NOT directly
# Get an up-to-date XML from the PMS # tell us: duration of item viewCount
# because PMS will NOT directly tell us: if session.get('duration') is None:
# duration of item xml = GetPlexMetadata(plex_id)
# viewCount
if currSess.get('duration') is None:
xml = GetPlexMetadata(ratingKey)
if xml in (None, 401): if xml in (None, 401):
log.error('Could not get up-to-date xml for item %s' log.error('Could not get up-to-date xml for item %s',
% ratingKey) plex_id)
continue continue
API = PlexAPI.API(xml[0]) api = PlexAPI.API(xml[0])
userdata = API.getUserData() userdata = api.getUserData()
currSess['duration'] = userdata['Runtime'] session['duration'] = userdata['Runtime']
currSess['viewCount'] = userdata['PlayCount'] session['viewCount'] = userdata['PlayCount']
# Sometimes, Plex tells us resume points in milliseconds and # Sometimes, Plex tells us resume points in milliseconds and
# not in seconds - thank you very much! # not in seconds - thank you very much!
if item.get('viewOffset') > currSess['duration']: if item['viewOffset'] > session['duration']:
resume = item.get('viewOffset') / 1000 resume = item['viewOffset'] / 1000
else: else:
resume = item.get('viewOffset') resume = item['viewOffset']
if resume >= v.MARK_PLAYED_AT and status not in ('stopped', 'ended'): if resume < v.IGNORE_SECONDS_AT_START:
# We need to drop these as we'll otherwise NOT mark an item as
# completely watched after having seen >90%
continue continue
# Append to list that we need to process try:
items.append({ completed = float(resume) / float(session['duration'])
'ratingKey': ratingKey, except (ZeroDivisionError, TypeError):
'kodi_id': kodi_info[0], log.error('Could not mark playstate for %s and session %s',
'file_id': kodi_info[1], data, session)
'kodi_type': kodi_info[4], continue
'viewOffset': resume, if completed >= v.MARK_PLAYED_AT:
'state': status, # Only mark completely watched ONCE
'duration': currSess['duration'], if session.get('marked_played') is None:
'viewCount': currSess['viewCount'], session['marked_played'] = True
'lastViewedAt': DateToKodi(getUnixTimestamp()) mark_played = True
}) else:
log.debug('Update playstate for user %s with id %s: %s' # Don't mark it as completely watched again
% (state.PLEX_USERNAME, continue
state.PLEX_USER_ID, else:
items[-1])) mark_played = False
# Now tell Kodi where we are log.debug('Update playstate for user %s with id %s for plex id %s',
for item in items: state.PLEX_USERNAME, state.PLEX_USER_ID, plex_id)
itemFkt = getattr(itemtypes, item_fkt = getattr(itemtypes,
v.ITEMTYPE_FROM_KODITYPE[item['kodi_type']]) v.ITEMTYPE_FROM_KODITYPE[session['kodi_type']])
with itemFkt() as Fkt: with item_fkt() as fkt:
Fkt.updatePlaystate(item) fkt.updatePlaystate(mark_played,
session['viewCount'],
resume,
session['duration'],
session['file_id'],
DateToKodi(getUnixTimestamp()))
def fanartSync(self, refresh=False): def fanartSync(self, refresh=False):
""" """

View file

@ -21,7 +21,13 @@ def tryDecode(string, encoding='utf-8'):
string = string.decode() string = string.decode()
return string return string
# Percent of playback progress for watching item as partially watched. Anything
# more and item will NOT be marked as partially, but fully watched
MARK_PLAYED_AT = 0.9 MARK_PLAYED_AT = 0.9
# How many seconds of playback do we ignore before marking an item as partially
# watched?
IGNORE_SECONDS_AT_START = 60
_ADDON = Addon() _ADDON = Addon()
ADDON_NAME = 'PlexKodiConnect' ADDON_NAME = 'PlexKodiConnect'