From 761dc8e1c58872d646b2d75d85d059f5758a98e8 Mon Sep 17 00:00:00 2001 From: Gorgorot38 Date: Sun, 23 Feb 2025 10:26:37 +0100 Subject: [PATCH 1/6] Add segment skip ability --- .../resource.language.en_gb/strings.po | 38 ++++- .../language/resource.language.fr/strings.po | 36 +++++ resources/lib/dialogs.py | 64 +++++++- resources/lib/intro_skipper.py | 145 ++++++++++++++++++ resources/lib/play_utils.py | 44 +++++- resources/lib/utils.py | 8 +- resources/settings.xml | 13 ++ resources/skins/default/720p/SkipDialog.xml | 25 +++ service.py | 9 +- 9 files changed, 376 insertions(+), 6 deletions(-) create mode 100644 resources/lib/intro_skipper.py create mode 100644 resources/skins/default/720p/SkipDialog.xml diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 8c9508a..4cb1359 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1155,4 +1155,40 @@ msgstr "Hide number of items to show on entry title" msgctxt "#30454" msgid " - Totally Unwatched" -msgstr " - Totally Unwatched" \ No newline at end of file +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" diff --git a/resources/language/resource.language.fr/strings.po b/resources/language/resource.language.fr/strings.po index 846bd98..a7d4925 100644 --- a/resources/language/resource.language.fr/strings.po +++ b/resources/language/resource.language.fr/strings.po @@ -1156,3 +1156,39 @@ msgstr "Revoir ensuite" msgctxt "#30453" msgid "Hide number of items to show on entry title" 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" diff --git a/resources/lib/dialogs.py b/resources/lib/dialogs.py index 292f290..11716a3 100644 --- a/resources/lib/dialogs.py +++ b/resources/lib/dialogs.py @@ -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,63 @@ class PlayNextDialog(xbmcgui.WindowXMLDialog): def get_play_called(self): 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 diff --git a/resources/lib/intro_skipper.py b/resources/lib/intro_skipper.py new file mode 100644 index 0000000..6548bb3 --- /dev/null +++ b/resources/lib/intro_skipper.py @@ -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 diff --git a/resources/lib/play_utils.py b/resources/lib/play_utils.py index 601bc17..60a71be 100644 --- a/resources/lib/play_utils.py +++ b/resources/lib/play_utils.py @@ -18,8 +18,8 @@ from six.moves.urllib.parse import urlencode from .jellyfin import api from .lazylogger import LazyLogger -from .dialogs import ResumeDialog -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 .dialogs import ResumeDialog, SkipDialog +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 .datamanager import clear_old_cache_data from .item_functions import extract_item_info, add_gui_item, get_art @@ -1182,6 +1182,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) @@ -1693,3 +1703,33 @@ 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: + 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")) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 159b44a..ee15174 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -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") + ")" \ No newline at end of file + 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) diff --git a/resources/settings.xml b/resources/settings.xml index 7346cab..626ff41 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -113,6 +113,19 @@ + + + + + + + + + + + + + diff --git a/resources/skins/default/720p/SkipDialog.xml b/resources/skins/default/720p/SkipDialog.xml new file mode 100644 index 0000000..5aa51fc --- /dev/null +++ b/resources/skins/default/720p/SkipDialog.xml @@ -0,0 +1,25 @@ + + + 9000 + 2 + + 1 + 0 + 0 + + + + + 1020 + 550 + 150 + 65 + true + + center + white.png + font12 + 3014 + + + diff --git a/service.py b/service.py index 8e86609..d68b84b 100644 --- a/service.py +++ b/service.py @@ -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") From af54d611b1af3e72c3a128f2bd7b5c79d57c29cd Mon Sep 17 00:00:00 2001 From: Gorgorot38 Date: Sun, 23 Feb 2025 14:51:38 +0100 Subject: [PATCH 2/6] fix return --- resources/lib/intro_skipper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/intro_skipper.py b/resources/lib/intro_skipper.py index 6548bb3..565773f 100644 --- a/resources/lib/intro_skipper.py +++ b/resources/lib/intro_skipper.py @@ -74,7 +74,7 @@ class IntroSkipperService(threading.Thread): # In case do nothing is selected return if intro_skip_action == "2": - return + return None if skip_intro_dialog is None: skip_intro_dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p") @@ -111,7 +111,7 @@ class IntroSkipperService(threading.Thread): # In case do nothing is selected return if credit_skip_action == "2": - return + return None if skip_credit_dialog is None: skip_credit_dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p") From 3e621f8f33f05182687bc0026cd9fe0c033ddbc3 Mon Sep 17 00:00:00 2001 From: Gorgorot38 Date: Mon, 17 Mar 2025 17:25:55 +0100 Subject: [PATCH 3/6] Refacto code Add logs Add support for recap + preview + commercial --- .../resource.language.en_gb/strings.po | 12 ++ resources/lib/dialogs.py | 26 ++-- resources/lib/intro_skipper.py | 135 ++++++++++-------- resources/lib/intro_skipper_utils.py | 69 +++++++++ resources/lib/play_utils.py | 26 +--- resources/settings.xml | 12 ++ 6 files changed, 180 insertions(+), 100 deletions(-) create mode 100644 resources/lib/intro_skipper_utils.py diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 4cb1359..f7a08a2 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1192,3 +1192,15 @@ 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" diff --git a/resources/lib/dialogs.py b/resources/lib/dialogs.py index 11716a3..bf35a94 100644 --- a/resources/lib/dialogs.py +++ b/resources/lib/dialogs.py @@ -213,11 +213,8 @@ 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 + start = None + end = None has_been_dissmissed = False @@ -239,24 +236,21 @@ class SkipDialog(xbmcgui.WindowXMLDialog): 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() - else: - log.debug("SkipDialog: onAction: {0}".format(action.getId())) 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.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)) - + 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): diff --git a/resources/lib/intro_skipper.py b/resources/lib/intro_skipper.py index 565773f..7161c1e 100644 --- a/resources/lib/intro_skipper.py +++ b/resources/lib/intro_skipper.py @@ -9,13 +9,15 @@ import xbmc import xbmcaddon import xbmcgui -from resources.lib.play_utils import set_correct_skip_info +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__) @@ -38,108 +40,119 @@ class IntroSkipperService(threading.Thread): 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_intros(plugin_path_real, skip_intro_dialog, item_id, current_ticks, player) - + 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_credits(plugin_path_real, skip_credit_dialog, item_id, current_ticks, player) + 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_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") + + 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 intro_skip_action == "2": + if skip_action == "2": + log.debug("SkipService: ignore {0} is selected".format(type)) return None - if skip_intro_dialog is None: - skip_intro_dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p") + if dialog is None: + log.debug("SkipService: init dialog") + dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p") - set_correct_skip_info(item_id, skip_intro_dialog) + set_correct_skip_info(item_id, dialog, segments, type) - is_intro = False - if skip_intro_dialog.intro_start is not None and skip_intro_dialog.intro_end is not None: + 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 < skip_intro_dialog.intro_start or current_ticks > skip_intro_dialog.intro_end: - skip_intro_dialog.has_been_dissmissed = False + 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 reseted beacause timecode is outised of segment") + 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 + is_segment = current_ticks >= dialog.start and current_ticks <= dialog.end - if intro_skip_action == "1" and is_intro: + 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(skip_intro_dialog.intro_end)) - xbmcgui.Dialog().notification("JellyCon", "Intro Skipped") - elif intro_skip_action == "0": + 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_intro and not skip_intro_dialog.has_been_dissmissed: - skip_intro_dialog.show() + 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 - skip_intro_dialog.close() + log.debug("SkipService: {0} is not playing, closing dialog".format(type)) + 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 None - - 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 + return dialog + def stop_service(self): log.debug("IntroSkipperService Stop Called") self.stop_thread = True diff --git a/resources/lib/intro_skipper_utils.py b/resources/lib/intro_skipper_utils.py new file mode 100644 index 0000000..ef3ef8b --- /dev/null +++ b/resources/lib/intro_skipper_utils.py @@ -0,0 +1,69 @@ +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") + +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") + +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") + +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 fetch media segments (happens twice per media - intro and outro - but it is a light call) + 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)) + \ No newline at end of file diff --git a/resources/lib/play_utils.py b/resources/lib/play_utils.py index 69704ca..33006e6 100644 --- a/resources/lib/play_utils.py +++ b/resources/lib/play_utils.py @@ -16,6 +16,8 @@ import xbmcvfs import xbmcplugin from six.moves.urllib.parse import urlencode +from typing import Literal + from .jellyfin import api from .lazylogger import LazyLogger from .dialogs import ResumeDialog, SkipDialog @@ -1709,28 +1711,6 @@ 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"] - -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")) diff --git a/resources/settings.xml b/resources/settings.xml index 626ff41..31c5bb8 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -124,6 +124,18 @@ + + + + + + + + + + + + From aa799e32b636268151fef4344d3974892226bf1d Mon Sep 17 00:00:00 2001 From: Gorgorot38 Date: Mon, 17 Mar 2025 17:32:09 +0100 Subject: [PATCH 4/6] Fix language --- .../language/resource.language.fr/strings.po | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/resources/language/resource.language.fr/strings.po b/resources/language/resource.language.fr/strings.po index a7d4925..846bd98 100644 --- a/resources/language/resource.language.fr/strings.po +++ b/resources/language/resource.language.fr/strings.po @@ -1156,39 +1156,3 @@ msgstr "Revoir ensuite" msgctxt "#30453" msgid "Hide number of items to show on entry title" 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" From 78ab2344be87d3ca24ed2414f07ff2429627b8d6 Mon Sep 17 00:00:00 2001 From: Gorgorot38 Date: Mon, 17 Mar 2025 17:35:42 +0100 Subject: [PATCH 5/6] fix git feedback --- resources/lib/intro_skipper_utils.py | 3 +++ resources/lib/play_utils.py | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/lib/intro_skipper_utils.py b/resources/lib/intro_skipper_utils.py index ef3ef8b..c3a0f5a 100644 --- a/resources/lib/intro_skipper_utils.py +++ b/resources/lib/intro_skipper_utils.py @@ -21,6 +21,7 @@ def get_setting_skip_action(type: Literal["Commercial", "Preview", "Recap", "Out 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() @@ -34,6 +35,7 @@ def get_setting_skip_start_offset(type: Literal["Commercial", "Preview", "Recap" return settings.getSettingInt("credit_skipper_start_offset") elif (type == "Intro"): return settings.getSettingInt("intro_skipper_start_offset") + return 1 def get_setting_skip_end_offset(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]): settings = xbmcaddon.Addon() @@ -47,6 +49,7 @@ def get_setting_skip_end_offset(type: Literal["Commercial", "Preview", "Recap", return settings.getSettingInt("credit_skipper_end_offset") elif (type == "Intro"): return settings.getSettingInt("intro_skipper_end_offset") + return 1 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: diff --git a/resources/lib/play_utils.py b/resources/lib/play_utils.py index 33006e6..54ca143 100644 --- a/resources/lib/play_utils.py +++ b/resources/lib/play_utils.py @@ -16,12 +16,10 @@ import xbmcvfs import xbmcplugin from six.moves.urllib.parse import urlencode -from typing import Literal - from .jellyfin import api from .lazylogger import LazyLogger -from .dialogs import ResumeDialog, SkipDialog -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 .dialogs import ResumeDialog +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 .kodi_utils import HomeWindow from .datamanager import clear_old_cache_data from .item_functions import extract_item_info, add_gui_item, get_art From fb8adbe27cd090cf8676a9574e7cc1f5ed724987 Mon Sep 17 00:00:00 2001 From: Gorgorot38 Date: Wed, 19 Mar 2025 14:16:30 +0100 Subject: [PATCH 6/6] typos --- resources/lib/intro_skipper.py | 2 +- resources/lib/intro_skipper_utils.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/intro_skipper.py b/resources/lib/intro_skipper.py index 7161c1e..86be68b 100644 --- a/resources/lib/intro_skipper.py +++ b/resources/lib/intro_skipper.py @@ -129,7 +129,7 @@ class IntroSkipperService(threading.Thread): 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 reseted beacause timecode is outised of segment") + 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 diff --git a/resources/lib/intro_skipper_utils.py b/resources/lib/intro_skipper_utils.py index c3a0f5a..1a9e46f 100644 --- a/resources/lib/intro_skipper_utils.py +++ b/resources/lib/intro_skipper_utils.py @@ -35,7 +35,7 @@ def get_setting_skip_start_offset(type: Literal["Commercial", "Preview", "Recap" return settings.getSettingInt("credit_skipper_start_offset") elif (type == "Intro"): return settings.getSettingInt("intro_skipper_start_offset") - return 1 + return 0 def get_setting_skip_end_offset(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]): settings = xbmcaddon.Addon() @@ -49,11 +49,11 @@ def get_setting_skip_end_offset(type: Literal["Commercial", "Preview", "Recap", return settings.getSettingInt("credit_skipper_end_offset") elif (type == "Intro"): return settings.getSettingInt("intro_skipper_end_offset") - return 1 + 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 fetch media segments (happens twice per media - intro and outro - but it is a light call) + # 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