def download_subtitle(url, file_name="", format='sami', proxy=None, replace=None): """Downloads a SAMI and stores the SRT in the cache folder Arguments: @param url: string - URL location of the SAMI file @param file_name: string - [opt] Filename to use to store the subtitle in SRT format. if not specified, an MD5 hash of the URL with .xml extension will be used @param format: string - Defines the source format. Defaults to Sami. @param proxy: Proxy - If specified, a proxy will be used @param replace: dict - Dictionary with key to will be replaced with their values @return: The full patch of the cached SRT file. """ if file_name == "": Logger.debug( "No filename present, generating filename using MD5 hash of url." ) file_name = "%s.srt" % (EncodingHelper.encode_md5(url), ) elif not file_name.endswith(".srt"): Logger.debug("No SRT extension present, appending it.") file_name = "%s.srt" % (file_name, ) srt = "" try: local_complete_path = os.path.join(Config.cacheDir, file_name) # no need to download it again! if os.path.exists(local_complete_path): return local_complete_path Logger.trace("Opening Subtitle URL") raw = UriHandler.open(url, proxy=proxy) if UriHandler.instance().status.error: Logger.warning("Could not retrieve subtitle from %s", url) return "" if raw == "": Logger.warning( "Empty Subtitle path found. Not setting subtitles.") return "" # try to decode it as `raw` should be a string. if isinstance(raw, bytes): try: raw = raw.decode() except: # fix some weird chars try: raw = raw.replace("\x96", "-") except: Logger.error("Error replacing some weird chars.") Logger.warning( "Converting input to UTF-8 using 'unicode_escape'") raw = raw.decode('unicode_escape') # do some auto detection if raw.startswith("WEBVTT") and format != "webvtt": Logger.info( "Discovered subtitle format 'webvtt' instead of '%s'", format) format = "webvtt" if format.lower() == 'sami': srt = SubtitleHelper.__convert_sami_to_srt(raw) elif format.lower() == 'srt': srt = raw elif format.lower() == 'webvtt': srt = SubtitleHelper.__convert_web_vtt_to_srt( raw) # With Krypton and Leia VTT is supported natively elif format.lower() == 'ttml': srt = SubtitleHelper.__convert_ttml_to_srt(raw) elif format.lower() == 'dcsubtitle': srt = SubtitleHelper.__convert_dc_subtitle_to_srt(raw) elif format.lower() == 'json': srt = SubtitleHelper.__convert_json_subtitle_to_srt(raw) elif format.lower() == 'm3u8srt': srt = SubtitleHelper.__convert_m3u8_srt_to_subtitle_to_srt( raw, url, proxy) else: error = "Uknown subtitle format: %s" % (format, ) raise NotImplementedError(error) if replace: Logger.debug("Replacing SRT data: %s", replace) for needle in replace: srt = srt.replace(needle, replace[needle]) with io.open(local_complete_path, 'w', encoding="utf-8") as f: f.write(srt) Logger.info("Saved SRT as %s", local_complete_path) return local_complete_path except: Logger.error("Error handling Subtitle file: [%s]", srt, exc_info=True) return ""
def __init__(self, addon_name, params, handle=0): # NOSONAR complexity """ Initialises the plugin with given arguments. :param str addon_name: The add-on name. :param str params: The input parameters from the query string. :param int handle: The Kodi directory handle. """ Logger.info("******** Starting %s add-on version %s/repo *********", Config.appName, Config.version) # noinspection PyTypeChecker self.handle = int(handle) super(Plugin, self).__init__(addon_name, params) Logger.debug( "Plugin Params: %s (%s)\n" "Handle: %s\n" "Name: %s\n" "Query: %s", self.params, len(self.params), self.handle, self.pluginName, params) # channel objects self.channelObject = None self.channelFile = "" self.channelCode = None self.contentType = "episodes" self.methodContainer = dict( ) # : storage for the inspect.getmembers(channel) method. Improves performance # are we in session? session_active = SessionHelper.is_session_active(Logger.instance()) # fetch some environment settings env_ctrl = envcontroller.EnvController(Logger.instance()) if not session_active: # do add-on start stuff Logger.info("Add-On start detected. Performing startup actions.") # print the folder structure env_ctrl.print_retrospect_settings_and_folders( Config, AddonSettings) # show notification XbmcWrapper.show_notification(None, LanguageHelper.get_localized_string( LanguageHelper.StartingAddonId) % (Config.appName, ), fallback=False, logger=Logger) # check for updates. Using local import for performance from updater import Updater up = Updater(Config.updateUrl, Config.version, UriHandler.instance(), Logger.instance(), AddonSettings.get_release_track()) if up.is_new_version_available(): Logger.info("Found new version online: %s vs %s", up.currentVersion, up.onlineVersion) notification = LanguageHelper.get_localized_string( LanguageHelper.NewVersion2Id) notification = notification % (Config.appName, up.onlineVersion) XbmcWrapper.show_notification(None, lines=notification, display_time=20000) # check for cache folder env_ctrl.cache_check() # do some cache cleanup env_ctrl.cache_clean_up(Config.cacheDir, Config.cacheValidTime) # create a session SessionHelper.create_session(Logger.instance()) #=============================================================================== # Start the plugin version of progwindow #=============================================================================== if len(self.params) == 0: # Show initial start if not in a session # now show the list if AddonSettings.show_categories(): self.show_categories() else: self.show_channel_list() #=============================================================================== # Start the plugin verion of the episode window #=============================================================================== else: # Determine what stage we are in. Check that there are more than 2 Parameters if len(self.params) > 1 and self.keywordChannel in self.params: # retrieve channel characteristics self.channelFile = os.path.splitext( self.params[self.keywordChannel])[0] self.channelCode = self.params[self.keywordChannelCode] Logger.debug( "Found Channel data in URL: channel='%s', code='%s'", self.channelFile, self.channelCode) # import the channel channel_register = ChannelIndex.get_register() channel = channel_register.get_channel(self.channelFile, self.channelCode) if channel is not None: self.channelObject = channel else: Logger.critical( "None or more than one channels were found, unable to continue." ) return # init the channel as plugin self.channelObject.init_channel() Logger.info("Loaded: %s", self.channelObject.channelName) elif self.keywordCategory in self.params \ or self.keywordAction in self.params and ( self.params[self.keywordAction] == self.actionAllFavourites or self.params[self.keywordAction] == self.actionRemoveFavourite): # no channel needed for these favourites actions. pass # =============================================================================== # Vault Actions # =============================================================================== elif self.keywordAction in self.params and \ self.params[self.keywordAction] in \ ( self.actionSetEncryptedValue, self.actionSetEncryptionPin, self.actionResetVault ): try: # Import vault here, as it is only used here or in a channel # that supports it from vault import Vault action = self.params[self.keywordAction] if action == self.actionResetVault: Vault.reset() return v = Vault() if action == self.actionSetEncryptionPin: v.change_pin() elif action == self.actionSetEncryptedValue: v.set_setting( self.params[self.keywordSettingId], self.params.get(self.keywordSettingName, ""), self.params.get(self.keywordSettingActionId, None)) finally: if self.keywordSettingTabFocus in self.params: AddonSettings.show_settings( self.params[self.keywordSettingTabFocus], self.params.get(self.keywordSettingSettingFocus, None)) return elif self.keywordAction in self.params and \ self.actionPostLog in self.params[self.keywordAction]: self.__send_log() return elif self.keywordAction in self.params and \ self.actionProxy in self.params[self.keywordAction]: # do this here to not close the busy dialog on the SetProxy when # a confirm box is shown title = LanguageHelper.get_localized_string( LanguageHelper.ProxyChangeConfirmTitle) content = LanguageHelper.get_localized_string( LanguageHelper.ProxyChangeConfirm) if not XbmcWrapper.show_yes_no(title, content): Logger.warning( "Stopping proxy update due to user intervention") return language = self.params.get(self.keywordLanguage, None) proxy_id = self.params.get(self.keywordProxy, None) local_ip = self.params.get(self.keywordLocalIP, None) self.__set_proxy(language, proxy_id, local_ip) return else: Logger.critical("Error determining Plugin action") return #=============================================================================== # See what needs to be done. #=============================================================================== if self.keywordAction not in self.params: Logger.critical( "Action parameters missing from request. Parameters=%s", self.params) return elif self.params[self.keywordAction] == self.actionListCategory: self.show_channel_list(self.params[self.keywordCategory]) elif self.params[ self.keywordAction] == self.actionConfigureChannel: self.__configure_channel(self.channelObject) elif self.params[self.keywordAction] == self.actionFavourites: # we should show the favourites self.show_favourites(self.channelObject) elif self.params[self.keywordAction] == self.actionAllFavourites: self.show_favourites(None) elif self.params[self.keywordAction] == self.actionListFolder: # channelName and URL is present, Parse the folder self.process_folder_list() elif self.params[self.keywordAction] == self.actionPlayVideo: self.play_video_item() elif not self.params[self.keywordAction] == "": self.on_action_from_context_menu( self.params[self.keywordAction]) else: Logger.warning( "Number of parameters (%s) or parameter (%s) values not implemented", len(self.params), self.params) self.__fetch_textures() return
def run_plugin(): """ Runs Retrospect as a Video Add-On """ log_file = None try: from retroconfig import Config from helpers.sessionhelper import SessionHelper # get a logger up and running from logger import Logger # only append if there are no active sessions if not SessionHelper.is_session_active(): # first call in the session, so do not append the log append_log_file = False else: append_log_file = True log_file = Logger.create_logger( os.path.join(Config.profileDir, Config.logFileNameAddon), Config.appName, append=append_log_file, dual_logger=lambda x, y=4: xbmc.log(x, y)) from urihandler import UriHandler from addonsettings import AddonSettings AddonSettings.set_language() from textures import TextureHandler # update the loglevel Logger.instance().minLogLevel = AddonSettings.get_log_level() use_caching = AddonSettings.cache_http_responses() cache_dir = None if use_caching: cache_dir = Config.cacheDir ignore_ssl_errors = AddonSettings.ignore_ssl_errors() UriHandler.create_uri_handler(cache_dir=cache_dir, cookie_jar=os.path.join( Config.profileDir, "cookiejar.dat"), ignore_ssl_errors=ignore_ssl_errors) # start texture handler TextureHandler.set_texture_handler(Config, Logger.instance(), UriHandler.instance()) # run the plugin import plugin plugin.Plugin(sys.argv[0], sys.argv[2], sys.argv[1]) # make sure we leave no references behind AddonSettings.clear_cached_addon_settings_object() # close the log to prevent locking on next call Logger.instance().close_log() log_file = None except: if log_file: log_file.critical("Error running plugin", exc_info=True) log_file.close_log() raise
def update_json_video_item(self, item): """ Updates an existing MediaItem with more data. Used to update none complete MediaItems (self.complete = False). This could include opening the item's URL to fetch more data and then process that data or retrieve it's real media-URL. The method should at least: * cache the thumbnail to disk (use self.noImage if no thumb is available). * set at least one MediaItemPart with a single MediaStream. * set self.complete = True. if the returned item does not have a MediaItemPart then the self.complete flag will automatically be set back to False. :param MediaItem item: the original MediaItem that needs updating. :return: The original item with more data added to it's properties. :rtype: MediaItem """ headers = {"accept": "application/vnd.sbs.ovp+json; version=2.0"} data = UriHandler.open(item.url, proxy=self.proxy, additional_headers=headers) if UriHandler.instance().status.code == 404: Logger.warning("No normal stream found. Trying newer method") new_url = item.url.replace("https://embed.kijk.nl/api/", "https://embed.kijk.nl/") item.url = new_url[:new_url.index("?")] return self.__update_embedded_video(item) json = JsonHelper(data) embed_url = json.get_value("metadata", "embedURL") if embed_url: Logger.warning( "Embed URL found. Using that to determine the streams.") item.url = embed_url try: return self.__update_embedded_video(item) except: Logger.warning("Failed to update embedded item:", exc_info=True) use_adaptive_with_encryption = AddonSettings.use_adaptive_stream_add_on( with_encryption=True, channel=self) mpd_info = json.get_value("entitlements", "play") # is there MPD information in the API response? if mpd_info is not None: return self.__update_video_from_mpd(item, mpd_info, use_adaptive_with_encryption) # Try the plain M3u8 streams part = item.create_new_empty_media_part() m3u8_url = json.get_value("playlist") use_adaptive = AddonSettings.use_adaptive_stream_add_on(channel=self) # with the Accept: application/vnd.sbs.ovp+json; version=2.0 header, the m3u8 streams that # are brightcove based have an url paramter instead of an empty m3u8 file Logger.debug("Trying standard M3u8 streams.") if m3u8_url != "https://embed.kijk.nl/api/playlist/.m3u8" \ and "hostingervice=brightcove" not in m3u8_url: for s, b in M3u8.get_streams_from_m3u8(m3u8_url, self.proxy, append_query_string=True): if "_enc_" in s: continue if use_adaptive: # we have at least 1 none encrypted streams Logger.info("Using HLS InputStreamAddon") strm = part.append_media_stream(m3u8_url, 0) M3u8.set_input_stream_addon_input(strm, proxy=self.proxy) item.complete = True return item part.append_media_stream(s, b) item.complete = True return item Logger.warning("No M3u8 data found. Falling back to BrightCove") video_id = json.get_value("vpakey") # videoId = json.get_value("videoId") -> Not all items have a videoId mpd_manifest_url = "https://embed.kijk.nl/video/%s?width=868&height=491" % ( video_id, ) referer = "https://embed.kijk.nl/video/%s" % (video_id, ) data = UriHandler.open(mpd_manifest_url, proxy=self.proxy, referer=referer) # First try to find an M3u8 m3u8_urls = Regexer.do_regex('https:[^"]+.m3u8', data) for m3u8_url in m3u8_urls: m3u8_url = m3u8_url.replace("\\", "") # We need the actual URI to make this work, so fetch it. m3u8_url = UriHandler.header(m3u8_url, proxy=self.proxy)[-1] Logger.debug("Found direct M3u8 in brightcove data.") if use_adaptive: # we have at least 1 none encrypted streams Logger.info("Using HLS InputStreamAddon") strm = part.append_media_stream(m3u8_url, 0) M3u8.set_input_stream_addon_input(strm, proxy=self.proxy) item.complete = True return item for s, b in M3u8.get_streams_from_m3u8(m3u8_url, self.proxy, append_query_string=True): item.complete = True part.append_media_stream(s, b) return item return self.__update_video_from_brightcove( item, data, use_adaptive_with_encryption)