Add skip intro functionality
This commit is contained in:
parent
f326e49ba7
commit
7f8939cee7
15 changed files with 218 additions and 12 deletions
|
@ -571,6 +571,10 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
|
|
|
@ -50,6 +50,8 @@ class App(object):
|
|||
self.metadata_thread = None
|
||||
# Instance of ImageCachingThread()
|
||||
self.caching_thread = None
|
||||
# Dialog to skip intro
|
||||
self.skip_intro_dialog = None
|
||||
|
||||
@property
|
||||
def is_playing(self):
|
||||
|
|
|
@ -33,7 +33,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):
|
||||
|
|
|
@ -334,6 +334,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
|
||||
|
@ -366,6 +370,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
|
||||
|
|
|
@ -470,7 +470,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",
|
||||
|
@ -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)
|
||||
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):
|
||||
|
@ -663,10 +670,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:
|
||||
|
@ -765,8 +775,9 @@ 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,
|
||||
parameters = {'includeMarkers': 1}
|
||||
xml = DU().downloadUrl("{server}/playQueues/%s" % playqueue_id,
|
||||
parameters=parameters,
|
||||
headerOptions={'Accept': 'application/xml'})
|
||||
try:
|
||||
xml.attrib
|
||||
|
|
|
@ -42,6 +42,7 @@ class Base(object):
|
|||
self._writers = []
|
||||
self._producers = []
|
||||
self._locations = []
|
||||
self._intro_markers = []
|
||||
self._guids = {}
|
||||
self._coll_match = None
|
||||
# Plex DB attributes
|
||||
|
@ -469,6 +470,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')
|
||||
|
|
|
@ -42,6 +42,16 @@ class Media(object):
|
|||
value = self.xml[0][self.part].get(key)
|
||||
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):
|
||||
"""
|
||||
Returns the video codec and resolution for the child and part selected.
|
||||
|
|
|
@ -472,6 +472,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,
|
||||
|
@ -511,7 +512,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,
|
||||
|
@ -801,7 +804,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')
|
||||
|
|
|
@ -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
|
||||
|
||||
###############################################################################
|
||||
|
@ -545,7 +546,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)
|
||||
|
|
33
resources/lib/skip_plex_intro.py
Normal file
33
resources/lib/skip_plex_intro.py
Normal 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)
|
68
resources/lib/windows/skip_intro.py
Normal file
68
resources/lib/windows/skip_intro.py
Normal 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)
|
|
@ -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" />
|
||||
|
|
52
resources/skins/default/1080i/skip_intro.xml
Normal file
52
resources/skins/default/1080i/skip_intro.xml
Normal 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>
|
BIN
resources/skins/default/media/skipintro-background.png
Normal file
BIN
resources/skins/default/media/skipintro-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
resources/skins/default/media/skipintro-button.png
Normal file
BIN
resources/skins/default/media/skipintro-button.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
Loading…
Reference in a new issue