Merge pull request #368 from Gorgorot38/skip_segments

Add segment skip ability
This commit is contained in:
mcarlton00
2025-04-15 07:45:12 -04:00
committed by GitHub
9 changed files with 419 additions and 4 deletions

View File

@@ -1155,4 +1155,52 @@ msgstr "Hide number of items to show on entry title"
msgctxt "#30454"
msgid " - Totally Unwatched"
msgstr " - Totally Unwatched"
msgstr " - Totally Unwatched"
msgctxt "#30666"
msgid "Segment Skipper"
msgstr "Segment Skipper"
msgctxt "#30667"
msgid "Action to take"
msgstr "Action to take"
msgctxt "#30668"
msgid "Start Offset (seconds)"
msgstr "Start Offset (seconds)"
msgctxt "#30669"
msgid "End Offset (seconds)"
msgstr "End Offset (seconds)"
msgctxt "#30670"
msgid "Intro Skipper"
msgstr "Intro Skipper"
msgctxt "#30671"
msgid "Credit Skipper"
msgstr "Credit Skipper"
msgctxt "#30672"
msgid "Skip"
msgstr "Skip"
msgctxt "#30673"
msgid "Ask"
msgstr "Ask"
msgctxt "#30674"
msgid "Do Nothing"
msgstr "Do Nothing"
msgctxt "#30675"
msgid "Commercial Skipper"
msgstr "Commercial Skipper"
msgctxt "#30676"
msgid "Preview Skipper"
msgstr "Preview Skipper"
msgctxt "#30677"
msgid "Recap Skipper"
msgstr "Recap Skipper"

View File

@@ -3,9 +3,11 @@ from __future__ import (
)
import xbmcgui
import xbmc
from .lazylogger import LazyLogger
from .utils import translate_string, send_event_notification
from .utils import seconds_to_ticks, ticks_to_seconds, translate_string, send_event_notification
log = LazyLogger(__name__)
@@ -206,3 +208,57 @@ class PlayNextDialog(xbmcgui.WindowXMLDialog):
def get_play_called(self):
return self.play_called
class SkipDialog(xbmcgui.WindowXMLDialog):
action_exitkeys_id = None
media_id = None
start = None
end = None
has_been_dissmissed = False
def __init__(self, *args, **kwargs):
log.debug("SkipDialog: __init__")
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
def onInit(self):
log.debug("SkipDialog: onInit")
self.action_exitkeys_id = [10, 13]
def onFocus(self, control_id):
pass
def doAction(self, action_id):
pass
def onMessage(self, message):
log.debug("SkipDialog: onMessage: {0}".format(message))
def onAction(self, action):
log.debug("SkipDialog: onAction: {0}".format(action.getId()))
if action.getId() == 10 or action.getId() == 92: # ACTION_PREVIOUS_MENU & ACTION_NAV_BACK
log.debug("SkipDialog: dismissing dialog so it does not open again")
self.has_been_dissmissed = True
self.close()
def onClick(self, control_id):
log.debug("SkipDialog: onClick: {0}".format(control_id))
player = xbmc.Player()
current_ticks = seconds_to_ticks(player.getTime())
if self.start is not None and self.end is not None and current_ticks >= self.start and current_ticks <= self.end:
log.debug("SkipDialog: skipping segment because current ticks ({0}) is in range".format(current_ticks))
# If click during segment, skip it
player.seekTime(ticks_to_seconds(self.end))
self.close()
def get_play_called(self):
return self.play_called
def is_button_shown(self):
try:
self.getFocus()
return True
except Exception:
return False

View File

