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