Skip to content

Commit

Permalink
Merge pull request #1926 from croneter/credits
Browse files Browse the repository at this point in the history
Support for skipping credits. If Plex detected end credits, videos will now only be marked as watched if the end credits have been reached
  • Loading branch information
croneter authored Feb 25, 2023
2 parents d69bb66 + f04f020 commit 9800dec
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 181 deletions.
7 changes: 6 additions & 1 deletion resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -576,11 +576,16 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr ""

# PKC Settings - Playback
# PKC Settings - Playback. Message displayed when playback is showing intro
msgctxt "#30525"
msgid "Skip intro"
msgstr ""

# PKC. Message displayed when playback is showing credits
msgctxt "#30526"
msgid "Skip credits"
msgstr ""

# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
Expand Down
4 changes: 2 additions & 2 deletions resources/lib/app/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ def __init__(self, entrypoint=False):
self.metadata_thread = None
# Instance of ImageCachingThread()
self.caching_thread = None
# Dialog to skip intro
self.skip_intro_dialog = None
# Dialog to skip markers such as intros and credits
self.skip_markers_dialog = None

@property
def is_playing(self):
Expand Down
2 changes: 1 addition & 1 deletion resources/lib/app/playstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class PlayState(object):
'playmethod': None,
'playcount': None,
'external_player': False, # bool - xbmc.Player().isExternalPlayer()
'intro_markers': [],
'markers': [],
}

def __init__(self):
Expand Down
110 changes: 63 additions & 47 deletions resources/lib/kodimonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ def PlayBackStart(self, data):
container_key = '/library/metadata/%s' % plex_id
# Mechanik for Plex skip intro feature
if utils.settings('enableSkipIntro') == 'true':
status['intro_markers'] = item.api.intro_markers()
status['markers'] = item.api.markers()
status['final_marker'] = item.api.final_marker()
if item.playmethod is None and path and not path.startswith('plugin://'):
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PATH
item.playerid = playerid
Expand Down Expand Up @@ -378,9 +379,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
if app.APP.skip_markers_dialog:
app.APP.skip_markers_dialog.close()
app.APP.skip_markers_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
Expand Down Expand Up @@ -419,48 +420,7 @@ def _record_playstate(status, ended):
# Item not (yet) in Kodi library
LOG.debug('No playstate update due to Plex id not found: %s', status)
return
totaltime = float(timing.kodi_time_to_millis(status['totaltime'])) / 1000
if status['external_player']:
# video has either been entirely watched - or not.
# "ended" won't work, need a workaround
ended = _external_player_correct_plex_watch_count(db_item)
if ended:
progress = 0.99
time = v.IGNORE_SECONDS_AT_START + 1
else:
progress = 0.0
time = 0.0
else:
if ended:
progress = 0.99
time = v.IGNORE_SECONDS_AT_START + 1
else:
time = float(timing.kodi_time_to_millis(status['time'])) / 1000
try:
progress = time / totaltime
except ZeroDivisionError:
progress = 0.0
LOG.debug('Playback progress %s (%s of %s seconds)',
progress, time, totaltime)
playcount = status['playcount']
last_played = timing.kodi_now()
if playcount is None:
LOG.debug('playcount not found, looking it up in the Kodi DB')
with kodi_db.KodiVideoDB() as kodidb:
playcount = kodidb.get_playcount(db_item['kodi_fileid'])
playcount = 0 if playcount is None else playcount
if time < v.IGNORE_SECONDS_AT_START:
LOG.debug('Ignoring playback less than %s seconds',
v.IGNORE_SECONDS_AT_START)
# Annoying Plex bug - it'll reset an already watched video to unwatched
playcount = None
last_played = None
time = 0
elif progress >= v.MARK_PLAYED_AT:
LOG.debug('Recording entirely played video since progress > %s',
v.MARK_PLAYED_AT)
playcount += 1
time = 0
time, totaltime, playcount, last_played = _playback_progress(status, ended, db_item)
with kodi_db.KodiVideoDB() as kodidb:
kodidb.set_resume(db_item['kodi_fileid'],
time,
Expand All @@ -474,15 +434,71 @@ def _record_playstate(status, ended):
totaltime,
playcount,
last_played)
# Hack to force "in progress" widget to appear if it wasn't visible before
if (app.APP.force_reload_skin and
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):
# Hack to force "in progress" widget to appear if it wasn't visible before
LOG.debug('Refreshing skin to update widgets')
xbmc.executebuiltin('ReloadSkin()')
else:
xbmc.executebuiltin('Container.Refresh')
task = backgroundthread.FunctionAsTask(_clean_file_table, None)
backgroundthread.BGThreader.addTasksToFront([task])


