From 10b3382e54bf8d52f7d5802b2a15a59c5cb0ba52 Mon Sep 17 00:00:00 2001 From: lumiru Date: Wed, 15 May 2024 20:44:16 +0200 Subject: [PATCH] feat: UpNext integration with skip credits button _compute_when_episode_ends was moved to videostream.py since it not need the player context to work and only need to expose data about video stream. 'best' upnext_mode was renamed to 'credits' (because 'best' does not mean anything). translations was updated with easier to understand wording. --- .../resource.language.de_de/strings.po | 12 +-- .../resource.language.en_gb/strings.po | 12 +-- .../resource.language.es_es/strings.po | 10 +-- .../resource.language.fr_fr/strings.po | 20 ++--- .../resource.language.pt_br/strings.po | 12 +-- resources/lib/videoplayer.py | 47 +++--------- resources/lib/videostream.py | 75 ++++++++++++++++++- resources/settings.xml | 2 +- 8 files changed, 116 insertions(+), 74 deletions(-) diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po index 2e05c82..bd01e67 100644 --- a/resources/language/resource.language.de_de/strings.po +++ b/resources/language/resource.language.de_de/strings.po @@ -277,21 +277,21 @@ msgid "UpNext fixed or unavailable end detection duration" msgstr "Vorlaufzeit für UpNext wenn \"Statisch\" oder keine Credits/Vorschau" msgctxt "#30090" -msgid "UpNext integration" +msgid "Show UpNext dialog at" msgstr "UpNext Integration" msgctxt "#30091" -msgid "At credits start, if nothing after" +msgid "credits start" msgstr "Zu Beginn der Credits, wenn danach keine Vorschau kommt" msgctxt "#30092" -msgid "At preview start" +msgid "preview start" msgstr "Zu Beginn der Vorschau" msgctxt "#30093" -msgid "Fixed" +msgid "fixed time before the end" msgstr "Statisch" msgctxt "#30094" -msgid "Disabled" -msgstr "Deaktiviert" \ No newline at end of file +msgid "never (disabled)" +msgstr "Deaktiviert" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 40cde65..9d7d005 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -279,21 +279,21 @@ msgid "UpNext fixed or unavailable end detection duration" msgstr "" msgctxt "#30090" -msgid "UpNext integration" +msgid "Show UpNext dialog at" msgstr "" msgctxt "#30091" -msgid "At credits start, if nothing after" +msgid "credits start" msgstr "" msgctxt "#30092" -msgid "At preview start" +msgid "preview start" msgstr "" msgctxt "#30093" -msgid "Fixed" +msgid "fixed time before the end" msgstr "" msgctxt "#30094" -msgid "Disabled" -msgstr "" \ No newline at end of file +msgid "never (disabled)" +msgstr "" diff --git a/resources/language/resource.language.es_es/strings.po b/resources/language/resource.language.es_es/strings.po index bbeae48..086ca6f 100644 --- a/resources/language/resource.language.es_es/strings.po +++ b/resources/language/resource.language.es_es/strings.po @@ -279,21 +279,21 @@ msgid "UpNext fixed or unavailable end detection duration" msgstr "" msgctxt "#30090" -msgid "UpNext integration" +msgid "Show UpNext dialog at" msgstr "" msgctxt "#30091" -msgid "At credits start, if nothing after" +msgid "credits start" msgstr "" msgctxt "#30092" -msgid "At preview start" +msgid "preview start" msgstr "" msgctxt "#30093" -msgid "Fixed" +msgid "fixed time before the end" msgstr "" msgctxt "#30094" -msgid "Disabled" +msgid "never (disabled)" msgstr "" diff --git a/resources/language/resource.language.fr_fr/strings.po b/resources/language/resource.language.fr_fr/strings.po index be8b094..dd82e30 100644 --- a/resources/language/resource.language.fr_fr/strings.po +++ b/resources/language/resource.language.fr_fr/strings.po @@ -275,21 +275,21 @@ msgid "UpNext fixed or unavailable end detection duration" msgstr "Durée fixe ou de détection indisponible pour UpNext" msgctxt "#30090" -msgid "UpNext integration" -msgstr "Intégration à UpNext" +msgid "Show UpNext dialog at" +msgstr "Afficher la fenêtre UpNext quand" msgctxt "#30091" -msgid "At credits start, if nothing after" -msgstr "Dès le générique, s'il n'y a rien après" +msgid "credits start" +msgstr "le générique commence" msgctxt "#30092" -msgid "At preview start" -msgstr "Au début de la bande-annonce du suivant" +msgid "preview start" +msgstr "la preview commence" msgctxt "#30093" -msgid "Fixed" -msgstr "Fixé" +msgid "fixed time before the end" +msgstr "une durée fixe avant la fin" msgctxt "#30094" -msgid "Disabled" -msgstr "Désactivé" +msgid "never (disabled)" +msgstr "jamais (désactivé)" diff --git a/resources/language/resource.language.pt_br/strings.po b/resources/language/resource.language.pt_br/strings.po index 47101db..7dde13b 100644 --- a/resources/language/resource.language.pt_br/strings.po +++ b/resources/language/resource.language.pt_br/strings.po @@ -276,21 +276,21 @@ msgid "UpNext fixed or unavailable end detection duration" msgstr "" msgctxt "#30090" -msgid "UpNext integration" +msgid "Show UpNext dialog at" msgstr "" msgctxt "#30091" -msgid "At credits start, if nothing after" +msgid "credits start" msgstr "" msgctxt "#30092" -msgid "At preview start" +msgid "preview start" msgstr "" msgctxt "#30093" -msgid "Fixed" +msgid "fixed time before the end" msgstr "" msgctxt "#30094" -msgid "Disabled" -msgstr "" \ No newline at end of file +msgid "never (disabled)" +msgstr "" diff --git a/resources/lib/videoplayer.py b/resources/lib/videoplayer.py index 8d2905e..ccab104 100644 --- a/resources/lib/videoplayer.py +++ b/resources/lib/videoplayer.py @@ -226,10 +226,10 @@ def _handle_upnext(self): }, "video_episode_play" ) - show_next_at_seconds = self._compute_when_episode_ends() + show_next_at_seconds = self._stream_data.end_timecode if show_next_at_seconds is not None: - # Needs to wait 1s, otherwise, upnext will show next dialog at episode start... - xbmc.sleep(1000) + # Needs to wait 10s, otherwise, upnext will show next dialog at episode start... + xbmc.sleep(10000) utils.crunchy_log("_handle_upnext: Next URL (shown at %ds / %ds): %s" % ( show_next_at_seconds, self._stream_data.playable_item.duration, @@ -247,37 +247,6 @@ def _handle_upnext(self): except Exception: utils.crunchy_log("_handle_upnext: Cannot send upnext notification", xbmc.LOGERROR) - def _compute_when_episode_ends(self) -> Optional[int]: - upnext_mode = G.args.addon.getSetting("upnext_mode") - if upnext_mode == "disabled": - return None - - video_end = self._stream_data.playable_item.duration - fixed_duration = int(G.args.addon.getSetting("upnext_fixed_duration"), 10) - result = video_end - fixed_duration - - skip_events_data = self._stream_data.unmodified_skip_events_data - if upnext_mode == "fixed" or not skip_events_data or (not skip_events_data.get("credits") and not skip_events_data.get("preview")): - return result - - credits_start = skip_events_data.get("credits", {}).get("start") - credits_end = skip_events_data.get("credits", {}).get("end") - preview_start = skip_events_data.get("preview", {}).get("start") - preview_end = skip_events_data.get("preview", {}).get("end") - # If there are outro and preview - # and if the outro ends when the preview start - if upnext_mode == "best" and credits_start and credits_end and preview_start and credits_end + 3 > preview_start: - result = credits_start - # If there is a preview - elif preview_start: - result = preview_start - # If there is outro without preview - # and if the outro ends in the last 20 seconds video - elif upnext_mode == "best" and credits_start and credits_end and video_end <= credits_end + 20: - result = credits_start - - return result - def thread_update_playhead(self): """ background thread to update playback with crunchyroll in intervals """ @@ -339,13 +308,17 @@ def _check_and_filter_skip_data(self) -> bool: if not self._stream_data.skip_events_data: return False + # never skip preview (fetched for upnext) + if self._stream_data.skip_events_data.get('preview'): + self._stream_data.skip_events_data.pop('preview', None) + # if not enabled in config, remove from our list - if G.args.addon.getSetting("enable_skip_intro") != "true" and self._stream_data.skip_events_data.get( + if G.args.addon.getSetting('enable_skip_intro') != 'true' and self._stream_data.skip_events_data.get( 'intro'): self._stream_data.skip_events_data.pop('intro', None) - if G.args.addon.getSetting("enable_skip_credits") != "true" and self._stream_data.skip_events_data.get( - 'credits'): + if (G.args.addon.getSetting('enable_skip_credits') != 'true' or self._stream_data.end_marker == 'credits') and ( + self._stream_data.skip_events_data.get('credits') ): self._stream_data.skip_events_data.pop('credits', None) return len(self._stream_data.skip_events_data) > 0 diff --git a/resources/lib/videostream.py b/resources/lib/videostream.py index 38ec4b2..b45dec2 100644 --- a/resources/lib/videostream.py +++ b/resources/lib/videostream.py @@ -39,7 +39,6 @@ def __init__(self): self.stream_url: str | None = None self.subtitle_urls: list[str] | None = None self.skip_events_data: Dict = {} - self.unmodified_skip_events_data: Dict = {} self.playheads_data: Dict = {} # PlayableItem which is about to be played, that contains cms object data self.playable_item: PlayableItem | None = None @@ -47,6 +46,8 @@ def __init__(self): self.playable_item_parent: PlayableItem | None = None self.token: str | None = None self.next_playable_item: PlayableItem | None = None + self.end_marker: str = "off" + self.end_timecode: int | None = None class VideoStream(Object): @@ -88,12 +89,16 @@ def get_player_stream_data(self) -> Optional[VideoPlayerStreamData]: video_player_stream_data.token = async_data.get('stream_data').get('token') video_player_stream_data.skip_events_data = async_data.get('skip_events_data') - video_player_stream_data.unmodified_skip_events_data = dict(async_data.get('skip_events_data')) video_player_stream_data.playheads_data = async_data.get('playheads_data') video_player_stream_data.playable_item = async_data.get('playable_item') video_player_stream_data.playable_item_parent = async_data.get('playable_item_parent') video_player_stream_data.next_playable_item = async_data.get('next_playable_item') + video_end = self._compute_when_episode_ends(video_player_stream_data) + + video_player_stream_data.end_marker = video_end.get('marker') + video_player_stream_data.end_timecode = video_end.get('timecode') + return video_player_stream_data async def _gather_async_data(self) -> Dict[str, Any]: @@ -355,7 +360,7 @@ async def _get_skip_events(episode_id) -> Optional[Dict]: return None # prepare the data a bit - supported_skips = ['intro', 'credits'] + supported_skips = ['intro', 'credits', 'preview'] prepared = dict() for skip_type in supported_skips: if req.get(skip_type) and req.get(skip_type).get('start') is not None and req.get(skip_type).get( @@ -392,3 +397,67 @@ async def _get_upnext_episode(id: str) -> Optional[Dict]: return None return req.get("data")[0] + + @staticmethod + def _compute_when_episode_ends(partial_stream_data: VideoPlayerStreamData) -> Dict[str, Any]: + """ Extract timecode for video end from skip_events_data. + + Extracted timecode depends on *upnext_mode* user setting and available skip events data. + upnext_mode can hold 4 different behaviour. + - "disabled", so no need to compute anything. + - "fixed", so we should send the timecode for the last 15s (user can change this duration by *upnext_fixed_duration* settings). + - "preview", which means we have to retrieve preview timecode from skip event API. + If preview timecode is not available, go back to the same behaviour as "fixed" mode. + - "credits", which means we have to retrieve credits and preview timecode from skip event API. + If credits timecode is not available, go back to the same behaviour as "preview" mode. + Additionaly, we have to check there is no additional scenes after credits, + so we check if preview starts at credits end. Otherwise, video end timecode will be the preview start timecode. + """ + + result = { + 'marker': 'off', + 'timecode': None + } + upnext_mode = G.args.addon.getSetting('upnext_mode') + if upnext_mode == 'disabled' or not partial_stream_data.next_playable_item: + return result + + video_end = partial_stream_data.playable_item.duration + fixed_duration = int(G.args.addon.getSetting('upnext_fixed_duration'), 10) + # Standard behaviour is to show upnext 15s before the end of the video + result = { + 'marker': 'fixed', + 'timecode': video_end - fixed_duration + } + + skip_events_data = partial_stream_data.skip_events_data + # If upnext selected mode is fixed or there is no available skip data + if upnext_mode == 'fixed' or not skip_events_data or (not skip_events_data.get('credits') and not skip_events_data.get('preview')): + return result + + # Extract skip data + credits_start = skip_events_data.get('credits', {}).get('start') + credits_end = skip_events_data.get('credits', {}).get('end') + preview_start = skip_events_data.get('preview', {}).get('start') + preview_end = skip_events_data.get('preview', {}).get('end') + + # If there is no data about preview but credits ends less than 20s before the end, consider time after credits_end is the preview + if not preview_start and credits_end and credits_end >= video_end - 20: + preview_start = credits_end + preview_end = video_end + + # If there are outro and preview + # and if the outro ends when the preview start + if upnext_mode == 'credits' and credits_start and credits_end and preview_start and credits_end + 3 > preview_start: + result = { + 'marker': 'credits', + 'timecode': credits_start + } + # If there is a preview + elif preview_start: + result = { + 'marker': 'preview', + 'timecode': preview_start + } + + return result diff --git a/resources/settings.xml b/resources/settings.xml index 49b70ec..430c29c 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -132,7 +132,7 @@ disabled - +