Merge pull request #1336 from croneter/py2-skipintro

Add skip intro functionality
This commit is contained in:
croneter 2021-02-09 21:10:23 +01:00 committed by GitHub
commit 474e4ac5d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 220 additions and 12 deletions

View file

@ -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"

View file

@ -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):

View file

@ -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):

View file

@ -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

View file

@ -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:

View file

@ -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')

View file

@ -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.

View file

@ -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')

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -108,6 +108,7 @@
<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="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="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" visible="false"/>
<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