diff --git a/README.md b/README.md index 1188862..284ec74 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Git repo: https://github.com/MrKrabat/plugin.video.crunchyroll - [x] Optionally soft-subs only - [x] Configure up to two languages for subtitles / dubs - [x] Crunchylists support +- [x] UpNext addon integration *** diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po index f6ee1f7..d000844 100644 --- a/resources/language/resource.language.de_de/strings.po +++ b/resources/language/resource.language.de_de/strings.po @@ -32,6 +32,10 @@ msgctxt "#30230" msgid "Other" msgstr "Sonstiges" +msgctxt "#30240" +msgid "Addons" +msgstr "Erweiterungen" + msgctxt "#30101" msgid "Email" msgstr "E-Mail" @@ -296,3 +300,27 @@ msgstr "Es sind zu viele Streams aktiv. Bitte probiere es später noch einmal." msgctxt "#30090" msgid "No search results" msgstr "Keine Suchergebnisse" + +msgctxt "#30319" +msgid "UpNext fixed or unavailable end detection duration" +msgstr "Vorlaufzeit für UpNext wenn \"Statisch\" oder keine Credits/Vorschau" + +msgctxt "#30300" +msgid "Show UpNext dialog at" +msgstr "UpNext Integration" + +msgctxt "#30301" +msgid "credits start" +msgstr "Zu Beginn der Credits, wenn danach keine Vorschau kommt" + +msgctxt "#30302" +msgid "preview start" +msgstr "Zu Beginn der Vorschau" + +msgctxt "#30303" +msgid "fixed time before the end" +msgstr "Statisch" + +msgctxt "#30304" +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 933652e..5334d9c 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -31,6 +31,10 @@ msgctxt "#30230" msgid "Other" msgstr "" +msgctxt "#30240" +msgid "Addons" +msgstr "" + msgctxt "#30001" msgid "Email" msgstr "" @@ -297,4 +301,28 @@ msgstr "" msgctxt "#30090" msgid "No search results" -msgstr "No search results" \ No newline at end of file +msgstr "No search results" + +msgctxt "#30319" +msgid "UpNext fixed or unavailable end detection duration" +msgstr "" + +msgctxt "#30300" +msgid "Show UpNext dialog at" +msgstr "" + +msgctxt "#30301" +msgid "credits start" +msgstr "" + +msgctxt "#30302" +msgid "preview start" +msgstr "" + +msgctxt "#30303" +msgid "fixed time before the end" +msgstr "" + +msgctxt "#30304" +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 6367f29..bcee097 100644 --- a/resources/language/resource.language.es_es/strings.po +++ b/resources/language/resource.language.es_es/strings.po @@ -31,6 +31,10 @@ msgctxt "#30230" msgid "Other" msgstr "Otro" +msgctxt "#30240" +msgid "Addons" +msgstr "" + msgctxt "#30001" msgid "Email" msgstr "Email" @@ -296,4 +300,28 @@ msgstr "" msgctxt "#30090" msgid "No search results" -msgstr "" \ No newline at end of file +msgstr "" + +msgctxt "#30319" +msgid "UpNext fixed or unavailable end detection duration" +msgstr "" + +msgctxt "#30300" +msgid "Show UpNext dialog at" +msgstr "" + +msgctxt "#30301" +msgid "credits start" +msgstr "" + +msgctxt "#30302" +msgid "preview start" +msgstr "" + +msgctxt "#30303" +msgid "fixed time before the end" +msgstr "" + +msgctxt "#30304" +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 fd379e5..e07c4ba 100644 --- a/resources/language/resource.language.fr_fr/strings.po +++ b/resources/language/resource.language.fr_fr/strings.po @@ -21,7 +21,7 @@ msgstr "Se connecter" msgctxt "#30210" msgid "Languages" -msgstr "Lanagages" +msgstr "Langages" msgctxt "#30220" msgid "Playback" @@ -31,6 +31,10 @@ msgctxt "#30230" msgid "Other" msgstr "Autre" +msgctxt "#30240" +msgid "Addons" +msgstr "Extensions" + msgctxt "#30001" msgid "Email" msgstr "Email" @@ -241,11 +245,11 @@ msgstr "Une erreur est survenue" msgctxt "#30062" msgid "You need to be logged in" -msgstr "Vus devez être connecté" +msgstr "Vous devez être connecté" msgctxt "#30063" msgid "You need to be a premium member" -msgstr "Voius devez être un membre premium" +msgstr "Vous devez être un membre premium" msgctxt "#30064" msgid "Failed to play video" @@ -253,7 +257,7 @@ msgstr "Erreur de lecture vidéo" msgctxt "#30065" msgid "Do you want to continue watching at %s%%?" -msgstr "Voulez vous continuer à regarder depuis %s%% ?" +msgstr "Voulez-vous continuer à regarder depuis %s%% ?" msgctxt "#30066" msgid "If the video does not start wait 30 seconds for the fallback to start." @@ -269,7 +273,7 @@ msgstr "Supprimer de la file d'attente" msgctxt "#30069" msgid "Alternative Subtitle Language" -msgstr "Langage de sous-titres alternatif" +msgstr "Langue de sous-titres alternatif" msgctxt "#30070" msgid "None" @@ -289,8 +293,32 @@ msgstr "Choisissez votre profil" msgctxt "#30080" msgid "There are too many active streams. Please try again later." -msgstr "" +msgstr "Il y a trop de streams actifs. Ré-essayez plus tard." msgctxt "#30090" msgid "No search results" -msgstr "" \ No newline at end of file +msgstr "Aucun résultat" + +msgctxt "#30319" +msgid "UpNext fixed or unavailable end detection duration" +msgstr "Durée fixe ou de détection indisponible pour UpNext" + +msgctxt "#30300" +msgid "Show UpNext dialog at" +msgstr "Afficher la fenêtre UpNext quand" + +msgctxt "#30301" +msgid "credits start" +msgstr "le générique commence" + +msgctxt "#30302" +msgid "preview start" +msgstr "la preview commence" + +msgctxt "#30303" +msgid "fixed time before the end" +msgstr "une durée fixe avant la fin" + +msgctxt "#30304" +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 b579680..0852cf5 100644 --- a/resources/language/resource.language.pt_br/strings.po +++ b/resources/language/resource.language.pt_br/strings.po @@ -31,6 +31,10 @@ msgctxt "#30230" msgid "Other" msgstr "Outros" +msgctxt "#30240" +msgid "Addons" +msgstr "" + msgctxt "#30001" msgid "Username" msgstr "E-mail" @@ -293,4 +297,28 @@ msgstr "Muitas streams ativas. Por favor, tente mais tarde" msgctxt "#30090" msgid "No search results" -msgstr "" \ No newline at end of file +msgstr "" + +msgctxt "#30319" +msgid "UpNext fixed or unavailable end detection duration" +msgstr "" + +msgctxt "#30300" +msgid "Show UpNext dialog at" +msgstr "" + +msgctxt "#30301" +msgid "credits start" +msgstr "" + +msgctxt "#30302" +msgid "preview start" +msgstr "" + +msgctxt "#30303" +msgid "fixed time before the end" +msgstr "" + +msgctxt "#30304" +msgid "never (disabled)" +msgstr "" diff --git a/resources/lib/addons/__init__.py b/resources/lib/addons/__init__.py new file mode 100644 index 0000000..b93054b --- /dev/null +++ b/resources/lib/addons/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/resources/lib/addons/upnext.py b/resources/lib/addons/upnext.py new file mode 100644 index 0000000..c7e2cb9 --- /dev/null +++ b/resources/lib/addons/upnext.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Crunchyroll +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from base64 import b64encode +from json import dumps +from typing import Optional + +from resources.lib.model import Args, PlayableItem, SeriesData + +from . import utils + + +def send_next_info( + args: Args, + current_episode: PlayableItem, + next_episode: PlayableItem, + play_url: str, + notification_offset: Optional[int] = None, + series: Optional[SeriesData] = None +): + """ + Notify next episode info to upnext. + See https://github.com/im85288/service.upnext/wiki/Integration#sending-data-to-up-next for implementation details. + """ + current_upnext_episode = UpnextEpisode(current_episode, series) + next_upnext_episode = UpnextEpisode(next_episode, series) + next_info = { + "current_episode": current_upnext_episode.__dict__, + "next_episode": next_upnext_episode.__dict__, + "play_url": play_url, + } + if notification_offset is not None: + next_info["notification_offset"] = notification_offset + upnext_signal(args.addon_id, next_info) + + +class UpnextEpisode: + def __init__(self, dto: PlayableItem, series_dto: Optional[SeriesData]): + self.episodeid: str | None = dto.episode_id + self.tvshowid: str | None = dto.series_id + self.title: str = dto.title_unformatted + self.art: dict = { + "thumb": dto.thumb, + } + if series_dto: + self.art.update({ + # "tvshow.clearart": series_dto.clearart, + # "tvshow.clearlogo": series_dto.clearlogo, + "tvshow.fanart": series_dto.fanart, + "tvshow.landscape": series_dto.fanart, + "tvshow.poster": series_dto.poster, + }) + self.season: int = dto.season + self.episode: str = dto.episode + self.showtitle: str = dto.tvshowtitle + self.plot: str = dto.plot + self.playcount: int = dto.playcount + # self.rating: str = dto.rating + self.firstaired: str = dto.year + self.runtime: int = dto.duration + + +def upnext_signal(sender, next_info): + """Send upnext_data to Kodi using JSON RPC""" + data = [utils.to_unicode(b64encode(dumps(next_info).encode()))] + utils.notify(sender=sender + '.SIGNAL', message='upnext_data', data=data) diff --git a/resources/lib/addons/utils.py b/resources/lib/addons/utils.py new file mode 100644 index 0000000..cc3adb4 --- /dev/null +++ b/resources/lib/addons/utils.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Crunchyroll +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +This file exposes functions to send notifications to other XBMC addons. +Its original version was taken from service.upnext wiki at +https://github.com/im85288/service.upnext/wiki/Example-source-code +""" +import xbmc + +def notify(sender, message, data): + """Send a notification to Kodi using JSON RPC""" + result = jsonrpc(method='JSONRPC.NotifyAll', params=dict( + sender=sender, + message=message, + data=data, + )) + if result.get('result') != 'OK': + xbmc.log('Failed to send notification: ' + result.get('error').get('message'), xbmc.LOGERROR) + return False + return True + +def jsonrpc(**kwargs): + """Perform JSONRPC calls""" + from json import dumps, loads + if kwargs.get('id') is None: + kwargs.update(id=0) + if kwargs.get('jsonrpc') is None: + kwargs.update(jsonrpc='2.0') + return loads(xbmc.executeJSONRPC(dumps(kwargs))) + +def to_unicode(text, encoding='utf-8', errors='strict'): + """Force text to unicode""" + if isinstance(text, bytes): + return text.decode(encoding, errors=errors) + return text diff --git a/resources/lib/api.py b/resources/lib/api.py index b4311c0..1b1693d 100644 --- a/resources/lib/api.py +++ b/resources/lib/api.py @@ -68,6 +68,7 @@ class API: RESUME_ENDPOINT = "https://beta-api.crunchyroll.com/content/v2/discover/{}/history" SEASONAL_TAGS_ENDPOINT = "https://beta-api.crunchyroll.com/content/v2/discover/seasonal_tags" CATEGORIES_ENDPOINT = "https://beta-api.crunchyroll.com/content/v1/tenant_categories" + UPNEXT_ENDPOINT = "https://beta-api.crunchyroll.com/content/v2/discover/up_next/{}" SKIP_EVENTS_ENDPOINT = "https://static.crunchyroll.com/skip-events/production/{}.json" # request w/o auth req. INTRO_V2_ENDPOINT = "https://static.crunchyroll.com/datalab-intro-v2/{}.json" diff --git a/resources/lib/videoplayer.py b/resources/lib/videoplayer.py index 5d66b95..70d38a7 100644 --- a/resources/lib/videoplayer.py +++ b/resources/lib/videoplayer.py @@ -24,11 +24,13 @@ import xbmcgui import xbmcplugin -from resources.lib import utils -from resources.lib.globals import G -from resources.lib.gui import SkipModalDialog, _show_modal_dialog -from resources.lib.model import Object, CrunchyrollError, LoginError -from resources.lib.videostream import VideoPlayerStreamData, VideoStream +from . import utils, view +from .addons import upnext +from .api import API +from .globals import G +from .gui import SkipModalDialog, _show_modal_dialog +from .model import Object, CrunchyrollError, LoginError +from .videostream import VideoPlayerStreamData, VideoStream class VideoPlayer(Object): @@ -59,6 +61,8 @@ def start_playback(self): self._handle_skipping() self._handle_update_playhead() + self._handle_upnext() + def is_playing(self) -> bool: """ Returns true if playback is running. Note that it also returns true when paused. """ @@ -198,6 +202,41 @@ def _handle_skipping(self): utils.crunchy_log("_handle_skipping: starting thread", xbmc.LOGINFO) threading.Thread(target=self.thread_check_skipping).start() + def _handle_upnext(self): + try: + next_episode = self._stream_data.next_playable_item + if not next_episode: + utils.crunchy_log("_handle_upnext: No episode or disabled upnext integration") + return + next_url = view.build_url( + { + "series_id": G.args.get_arg("series_id"), + "episode_id": next_episode.episode_id, + "stream_id": next_episode.stream_id + }, + "video_episode_play" + ) + show_next_at_seconds = self._stream_data.end_timecode + if show_next_at_seconds is not None: + # 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, + next_url + )) + + upnext.send_next_info( + G.args, + self._stream_data.playable_item, + next_episode, + next_url, + show_next_at_seconds, + self._stream_data.playable_item_parent + ) + except Exception: + utils.crunchy_log("_handle_upnext: Cannot send upnext notification", xbmc.LOGERROR) + def thread_update_playhead(self): """ background thread to update playback with crunchyroll in intervals """ @@ -259,13 +298,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 @@ -301,7 +344,7 @@ def clear_active_stream(self): Crunchyroll keeps track of started streams. If they are not released, CR will block starting a new one. """ - if not G.args.get_arg('episode_id') or not self._stream_data.token: + if not G.args.get_arg('episode_id') or not self._stream_data or not self._stream_data.token: return try: diff --git a/resources/lib/videostream.py b/resources/lib/videostream.py index 03ddb1f..684bc9a 100644 --- a/resources/lib/videostream.py +++ b/resources/lib/videostream.py @@ -45,6 +45,9 @@ def __init__(self): # PlayableItem which contains cms obj data of playable_item's parent, if exists (Episodes, not Movies). currently not used. 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): @@ -89,35 +92,45 @@ def get_player_stream_data(self) -> Optional[VideoPlayerStreamData]: 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]: """ gather data asynchronously and return them as a dictionary """ + episode_id = G.args.get_arg('episode_id') + series_id = G.args.get_arg('series_id') + # create threads # actually not sure if this works, as the requests lib is not async # also not sure if this is thread safe in any way, what if session is timed-out when starting this? t_stream_data = asyncio.create_task(self._get_stream_data_from_api()) - t_skip_events_data = asyncio.create_task(self._get_skip_events(G.args.get_arg('episode_id'))) - t_playheads = asyncio.create_task(get_playheads_from_api(G.args.get_arg('episode_id'))) - t_item_data = asyncio.create_task( - get_cms_object_data_by_ids([G.args.get_arg('episode_id')])) - # t_item_parent_data = asyncio.create_task(get_cms_object_data_by_ids(G.args, G.api, G.args.get_arg('series_id'))) + t_skip_events_data = asyncio.create_task(self._get_skip_events(episode_id)) + t_playheads = asyncio.create_task(get_playheads_from_api(episode_id)) + t_item_data = asyncio.create_task(get_cms_object_data_by_ids([episode_id, series_id])) + t_upnext_data = asyncio.create_task(self._get_upnext_episode(episode_id)) # start async requests and fetch results - results = await asyncio.gather(t_stream_data, t_skip_events_data, t_playheads, t_item_data) + results = await asyncio.gather(t_stream_data, t_skip_events_data, t_playheads, t_item_data, t_upnext_data) - playable_item = get_listables_from_response([results[3].get(G.args.get_arg('episode_id'))]) if \ - results[3] else None + listable_items = get_listables_from_response([value for key, value in results[3].items()]) if results[3] else [] + playable_items = [item for item in listable_items if item.id == episode_id] + parent_listables = [item for item in listable_items if item.id == series_id] + upnext_items = get_listables_from_response([results[4]]) if results[4] else None return { 'stream_data': results[0] or {}, 'skip_events_data': results[1] or {}, 'playheads_data': results[2] or {}, - 'playable_item': playable_item[0] if playable_item else None, - 'playable_item_parent': None - # get_listables_from_response([results[4]])[0] if results[4] else None + 'playable_item': playable_items[0] if playable_items else None, + 'playable_item_parent': parent_listables[0] if parent_listables else None, + 'next_playable_item': upnext_items[0] if upnext_items else None, } @staticmethod @@ -308,7 +321,8 @@ async def _get_skip_events(episode_id) -> Optional[Dict]: # if none of the skip options are enabled in setting, don't fetch that data if (G.args.addon.getSetting("enable_skip_intro") != "true" and - G.args.addon.getSetting("enable_skip_credits") != "true"): + G.args.addon.getSetting("enable_skip_credits") != "true" and + G.args.addon.getSetting("upnext_mode") == "disabled"): return None try: @@ -343,7 +357,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( @@ -356,3 +370,91 @@ async def _get_skip_events(episode_id) -> Optional[Dict]: crunchy_log("_get_skip_events: check for %s FAILED" % skip_type, xbmc.LOGINFO) return prepared if len(prepared) > 0 else None + + @staticmethod + async def _get_upnext_episode(id: str) -> Optional[Dict]: + """ fetch upnext episode data from api """ + + # if upnext integration is disabled, don't fetch data + if G.args.addon.getSetting("upnext_mode") == "disabled": + return None + + try: + req = G.api.make_request( + method="GET", + url=G.api.UPNEXT_ENDPOINT.format(id), + params={ + "locale": G.args.subtitle + } + ) + except (CrunchyrollError, requests.exceptions.RequestException) as e: + crunchy_log("_get_upnext_episode: failed to load for: %s" % id) + return None + if not req or "error" in req or len(req.get("data", [])) == 0: + 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 c40b5fd..360ff03 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -149,5 +149,31 @@ + + + + 0 + disabled + + + + + + + + + + 30300 + + + + 0 + 15 + + 30319 + + + +