def get_setting(self, setting_id): """ Retrieves an encrypted setting from the Kodi Add-on Settings. :param str setting_id: the ID for the setting to retrieve. :return: the decrypted value for the setting. :rtype: str """ Logger.info("Decrypting value for setting '%s'", setting_id) encrypted_value = AddonSettings.get_setting(setting_id) if not encrypted_value: Logger.warning("Found empty string as encrypted data") return encrypted_value try: decrypted_value = self.__decrypt(encrypted_value, Vault.__Key) if not decrypted_value.startswith(setting_id): Logger.error("Invalid decrypted value for setting '%s'", setting_id) return None decrypted_value = decrypted_value[len(setting_id) + 1:] Logger.info("Successfully decrypted value for setting '%s'", setting_id) except UnicodeDecodeError: Logger.error( "Invalid Unicode data returned from decryption. Must be wrong data" ) return None return decrypted_value
def create_category_item(self, result_set): """ Creates a MediaItem of type 'folder' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'folder'. :rtype: MediaItem|None """ if 'thumburl' in result_set and not result_set['thumburl'].startswith( "http"): result_set['thumburl'] = "%s/%s" % (self.baseUrl, result_set["thumburl"]) slug = result_set["slug"] url = self.__cateogory_urls.get(slug) if url is None: Logger.warning("Missing category in list: %s", slug) return None result_set["url"] = url return chn_class.Channel.create_folder_item(self, result_set)
def create_json_item(self, result_set): """ Creates a new MediaItem for an folder or video. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'folder'. :rtype: MediaItem|None """ Logger.trace(result_set) item_type = result_set["format"] if item_type == "video": return self.create_video_item_json(result_set) elif item_type == "audio": # Apparently the audio is always linking to a show folder. return self.create_episode_json_item(result_set) else: Logger.warning("Found unknown type: %s", item_type) return None
def create_video_item(self, result_set): """ Creates a MediaItem of type 'video' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.update_video_item method is called if the item is focussed or selected for playback. :param result_set: The result_set of the self.episodeItemRegex :type result_set: list[str]|dict[str,dict[str,dict]] :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). :rtype: MediaItem|None """ Logger.trace(result_set) if "fullengthSegment" in result_set and "segment" in result_set[ "fullengthSegment"]: video_id = result_set["fullengthSegment"]["segment"]["id"] geo_location = result_set["fullengthSegment"]["segment"][ "geolocation"] geo_block = False if "flags" in result_set["fullengthSegment"]["segment"]: geo_block = result_set["fullengthSegment"]["segment"][ "flags"].get("geoblock", None) Logger.trace("Found geoLocation/geoBlock: %s/%s", geo_location, geo_block) else: Logger.warning("No video information found.") return None url = "http://www.srf.ch/player/webservice/videodetail/index?id=%s" % ( video_id, ) item = MediaItem(result_set["titleFull"], url) item.type = "video" # noinspection PyTypeChecker item.thumb = result_set.get("segmentThumbUrl", None) # apparently only the 144 return the correct HEAD info # item.thumb = "%s/scale/width/288" % (item.thumb, ) # the HEAD will not return a size, so Kodi can't handle it # item.fanart = resultSet.get("imageUrl", None) item.description = result_set.get("description", "") date_value = str(result_set["time_published"]) # 2015-01-20 22:17:59" date_time = DateHelper.get_date_from_string(date_value, "%Y-%m-%d %H:%M:%S") item.set_date(*date_time[0:6]) item.icon = self.icon item.httpHeaders = self.httpHeaders item.complete = False return item
def create_video_item_new(self, result_set): """ Creates a MediaItem of type 'video' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.update_video_item method is called if the item is focussed or selected for playback. :param result_set: The result_set of the self.episodeItemRegex :type result_set: list[str]|dict[str,str] :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). :rtype: MediaItem|None """ Logger.trace(result_set) videos = self.__get_nested_value(result_set, "Assets", "Video") if not videos: Logger.warning("No video information found.") return None video_infos = [vi for vi in videos if vi["fullLength"]] if len(video_infos) > 0: video_info = video_infos[0] else: Logger.warning("No full length video found.") return None # noinspection PyTypeChecker video_id = video_info["id"] url = "http://il.srgssr.ch/integrationlayer/1.0/ue/srf/video/play/%s.json" % ( video_id, ) item = MediaItem(result_set["title"], url) item.type = "video" item.thumb = self.__get_nested_value(video_info, "Image", "ImageRepresentations", "ImageRepresentation", 0, "url") item.description = self.__get_nested_value(video_info, "AssetMetadatas", "AssetMetadata", 0, "description") date_value = str(result_set["publishedDate"]) date_value = date_value[0:-6] # 2015-01-20T22:17:59" date_time = DateHelper.get_date_from_string(date_value, "%Y-%m-%dT%H:%M:%S") item.set_date(*date_time[0:6]) item.icon = self.icon item.httpHeaders = self.httpHeaders item.complete = False return item
def __initialise_channel_set(self, channel_info): # type: (ChannelInfo) -> None """ Initialises a channelset (.py file) WARNING: these actions are done ONCE per python file, not per channel. Arguments: channelInfo : ChannelInfo - The channelinfo Keyword Arguments: abortOnNew : Boolean - If set to true, channel initialisation will not continue if a new channel was found. This will have to be done later. Returns True if any operations where executed """ Logger.info("Initialising channel set at: %s.", channel_info.path) # now import (required for the PerformFirstTimeActions sys.path.append(channel_info.path) # make sure a pyo or pyc exists # __import__(channelInfo.moduleName) # The debugger won't compile if __import__ is used. So let's use this one. import py_compile py_compile.compile(os.path.join(channel_info.path, "%s.py" % (channel_info.moduleName,))) # purge the texture cache. if TextureHandler.instance(): TextureHandler.instance().purge_texture_cache(channel_info) else: Logger.warning("Could not purge_texture_cache: no TextureHandler available") return
def extract_json(self, data): """ Performs pre-process actions for data processing. Accepts an data from the process_folder_list method, BEFORE the items are processed. Allows setting of parameters (like title etc) for the channel. Inside this method the <data> could be changed and additional items can be created. The return values should always be instantiated in at least ("", []). :param str data: The retrieve data that was loaded for the current item and URL. :return: A tuple of the data and a list of MediaItems that were generated. :rtype: tuple[str|JsonHelper,list[MediaItem]] """ Logger.info("Performing Pre-Processing") items = [] json_data = Regexer.do_regex('type="application/json">([^<]+)<', data) if not json_data: Logger.warning("No JSON data found.") return data, items json = JsonHelper(json_data[0]) result = [] for key, value in json.json.items(): result.append(value) value["title"] = key # set new json and return JsonHelper object json.json = result return json, items
def __update_video_from_brightcove(self, item, data, use_adaptive_with_encryption): """ Updates an existing MediaItem with more data based on an MPD stream. :param str data: Stream info retrieved from BrightCove. :param bool use_adaptive_with_encryption: Do we use the Adaptive InputStream add-on? :param MediaItem item: The original MediaItem that needs updating. :return: The original item with more data added to it's properties. :rtype: MediaItem """ part = item.create_new_empty_media_part() # Then try the new BrightCove JSON bright_cove_regex = '<video[^>]+data-video-id="(?<videoId>[^"]+)[^>]+data-account="(?<videoAccount>[^"]+)' bright_cove_data = Regexer.do_regex( Regexer.from_expresso(bright_cove_regex), data) if not bright_cove_data: Logger.warning("Error updating using BrightCove data: %s", item) return item Logger.info("Found new BrightCove JSON data") bright_cove_url = 'https://edge.api.brightcove.com/playback/v1/accounts/' \ '%(videoAccount)s/videos/%(videoId)s' % bright_cove_data[0] headers = { "Accept": "application/json;pk=BCpkADawqM3ve1c3k3HcmzaxBvD8lXCl89K7XEHiKutxZArg2c5RhwJHJANOwPwS_4o7UsC4RhIzXG8Y69mrwKCPlRkIxNgPQVY9qG78SJ1TJop4JoDDcgdsNrg" } bright_cove_data = UriHandler.open(bright_cove_url, additional_headers=headers) bright_cove_json = JsonHelper(bright_cove_data) streams = [ d for d in bright_cove_json.get_value("sources") if d["container"] == "M2TS" ] # Old filter # streams = filter(lambda d: d["container"] == "M2TS", bright_cove_json.get_value("sources")) if not streams: Logger.warning("Error extracting streams from BrightCove data: %s", item) return item # noinspection PyTypeChecker stream_url = streams[0]["src"] # these streams work better with the the InputStreamAddon because it removes the # "range" http header if use_adaptive_with_encryption: Logger.info("Using InputStreamAddon for playback of HLS stream") strm = part.append_media_stream(stream_url, 0) M3u8.set_input_stream_addon_input(strm) item.complete = True return item for s, b in M3u8.get_streams_from_m3u8(stream_url): item.complete = True part.append_media_stream(s, b) return item
def create_api_typed_item(self, result_set): """ Creates a new MediaItem based on the __typename attribute. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. :param list[str]|dict result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'folder'. :rtype: MediaItem|None """ api_type = result_set["__typename"] Logger.trace("%s: %s", api_type, result_set) if api_type == "Program": item = self.create_api_program_type(result_set) elif api_type == "ProgramCard": item = self.create_api_program_type(result_set.get("program")) else: Logger.warning("Missing type: %s", api_type) return None return item
def __get_proxies(self, proxy, url): """ :param ProxyInfo proxy: :param url: :return: :rtype: dict[str, str] """ if proxy is None: return None elif not proxy.use_proxy_for_url(url): Logger.debug("Not using proxy due to filter mismatch") elif proxy.Scheme == "http": Logger.debug("Using a http(s) %s", proxy) proxy_address = proxy.get_proxy_address() return {"http": proxy_address, "https": proxy_address} elif proxy.Scheme == "dns": Logger.debug("Using a DNS %s", proxy) return {"dns": proxy.Proxy} Logger.warning("Unsupported Proxy Scheme: %s", proxy.Scheme) return None
def execute(self): Logger.debug("Performing Custom Contextmenu command: %s", self.__action) item = self.__media_item if not item.complete: Logger.debug( "The contextmenu action requires a completed item. Updating %s", item) item = self.__channel.process_video_item(item) if not item.complete: Logger.warning( "update_video_item returned an item that had item.complete = False:\n%s", item) # invoke the call function_string = "returnItem = channel_object.%s(item)" % ( self.__action, ) Logger.debug("Calling '%s'", function_string) try: # noinspection PyRedundantParentheses exec(function_string) # NOSONAR We just need this here. except: Logger.error("on_action_from_context_menu :: Cannot execute '%s'.", function_string, exc_info=True) return
def on_action_from_context_menu(self, action): """Peforms the action from a custom contextmenu Arguments: action : String - The name of the method to call """ Logger.debug("Performing Custom Contextmenu command: %s", action) item = self.media_item if not item.complete: Logger.debug( "The contextmenu action requires a completed item. Updating %s", item) item = self.channelObject.process_video_item(item) if not item.complete: Logger.warning( "update_video_item returned an item that had item.complete = False:\n%s", item) # invoke function_string = "returnItem = self.channelObject.%s(item)" % ( action, ) Logger.debug("Calling '%s'", function_string) try: # noinspection PyRedundantParentheses exec(function_string) # NOSONAR We just need this here. except: Logger.error("on_action_from_context_menu :: Cannot execute '%s'.", function_string, exc_info=True) return
def __init__(self): """ Creates a new instance of the Vault class """ self.__newKeyGeneratedInConstructor = False # : This was the very first time a key was generated # ask for PIN of no key is present if Vault.__Key is None: howto_shown = self.__show_howto() key = self.__get_application_key() # type: bytes # was there a key? No, let's initialize it. if key is None: Logger.warning( "No Application Key present. Initializing a new one.") # Show the how to if it was not already shown during this __init__() if not howto_shown: self.__show_howto(force=True) key = self.__get_new_key() if not self.change_pin(key): raise RuntimeError("Error creating Application Key.") Logger.info( "Created a new Application Key with MD5: %s (length=%s)", EncodingHelper.encode_md5(key), len(key)) self.__newKeyGeneratedInConstructor = True Vault.__Key = key Logger.trace("Using Application Key with MD5: %s (length=%s)", EncodingHelper.encode_md5(key), len(key))
def execute(self): title = LanguageHelper.get_localized_string( LanguageHelper.CleanupCache)[:-1] clean = \ XbmcWrapper.show_yes_no(title, LanguageHelper.CleanupConfirmation) if not clean: Logger.warning("Clean-up cancelled") return files_to_remove = { "channelindex.json": "Cleaning: Channel Index", "cookiejar.dat": "Cleaning: Cookies in cookiejar.dat", "xot.session.lock": "Cleaning: Session lock" } for file_name, log_line in files_to_remove.items(): Logger.info(log_line) files_to_remove = os.path.join(Config.profileDir, file_name) if os.path.isfile(files_to_remove): os.remove(files_to_remove) Logger.info("Cleaning: PickeStore objects") self.parameter_parser.pickler.purge_store(Config.addonId, age=0) Logger.info("Cleaning: Cache objects in cache folder") env_ctrl = EnvController(Logger.instance()) env_ctrl.cache_clean_up(Config.cacheDir, 0)
def __get_context_menu_items(self, channel, item=None): """ Retrieves the custom context menu items to display. favouritesList : Boolean - Indication that the menu is for the favorites :param Channel|None channel: The channel from which to get the context menu items. The channel might be None in case of some actions that do not require a channel. :param MediaItem|None item: The item to which the context menu belongs. :return: A list of context menu names and their commands. :rtype: list[tuple[str,str]] """ context_menu_items = [] # Genenric, none-Python menu items that would normally cause an unwanted reload of the # Python interpreter instance within Kodi. refresh = LanguageHelper.get_localized_string(LanguageHelper.RefreshListId) context_menu_items.append((refresh, 'XBMC.Container.Refresh()')) if item is None: return context_menu_items # if it was a favourites list, don't add the channel methods as they might be from a different channel if channel is None: return context_menu_items # now we process the other items possible_methods = self.__get_members(channel) # Logger.Debug(possible_methods) for menu_item in channel.contextMenuItems: # Logger.Debug(menu_item) if menu_item.itemTypes is None or item.type in menu_item.itemTypes: # We don't care for complete here! # if menu_item.completeStatus == None or menu_item.completeStatus == item.complete: # see if the method is available method_available = False for method in possible_methods: if method == menu_item.functionName: method_available = True # break from the method loop break if not method_available: Logger.warning("No method for: %s", menu_item) continue cmd_url = self._create_action_url(channel, action=menu_item.functionName, item=item) cmd = "XBMC.RunPlugin(%s)" % (cmd_url,) title = "Retro: %s" % (menu_item.label,) Logger.trace("Adding command: %s | %s", title, cmd) context_menu_items.append((title, cmd)) return context_menu_items
def execute(self): """ Shows the current channels settings dialog. """ if not self.__channel_info: Logger.warning("Cannot configure channel without channel info") Logger.info("Configuring channel: %s", self.__channel_info) AddonSettings.show_channel_settings(self.__channel_info)
def update_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 """ Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) if not item.url.endswith(".js"): data = UriHandler.open(item.url) data_id = Regexer.do_regex(r'data-id="(\d+)"[^>]+data-playout', data) if data_id is None: Logger.warning("Cannot find stream-id for L1 stream.") return item data_url = "https://limburg.bbvms.com/p/L1_video/c/{}.json".format(data_id[0]) else: data_url = item.url data = UriHandler.open(data_url) json = JsonHelper(data, logger=Logger.instance()) Logger.trace(json) base_url = json.get_value("publicationData", "defaultMediaAssetPath") streams = json.get_value("clipData", "assets") item.MediaItemParts = [] part = item.create_new_empty_media_part() for stream in streams: url = stream.get("src", None) if "://" not in url: url = "{}{}".format(base_url, url) bitrate = stream.get("bandwidth", None) if url: part.append_media_stream(url, bitrate) if not item.thumb and json.get_value("thumbnails"): url = json.get_value("thumbnails")[0].get("src", None) if url and "http:/" not in url: url = "%s%s" % (self.baseUrl, url) item.thumb = url item.complete = True return item
def extract_json_video(self, data): """ Performs pre-process actions for data processing. Accepts an data from the process_folder_list method, BEFORE the items are processed. Allows setting of parameters (like title etc) for the channel. Inside this method the <data> could be changed and additional items can be created. The return values should always be instantiated in at least ("", []). :param str data: The retrieve data that was loaded for the current item and URL. :return: A tuple of the data and a list of MediaItems that were generated. :rtype: tuple[str|JsonHelper,list[MediaItem]] """ Logger.info("Performing Pre-Processing") items = [] data = Regexer.do_regex(r'window.__DATA__ = ([\w\W]+?});\s*window.__PUSH_STATE__', data)[0] json_data = JsonHelper(data) # Get the main content container main_container = [m for m in json_data.get_value("children") if m["type"] == "MainContainer"] # Extract seasons seasons = [] if not self.parentItem.metaData.get("is_season", False): seasons = [ lst["props"]["items"] for lst in main_container[0]["children"] if lst["type"] == "SeasonSelector" ] if seasons: seasons = [s for s in seasons[0] if s["url"]] # Inject them json_data.json["seasons"] = seasons # Find the actual line_lists = [lst for lst in main_container[0]["children"] if lst["type"] == "LineList"] for line_list in line_lists: if line_list.get("props", {}).get("type") == "video-guide": json_data.json = line_list["props"] # Get the actual full episode list all_episodes = json_data.get_value("filters", "items", 0, "url") url_all_episodes = "{}{}".format(self.baseUrl, all_episodes) data = UriHandler.open(url_all_episodes) json_data = JsonHelper(data) # And append seasons again if seasons: json_data.json["seasons"] = seasons return json_data, items Logger.warning("Cannot extract video items") return json_data, items
def update_user_agent(): """ Creates a user agent for this instance of XOT this is a very slow action on lower end systems (ATV and rPi) so we minimize the number of runs :return: Nothing :rtype: None Actual: User-Agent: Kodi/16.1 (Windows NT 10.0; WOW64) App_Bitness/32 Version/16.1-Git:20160424-c327c53 Retro: User-Agent: Kodi/16.1 Git:20160424-c327c53 (Windows 10;AMD64; http://kodi.tv) Firefox: User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0 """ # there are slow imports, so only do them here import platform from resources.lib.envcontroller import EnvController # noinspection PyNoneFunctionAssignment version = AddonSettings.get_kodi_version() Logger.debug("Found Kodi version: %s", version) git = "" try: # noinspection PyNoneFunctionAssignment if "Git:" in version: version, git = version.split("Git:", 1) version = version.rstrip() # The platform.<method> are not working on rPi and IOS # kernel = platform.architecture() # Logger.Trace(kernel) # machine = platform.machine() # Logger.Trace(machine) uname = platform.uname() Logger.trace(uname) if git: user_agent = "Kodi/%s (%s %s; %s; http://kodi.tv) Version/%s Git:%s" % \ (version, uname[0], uname[2], uname[4], version, git) else: user_agent = "Kodi/%s (%s %s; %s; http://kodi.tv) Version/%s" % \ (version, uname[0], uname[2], uname[4], version) except: Logger.warning("Error setting user agent", exc_info=True) current_env = EnvController.get_platform(True) # Kodi/14.2 (Windows NT 6.1; WOW64) App_Bitness/32 Version/14.2-Git:20150326-7cc53a9 user_agent = "Kodi/%s (%s; <unknown>; http://kodi.tv)" % (version, current_env) # now we store it AddonSettings.store(LOCAL).set_setting(AddonSettings.__USER_AGENT_SETTING, user_agent) AddonSettings.__user_agent = user_agent Logger.info("User agent set to: %s", user_agent) return
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 """ data = UriHandler.open(item.url) video_data = JsonHelper(data) stream_data = video_data.get_value("playable") if not stream_data: return item part = item.create_new_empty_media_part() for stream_info in stream_data["assets"]: url = stream_info["url"] stream_type = stream_info["format"] if stream_type == "HLS": item.complete = M3u8.update_part_with_m3u8_streams(part, url) else: Logger.warning("Found unknow stream type: %s", stream_type) if "subtitles" not in stream_data or not stream_data["subtitles"]: return item for sub in stream_data["subtitles"]: sub_url = None sub_type = sub["type"] default_sub = sub["defaultOn"] if default_sub: sub_url = sub["webVtt"] sub_type = "webvtt" # set Retrospect type if sub_url: part.Subtitle = SubtitleHelper.download_subtitle( sub_url, format=sub_type) break return item
def __configure_channel(self, channel_info): """ Shows the current channels settings dialog. :param ChannelInfo channel_info: The channel info for the channel """ if not channel_info: Logger.warning("Cannot configure channel without channel info") Logger.info("Configuring channel: %s", channel_info) AddonSettings.show_channel_settings(channel_info)
def __update_m3u8(self, url, part, headers, use_kodi_hls): """ Update a video that has M3u8 streams. :param str url: The URL for the stream. :param MediaItemPart part: The new part that needs updating. :param dict[str,str] headers: The URL headers to use. :param bool use_kodi_hls: Should we use the InputStream Adaptive add-on? """ # first see if there are streams in this file, else check the second location. for s, b in M3u8.get_streams_from_m3u8(url, self.proxy, headers=headers): if use_kodi_hls: strm = part.append_media_stream(url, 0) M3u8.set_input_stream_addon_input(strm, headers=headers) # Only the main M3u8 is needed break else: part.append_media_stream(s, b) if not part.MediaStreams and "manifest.m3u8" in url: Logger.warning( "No streams found in %s, trying alternative with 'master.m3u8'", url) url = url.replace("manifest.m3u8", "master.m3u8") for s, b in M3u8.get_streams_from_m3u8(url, self.proxy, headers=headers): if use_kodi_hls: strm = part.append_media_stream(url, 0) M3u8.set_input_stream_addon_input(strm, headers=headers) # Only the main M3u8 is needed break else: part.append_media_stream(s, b) # check for subs # https://mtgxse01-vh.akamaihd.net/i/201703/13/DCjOLN_1489416462884_427ff3d3_,48,260,460,900,1800,2800,.mp4.csmil/master.m3u8?__b__=300&hdnts=st=1489687185~exp=3637170832~acl=/*~hmac=d0e12e62c219d96798e5b5ef31b11fa848724516b255897efe9808c8a499308b&cc1=name=Svenska%20f%C3%B6r%20h%C3%B6rselskadade~default=no~forced=no~lang=sv~uri=https%3A%2F%2Fsubstitch.play.mtgx.tv%2Fsubtitle%2Fconvert%2Fxml%3Fsource%3Dhttps%3A%2F%2Fcdn-subtitles-mtgx-tv.akamaized.net%2Fpitcher%2F20xxxxxx%2F2039xxxx%2F203969xx%2F20396967%2F20396967-swt.xml%26output%3Dm3u8 # https://cdn-subtitles-mtgx-tv.akamaized.net/pitcher/20xxxxxx/2039xxxx/203969xx/20396967/20396967-swt.xml&output=m3u8 if "uri=" in url and not part.Subtitle: Logger.debug("Extracting subs from M3u8") sub_url = url.rsplit("uri=")[-1] sub_url = HtmlEntityHelper.url_decode(sub_url) sub_data = UriHandler.open(sub_url, proxy=self.proxy) subs = [ line for line in sub_data.split("\n") if line.startswith("http") ] if subs: part.Subtitle = SubtitleHelper.download_subtitle( subs[0], format='webvtt', proxy=self.proxy) return
def create_video_item(self, result_set): """ Creates a MediaItem of type 'video' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.update_video_item method is called if the item is focussed or selected for playback. :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). :rtype: MediaItem|None """ Logger.trace(result_set) url = "%s%s" % (self.baseUrl, result_set["url"]) if self.parentItem.url not in url: return None name = result_set["title"] desc = result_set.get("description", "") thumb = result_set["thumburl"] if thumb and not thumb.startswith("http://"): thumb = "%s%s" % (self.baseUrl, thumb) item = MediaItem(name, url) item.thumb = thumb item.description = desc item.icon = self.icon item.type = 'video' item.complete = False try: name_parts = name.rsplit("/", 3) if len(name_parts) == 3: Logger.debug("Found possible date in name: %s", name_parts) year = name_parts[2] if len(year) == 2: year = 2000 + int(year) month = name_parts[1] day = name_parts[0].rsplit(" ", 1)[1] Logger.trace("%s - %s - %s", year, month, day) item.set_date(year, month, day) except: Logger.warning("Apparently it was not a date :)") return item
def create_video_item(self, result_set): """ Creates a MediaItem of type 'video' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.update_video_item method is called if the item is focussed or selected for playback. :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). :rtype: MediaItem|None """ Logger.trace(result_set) # Validate the input and raise errors if not isinstance(result_set, dict): Logger.critical( "No Dictionary as a result_set. Implement a custom create_video_item" ) raise NotImplementedError( "No Dictionary as a result_set. Implement a custom create_video_item" ) elif "title" not in result_set or "url" not in result_set: Logger.warning("No ?P<title> or ?P<url> in result_set") raise LookupError("No ?P<title> or ?P<url> in result_set") # The URL url = self._prefix_urls(result_set["url"]) # The title if "subtitle" in result_set and result_set["subtitle"]: # noinspection PyStringFormat title = "%(title)s - %(subtitle)s" % result_set else: title = result_set["title"] if title.isupper(): title = title.title() item = MediaItem(title, url) item.thumb = self._prefix_urls(result_set.get("thumburl", "")) item.description = result_set.get("description", "") item.type = 'video' item.HttpHeaders = self.httpHeaders item.complete = False return item
def create_json_video_item(self, result_set, prepend_serie=False): """ Creates a MediaItem of type 'video' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.update_video_item method is called if the item is focussed or selected for playback. :param list[str]|dict result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). :rtype: MediaItem|None """ Logger.trace(result_set) if not result_set.get("available", True): Logger.warning("Item not available: %s", result_set) return None item = self.create_json_episode_item(result_set) if item is None: return None if prepend_serie and 'seriesTitle' in result_set: item.name = "{0} - {1}".format(item.name, result_set['seriesTitle']) elif 'seriesTitle' in result_set: item.name = result_set['seriesTitle'] item.type = "video" # Older URL item.url = "https://embed.kijk.nl/api/video/%(id)s?id=kijkapp&format=DASH&drm=CENC" % result_set # New URL # item.url = "https://embed.kijk.nl/video/%(id)s" % result_set if 'subtitle' in result_set: item.name = "{0} - {1}".format(item.name, result_set['subtitle']) if "date" in result_set: date = result_set["date"].split("+")[0] # 2016-12-25T17:58:00+01:00 time_stamp = DateHelper.get_date_from_string( date, "%Y-%m-%dT%H:%M:%S") item.set_date(*time_stamp[0:6]) return item
def update_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 """ Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) xml_data = UriHandler.open(item.url, proxy=self.proxy) # <ref type='adaptive' device='pc' host='http://manifest.us.rtl.nl' href='/rtlxl/network/pc/adaptive/components/videorecorder/27/278629/278630/d009c025-6e8c-3d11-8aba-dc8579373134.ssm/d009c025-6e8c-3d11-8aba-dc8579373134.m3u8' /> m3u8_urls = Regexer.do_regex( "<ref type='adaptive' device='pc' host='([^']+)' href='/([^']+)' />", xml_data) if not m3u8_urls: Logger.warning("No m3u8 data found for: %s", item) return item m3u8_url = "%s/%s" % (m3u8_urls[0][0], m3u8_urls[0][1]) part = item.create_new_empty_media_part() # prevent the "418 I'm a teapot" error part.HttpHeaders[ "user-agent"] = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0" # Remove the Range header to make all streams start at the beginning. # Logger.debug("Setting an empty 'Range' http header to force playback at the start of a stream") # part.HttpHeaders["Range"] = '' item.complete = M3u8.update_part_with_m3u8_streams( part, m3u8_url, proxy=self.proxy, headers=part.HttpHeaders, channel=self) return item
def create_video_item(self, result_set): """ Creates a MediaItem of type 'video' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.update_video_item method is called if the item is focussed or selected for playback. :param dict[str,str|dict] result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). :rtype: MediaItem|None """ Logger.trace(result_set) title = result_set["title"] if title is None: Logger.warning("Found item with all <null> items. Skipping") return None if "subtitle" in result_set and result_set['subtitle'].lower( ) not in title.lower(): title = "%(title)s - %(subtitle)s" % result_set url = "http://m.schooltv.nl/api/v1/afleveringen/%(mid)s.json" % result_set item = MediaItem(title, url) item.description = result_set.get("description", "") age_groups = result_set.get('ageGroups', ['Onbekend']) item.description = "%s\n\nLeeftijden: %s" % (item.description, ", ".join(age_groups)) item.thumb = result_set.get("image", "") item.icon = self.icon item.type = 'video' item.fanart = self.fanart item.complete = False item.set_info_label("duration", result_set['duration']) if "publicationDate" in result_set: broadcast_date = DateHelper.get_date_from_posix( int(result_set['publicationDate'])) item.set_date(broadcast_date.year, broadcast_date.month, broadcast_date.day, broadcast_date.hour, broadcast_date.minute, broadcast_date.second) return item
def create_json_video_item(self, result_set): """ Creates a MediaItem of type 'video' using the result_set from the regex. This method creates a new MediaItem from the Regular Expression or Json results <result_set>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.update_video_item method is called if the item is focussed or selected for playback. :param dict[str,str|None] result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). :rtype: MediaItem|None """ Logger.trace(result_set) url = result_set["url"] if not url.startswith("http"): url = "{}{}".format(self.baseUrl, url) title = result_set["title"] item = MediaItem(title, url) item.description = result_set.get("synopsis", None) item.thumb = result_set.get("photo", self.noImage) item.type = "video" if "publicationTimeString" in result_set: try: # publicationTimeString=7 jun 2018 17:20 uur date_parts = result_set["publicationTimeString"].split(" ") day = int(date_parts[0]) month = DateHelper.get_month_from_name(date_parts[1], language="nl", short=True) year = int(date_parts[2]) hours, minutes = date_parts[3].split(":") hours = int(hours) minutes = int(minutes) item.set_date(year, month, day, hours, minutes, 0) except: Logger.warning("Error parsing date %s", result_set["publicationTimeString"], exc_info=True) item.complete = False return item
def get_channel(self, channel_id, channel_code, info_only=False): """ Fetches a single channel for a given className and channelCode If updated channels are found, the those channels are indexed and the channel index is rebuild. :param str|unicode channel_id: The chn_<name> class name. :param str|unicode channel_code: A possible channel code within the channel set. :param bool info_only: Only return the ChannelInfo. :return: a Channel object :rtype: Channel """ # determine the channel folder channel_path = os.path.join(Config.rootDir, self.__INTERNAL_CHANNEL_PATH) channel_pack, channel_set = channel_id.rsplit(".", 1) channel_set_info_path = os.path.join(channel_path, channel_pack, channel_set, "chn_{}.json".format(channel_set)) channel_infos = ChannelInfo.from_json(channel_set_info_path) if channel_code is None: channel_infos = [ci for ci in channel_infos if ci.channelCode is None] else: channel_infos = [ci for ci in channel_infos if ci.channelCode == channel_code] if len(channel_infos) != 1: Logger.error("Found none or more than 1 matches for '%s' and '%s' in the channel index.", channel_id, channel_code or "None") return None else: Logger.debug("Found single channel in the channel index: %s.", channel_infos[0]) channel_info = channel_infos[0] if self.__is_channel_set_updated(channel_info): Logger.warning("Found updated channel_set: %s.", channel_set_info_path) # new we should init all channels by loading them all, just to be sure that all is ok Logger.debug("Going to fetching all channels to init them all.") self.get_channels() return self.get_channel(channel_id, channel_code) if channel_info.ignore: Logger.warning("Channel %s is ignored in channel set", channel_info) return None if info_only: return channel_info return channel_info.get_channel()
def set_season_info(self, season, episode): """ Set season and episode information :param str|int season: The Season Number :param str|int episode: The Episode Number """ if season is None or episode is None: Logger.warning("Cannot set EpisodeInfo without season and episode") return self.__infoLabels["Episode"] = int(episode) self.__infoLabels["Season"] = int(season) return