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")