Rewire PKC resume mechanism

This commit is contained in:
croneter 2019-10-25 17:06:50 +02:00
parent 76e1b1c629
commit 9f2210a5e7
5 changed files with 91 additions and 88 deletions

View file

@ -30,7 +30,8 @@ RESOLVE = True
############################################################################### ###############################################################################
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True,
resume=False):
""" """
Hit this function for addon path playback, Plex trailers, etc. Hit this function for addon path playback, Plex trailers, etc.
Will setup playback first, then on second call complete playback. Will setup playback first, then on second call complete playback.
@ -47,7 +48,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
service.py Python instance service.py Python instance
""" """
try: try:
_playback_triage(plex_id, plex_type, path, resolve) _playback_triage(plex_id, plex_type, path, resolve, resume)
finally: finally:
# Reset some playback variables the user potentially set to init # Reset some playback variables the user potentially set to init
# playback # playback
@ -56,10 +57,10 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
app.PLAYSTATE.resume_playback = None app.PLAYSTATE.resume_playback = None
def _playback_triage(plex_id, plex_type, path, resolve): def _playback_triage(plex_id, plex_type, path, resolve, resume):
plex_id = utils.cast(int, plex_id) plex_id = utils.cast(int, plex_id)
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, ' LOG.debug('playback_triage called with plex_id %s, plex_type %s, path %s, '
'resolve %s', plex_id, plex_type, path, resolve) 'resolve %s, resume %s', plex_id, plex_type, path, resolve, resume)
global RESOLVE global RESOLVE
# If started via Kodi context menu, we never resolve # If started via Kodi context menu, we never resolve
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
@ -85,12 +86,12 @@ def _playback_triage(plex_id, plex_type, path, resolve):
except KeyError: except KeyError:
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for # Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
# add-on paths # add-on paths
LOG.info('No position returned from player! Assuming playlist') LOG.debug('No position returned from player! Assuming playlist')
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO) playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
try: try:
pos = js.get_position(playqueue.playlistid) pos = js.get_position(playqueue.playlistid)
except KeyError: except KeyError:
LOG.info('Assuming video instead of audio playlist playback') LOG.debug('Assuming video instead of audio playlist playback')
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO) playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
try: try:
pos = js.get_position(playqueue.playlistid) pos = js.get_position(playqueue.playlistid)
@ -108,12 +109,12 @@ def _playback_triage(plex_id, plex_type, path, resolve):
try: try:
item = items[pos] item = items[pos]
except IndexError: except IndexError:
LOG.info('Could not apply playlist hack! Probably Widget playback') LOG.debug('Could not apply playlist hack! Probably Widget playback')
else: else:
if ('id' not in item and if ('id' not in item and
item.get('type') == 'unknown' and item.get('title') == ''): item.get('type') == 'unknown' and item.get('title') == ''):
LOG.info('Kodi playlist play detected') LOG.debug('Kodi playlist play detected')
_playlist_playback(plex_id, plex_type) _playlist_playback(plex_id)
return return
# Can return -1 (as in "no playlist") # Can return -1 (as in "no playlist")
@ -127,7 +128,7 @@ def _playback_triage(plex_id, plex_type, path, resolve):
initiate = True initiate = True
else: else:
if item.plex_id != plex_id: if item.plex_id != plex_id:
LOG.debug('Received new plex_id %s, expected %s', LOG.debug('Received new plex_id%s, expected %s',
plex_id, item.plex_id) plex_id, item.plex_id)
initiate = True initiate = True
else: else:
@ -136,13 +137,14 @@ def _playback_triage(plex_id, plex_type, path, resolve):
LOG.debug('Detected re-playing of the same item') LOG.debug('Detected re-playing of the same item')
initiate = True initiate = True
if initiate: if initiate:
_playback_init(plex_id, plex_type, playqueue, pos) _playback_init(plex_id, plex_type, playqueue, pos, resume)
else: else:
# kick off playback on second pass # kick off playback on second pass, resume was already set on first
# pass (threaded_playback will seek to resume)
_conclude_playback(playqueue, pos) _conclude_playback(playqueue, pos)
def _playlist_playback(plex_id, plex_type): def _playlist_playback(plex_id):
""" """
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some- Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
where, causing Playlist.onAdd to fire for each item like this: where, causing Playlist.onAdd to fire for each item like this:
@ -175,11 +177,11 @@ def _playlist_playback(plex_id, plex_type):
_conclude_playback(playqueue, pos=0) _conclude_playback(playqueue, pos=0)
def _playback_init(plex_id, plex_type, playqueue, pos): def _playback_init(plex_id, plex_type, playqueue, pos, force_resume):
""" """
Playback setup if Kodi starts playing an item for the first time. Playback setup if Kodi starts playing an item for the first time.
""" """
LOG.info('Initializing PKC playback') LOG.debug('Initializing PKC playback')
# Stop playback so we don't get an error message that the last item of the # Stop playback so we don't get an error message that the last item of the
# queue failed to play # queue failed to play
app.APP.player.stop() app.APP.player.stop()
@ -210,19 +212,8 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
# playqueues # playqueues
# Release default.py # Release default.py
_ensure_resolve() _ensure_resolve()
api = API(xml[0]) LOG.debug('Using force_resume %s', force_resume)
if api.resume_point() and (app.SYNC.direct_paths or resume = force_resume or False
app.PLAYSTATE.context_menu_play):
# Since Kodi won't ask if user wants to resume playback -
# we need to ask ourselves
resume = resume_dialog(int(api.resume_point()))
if resume is None:
LOG.info('User cancelled resume dialog')
return
elif app.SYNC.direct_paths:
resume = False
else:
resume = app.PLAYSTATE.resume_playback or False
trailers = False trailers = False
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
utils.settings('enableCinema') == "true"): utils.settings('enableCinema') == "true"):
@ -251,11 +242,17 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
PL.get_playlist_details_from_xml(playqueue, xml) PL.get_playlist_details_from_xml(playqueue, xml)
stack = _prep_playlist_stack(xml, resume) stack = _prep_playlist_stack(xml, resume)
_process_stack(playqueue, stack) _process_stack(playqueue, stack)
offset = _use_kodi_db_offset(playqueue.items[pos].plex_id,
playqueue.items[pos].plex_type,
playqueue.items[pos].offset) if resume else 0
# New thread to release this one sooner (e.g. harddisk spinning up) # New thread to release this one sooner (e.g. harddisk spinning up)
thread = Thread(target=threaded_playback, thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, pos, None)) args=(playqueue.kodi_pl, pos, offset))
thread.setDaemon(True) thread.setDaemon(True)
LOG.info('Done initializing playback, starting Kodi player at pos %s', pos) LOG.debug('Done initializing playback, starting Kodi player at pos %s and '
'offset %s', pos, offset)
# Ensure that PKC playqueue monitor ignores the changes we just made
playqueue.pkc_edit = True
# By design, PKC will start Kodi playback using Player().play(). Kodi # By design, PKC will start Kodi playback using Player().play(). Kodi
# caches paths like our plugin://pkc. If we use Player().play() between # caches paths like our plugin://pkc. If we use Player().play() between
# 2 consecutive startups of exactly the same Kodi library item, Kodi's # 2 consecutive startups of exactly the same Kodi library item, Kodi's
@ -263,8 +260,6 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
# plugin://pkc will be lost; Kodi will try to startup playback for an empty # plugin://pkc will be lost; Kodi will try to startup playback for an empty
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>" # path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
thread.start() thread.start()
# Ensure that PKC playqueue monitor ignores the changes we just made
playqueue.pkc_edit = True
def _ensure_resolve(abort=False): def _ensure_resolve(abort=False):
@ -376,7 +371,7 @@ def _prep_playlist_stack(xml, resume):
'part': part, 'part': part,
'playcount': api.viewcount(), 'playcount': api.viewcount(),
'offset': api.resume_point(), 'offset': api.resume_point(),
'resume': resume if i + 1 == len(xml) and part == 0 else False, 'resume': resume if part == 0 and i + 1 == len(xml) else None,
'id': api.item_id() 'id': api.item_id()
}) })
return stack return stack
@ -413,33 +408,20 @@ def _process_stack(playqueue, stack):
pos += 1 pos += 1
def _set_resume(listitem, item, api): def _use_kodi_db_offset(plex_id, plex_type, plex_offset):
if item.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP): """
return Do NOT use item.offset directly but get it from the Kodi DB (Plex might not
if item.resume is True: have gotten the last resume point)
# Do NOT use item.offset directly but get it from the DB """
# (user might have initiated same video twice) if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE):
with PlexDB(lock=False) as plexdb: return plex_offset
db_item = plexdb.item_by_id(item.plex_id, item.plex_type) with PlexDB(lock=False) as plexdb:
if db_item: db_item = plexdb.item_by_id(plex_id, plex_type)
file_id = db_item['kodi_fileid'] if db_item:
with KodiVideoDB(lock=False) as kodidb: with KodiVideoDB(lock=False) as kodidb:
item.offset = kodidb.get_resume(file_id) return kodidb.get_resume(db_item['kodi_fileid'])
LOG.info('Resuming playback at %s', item.offset) else:
if v.KODIVERSION >= 18 and api: return plex_offset
# Kodi 18 Alpha 3 broke StartOffset
try:
percent = (item.offset or api.resume_point()) / api.runtime() * 100.0
except ZeroDivisionError:
percent = 0.0
LOG.debug('Resuming at %s percent', percent)
listitem.setProperty('StartPercent', str(percent))
else:
listitem.setProperty('StartOffset', str(item.offset))
listitem.setProperty('resumetime', str(item.offset))
elif v.KODIVERSION >= 18:
# Make sure that the video starts from the beginning
listitem.setProperty('StartPercent', '0')
def _conclude_playback(playqueue, pos): def _conclude_playback(playqueue, pos):
@ -457,19 +439,14 @@ def _conclude_playback(playqueue, pos):
start playback start playback
return PKC listitem attached to result return PKC listitem attached to result
""" """
LOG.info('Concluding playback for playqueue position %s', pos) LOG.debug('Concluding playback for playqueue position %s', pos)
item = playqueue.items[pos] item = playqueue.items[pos]
if item.xml is not None: api = API(item.xml)
# Got a Plex element api.part = item.part or 0
api = API(item.xml) listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
api.part = item.part or 0 set_playurl(api, item)
listitem = api.listitem(listitem=transfer.PKCListItem)
set_playurl(api, item)
else:
listitem = transfer.PKCListItem()
api = None
if not item.file: if not item.file:
LOG.info('Did not get a playurl, aborting playback silently') LOG.debug('Did not get a playurl, aborting playback silently')
_ensure_resolve() _ensure_resolve()
return return
listitem.setPath(item.file.encode('utf-8')) listitem.setPath(item.file.encode('utf-8'))
@ -478,9 +455,8 @@ def _conclude_playback(playqueue, pos):
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_STREAM, elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_STREAM,
v.PLAYBACK_METHOD_TRANSCODE): v.PLAYBACK_METHOD_TRANSCODE):
audio_subtitle_prefs(api, listitem) audio_subtitle_prefs(api, listitem)
_set_resume(listitem, item, api)
transfer.send(listitem) transfer.send(listitem)
LOG.info('Done concluding playback') LOG.debug('Done concluding playback')
def process_indirect(key, offset, resolve=True): def process_indirect(key, offset, resolve=True):
@ -494,8 +470,8 @@ def process_indirect(key, offset, resolve=True):
Set resolve to False if playback should be kicked off directly, not via Set resolve to False if playback should be kicked off directly, not via
setResolvedUrl setResolvedUrl
""" """
LOG.info('process_indirect called with key: %s, offset: %s, resolve: %s', LOG.debug('process_indirect called with key: %s, offset: %s, resolve: %s',
key, offset, resolve) key, offset, resolve)
global RESOLVE global RESOLVE
RESOLVE = resolve RESOLVE = resolve
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
@ -513,7 +489,7 @@ def process_indirect(key, offset, resolve=True):
return return
api = API(xml[0]) api = API(xml[0])
listitem = api.listitem(listitem=transfer.PKCListItem) listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
playqueue = PQ.get_playqueue_from_type( playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type]) v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
playqueue.clear() playqueue.clear()
@ -552,7 +528,7 @@ def process_indirect(key, offset, resolve=True):
args={'item': utils.try_encode(playurl), args={'item': utils.try_encode(playurl),
'listitem': listitem}) 'listitem': listitem})
thread.setDaemon(True) thread.setDaemon(True)
LOG.info('Done initializing PKC playback, starting Kodi player') LOG.debug('Done initializing PKC playback, starting Kodi player')
thread.start() thread.start()
@ -581,21 +557,38 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
LOG.debug('Playqueue after play_xml update: %s', playqueue) LOG.debug('Playqueue after play_xml update: %s', playqueue)
thread = Thread(target=threaded_playback, thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, startpos, offset)) args=(playqueue.kodi_pl, startpos, offset))
LOG.info('Done play_xml, starting Kodi player at position %s', startpos) LOG.debug('Done play_xml, starting Kodi player at position %s', startpos)
thread.start() thread.start()
def threaded_playback(kodi_playlist, startpos, offset): def threaded_playback(kodi_playlist, startpos, offset):
""" """
Seek immediately after kicking off playback is not reliable. Seek immediately after kicking off playback is not reliable. We even seek
to 0 (starting position) in case Kodi wants to resume but we want to start
over.
offset: resume position in seconds [int/float]
""" """
LOG.debug('threaded_playback with startpos %s, offset %s',
startpos, offset)
app.APP.player.play(kodi_playlist, None, False, startpos) app.APP.player.play(kodi_playlist, None, False, startpos)
if offset and offset != '0': if offset:
i = 0 i = 0
while not app.APP.is_playing or not js.get_player_ids(): while not app.APP.is_playing or not js.get_player_ids():
app.APP.monitor.waitForAbort(0.1) app.APP.monitor.waitForAbort(0.1)
i += 1 i += 1
if i > 100: if i > 200:
LOG.error('Could not seek to %s', offset) LOG.error('Could not seek to %s', offset)
return return
js.seek_to(int(offset)) i = 0
answ = js.seek_to(offset * 1000)
while 'error' in answ:
# Kodi sometimes returns {u'message': u'Failed to execute method.',
# u'code': -32100} if user quickly switches videos
i += 1
if i > 10:
LOG.error('Failed to seek to %s', offset)
return
app.APP.monitor.waitForAbort(0.1)
answ = js.seek_to(offset * 1000)
LOG.debug('Seek to offset %s successful', offset)

