Merge pull request #1336 from croneter/py2-skipintro
Add skip intro functionality
This commit is contained in:
commit
474e4ac5d1
15 changed files with 220 additions and 12 deletions
|
@ -567,6 +567,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"
|
||||||
|
|
|
@ -51,6 +51,8 @@ class App(object):
|
||||||
self.fanart_thread = None
|
self.fanart_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):
|
||||||
|
|
|
@ -36,7 +36,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):
|
||||||
|
|
|
@ -335,6 +335,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
|
||||||
|
@ -367,6 +371,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
|
||||||
|
|
|
@ -478,7 +478,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",
|
||||||
|
@ -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)
|
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):
|
||||||
|
@ -671,10 +678,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:
|
||||||
|
@ -773,9 +783,10 @@ 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,
|
||||||
headerOptions={'Accept': 'application/xml'})
|
parameters=parameters,
|
||||||
|
headerOptions={'Accept': 'application/xml'})
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|
|
@ -43,6 +43,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
|
||||||
|
@ -470,6 +471,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')
|
||||||
|
|
|
@ -28,6 +28,16 @@ class Media(object):
|
||||||
"""
|
"""
|
||||||
return self.xml[0][self.part].get(key, self.xml[0].get(key))
|
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):
|
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.
|
||||||
|
|
|
@ -479,6 +479,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,
|
||||||
|
@ -518,7 +519,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,
|
||||||
|
@ -806,7 +809,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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -552,7 +553,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)
|
||||||
|
|
34
resources/lib/skip_plex_intro.py
Normal file
34
resources/lib/skip_plex_intro.py
Normal 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)
|
69
resources/lib/windows/skip_intro.py
Normal file
69
resources/lib/windows/skip_intro.py
Normal 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)
|
|
@ -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" />
|
||||||
|
|
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