Add skip intro functionality

This commit is contained in:
croneter 2021-02-07 21:42:47 +01:00
parent f326e49ba7
commit 7f8939cee7
15 changed files with 218 additions and 12 deletions

View file

@ -571,6 +571,10 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync" msgid "Select Plex libraries to sync"
msgstr "" msgstr ""
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback # PKC Settings - Playback
msgctxt "#30527" msgctxt "#30527"

View file

@ -50,6 +50,8 @@ class App(object):
self.metadata_thread = None self.metadata_thread = None
# Instance of ImageCachingThread() # Instance of ImageCachingThread()
self.caching_thread = None self.caching_thread = None
# Dialog to skip intro
self.skip_intro_dialog = None
@property @property
def is_playing(self): def is_playing(self):

View file

@ -33,7 +33,8 @@ class PlayState(object):
'muted': False, 'muted': False,
'playmethod': None, 'playmethod': None,
'playcount': None, 'playcount': None,
'external_player': False # bool - xbmc.Player().isExternalPlayer() 'external_player': False, # bool - xbmc.Player().isExternalPlayer()
'intro_markers': [],
} }
def __init__(self): def __init__(self):

View file

@ -334,6 +334,10 @@ class KodiMonitor(xbmc.Monitor):
container_key = '/playQueues/%s' % playqueue.id container_key = '/playQueues/%s' % playqueue.id
else: else:
container_key = '/library/metadata/%s' % plex_id 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 # Remember the currently playing item
app.PLAYSTATE.item = item app.PLAYSTATE.item = item
# Remember that this player has been active # Remember that this player has been active
@ -366,6 +370,9 @@ def _playback_cleanup(ended=False):
""" """
LOG.debug('playback_cleanup called. Active players: %s', LOG.debug('playback_cleanup called. Active players: %s',
app.PLAYSTATE.active_players) 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 # 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) # Companion (if we could not use the playqueue to store the token)
app.CONN.plex_transient_token = None app.CONN.plex_transient_token = None

View file

@ -470,7 +470,8 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None):
params = { params = {
'next': 0, 'next': 0,
'type': playlist.type, '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, xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
action_type="POST", action_type="POST",
@ -562,9 +563,15 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
item = playlist_item_from_plex(plex_id) item = playlist_item_from_plex(plex_id)
else: else:
item = playlist_item_from_kodi(kodi_item) 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 # 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: try:
xml[-1].attrib xml[-1].attrib
except (TypeError, AttributeError, KeyError, IndexError): except (TypeError, AttributeError, KeyError, IndexError):
@ -663,10 +670,13 @@ def get_PMS_playlist(playlist, playlist_id=None):
Raises PlaylistError if something went wrong Raises PlaylistError if something went wrong
""" """
playlist_id = playlist_id if playlist_id else playlist.id playlist_id = playlist_id if playlist_id else playlist.id
parameters = {'includeMarkers': 1}
if playlist.kind == 'playList': 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: else:
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id) xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id,
parameters=parameters)
try: try:
xml.attrib xml.attrib
except AttributeError: except AttributeError:
@ -765,8 +775,9 @@ def get_pms_playqueue(playqueue_id):
""" """
Returns the Plex playqueue as an etree XML or None if unsuccessful Returns the Plex playqueue as an etree XML or None if unsuccessful
""" """
xml = DU().downloadUrl( parameters = {'includeMarkers': 1}
"{server}/playQueues/%s" % playqueue_id, xml = DU().downloadUrl("{server}/playQueues/%s" % playqueue_id,
parameters=parameters,
headerOptions={'Accept': 'application/xml'}) headerOptions={'Accept': 'application/xml'})
try: try:
xml.attrib xml.attrib

View file

@ -42,6 +42,7 @@ class Base(object):
self._writers = [] self._writers = []
self._producers = [] self._producers = []
self._locations = [] self._locations = []
self._intro_markers = []
self._guids = {} self._guids = {}
self._coll_match = None self._coll_match = None
# Plex DB attributes # Plex DB attributes
@ -469,6 +470,14 @@ class Base(object):
guid = child.get('id') guid = child.get('id')
guid = guid.split('://', 1) guid = guid.split('://', 1)
self._guids[guid[0]] = guid[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 # Plex Movie agent (legacy) or "normal" Plex tv show agent
if not self._guids: if not self._guids:
guid = self.xml.get('guid') guid = self.xml.get('guid')

View file

@ -42,6 +42,16 @@ class Media(object):
value = self.xml[0][self.part].get(key) value = self.xml[0][self.part].get(key)
return value return value
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): def video_codec(self):
""" """
Returns the video codec and resolution for the child and part selected. Returns the video codec and resolution for the child and part selected.

View file

