diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b6affbf5..f5337836 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1505,6 +1505,16 @@ class API(): """ return self.item.attrib.get('year', None) + def getResume(self): + """ + Returns the resume point of time in seconds as int. 0 if not found + """ + try: + resume = float(self.item.attrib['viewOffset']) + except (KeyError, ValueError): + resume = 0.0 + return int(resume * v.PLEX_TO_KODI_TIMEFACTOR) + def getRuntime(self): """ Resume point of time and runtime/totaltime in rounded to seconds. @@ -2521,7 +2531,9 @@ class API(): 'mpaa': self.getMpaa(), 'aired': self.getPremiereDate() } - listItem.setProperty('resumetime', str(userdata['Resume'])) + # Do NOT set resumetime - otherwise Kodi always resumes at that time + # even if the user chose to start element from the beginning + # listItem.setProperty('resumetime', str(userdata['Resume'])) listItem.setProperty('totaltime', str(userdata['Runtime'])) if typus == v.PLEX_TYPE_EPISODE: diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 38e848f1..4d4410ec 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -3,11 +3,15 @@ PKC Kodi Monitoring implementation """ from logging import getLogger from json import loads +from threading import Thread -from xbmc import Monitor, Player, sleep +from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ + getLocalizedString +from xbmcgui import Window import plexdb_functions as plexdb -from utils import window, settings, CatchExceptions, plex_command +from utils import window, settings, CatchExceptions, plex_command, \ + thread_methods from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER @@ -391,3 +395,31 @@ class KodiMonitor(Monitor): else: window('plex_%s.playmethod' % currentFile, value="DirectPlay") LOG.debug('Window properties set for direct paths!') + + +@thread_methods +class SpecialMonitor(Thread): + """ + Detect the resume dialog for widgets. + Could also be used to detect external players (see Emby implementation) + """ + def run(self): + LOG.info("----====# Starting Special Monitor #====----") + player = Player() + while not self.thread_stopped(): + is_playing = player.isPlaying() + + if (not is_playing and + getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and + not getCondVisibility('Window.IsVisible(MyVideoNav.xml)') and + getInfoLabel('Control.GetLabel(1002)') == getLocalizedString(12021)): + control = int(Window(10106).getFocusId()) + if control == 1002: + # Start from beginning + LOG.info("Resume dialog: Start from beginning selected") + state.RESUME_PLAYBACK = False + else: + LOG.info("Resume dialog: resume selected") + state.RESUME_PLAYBACK = True + sleep(200) + LOG.info("#====---- Special Monitor Stopped ----====#") diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py index b5579cd4..6e660dd0 100644 --- a/resources/lib/pickler.py +++ b/resources/lib/pickler.py @@ -32,7 +32,7 @@ def pickle_me(obj, window_var='plex_result'): obj can be pretty much any Python object. However, classes and functions won't work. See the Pickle documentation """ - log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG) + log('%sStart pickling' % PREFIX, level=LOGDEBUG) pickl_window(window_var, value=dumps(obj)) log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index ddfb02d3..8e0dd586 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -66,6 +66,44 @@ def playback_triage(plex_id=None, plex_type=None, path=None): conclude_playback(playqueue, pos) +def play_resume(playqueue, xml, stack): + """ + If there exists a resume point, Kodi will ask the user whether to continue + playback. We thus need to use setResolvedUrl "correctly". Mind that there + might be several parts! + """ + result = Playback_Successful() + listitem = PKC_ListItem() + # Only get the very first item of our playqueue (i.e. the very first part) + stack_item = stack.pop(0) + api = API(xml[0]) + item = PL.playlist_item_from_xml(playqueue, + xml[0], + kodi_id=stack_item['kodi_id'], + kodi_type=stack_item['kodi_type']) + api.setPartNumber(item.part) + item.playcount = stack_item['playcount'] + item.offset = stack_item['offset'] + item.part = stack_item['part'] + item.init_done = True + api.CreateListItemFromPlexItem(listitem) + playutils = PlayUtils(api, item) + playurl = playutils.getPlayUrl() + listitem.setPath(playurl) + if item.playmethod in ('DirectStream', 'DirectPlay'): + listitem.setSubtitles(api.externalSubs()) + else: + playutils.audio_subtitle_prefs(listitem) + result.listitem = listitem + # Add to our playlist + playqueue.items.append(item) + # This will release default.py with setResolvedUrl + pickle_me(result) + # Add remaining parts to the playlist, if any + if stack: + _process_stack(playqueue, stack) + + def playback_init(plex_id, plex_type, playqueue): """ Playback setup if Kodi starts playing an item for the first time. @@ -112,10 +150,16 @@ def playback_init(plex_id, plex_type, playqueue): playqueue.clear() PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) + # if resume: + # # Need to handle this differently so only 1 dialog is displayed whether + # # user wants to resume to start at the beginning + # LOG.info('Resume detected') + # play_resume(playqueue, xml, stack) + # return # Release our default.py before starting our own Kodi player instance pickle_me(Playback_Successful()) # Sleep a bit to let setResolvedUrl do its thing - bit ugly - sleep(200) + sleep(100) _process_stack(playqueue, stack) # New thread to release this one sooner (e.g. harddisk spinning up) thread = Thread(target=Player().play, @@ -138,7 +182,6 @@ def _prep_playlist_stack(xml): # We will never store clips (trailers) in the Kodi DB kodi_id = None kodi_type = None - resume, _ = api.getRuntime() for part, _ in enumerate(item[0]): api.setPartNumber(part) if kodi_id is None: @@ -164,7 +207,7 @@ def _prep_playlist_stack(xml): 'listitem': listitem, 'part': part, 'playcount': api.getViewCount(), - 'offset': resume + 'offset': api.getResume() }) return stack @@ -213,6 +256,7 @@ def conclude_playback(playqueue, pos): start playback return PKC listitem attached to result """ + LOG.info('Concluding playback for playqueue position %s', pos) result = Playback_Successful() listitem = PKC_ListItem() item = playqueue.items[pos] @@ -226,10 +270,16 @@ def conclude_playback(playqueue, pos): else: playurl = item.file listitem.setPath(playurl) - if item.playmethod in ("DirectStream", "DirectPlay"): + if item.playmethod in ('DirectStream', 'DirectPlay'): listitem.setSubtitles(api.externalSubs()) else: playutils.audio_subtitle_prefs(listitem) listitem.setPath(playurl) + if state.RESUME_PLAYBACK is True: + state.RESUME_PLAYBACK = False + LOG.info('Resuming playback at %s', item.offset) + listitem.setProperty('StartOffset', str(item.offset)) + listitem.setProperty('resumetime', str(item.offset)) result.listitem = listitem pickle_me(result) + LOG.info('Done concluding playback') diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index af7e267d..e312232b 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -47,6 +47,7 @@ class PlayUtils(): }) self.item.playmethod = 'Transcode' LOG.info("The playurl is: %s", playurl) + self.item.file = playurl return playurl def isDirectPlay(self): diff --git a/resources/lib/state.py b/resources/lib/state.py index db267be5..54c9f9a3 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -128,6 +128,9 @@ PLAYSTATE = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} +# Set by SpecialMonitor - did user choose to resume playback or start from the +# beginning? +RESUME_PLAYBACK = False # Kodi webserver details WEBSERVER_PORT = 8080 diff --git a/service.py b/service.py index 246c2daa..04525b5b 100644 --- a/service.py +++ b/service.py @@ -31,7 +31,7 @@ sys_path.append(_base_resource) from utils import settings, window, language as lang, dialog, tryDecode from userclient import UserClient import initialsetup -from kodimonitor import KodiMonitor +from kodimonitor import KodiMonitor, SpecialMonitor from librarysync import LibrarySync import videonodes from websocket_client import PMS_Websocket, Alexa_Websocket @@ -155,6 +155,7 @@ class Service(): self.alexa = Alexa_Websocket() self.library = LibrarySync() self.plexCompanion = PlexCompanion() + self.specialMonitor = SpecialMonitor() self.playback_starter = Playback_Starter() if settings('enableTextureCache') == "true": self.image_cache_thread = Image_Cache_Thread() @@ -197,6 +198,7 @@ class Service(): sound=False) # Start monitoring kodi events self.kodimonitor_running = KodiMonitor() + self.specialMonitor.start() # Start the Websocket Client if not self.ws_running: self.ws_running = True