def _playback_progress(status, ended, db_item):
totaltime = float(timing.kodi_time_to_millis(status['totaltime'])) / 1000
progress = 0.0
last_played = timing.kodi_now()
playcount = status['playcount']
if playcount is None:
LOG.debug('playcount not found, looking it up in the Kodi DB')
with kodi_db.KodiVideoDB() as kodidb:
playcount = kodidb.get_playcount(db_item['kodi_fileid'])
playcount = 0 if playcount is None else playcount
if status['external_player']:
# video has either been entirely watched - or not.
# "ended" won't work, need a workaround
ended = _external_player_correct_plex_watch_count(db_item)
else:
time = float(timing.kodi_time_to_millis(status['time'])) / 1000
LOG.debug('Playtime: %s', time)
if status['final_marker']:
LOG.debug('Plex told us there are credits starting at %s',
status['final_marker'])
# Credits will show when video is done - if we're not in the
# credits, the video is not done
if time >= status['final_marker']:
ended = True
else:
ended = False
if not ended and not status['final_marker']:
# Plex does not know when the video is actually done
try:
progress = time / totaltime
except ZeroDivisionError:
pass
if time < v.IGNORE_SECONDS_AT_START:
LOG.debug('Ignoring playback less than %s seconds',
v.IGNORE_SECONDS_AT_START)
# Annoying Plex bug - it'll reset an already watched video to unwatched
playcount = None
last_played = None
time = 0
elif ended:
LOG.debug('Video has been played completely')
playcount += 1
time = 0
progress = 100
elif progress >= v.MARK_PLAYED_AT:
LOG.debug('Recording entirely played video since progress %s > %s',
progress, v.MARK_PLAYED_AT)
playcount += 1
time = 0
LOG.debug('Playback progress %s (%s of %s seconds), playcount %s',
progress, time, totaltime, playcount)
return time, totaltime, playcount, last_played


def _external_player_correct_plex_watch_count(db_item):
"""
Kodi won't safe playstate at all for external players
Expand Down
19 changes: 12 additions & 7 deletions resources/lib/plex_api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(self, xml):
self._writers = []
self._producers = []
self._locations = []
self._intro_markers = []
self._markers = []
self._guids = {}
self._coll_match = None
# Plex DB attributes
Expand Down Expand Up @@ -522,14 +522,19 @@ def _scan_children(self):
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:
elif child.tag == 'Marker':
start = cast(float, child.get('startTimeOffset'))
end = cast(float, child.get('endTimeOffset'))
typus = child.get('type')
if None in (start, end, typus):
# Safety net if PMS xml is not as expected
continue
intro = (intro[0] / 1000.0, intro[1] / 1000.0)
self._intro_markers.append(intro)
final_credits_start = start / 1000.0 if child.get('final') == '1' \
else 0.0
self._markers.append((start / 1000.0,
end / 1000.0,
typus,
final_credits_start))
# Plex Movie agent (legacy) or "normal" Plex tv show agent
if not self._guids:
guid = self.xml.get('guid')
Expand Down
22 changes: 16 additions & 6 deletions resources/lib/plex_api/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,25 @@ def _from_stream_or_part(self, key):
value = self.xml[0][self.part].get(key)
return value

def intro_markers(self):
def 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
Returns a list of tuples (startTimeOffset [float], endTimeOffset
[float], marker type [str, currently 'intro' or 'credits'], final
[bool]) in Koditime or an empty list. Each entry represents an
(episode) intro or credit that Plex detected and that can be skipped to
endTimeOffset. If final is set to True, this means that the marker is
located at the end of the video
"""
self._scan_children()
return self._intro_markers
return self._markers

def final_marker(self):
"""
Returns the starting time of the marker where the flag 'final' is set
to '1', meaning the credits are at the end of the video and thus signal
that the video has indeed ended
"""
return max(x[3] for x in self.markers())

def video_codec(self):
"""
Expand Down
4 changes: 2 additions & 2 deletions resources/lib/service_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from . import app
from . import loghandler
from . import backgroundthread
from . import skip_plex_intro
from . import skip_plex_markers
from .windows import userselect

###############################################################################
Expand Down Expand Up @@ -561,7 +561,7 @@ def ServiceEntryPoint(self):
self.alexa_ws.start()

elif app.APP.is_playing:
skip_plex_intro.check()
skip_plex_markers.check()

xbmc.sleep(200)

Expand Down
43 changes: 0 additions & 43 deletions resources/lib/skip_plex_intro.py

This file was deleted.

53 changes: 53 additions & 0 deletions resources/lib/skip_plex_markers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from .windows.skip_marker import SkipMarkerDialog
from . import app, utils, variables as v


# Supported types of markers that can be skipped; values here will be
# displayed to the user when skipping is available
MARKERS = {
'intro': utils.lang(30525), # Skip intro
'credits': utils.lang(30526) # Skip credits
}


def skip_markers(markers):
try:
progress = app.APP.player.getTime()
except RuntimeError:
# XBMC is not playing any media file yet
return
within_marker = False
for start, end, typus, _ in markers:
if start <= progress < end:
within_marker = True
break
if within_marker and app.APP.skip_markers_dialog is None:
# WARNING: This Dialog only seems to work if called from the main
# thread. Otherwise, onClick and onAction won't work
app.APP.skip_markers_dialog = SkipMarkerDialog(
'script-plex-skip_marker.xml',
v.ADDON_PATH,
'default',
'1080i',
marker_message=MARKERS[typus],
marker_end=end)
if utils.settings('enableAutoSkipIntro') == "true":
app.APP.skip_markers_dialog.seekTimeToEnd()
else:
app.APP.skip_markers_dialog.show()
elif not within_marker and app.APP.skip_markers_dialog is not None:
app.APP.skip_markers_dialog.close()
app.APP.skip_markers_dialog = None


def check():
with app.APP.lock_playqueues:
if len(app.PLAYSTATE.active_players) != 1:
return
playerid = list(app.PLAYSTATE.active_players)[0]
markers = app.PLAYSTATE.player_states[playerid]['markers']
if not markers:
return
skip_markers(markers)
Loading

0 comments on commit 9800dec

Please sign in to comment.