View file

@ -37,10 +37,15 @@ class PlaybackTask(backgroundthread.Task):
resolve = False if params.get('handle') == '-1' else True resolve = False if params.get('handle') == '-1' else True
LOG.debug('Received mode: %s, params: %s', mode, params) LOG.debug('Received mode: %s, params: %s', mode, params)
if mode == 'play': if mode == 'play':
if params.get('resume'):
resume = params.get('resume') == '1'
else:
resume = None
playback.playback_triage(plex_id=params.get('plex_id'), playback.playback_triage(plex_id=params.get('plex_id'),
plex_type=params.get('plex_type'), plex_type=params.get('plex_type'),
path=params.get('path'), path=params.get('path'),
resolve=resolve) resolve=resolve,
resume=resume)
elif mode == 'plex_node': elif mode == 'plex_node':
playback.process_indirect(params['key'], playback.process_indirect(params['key'],
params['offset'], params['offset'],

View file

@ -213,6 +213,7 @@ class PlaylistItem(object):
"'guid': '{self.guid}', " "'guid': '{self.guid}', "
"'playmethod': '{self.playmethod}', " "'playmethod': '{self.playmethod}', "
"'playcount': {self.playcount}, " "'playcount': {self.playcount}, "
"'resume': {self.resume},"
"'offset': {self.offset}, " "'offset': {self.offset}, "
"'force_transcode': {self.force_transcode}, " "'force_transcode': {self.force_transcode}, "
"'part': {self.part}".format(self=self)) "'part': {self.part}".format(self=self))

