Improve video resuming behavior; add a resume dialog for playback startup via PMS

This commit is contained in:
croneter 2019-06-01 16:00:47 +02:00
parent bcb89aed88
commit 04d328de3e
4 changed files with 91 additions and 55 deletions

View file

@ -6,6 +6,7 @@ Used to kick off Kodi playback
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread
import datetime
import xbmc import xbmc
@ -45,6 +46,17 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
the first pass - e.g. if you're calling this function from the original the first pass - e.g. if you're calling this function from the original
service.py Python instance service.py Python instance
""" """
try:
_playback_triage(plex_id, plex_type, path, resolve)
finally:
# Reset some playback variables the user potentially set to init
# playback
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
app.PLAYSTATE.resume_playback = False
def _playback_triage(plex_id, plex_type, path, resolve):
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.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
'resolve %s', plex_id, plex_type, path, resolve) 'resolve %s', plex_id, plex_type, path, resolve)
@ -185,11 +197,6 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
_init_existing_kodi_playlist(playqueue, pos) _init_existing_kodi_playlist(playqueue, pos)
except PL.PlaylistError: except PL.PlaylistError:
LOG.error('Playback_init for existing Kodi playlist failed') LOG.error('Playback_init for existing Kodi playlist failed')
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True) _ensure_resolve(abort=True)
return return
# Now we need to use setResolvedUrl for the item at position ZERO # Now we need to use setResolvedUrl for the item at position ZERO
@ -201,9 +208,19 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
# Release default.py # Release default.py
_ensure_resolve() _ensure_resolve()
api = API(xml[0]) api = API(xml[0])
if app.SYNC.direct_paths and api.resume_point():
# 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
trailers = False trailers = False
if (plex_type == v.PLEX_TYPE_MOVIE and if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
not app.PLAYSTATE.resume_playback and
utils.settings('enableCinema') == "true"): utils.settings('enableCinema') == "true"):
if utils.settings('askCinema') == "true": if utils.settings('askCinema') == "true":
# "Play trailers?" # "Play trailers?"
@ -227,25 +244,15 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
utils.lang(30128), utils.lang(30128),
icon='{error}') icon='{error}')
# Do NOT use _ensure_resolve() because we resolved above already # Do NOT use _ensure_resolve() because we resolved above already
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
app.PLAYSTATE.resume_playback = False
return return
PL.get_playlist_details_from_xml(playqueue, xml) PL.get_playlist_details_from_xml(playqueue, xml)
stack = _prep_playlist_stack(xml) stack = _prep_playlist_stack(xml, resume)
_process_stack(playqueue, stack) _process_stack(playqueue, stack)
# Always resume if playback initiated via PMS and there IS a resume
# point
offset = api.resume_point() * 1000 if app.PLAYSTATE.context_menu_play else None
# Reset some playback variables
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
# 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, offset)) args=(playqueue.kodi_pl, pos, None))
thread.setDaemon(True) thread.setDaemon(True)
LOG.info('Done initializing playback, starting Kodi player at pos %s and ' LOG.info('Done initializing playback, starting Kodi player at pos %s', pos)
'resume point %s', pos, offset)
# 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
@ -277,10 +284,22 @@ def _ensure_resolve(abort=False):
message=utils.lang(30128), message=utils.lang(30128),
icon='{error}', icon='{error}',
time=3000) time=3000)
# Reset some playback variables
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False def resume_dialog(resume):
app.PLAYSTATE.resume_playback = False """
Pass the resume [int] point in seconds. Returns True if user chose to
resume. Returns None if user cancelled
"""
# "Resume from {0:s}"
# "Start from beginning"
resume = datetime.timedelta(seconds=resume)
answ = utils.dialog('contextmenu',
[utils.lang(12022).replace('{0:s}', '{0}').format(unicode(resume)),
utils.lang(12021)])
if answ == -1:
return
return answ == 0
def _init_existing_kodi_playlist(playqueue, pos): def _init_existing_kodi_playlist(playqueue, pos):
@ -301,9 +320,13 @@ def _init_existing_kodi_playlist(playqueue, pos):
LOG.debug('Done init_existing_kodi_playlist') LOG.debug('Done init_existing_kodi_playlist')
def _prep_playlist_stack(xml): def _prep_playlist_stack(xml, resume):
"""
resume [bool] will set the resume point of the LAST item of the stack, for
part 1 only
"""
stack = [] stack = []
for item in xml: for i, item in enumerate(xml):
api = API(item) api = API(item)
if (app.PLAYSTATE.context_menu_play is False and if (app.PLAYSTATE.context_menu_play is False and
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)): api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
@ -326,7 +349,7 @@ def _prep_playlist_stack(xml):
api.set_part_number(part) api.set_part_number(part)
if kodi_id is None: if kodi_id is None:
# Need to redirect again to PKC to conclude playback # Need to redirect again to PKC to conclude playback
path = api.path() path = api.path(force_addon=True, force_first_media=True)
listitem = api.create_listitem() listitem = api.create_listitem()
listitem.setPath(path.encode('utf-8')) listitem.setPath(path.encode('utf-8'))
else: else:
@ -342,6 +365,7 @@ def _prep_playlist_stack(xml):
'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,
'id': api.item_id() 'id': api.item_id()
}) })
return stack return stack
@ -374,9 +398,39 @@ def _process_stack(playqueue, stack):
playlist_item.part = item['part'] playlist_item.part = item['part']
playlist_item.id = item['id'] playlist_item.id = item['id']
playlist_item.force_transcode = app.PLAYSTATE.force_transcode playlist_item.force_transcode = app.PLAYSTATE.force_transcode
playlist_item.resume = item['resume']
pos += 1 pos += 1
def _set_resume(listitem, item, api):
if item.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP):
return
if item.resume is True:
# Do NOT use item.offset directly but get it from the DB
# (user might have initiated same video twice)
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(item.plex_id, item.plex_type)
if db_item:
file_id = db_item['kodi_fileid']
with KodiVideoDB(lock=False) as kodidb:
item.offset = kodidb.get_resume(file_id)
LOG.info('Resuming playback at %s', item.offset)
if v.KODIVERSION >= 18 and api:
# 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):
""" """
ONLY if actually being played (e.g. at 5th position of a playqueue). ONLY if actually being played (e.g. at 5th position of a playqueue).
@ -414,32 +468,7 @@ def _conclude_playback(playqueue, pos):
listitem.setSubtitles(api.cache_external_subs()) listitem.setSubtitles(api.cache_external_subs())
elif item.playmethod == 'Transcode': elif item.playmethod == 'Transcode':
playutils.audio_subtitle_prefs(listitem) playutils.audio_subtitle_prefs(listitem)
_set_resume(listitem, item, api)
if app.PLAYSTATE.resume_playback is True:
app.PLAYSTATE.resume_playback = False
if item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP):
# Do NOT use item.offset directly but get it from the DB
# (user might have initiated same video twice)
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(item.plex_id, item.plex_type)
file_id = db_item['kodi_fileid'] if db_item else None
with KodiVideoDB(lock=False) as kodidb:
item.offset = kodidb.get_resume(file_id)
LOG.info('Resuming playback at %s', item.offset)
if v.KODIVERSION >= 18 and api:
# 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:
listitem.setProperty('StartPercent', '0')
# Reset the resumable flag
transfer.send(listitem) transfer.send(listitem)
LOG.info('Done concluding playback') LOG.info('Done concluding playback')
@ -529,7 +558,7 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
""" """
LOG.info("play_xml called with offset %s, start_plex_id %s", LOG.info("play_xml called with offset %s, start_plex_id %s",
offset, start_plex_id) offset, start_plex_id)
stack = _prep_playlist_stack(xml) stack = _prep_playlist_stack(xml, offset=offset or 0)
_process_stack(playqueue, stack) _process_stack(playqueue, stack)
LOG.debug('Playqueue after play_xml update: %s', playqueue) LOG.debug('Playqueue after play_xml update: %s', playqueue)
if start_plex_id is not None: if start_plex_id is not None:

