diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 58da1619..650dbe2a 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -567,6 +567,10 @@ msgctxt "#30524" msgid "Select Plex libraries to sync" msgstr "" +# PKC Settings - Playback +msgctxt "#30525" +msgid "Skip intro" +msgstr "" # PKC Settings - Playback msgctxt "#30527" diff --git a/resources/lib/app/application.py b/resources/lib/app/application.py index 318276fc..dd32e3f4 100644 --- a/resources/lib/app/application.py +++ b/resources/lib/app/application.py @@ -51,6 +51,8 @@ class App(object): self.fanart_thread = None # Instance of ImageCachingThread() self.caching_thread = None + # Dialog to skip intro + self.skip_intro_dialog = None @property def is_playing(self): diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index 13d61ac3..7478e6ac 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -36,7 +36,8 @@ class PlayState(object): 'muted': False, 'playmethod': None, 'playcount': None, - 'external_player': False # bool - xbmc.Player().isExternalPlayer() + 'external_player': False, # bool - xbmc.Player().isExternalPlayer() + 'intro_markers': [], } def __init__(self): diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 9c196f03..8580ec75 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -335,6 +335,10 @@ class KodiMonitor(xbmc.Monitor): container_key = '/playQueues/%s' % playqueue.id else: container_key = '/library/metadata/%s' % plex_id + # Mechanik for Plex skip intro feature + if utils.settings('enableSkipIntro') == 'true': + api = API(item.xml) + status['intro_markers'] = api.intro_markers() # Remember the currently playing item app.PLAYSTATE.item = item # Remember that this player has been active @@ -367,6 +371,9 @@ def _playback_cleanup(ended=False): """ LOG.debug('playback_cleanup called. Active players: %s', app.PLAYSTATE.active_players) + if app.APP.skip_intro_dialog: + app.APP.skip_intro_dialog.close() + app.APP.skip_intro_dialog = None # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) app.CONN.plex_transient_token = None diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index c1500bc2..c5aee32f 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -478,7 +478,8 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None): params = { 'next': 0, 'type': playlist.type, - 'uri': item.uri + 'uri': item.uri, + 'includeMarkers': 1, # e.g. start + stop of intros } xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, action_type="POST", @@ -570,9 +571,15 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None): item = playlist_item_from_plex(plex_id) else: item = playlist_item_from_kodi(kodi_item) - url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri) + url = "{server}/%ss/%s" % (playlist.kind, playlist.id) + parameters = { + 'uri': item.uri, + 'includeMarkers': 1, # e.g. start + stop of intros + } # Will always put the new item at the end of the Plex playlist - xml = DU().downloadUrl(url, action_type="PUT") + xml = DU().downloadUrl(url, + action_type="PUT", + parameters=parameters) try: xml[-1].attrib except (TypeError, AttributeError, KeyError, IndexError): @@ -671,10 +678,13 @@ def get_PMS_playlist(playlist, playlist_id=None): Raises PlaylistError if something went wrong """ playlist_id = playlist_id if playlist_id else playlist.id + parameters = {'includeMarkers': 1} if playlist.kind == 'playList': - xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id) + xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id, + parameters=parameters) else: - xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id) + xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id, + parameters=parameters) try: xml.attrib except AttributeError: @@ -773,9 +783,10 @@ def get_pms_playqueue(playqueue_id): """ Returns the Plex playqueue as an etree XML or None if unsuccessful """ - xml = DU().downloadUrl( - "{server}/playQueues/%s" % playqueue_id, - headerOptions={'Accept': 'application/xml'}) + parameters = {'includeMarkers': 1} + xml = DU().downloadUrl("{server}/playQueues/%s" % playqueue_id, + parameters=parameters, + headerOptions={'Accept': 'application/xml'}) try: xml.attrib except AttributeError: diff --git a/resources/lib/plex_api/base.py b/resources/lib/plex_api/base.py index b4c4a859..9a749a60 100644 --- a/resources/lib/plex_api/base.py +++ b/resources/lib/plex_api/base.py @@ -43,6 +43,7 @@ class Base(object): self._writers = [] self._producers = [] self._locations = [] + self._intro_markers = [] self._guids = {} self._coll_match = None # Plex DB attributes @@ -470,6 +471,14 @@ class Base(object): guid = child.get('id') guid = guid.split('://', 1) self._guids[guid[0]] = guid[1] + elif child.tag == 'Marker' and child.get('type') == 'intro': + intro = (cast(float, child.get('startTimeOffset')), + cast(float, child.get('endTimeOffset'))) + if None in intro: + # Safety net if PMS xml is not as expected + continue + intro = (intro[0] / 1000.0, intro[1] / 1000.0) + self._intro_markers.append(intro) # Plex Movie agent (legacy) or "normal" Plex tv show agent if not self._guids: guid = self.xml.get('guid') diff --git a/resources/lib/plex_api/media.py b/resources/lib/plex_api/media.py index 7ddb14ca..adf27514 100644 --- a/resources/lib/plex_api/media.py +++ b/resources/lib/plex_api/media.py @@ -28,6 +28,16 @@ class Media(object): """ return self.xml[0][self.part].get(key, self.xml[0].get(key)) + def intro_markers(self): + """ + Returns a list of tuples with floats (startTimeOffset, endTimeOffset) + in Koditime or an empty list. + Each entry represents an (episode) intro that Plex detected and that + can be skipped + """ + self._scan_children() + return self._intro_markers + def video_codec(self): """ Returns the video codec and resolution for the child and part selected. diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index 2d49441b..3d421122 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -479,6 +479,7 @@ def GetPlexMetadata(key, reraise=False): 'includeReviews': 1, 'includeRelated': 0, # Similar movies => Video -> Related 'skipRefresh': 1, + 'includeMarkers': 1, # e.g. start + stop of intros # 'includeRelatedCount': 0, # 'includeOnDeck': 1, # 'includeChapters': 1, @@ -518,7 +519,9 @@ def get_playback_xml(url, server_name, authenticate=True, token=None): """ Returns None if something went wrong """ - header_options = {'X-Plex-Token': token} if not authenticate else None + header_options = {'includeMarkers': 1} + if not authenticate: + header_options['X-Plex-Token'] = token try: xml = DU().downloadUrl(url, authenticate=authenticate, @@ -806,7 +809,8 @@ def init_plex_playqueue(plex_id, plex_type, section_uuid, trailers=False): (app.CONN.machine_identifier, plex_id)), 'includeChapters': '1', 'shuffle': '0', - 'repeat': '0' + 'repeat': '0', + 'includeMarkers': 1, # e.g. start + stop of intros } if trailers is True: args['extrasPrefixCount'] = utils.settings('trailerNumber') diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index cdc613e1..444d1f53 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -18,6 +18,7 @@ from . import variables as v from . import app from . import loghandler from . import backgroundthread +from . import skip_plex_intro from .windows import userselect ############################################################################### @@ -552,7 +553,10 @@ class Service(object): self.playqueue.start() self.alexa.start() - xbmc.sleep(100) + elif app.APP.is_playing: + skip_plex_intro.check() + + xbmc.sleep(200) # EXITING PKC # Tell all threads to terminate (e.g. several lib sync threads) diff --git a/resources/lib/skip_plex_intro.py b/resources/lib/skip_plex_intro.py new file mode 100644 index 00000000..c5ea2d6c --- /dev/null +++ b/resources/lib/skip_plex_intro.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from .windows.skip_intro import SkipIntroDialog +from . import app, variables as v + + +def skip_intro(intros): + progress = app.APP.player.getTime() + in_intro = False + for start, end in intros: + if start <= progress < end: + in_intro = True + if in_intro and app.APP.skip_intro_dialog is None: + app.APP.skip_intro_dialog = SkipIntroDialog('skip_intro.xml', + v.ADDON_PATH, + 'default', + '1080i', + intro_end=end) + app.APP.skip_intro_dialog.show() + elif not in_intro and app.APP.skip_intro_dialog is not None: + app.APP.skip_intro_dialog.close() + app.APP.skip_intro_dialog = None + + +def check(): + with app.APP.lock_playqueues: + if len(app.PLAYSTATE.active_players) != 1: + return + playerid = list(app.PLAYSTATE.active_players)[0] + intros = app.PLAYSTATE.player_states[playerid]['intro_markers'] + if not intros: + return + skip_intro(intros) diff --git a/resources/lib/windows/skip_intro.py b/resources/lib/windows/skip_intro.py new file mode 100644 index 00000000..e369ec28 --- /dev/null +++ b/resources/lib/windows/skip_intro.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger + +from xbmcgui import WindowXMLDialog + +from .. import app + +logger = getLogger('PLEX.skipintro') + + +class SkipIntroDialog(WindowXMLDialog): + + def __init__(self, *args, **kwargs): + self.intro_end = kwargs.pop('intro_end', None) + + self._showing = False + self._on_hold = False + + logger.debug('SkipIntroDialog initialized, ends at %s', + self.intro_end) + WindowXMLDialog.__init__(self, *args, **kwargs) + + def show(self): + if not self.intro_end: + self.close() + return + + if not self.on_hold and not self.showing: + logger.debug('Showing dialog') + self.showing = True + WindowXMLDialog.show(self) + + def close(self): + if self.showing: + self.showing = False + logger.debug('Closing dialog') + WindowXMLDialog.close(self) + + def onClick(self, control_id): # pylint: disable=invalid-name + if self.intro_end and control_id == 3002: # 3002 = Skip Intro button + if app.APP.is_playing: + self.on_hold = True + logger.info('Skipping intro, seeking to %s', self.intro_end) + app.APP.player.seekTime(self.intro_end) + self.close() + + def onAction(self, action): # pylint: disable=invalid-name + close_actions = [10, 13, 92] + # 10 = previousmenu, 13 = stop, 92 = back + if action in close_actions: + self.on_hold = True + self.close() + + @property + def showing(self): + return self._showing + + @showing.setter + def showing(self, value): + self._showing = bool(value) + + @property + def on_hold(self): + return self._on_hold + + @on_hold.setter + def on_hold(self, value): + self._on_hold = bool(value) diff --git a/resources/settings.xml b/resources/settings.xml index 6b943a83..b046ce12 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -108,6 +108,7 @@ + diff --git a/resources/skins/default/1080i/skip_intro.xml b/resources/skins/default/1080i/skip_intro.xml new file mode 100644 index 00000000..6b0ca7c8 --- /dev/null +++ b/resources/skins/default/1080i/skip_intro.xml @@ -0,0 +1,52 @@ + + + 3002 + Dialog.Close(fullscreeninfo,true) + Dialog.Close(videoosd,true) + + + + + + + + + + 64 + + 100% + 64 + skipintro-background.png + + + 12 + 20 + 70% + + horizontal + 40 + 10 + right + + + 40 + auto + font20_title + 32 + ddffffff + eeffffff + ddffffff + 22000000 + center + center + skipintro-button.png + skipintro-button.png + skipintro-button.png + skipintro-button.png + + + + + + + diff --git a/resources/skins/default/media/skipintro-background.png b/resources/skins/default/media/skipintro-background.png new file mode 100644 index 00000000..215944e6 Binary files /dev/null and b/resources/skins/default/media/skipintro-background.png differ diff --git a/resources/skins/default/media/skipintro-button.png b/resources/skins/default/media/skipintro-button.png new file mode 100644 index 00000000..9ca087c7 Binary files /dev/null and b/resources/skins/default/media/skipintro-button.png differ