View file

@ -605,11 +605,16 @@ class Base(object):
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP)) % (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
return url return url
def listitem(self, listitem=xbmcgui.ListItem): def listitem(self, listitem=xbmcgui.ListItem, resume=True):
""" """
Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element
Pass resume=False in order to NOT set a resume point (but let Kodi
automatically handle it)
""" """
item = widgets.generate_item(self) item = widgets.generate_item(self)
if not resume and 'resume' in item:
del item['resume']
item = widgets.prepare_listitem(item) item = widgets.prepare_listitem(item)
return widgets.create_listitem(item, as_tuple=False, listitem=listitem) return widgets.create_listitem(item, as_tuple=False, listitem=listitem)

View file

@ -151,11 +151,10 @@ class PlexCompanion(backgroundthread.KillableThread):
playback.play_xml(playqueue, xml, offset) playback.play_xml(playqueue, xml, offset)
else: else:
app.CONN.plex_transient_token = data.get('token') app.CONN.plex_transient_token = data.get('token')
if data.get('offset') != '0':
app.PLAYSTATE.resume_playback = True
playback.playback_triage(api.plex_id, playback.playback_triage(api.plex_id,
api.plex_type, api.plex_type,
resolve=False) resolve=False,
resume=data.get('offset') not in ('0', None))
@staticmethod @staticmethod
def _process_node(data): def _process_node(data):