@ -472,6 +472,7 @@ def GetPlexMetadata(key, reraise=False):
'includeReviews': 1, 'includeReviews': 1,
'includeRelated': 0, # Similar movies => Video -> Related 'includeRelated': 0, # Similar movies => Video -> Related
'skipRefresh': 1, 'skipRefresh': 1,
'includeMarkers': 1, # e.g. start + stop of intros
# 'includeRelatedCount': 0, # 'includeRelatedCount': 0,
# 'includeOnDeck': 1, # 'includeOnDeck': 1,
# 'includeChapters': 1, # 'includeChapters': 1,
@ -511,7 +512,9 @@ def get_playback_xml(url, server_name, authenticate=True, token=None):
""" """
Returns None if something went wrong 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: try:
xml = DU().downloadUrl(url, xml = DU().downloadUrl(url,
authenticate=authenticate, authenticate=authenticate,
@ -801,7 +804,8 @@ def init_plex_playqueue(plex_id, plex_type, section_uuid, trailers=False):
(app.CONN.machine_identifier, plex_id)), (app.CONN.machine_identifier, plex_id)),
'includeChapters': '1', 'includeChapters': '1',
'shuffle': '0', 'shuffle': '0',
'repeat': '0' 'repeat': '0',
'includeMarkers': 1, # e.g. start + stop of intros
} }
if trailers is True: if trailers is True:
args['extrasPrefixCount'] = utils.settings('trailerNumber') args['extrasPrefixCount'] = utils.settings('trailerNumber')

View file

@ -18,6 +18,7 @@ from . import variables as v
from . import app from . import app
from . import loghandler from . import loghandler
from . import backgroundthread from . import backgroundthread
from . import skip_plex_intro
from .windows import userselect from .windows import userselect
############################################################################### ###############################################################################
@ -545,7 +546,10 @@ class Service(object):
self.playqueue.start() self.playqueue.start()
self.alexa.start() self.alexa.start()
xbmc.sleep(100) elif app.APP.is_playing:
skip_plex_intro.check()
xbmc.sleep(200)
# EXITING PKC # EXITING PKC
# Tell all threads to terminate (e.g. several lib sync threads) # Tell all threads to terminate (e.g. several lib sync threads)

View file

@ -0,0 +1,33 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
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)

View file

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
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)

View file

@ -108,6 +108,7 @@
<setting id="enableCinema" type="bool" label="30518" default="false" /> <setting id="enableCinema" type="bool" label="30518" default="false" />
<setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" /> <setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" />
<setting id="trailerNumber" type="slider" label="39000" default="3" visible="eq(-2,true)" range="1,1,15" option="int" /> <setting id="trailerNumber" type="slider" label="39000" default="3" visible="eq(-2,true)" range="1,1,15" option="int" />
<setting id="enableSkipIntro" type="bool" label="30525" default="true" /><!-- Enable skipping of intros -->
<setting id="ignoreSpecialsNextEpisodes" type="bool" label="30527" default="false" /> <setting id="ignoreSpecialsNextEpisodes" type="bool" label="30527" default="false" />
<setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" visible="false"/> <setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" visible="false"/>
<setting type="sep" /> <setting type="sep" />

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">3002</defaultcontrol>
<onload>Dialog.Close(fullscreeninfo,true)</onload>
<onload>Dialog.Close(videoosd,true)</onload>
<controls>
<control type="group">
<animation type="WindowOpen" reversible="false">
<effect type="fade" start="0" end="100" time="500"/>
</animation>
<animation type="WindowClose" reversible="false">
<effect type="fade" start="100" end="0" time="500"/>
</animation>
<control type="group">
<height>64</height>
<control type="image">
<width>100%</width>
<height>64</height>
<texture colordiffuse="44000000">skipintro-background.png</texture>
</control>
<control type="group">
<top>12</top>
<right>20</right>
<width>70%</width>
<control type="grouplist" id="3001">
<orientation>horizontal</orientation>
<height>40</height>
<itemgap>10</itemgap>
<align>right</align>
<control type="button" id="3002">
<label>$ADDON[plugin.video.plexkodiconnect 30525]</label>
<height>40</height>
<width min="50">auto</width>
<font>font20_title</font>
<textoffsetx>32</textoffsetx>
<textcolor>ddffffff</textcolor>
<focusedcolor>eeffffff</focusedcolor>
<selectedcolor>ddffffff</selectedcolor>
<shadowcolor>22000000</shadowcolor>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="FF063FB2">skipintro-button.png</texturefocus>
<texturenofocus border="10" colordiffuse="FF323232">skipintro-button.png</texturenofocus>
<alttexturefocus border="10" colordiffuse="FF063FB2">skipintro-button.png</alttexturefocus>
<alttexturenofocus border="10" colordiffuse="FF323232">skipintro-button.png</alttexturenofocus>
</control>
</control>
</control>
</control>
</control>
</controls>
</window>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB