Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add segment skip ability #368

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -1155,4 +1155,40 @@ 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"
36 changes: 36 additions & 0 deletions resources/language/resource.language.fr/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
64 changes: 63 additions & 1 deletion resources/lib/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
)

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

Expand Down Expand Up @@ -206,3 +208,63 @@ def set_episode_info(self, info):

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
145 changes: 145 additions & 0 deletions resources/lib/intro_skipper.py
Original file line number Diff line number Diff line change
@@ -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 None

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

def stop_service(self):
log.debug("IntroSkipperService Stop Called")
self.stop_thread = True
44 changes: 42 additions & 2 deletions resources/lib/play_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"))
8 changes: 7 additions & 1 deletion resources/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading