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 @@ + + + + + + + + + + + +