Add segment skip ability

This commit is contained in:
Gorgorot38
2025-02-23 10:26:37 +01:00
parent 9a7a6e6896
commit 761dc8e1c5
9 changed files with 376 additions and 6 deletions

View File

@@ -1155,4 +1155,40 @@ msgstr "Hide number of items to show on entry title"
msgctxt "#30454" msgctxt "#30454"
msgid " - Totally Unwatched" 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"

View File

@@ -1156,3 +1156,39 @@ msgstr "Revoir ensuite"
msgctxt "#30453" msgctxt "#30453"
msgid "Hide number of items to show on entry title" msgid "Hide number of items to show on entry title"
msgstr "Cacher ne nombre d'éléments à montrer dans les titres d'entrées" msgstr "Cacher ne nombre d'éléments à montrer dans les titres d'entrées"
msgctxt "#30666"
msgid "Segment Skipper"
msgstr "Passer les segments"
msgctxt "#30667"
msgid "Action to take"
msgstr "Action"
msgctxt "#30668"
msgid "Start Offset (seconds)"
msgstr "Décalage début de segment (secondes)"
msgctxt "#30669"
msgid "End Offset (seconds)"
msgstr "Décalage fin de segment (secondes)"
msgctxt "#30670"
msgid "Intro Skipper"
msgstr "Introductions"
msgctxt "#30671"
msgid "Credit Skipper"
msgstr "Crédits"
msgctxt "#30672"
msgid "Skip"
msgstr "Passer"
msgctxt "#30673"
msgid "Ask"
msgstr "Passer"
msgctxt "#30674"
msgid "Do Nothing"
msgstr "Ne rien faire"

View File