@@ -0,0 +1,158 @@
from __future__ import (
division, absolute_import, print_function, unicode_literals
)
import os
import threading
import xbmc
import xbmcaddon
import xbmcgui
from resources.lib.play_utils import get_media_segments
from resources.lib.utils import seconds_to_ticks, ticks_to_seconds, translate_path
from resources.lib.intro_skipper_utils import get_setting_skip_action, set_correct_skip_info
from .lazylogger import LazyLogger
from .dialogs import SkipDialog
from typing import Literal
log = LazyLogger(__name__)
class IntroSkipperService(threading.Thread):
stop_thread = False
monitor = None
def __init__(self, play_monitor):
super(IntroSkipperService, self).__init__()
self.monitor = play_monitor
def run(self):
from .play_utils import get_jellyfin_playing_item
settings = xbmcaddon.Addon()
plugin_path = settings.getAddonInfo('path')
plugin_path_real = translate_path(os.path.join(plugin_path))
skip_intro_dialog = None
skip_credit_dialog = None
skip_commercial_dialog = None
skip_preview_dialog = None
skip_recap_dialog = None
segments = None
playing_item_id = None
log.debug("SkipService: starting service")
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
player = xbmc.Player()
if player.isPlaying():
item_id = get_jellyfin_playing_item()
if item_id is not None:
log.debug("SkipService: playing item is from jellyfin : {0}".format(item_id))
# If item id has changed or is new, retrieve segments
if playing_item_id is None or playing_item_id != item_id :
log.debug("SkipService: item is new, retrieving media segments : {0}".format(item_id))
segments = get_media_segments(item_id)
# Setting global playing item to current playing item
playing_item_id = item_id
# Handle skip only on jellyfin items
current_ticks = seconds_to_ticks(player.getTime())
# Handle Intros
skip_intro_dialog = self.handle_dialog(plugin_path_real, skip_intro_dialog, item_id, current_ticks, player, segments, "Intro")
# Handle Credits
skip_credit_dialog = self.handle_dialog(plugin_path_real, skip_credit_dialog, item_id, current_ticks, player, segments, "Outro")
# Handle commercial
skip_commercial_dialog = self.handle_dialog(plugin_path_real, skip_commercial_dialog, item_id, current_ticks, player, segments, "Commercial")
# Handle preview
skip_preview_dialog = self.handle_dialog(plugin_path_real, skip_preview_dialog, item_id, current_ticks, player, segments, "Preview")
# Handle recap
skip_recap_dialog = self.handle_dialog(plugin_path_real, skip_recap_dialog, item_id, current_ticks, player, segments, "Recap")
else:
playing_item_id = None
if skip_intro_dialog is not None:
log.debug("SkipService: Playback stopped, killing Intro dialog")
skip_intro_dialog.close()
skip_intro_dialog = None
if skip_credit_dialog is not None:
log.debug("SkipService: Playback stopped, killing Outro dialog")
skip_credit_dialog.close()
skip_credit_dialog = None
if skip_commercial_dialog is not None:
log.debug("SkipService: Playback stopped, killing Commercial dialog")
skip_commercial_dialog.close()
skip_commercial_dialog = None
if skip_preview_dialog is not None:
log.debug("SkipService: Playback stopped, killing Preview dialog")
skip_preview_dialog.close()
skip_preview_dialog = None
if skip_recap_dialog is not None:
log.debug("SkipService: Playback stopped, killing Recap dialog")
skip_recap_dialog.close()
skip_recap_dialog = None
if xbmc.Monitor().waitForAbort(1):
break
xbmc.sleep(200)
def handle_dialog(self, plugin_path_real: str, dialog: SkipDialog, item_id: str, current_ticks: float, player: xbmc.Player, segments, type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
skip_action = get_setting_skip_action(type)
# In case do nothing is selected return
if skip_action == "2":
log.debug("SkipService: ignore {0} is selected".format(type))
return None
if dialog is None:
log.debug("SkipService: init dialog")
dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p")
set_correct_skip_info(item_id, dialog, segments, type)
is_segment = False
if dialog.start is not None and dialog.end is not None:
# Resets the dismiss var so that button can reappear in case of navigation in the timecodes
if (current_ticks < dialog.start or current_ticks > dialog.end) and dialog.has_been_dissmissed is True:
log.debug("SkipService: {0} skip was dismissed. It is reset beacause timecode is outside of segment")
dialog.has_been_dissmissed = False
# Checks if segment is playing
is_segment = current_ticks >= dialog.start and current_ticks <= dialog.end
if skip_action == "1" and is_segment:
log.debug("SkipService: {0} is set to automatic skip, skipping segment".format(type))
# If auto skip is enabled, skips to semgent ends automatically
player.seekTime(ticks_to_seconds(dialog.end))
xbmcgui.Dialog().notification("JellyCon", "{0} Skipped".format(type))
elif skip_action == "0":
# Otherwise show skip dialog
if is_segment and not dialog.has_been_dissmissed:
log.debug("SkipService: {0} is playing, showing dialog".format(type))
dialog.show()
else:
# Could not find doc on what happens when closing a closed dialog, but it seems fine
log.debug("SkipService: {0} is not playing, closing dialog".format(type))
dialog.close()
return dialog
def stop_service(self):
log.debug("IntroSkipperService Stop Called")
self.stop_thread = True

View File

@@ -0,0 +1,72 @@
from typing import Literal
import xbmcaddon
from .lazylogger import LazyLogger
from .dialogs import SkipDialog
from .utils import seconds_to_ticks
log = LazyLogger(__name__)
def get_setting_skip_action(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
settings = xbmcaddon.Addon()
if (type == "Commercial"):
return settings.getSetting("commercial_skipper_action")
elif (type == "Preview"):
return settings.getSetting("preview_skipper_action")
elif (type == "Recap"):
return settings.getSetting("recap_skipper_action")
elif (type == "Outro"):
return settings.getSetting("credit_skipper_action")
elif (type == "Intro"):
return settings.getSetting("intro_skipper_action")
return ""
def get_setting_skip_start_offset(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
settings = xbmcaddon.Addon()
if (type == "Commercial"):
return settings.getSettingInt("commercial_skipper_start_offset")
elif (type == "Preview"):
return settings.getSettingInt("preview_skipper_start_offset")
elif (type == "Recap"):
return settings.getSettingInt("recap_skipper_start_offset")
elif (type == "Outro"):
return settings.getSettingInt("credit_skipper_start_offset")
elif (type == "Intro"):
return settings.getSettingInt("intro_skipper_start_offset")
return 0
def get_setting_skip_end_offset(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
settings = xbmcaddon.Addon()
if (type == "Commercial"):
return settings.getSettingInt("commercial_skipper_end_offset")
elif (type == "Preview"):
return settings.getSettingInt("preview_skipper_end_offset")
elif (type == "Recap"):
return settings.getSettingInt("recap_skipper_end_offset")
elif (type == "Outro"):
return settings.getSettingInt("credit_skipper_end_offset")
elif (type == "Intro"):
return settings.getSettingInt("intro_skipper_end_offset")
return 0
def set_correct_skip_info(item_id: str, skip_dialog: SkipDialog, segments, type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
if (skip_dialog.media_id is None or skip_dialog.media_id != item_id) and item_id is not None:
# If playback item has changed (or is new), sets its id and set media segments info
log.debug("SkipDialogInfo : Media Id has changed to {0}, setting segments".format(item_id))
skip_dialog.media_id = item_id
skip_dialog.has_been_dissmissed = False
if segments is not None:
# Find the intro and outro timings
start = next((segment["StartTicks"] for segment in segments if segment["Type"] == type), None)
end = next((segment["EndTicks"] for segment in segments if segment["Type"] == type), None)
# Sets timings with offsets if defined in settings
if start is not None:
skip_dialog.start = start + seconds_to_ticks(get_setting_skip_start_offset(type))
log.debug("SkipDialogInfo : Setting {0} start to {1}".format(type, skip_dialog.start))
if end is not None:
skip_dialog.end = end - seconds_to_ticks(get_setting_skip_end_offset(type))
log.debug("SkipDialogInfo : Setting {0} end to {1}".format(type, skip_dialog.end))

View File

@@ -1183,6 +1183,16 @@ def get_playing_data():
return {}
def get_jellyfin_playing_item():
home_window = HomeWindow()
play_data_string = home_window.get_property('now_playing')
try:
play_data = json.loads(play_data_string)
except ValueError:
# This isn't a JellyCon item
return None
return play_data.get("item_id")
def get_play_url(media_source, play_session_id, channel_id=None):
log.debug("get_play_url - media_source: {0}", media_source)
@@ -1694,3 +1704,11 @@ def get_item_playback_info(item_id, force_transcode):
log.debug("PlaybackInfo : {0}".format(play_info_result))
return play_info_result
def get_media_segments(item_id):
url = "/MediaSegments/{}".format(item_id)
result = api.get(url)
if result is None or result["Items"] is None:
log.debug("GetMediaSegments : Media segments cloud not be retrieved")
return None
return result["Items"]

View File

@@ -457,4 +457,10 @@ def get_filtered_items_count_text():
if settings.getSetting("hide_x_filtered_items_count") == 'true' :
return ""
else:
return " (" + settings.getSetting("show_x_filtered_items") + ")"
return " (" + settings.getSetting("show_x_filtered_items") + ")"
def seconds_to_ticks(seconds:float):
return seconds * 10000000
def ticks_to_seconds(ticks:int):
return round(ticks / 10000000, 1)

View File

@@ -113,6 +113,31 @@
<setting id="interface_mode" type="select" label="30225" lvalues="30226|30227" default="0" visible="true"/>
</category>
<category label="30666">
<setting label="30670" type="lsep"/>
<setting id="intro_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
<setting id="intro_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true"/>
<setting id="intro_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true"/>
<setting label="30671" type="lsep"/>
<setting id="credit_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
<setting id="credit_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true" />
<setting id="credit_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true" />
<setting label="30675" type="lsep"/>
<setting id="commercial_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
<setting id="commercial_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true"/>
<setting id="commercial_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true"/>
<setting label="30676" type="lsep"/>
<setting id="preview_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
<setting id="preview_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true" />
<setting id="preview_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true" />
<setting label="30677" type="lsep"/>
<setting id="recap_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
<setting id="recap_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true"/>
<setting id="recap_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true"/>
</category>
<category label="30111">
<setting label="30343" type="lsep"/>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<window id="10000">
<defaultcontrol always="true">9000</defaultcontrol>
<zorder>2</zorder>
<coordinates>
<system>1</system>
<left>0</left>
<top>0</top>
</coordinates>
<controls>
<control type="button" id="9000">
<left>1020</left>
<top>550</top>
<width>150</width>
<height>65</height>
<visible>true</visible>
<label>Skip</label>
<align>center</align>
<texture border="1" colordiffuse="ff161616">white.png</texture>
<font>font12</font>
<onright>3014</onright>
</control>
</controls>
</window>

View File

@@ -20,6 +20,7 @@ from resources.lib.datamanager import clear_old_cache_data
from resources.lib.tracking import set_timing_enabled
from resources.lib.image_server import HttpImageServerThread
from resources.lib.playnext import PlayNextService
from resources.lib.intro_skipper import IntroSkipperService
settings = xbmcaddon.Addon()
@@ -87,6 +88,10 @@ if context_menu:
context_monitor = ContextMonitor()
context_monitor.start()
# Start the skip service monitor
intro_skipper = IntroSkipperService(monitor)
intro_skipper.start()
background_interval = int(settings.getSetting('background_interval'))
newcontent_interval = int(settings.getSetting('new_content_check_interval'))
random_movie_list_interval = int(settings.getSetting('random_movie_refresh_interval'))
@@ -104,7 +109,6 @@ first_run = True
home_window.set_property('exit', 'False')
while home_window.get_property('exit') == 'False':
try:
if xbmc.Player().isPlaying():
last_random_movie_update = time.time() - (random_movie_list_interval - 15)
@@ -183,6 +187,9 @@ if play_next_service:
# call stop on the context menu monitor
if context_monitor:
context_monitor.stop_monitor()
if intro_skipper:
intro_skipper.stop_service()
# clear user and token when logging off
home_window.clear_property("user_name")