From 04d328de3ef7c0502f157a1578330fcc74d2cd35 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 1 Jun 2019 16:00:47 +0200 Subject: [PATCH] Improve video resuming behavior; add a resume dialog for playback startup via PMS --- resources/lib/playback.py | 137 ++++++++++++++++++++------------- resources/lib/playlist_func.py | 5 ++ resources/lib/plex_api.py | 1 + resources/lib/utils.py | 3 +- 4 files changed, 91 insertions(+), 55 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 8a0ce582..abcd39a5 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -6,6 +6,7 @@ Used to kick off Kodi playback from __future__ import absolute_import, division, unicode_literals from logging import getLogger from threading import Thread +import datetime 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 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) LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, ' '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) except PL.PlaylistError: 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) return # 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 _ensure_resolve() 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 - if (plex_type == v.PLEX_TYPE_MOVIE and - not app.PLAYSTATE.resume_playback and + if (not resume and plex_type == v.PLEX_TYPE_MOVIE and utils.settings('enableCinema') == "true"): if utils.settings('askCinema') == "true": # "Play trailers?" @@ -227,25 +244,15 @@ def _playback_init(plex_id, plex_type, playqueue, pos): utils.lang(30128), icon='{error}') # 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 PL.get_playlist_details_from_xml(playqueue, xml) - stack = _prep_playlist_stack(xml) + stack = _prep_playlist_stack(xml, resume) _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) thread = Thread(target=threaded_playback, - args=(playqueue.kodi_pl, pos, offset)) + args=(playqueue.kodi_pl, pos, None)) thread.setDaemon(True) - LOG.info('Done initializing playback, starting Kodi player at pos %s and ' - 'resume point %s', pos, offset) + LOG.info('Done initializing playback, starting Kodi player at pos %s', pos) # By design, PKC will start Kodi playback using Player().play(). Kodi # caches paths like our plugin://pkc. If we use Player().play() between # 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), icon='{error}', time=3000) - # Reset some playback variables - app.PLAYSTATE.context_menu_play = False - app.PLAYSTATE.force_transcode = False - app.PLAYSTATE.resume_playback = False + + +def resume_dialog(resume): + """ + 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): @@ -301,9 +320,13 @@ def _init_existing_kodi_playlist(playqueue, pos): 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 = [] - for item in xml: + for i, item in enumerate(xml): api = API(item) if (app.PLAYSTATE.context_menu_play is False and 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) if kodi_id is None: # 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.setPath(path.encode('utf-8')) else: @@ -342,6 +365,7 @@ def _prep_playlist_stack(xml): 'part': part, 'playcount': api.viewcount(), 'offset': api.resume_point(), + 'resume': resume if i + 1 == len(xml) and part == 0 else False, 'id': api.item_id() }) return stack @@ -374,9 +398,39 @@ def _process_stack(playqueue, stack): playlist_item.part = item['part'] playlist_item.id = item['id'] playlist_item.force_transcode = app.PLAYSTATE.force_transcode + playlist_item.resume = item['resume'] 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): """ 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()) elif item.playmethod == 'Transcode': playutils.audio_subtitle_prefs(listitem) - - 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 + _set_resume(listitem, item, api) transfer.send(listitem) 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", offset, start_plex_id) - stack = _prep_playlist_stack(xml) + stack = _prep_playlist_stack(xml, offset=offset or 0) _process_stack(playqueue, stack) LOG.debug('Playqueue after play_xml update: %s', playqueue) if start_plex_id is not None: diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index a0ba8645..1f00cdca 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -167,6 +167,11 @@ class Playlist_Item(object): # If Plex video consists of several parts; part number self._part = 0 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 def plex_id(self): diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index f8152abf..d3a5e718 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -1388,6 +1388,7 @@ class API(object): option = cast(str, option.strip()) dialoglist.append(option) media = utils.dialog('select', 'Select stream', dialoglist) + LOG.info('User chose media stream number: %s', media) if media == -1: LOG.info('User cancelled media stream selection') return diff --git a/resources/lib/utils.py b/resources/lib/utils.py index afcafdb1..a6d34735 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -211,7 +211,8 @@ def dialog(typus, *args, **kwargs): 'notification': dia.notification, 'input': dia.input, 'select': dia.select, - 'numeric': dia.numeric + 'numeric': dia.numeric, + 'contextmenu': dia.contextmenu } return types[typus](*args, **kwargs)