@@ -3,9 +3,11 @@ from __future__ import (
) )
import xbmcgui import xbmcgui
import xbmc
from .lazylogger import LazyLogger 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__) log = LazyLogger(__name__)
@@ -206,3 +208,63 @@ class PlayNextDialog(xbmcgui.WindowXMLDialog):
def get_play_called(self): def get_play_called(self):
return self.play_called return self.play_called
class SkipDialog(xbmcgui.WindowXMLDialog):
action_exitkeys_id = None
media_id = None
is_intro = False
intro_start = None
intro_end = None
credit_start = None
credit_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):
if action.getId() == 10 or action.getId() == 92: # ACTION_PREVIOUS_MENU & ACTION_NAV_BACK
self.has_been_dissmissed = True
self.close()
else:
log.debug("SkipDialog: onAction: {0}".format(action.getId()))
def onClick(self, control_id):
player = xbmc.Player()
current_ticks = seconds_to_ticks(player.getTime())
if self.intro_start is not None and self.intro_end is not None and current_ticks >= self.intro_start and current_ticks <= self.intro_end:
# If click during intro, skip it
player.seekTime(ticks_to_seconds(self.intro_end))
elif self.credit_start is not None and self.credit_end is not None and current_ticks >= self.credit_start and current_ticks <= self.credit_end:
# If click during outro, skip it
player.seekTime(ticks_to_seconds(self.credit_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,145 @@
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 set_correct_skip_info
from resources.lib.utils import seconds_to_ticks, ticks_to_seconds, translate_path
from .lazylogger import LazyLogger
from .dialogs import SkipDialog
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
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:
# Handle skip only on jellyfin items
current_ticks = seconds_to_ticks(player.getTime())
# Handle Intros
skip_intro_dialog = self.handle_intros(plugin_path_real, skip_intro_dialog, item_id, current_ticks, player)
# Handle Credits
skip_credit_dialog = self.handle_credits(plugin_path_real, skip_credit_dialog, item_id, current_ticks, player)
else:
if skip_intro_dialog is not None:
skip_intro_dialog.close()
skip_intro_dialog = None
if skip_credit_dialog is not None:
skip_credit_dialog.close()
skip_credit_dialog = None
if xbmc.Monitor().waitForAbort(1):
break
xbmc.sleep(200)
def handle_intros(self, plugin_path_real: str, skip_intro_dialog: SkipDialog, item_id: str, current_ticks: float, player: xbmc.Player):
settings = xbmcaddon.Addon()
intro_skip_action = settings.getSetting("intro_skipper_action")
# In case do nothing is selected return
if intro_skip_action == "2":
return
if skip_intro_dialog is None:
skip_intro_dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p")
set_correct_skip_info(item_id, skip_intro_dialog)
is_intro = False
if skip_intro_dialog.intro_start is not None and skip_intro_dialog.intro_end is not None:
# Resets the dismiss var so that button can reappear in case of navigation in the timecodes
if current_ticks < skip_intro_dialog.intro_start or current_ticks > skip_intro_dialog.intro_end:
skip_intro_dialog.has_been_dissmissed = False
# Checks if segment is playing
is_intro = current_ticks >= skip_intro_dialog.intro_start and current_ticks <= skip_intro_dialog.intro_end
if intro_skip_action == "1" and is_intro:
# If auto skip is enabled, skips to semgent ends automatically
player.seekTime(ticks_to_seconds(skip_intro_dialog.intro_end))
xbmcgui.Dialog().notification("JellyCon", "Intro Skipped")
elif intro_skip_action == "0":
# Otherwise show skip dialog
if is_intro and not skip_intro_dialog.has_been_dissmissed:
skip_intro_dialog.show()
else:
# Could not find doc on what happens when closing a closed dialog, but it seems fine
skip_intro_dialog.close()
return skip_intro_dialog
def handle_credits(self, plugin_path_real: str, skip_credit_dialog: SkipDialog, item_id: str, current_ticks: float, player: xbmc.Player):
settings = xbmcaddon.Addon()
credit_skip_action = settings.getSetting("credit_skipper_action")
# In case do nothing is selected return
if credit_skip_action == "2":
return
if skip_credit_dialog is None:
skip_credit_dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p")
set_correct_skip_info(item_id, skip_credit_dialog)
is_credit = False
if skip_credit_dialog.credit_start is not None and skip_credit_dialog.credit_end is not None:
# Resets the dismiss var so that button can reappear in case of navigation in the timecodes
if current_ticks < skip_credit_dialog.credit_start or current_ticks > skip_credit_dialog.credit_end:
skip_credit_dialog.has_been_dissmissed = False
# Checks if segment is playing
is_credit = current_ticks >= skip_credit_dialog.credit_start and current_ticks <= skip_credit_dialog.credit_end
if credit_skip_action == "1" and is_credit:
# If auto skip is enabled, skips to semgent ends automatically
player.seekTime(ticks_to_seconds(skip_credit_dialog.credit_end))
xbmcgui.Dialog().notification("JellyCon", "Credit Skipped")
elif credit_skip_action == "0":
# Otherwise show skip dialog
if is_credit and not skip_credit_dialog.has_been_dissmissed:
skip_credit_dialog.show()
else:
skip_credit_dialog.close()
return skip_credit_dialog
def stop_service(self):
log.debug("IntroSkipperService Stop Called")
self.stop_thread = True

View File

@@ -18,8 +18,8 @@ from six.moves.urllib.parse import urlencode
from .jellyfin import api from .jellyfin import api
from .lazylogger import LazyLogger from .lazylogger import LazyLogger
from .dialogs import ResumeDialog from .dialogs import ResumeDialog, SkipDialog
from .utils import send_event_notification, convert_size, get_device_id, translate_string, load_user_details, translate_path, get_jellyfin_url, download_external_sub, get_bitrate from .utils import seconds_to_ticks, send_event_notification, convert_size, get_device_id, translate_string, load_user_details, translate_path, get_jellyfin_url, download_external_sub, get_bitrate
from .kodi_utils import HomeWindow from .kodi_utils import HomeWindow
from .datamanager import clear_old_cache_data from .datamanager import clear_old_cache_data
from .item_functions import extract_item_info, add_gui_item, get_art from .item_functions import extract_item_info, add_gui_item, get_art
@@ -1182,6 +1182,16 @@ def get_playing_data():
return {} 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): def get_play_url(media_source, play_session_id, channel_id=None):
log.debug("get_play_url - media_source: {0}", media_source) log.debug("get_play_url - media_source: {0}", media_source)
@@ -1693,3 +1703,33 @@ def get_item_playback_info(item_id, force_transcode):
log.debug("PlaybackInfo : {0}".format(play_info_result)) log.debug("PlaybackInfo : {0}".format(play_info_result))
return 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:
return None
return result["Items"]
def set_correct_skip_info(item_id: str, skip_dialog: SkipDialog):
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 fetch media segments (happens twice per media - intro and outro - but it is a light call)
skip_dialog.media_id = item_id
skip_dialog.has_been_dissmissed = False
segments = get_media_segments(item_id)
if segments is not None:
# Find the intro and outro timings
intro_start = next((segment["StartTicks"] for segment in segments if segment["Type"] == "Intro"), None)
intro_end = next((segment["EndTicks"] for segment in segments if segment["Type"] == "Intro"), None)
credit_start = next((segment["StartTicks"] for segment in segments if segment["Type"] == "Outro"), None)
credit_end = next((segment["EndTicks"] for segment in segments if segment["Type"] == "Outro"), None)
# Sets timings with offsets if defined in settings
if intro_start is not None:
skip_dialog.intro_start = intro_start + seconds_to_ticks(settings.getSettingInt("intro_skipper_start_offset"))
if intro_end is not None:
skip_dialog.intro_end = intro_end - seconds_to_ticks(settings.getSettingInt("intro_skipper_end_offset"))
if credit_start is not None:
skip_dialog.credit_start = credit_start + seconds_to_ticks(settings.getSettingInt("credit_skipper_start_offset"))
if credit_end is not None:
skip_dialog.credit_end = credit_end - seconds_to_ticks(settings.getSettingInt("credit_skipper_end_offset"))

View File

@@ -457,4 +457,10 @@ def get_filtered_items_count_text():
if settings.getSetting("hide_x_filtered_items_count") == 'true' : if settings.getSetting("hide_x_filtered_items_count") == 'true' :
return "" return ""
else: 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,19 @@
<setting id="interface_mode" type="select" label="30225" lvalues="30226|30227" default="0" visible="true"/> <setting id="interface_mode" type="select" label="30225" lvalues="30226|30227" default="0" visible="true"/>
</category> </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" />
</category>
<category label="30111"> <category label="30111">
<setting label="30343" type="lsep"/> <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.tracking import set_timing_enabled
from resources.lib.image_server import HttpImageServerThread from resources.lib.image_server import HttpImageServerThread
from resources.lib.playnext import PlayNextService from resources.lib.playnext import PlayNextService
from resources.lib.intro_skipper import IntroSkipperService
settings = xbmcaddon.Addon() settings = xbmcaddon.Addon()
@@ -87,6 +88,10 @@ if context_menu:
context_monitor = ContextMonitor() context_monitor = ContextMonitor()
context_monitor.start() context_monitor.start()
# Start the skip service monitor
intro_skipper = IntroSkipperService(monitor)
intro_skipper.start()
background_interval = int(settings.getSetting('background_interval')) background_interval = int(settings.getSetting('background_interval'))
newcontent_interval = int(settings.getSetting('new_content_check_interval')) newcontent_interval = int(settings.getSetting('new_content_check_interval'))
random_movie_list_interval = int(settings.getSetting('random_movie_refresh_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') home_window.set_property('exit', 'False')
while home_window.get_property('exit') == 'False': while home_window.get_property('exit') == 'False':
try: try:
if xbmc.Player().isPlaying(): if xbmc.Player().isPlaying():
last_random_movie_update = time.time() - (random_movie_list_interval - 15) 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 # call stop on the context menu monitor
if context_monitor: if context_monitor:
context_monitor.stop_monitor() context_monitor.stop_monitor()
if intro_skipper:
intro_skipper.stop_service()
# clear user and token when logging off # clear user and token when logging off
home_window.clear_property("user_name") home_window.clear_property("user_name")