View file

@ -167,6 +167,11 @@ class Playlist_Item(object):
# If Plex video consists of several parts; part number # If Plex video consists of several parts; part number
self._part = 0 self._part = 0
self.force_transcode = False self.force_transcode = False
# Shall we ask user to resume this item?
# None: ask user to resume
# False: do NOT resume, don't ask user
# True: do resume, don't ask user
self.resume = None
@property @property
def plex_id(self): def plex_id(self):

View file

@ -1388,6 +1388,7 @@ class API(object):
option = cast(str, option.strip()) option = cast(str, option.strip())
dialoglist.append(option) dialoglist.append(option)
media = utils.dialog('select', 'Select stream', dialoglist) media = utils.dialog('select', 'Select stream', dialoglist)
LOG.info('User chose media stream number: %s', media)
if media == -1: if media == -1:
LOG.info('User cancelled media stream selection') LOG.info('User cancelled media stream selection')
return return

View file

@ -211,7 +211,8 @@ def dialog(typus, *args, **kwargs):
'notification': dia.notification, 'notification': dia.notification,
'input': dia.input, 'input': dia.input,
'select': dia.select, 'select': dia.select,
'numeric': dia.numeric 'numeric': dia.numeric,
'contextmenu': dia.contextmenu
} }
return types[typus](*args, **kwargs) return types[typus](*args, **kwargs)