def __get_api_persisted_url(self, operation, hash_value, variables): # NOSONAR """ Generates a GraphQL url :param str operation: The operation to use :param str hash_value: The hash of the Query :param dict variables: Any variables to pass :return: A GraphQL string :rtype: str """ extensions = { "persistedQuery": { "version": 1, "sha256Hash": hash_value } } extensions = HtmlEntityHelper.url_encode( JsonHelper.dump(extensions, pretty_print=False)) variables = HtmlEntityHelper.url_encode( JsonHelper.dump(variables, pretty_print=False)) url = "https://graph.kijk.nl/graphql?" \ "operationName={}&" \ "variables={}&" \ "extensions={}".format(operation, variables, extensions) return url
def __get_api_url(self, operation, hash_value, variables=None): """ Generates a GraphQL url :param str operation: The operation to use :param str hash_value: The hash of the Query :param dict variables: Any variables to pass :return: A GraphQL string :rtype: str """ extensions = { "persistedQuery": { "version": 1, "sha256Hash": hash_value } } extensions = HtmlEntityHelper.url_encode( JsonHelper.dump(extensions, pretty_print=False)) final_vars = {"order_by": "NAME", "per_page": 1000} if variables: final_vars = variables final_vars = HtmlEntityHelper.url_encode( JsonHelper.dump(final_vars, pretty_print=False)) url = "https://graphql.tv4play.se/graphql?" \ "operationName={}&" \ "variables={}&" \ "extensions={}".format(operation, final_vars, extensions) return url
def search_site(self, url=None): """ Creates an list of items by searching the site. This method is called when the URL of an item is "searchSite". The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. The %s the url will be replaced with an URL encoded representation of the text to search for. :param str|None url: Url to use to search with a %s for the search parameters. :return: A list with search results as MediaItems. :rtype: list[MediaItem] """ items = [] if url is None: item = MediaItem("Search Not Implented", "", type='video') items.append(item) else: items = [] needle = XbmcWrapper.show_key_board() if needle: Logger.debug("Searching for '%s'", needle) # convert to HTML needle = HtmlEntityHelper.url_encode(needle) search_url = url % (needle, ) temp = MediaItem("Search", search_url) return self.process_folder_list(temp) return items
def __init__(self, realm, api_key): """ Initializes a handler for the authentication provider :param str api_key: The API key to use :param str realm: The realm for this handler """ if not api_key: raise ValueError("API Key required for RTL XL via Gigya") super(RtlXlHandler, self).__init__(realm, device_id=None) self.api_key = api_key self.__setting_signature = "{}:signature".format(realm) # internal data self.__signature = None self.__user_id = None self.__signature_timestamp = None self.__common_param_dict = { "APIKey": self.api_key, "authMode": "cookie" } self.__common_params = \ "APIKey={}&authMode=cookie".format(HtmlEntityHelper.url_encode(self.api_key))
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 """ Logger.trace(result_set) cat = HtmlEntityHelper.url_encode(result_set['nid']) url = "https://api.tv4play.se/play/programs?platform=tablet&category=%s" \ "&fl=nid,name,program_image,category,logo,is_premium" \ "&per_page=1000&is_active=true&start=0" % (cat, ) item = MediaItem(result_set['name'], url) item.thumb = self.noImage item.type = 'folder' item.complete = True return item
def create_api_swipefolder_type(self, result_set): """ Creates a new MediaItem for a folder listing 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 dict result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'folder'. :rtype: MediaItem|None """ title = result_set["title"] if title == "Sista chansen": title = LanguageHelper.get_localized_string(LanguageHelper.LastChance) elif title == "Mest sedda programmen": title = LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes) elif title.startswith("Popul"): title = LanguageHelper.get_localized_string(LanguageHelper.Popular) elif title.startswith("Nyheter"): title = LanguageHelper.get_localized_string(LanguageHelper.LatestNews) item = MediaItem(title, "swipe://{}".format(HtmlEntityHelper.url_encode(title))) for card in result_set["cards"]: child = self.create_api_typed_item(card) if not child: continue item.items.append(child) return item
def __send_paste_bin(self, name, code, expire='1M', paste_format=None, user_key=None): """ Send a file to pastebin.com :param str|unicode name: Name of the logfile paste/gist. :param str code: The content to post. :param str|unicode expire: Expiration time. :param str|unicode paste_format: The format for the file. :param str|unicode user_key: The user API key. :return: The result of the upload. :rtype: any """ if not name: raise ValueError("Name missing") if not code: raise ValueError("No code data specified") params = { 'api_option': 'paste', 'api_paste_private': 1, # 0=public 1=unlisted 2=private 'api_paste_name': name, 'api_paste_expire_date': expire, 'api_dev_key': self.__apiKey, 'api_paste_code': code, } if paste_format: params['api_paste_format'] = paste_format if user_key: params['api_user_key'] = user_key post_params = "" for k in params.keys(): post_params = "{0}&{1}={2}".format( post_params, k, HtmlEntityHelper.url_encode(str(params[k]))) post_params = post_params.lstrip("&") if self.__logger: self.__logger.debug("Posting %d chars to pastebin.com", len(code)) data = UriHandler.open("http://pastebin.com/api/api_post.php", params=post_params, proxy=self.__proxy) if "pastebin.com" not in data: raise IOError(data) if self.__logger: self.__logger.info("PasteBin: %s", data) return data
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) # 1 - get the overal config file guid_regex = 'http://[^:]+/mgid:[^"]+:([0-9a-f-]+)"' rtmp_regex = r'type="video/([^"]+)" bitrate="(\d+)">\W+<src>([^<]+)</src>' data = UriHandler.open(item.url, proxy=self.proxy) guids = Regexer.do_regex(guid_regex, data) item.MediaItemParts = [] for guid in guids: # get the info for this part Logger.debug("Processing part with GUID: %s", guid) # reset stuff part = None # http://www.southpark.nl/feeds/video-player/mediagen?uri=mgid%3Aarc%3Aepisode%3Acomedycentral.com%3Aeb2a53f7-e370-4049-a6a9-57c195367a92&suppressRegisterBeacon=true guid = HtmlEntityHelper.url_encode("mgid:arc:episode:comedycentral.com:%s" % (guid,)) info_url = "%s/feeds/video-player/mediagen?uri=%s&suppressRegisterBeacon=true" % (self.baseUrl, guid) # 2- Get the GUIDS for the different ACTS info_data = UriHandler.open(info_url, proxy=self.proxy) rtmp_streams = Regexer.do_regex(rtmp_regex, info_data) for rtmp_stream in rtmp_streams: # if this is the first stream for the part, create an new part if part is None: part = item.create_new_empty_media_part() part.append_media_stream(self.get_verifiable_video_url(rtmp_stream[2]), rtmp_stream[1]) item.complete = True Logger.trace("Media item updated: %s", item) return item
def get_license_key(key_url, key_type="R", key_headers=None, key_value=None, json_filter=""): """ Generates a propery license key value # A{SSM} -> not implemented # R{SSM} -> raw format # B{SSM} -> base64 format URL encoded (b{ssmm} will not URL encode) # D{SSM} -> decimal format The generic format for a LicenseKey is: |<url>|<headers>|<key with placeholders>|<optional json filter> The Widevine Decryption Key Identifier (KID) can be inserted via the placeholder {KID} :param str key_url: The URL where the license key can be obtained. :param str|None key_type: The key type (A, R, B or D). :param dict[str,str] key_headers: A dictionary that contains the HTTP headers to pass. :param str key_value: The value that is beging passed on as the key value. :param str json_filter: If specified selects that json element to extract the key response. :return: A formated license string that can be passed to the adaptive input add-on. :rtype: str """ header = "" if key_headers: for k, v in key_headers.items(): header = "{0}&{1}={2}".format(header, k, HtmlEntityHelper.url_encode(v)) if key_type in ("A", "R", "B"): key_value = "{0}{{SSM}}".format(key_type) elif key_type == "D": if "D{SSM}" not in key_value: raise ValueError("Missing D{SSM} placeholder") key_value = HtmlEntityHelper.url_encode(key_value) return "{0}|{1}|{2}|{3}".format(key_url, header.strip("&"), key_value, json_filter)
def search_site(self, url=None): """ Creates an list of items by searching the site. This method is called when the URL of an item is "searchSite". The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. The %s the url will be replaced with an URL encoded representation of the text to search for. :param str url: Url to use to search with a %s for the search parameters. :return: A list with search results as MediaItems. :rtype: list[MediaItem] """ if self.primaryChannelId: shows_url = "https://{0}/content/shows?" \ "include=genres%%2Cimages%%2CprimaryChannel.images&" \ "filter%%5BprimaryChannel.id%%5D={1}&" \ "page%%5Bsize%%5D={2}&query=%s"\ .format(self.baseUrlApi, self.primaryChannelId or "", self.programPageSize) videos_url = "https://{0}/content/videos?decorators=viewingHistory&" \ "include=images%%2CprimaryChannel%%2Cshow&" \ "filter%%5BprimaryChannel.id%%5D={1}&" \ "page%%5Bsize%%5D={2}&query=%s"\ .format(self.baseUrlApi, self.primaryChannelId or "", self.videoPageSize) else: shows_url = "https://{0}/content/shows?" \ "include=genres%%2Cimages%%2CprimaryChannel.images&" \ "page%%5Bsize%%5D={1}&query=%s" \ .format(self.baseUrlApi, self.programPageSize) videos_url = "https://{0}/content/videos?decorators=viewingHistory&" \ "include=images%%2CprimaryChannel%%2Cshow&" \ "page%%5Bsize%%5D={1}&query=%s" \ .format(self.baseUrlApi, self.videoPageSize) needle = XbmcWrapper.show_key_board() if needle: Logger.debug("Searching for '%s'", needle) needle = HtmlEntityHelper.url_encode(needle) search_url = videos_url % (needle, ) temp = MediaItem("Search", search_url) episodes = self.process_folder_list(temp) search_url = shows_url % (needle, ) temp = MediaItem("Search", search_url) shows = self.process_folder_list(temp) return shows + episodes return []
def create_api_program_type(self, result_set): """ Creates a new MediaItem for an episode. 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 """ # Logger.Trace(result_set) json = result_set title = json["name"] program_id = json["nid"] program_id = HtmlEntityHelper.url_encode(program_id) url = "https://api.tv4play.se/play/video_assets" \ "?platform=tablet&per_page=%s&is_live=false&type=episode&" \ "page=1&node_nids=%s&start=0" % (self.__maxPageSize, program_id,) item = MediaItem(title, url) item.description = result_set.get("description", None) item.thumb = result_set.get("image") if item.thumb is not None: item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ "&quality=70&resize=520x293&source={}"\ .format(HtmlEntityHelper.url_encode(item.thumb)) item.fanart = result_set.get("image") if item.fanart is not None: item.fanart = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ "&quality=70&resize=1280x720&source={}" \ .format(HtmlEntityHelper.url_encode(item.fanart)) item.isPaid = result_set.get("is_premium", False) return item
def create_api_program_type(self, result_set): """ Creates a new MediaItem for an episode. 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 """ json = result_set title = json["name"] # https://graphql.tv4play.se/graphql?operationName=cdp&variables={"nid":"100-penisar"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"255449d35b5679b2cb5a9b85e63afd532c68d50268ae2740ae82f24d83a84774"}} program_id = json["nid"] url = self.__get_api_query('{program(nid:"%s"){name,description,videoPanels{id,name,subheading,assetType}}}' % (program_id,)) item = MediaItem(title, url) item.description = result_set.get("description", None) item.thumb = result_set.get("image") if item.thumb is not None: item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ "&quality=70&resize=520x293&source={}"\ .format(HtmlEntityHelper.url_encode(item.thumb)) item.fanart = result_set.get("image") if item.fanart is not None: item.fanart = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ "&quality=70&resize=1280x720&source={}" \ .format(HtmlEntityHelper.url_encode(item.fanart)) item.isPaid = result_set.get("is_premium", False) return item
def __get_video_streams(self, video_id, part): """ Fetches the video stream for a given videoId @param video_id: (integer) the videoId @param part: (MediaPart) the mediapart to add the streams to @return: (bool) indicating a successfull retrieval """ # hardcoded for now as it does not seem top matter dscgeo = '{"countryCode":"%s","expiry":1446917369986}' % ( self.language.upper(), ) dscgeo = HtmlEntityHelper.url_encode(dscgeo) headers = {"Cookie": "dsc-geo=%s" % (dscgeo, )} # send the data http, nothing, host, other = self.baseUrl.split("/", 3) subdomain, domain = host.split(".", 1) url = "https://secure.%s/secure/api/v2/user/authorization/stream/%s?stream_type=hls" \ % (domain, video_id,) data = UriHandler.open(url, proxy=self.proxy, additional_headers=headers, no_cache=True) json = JsonHelper(data) url = json.get_value("hls") if url is None: return False streams_found = False if "?" in url: qs = url.split("?")[-1] else: qs = None for s, b in M3u8.get_streams_from_m3u8(url, self.proxy): # and we need to append the original QueryString if "X-I-FRAME-STREAM" in s: continue streams_found = True if qs is not None: if "?" in s: s = "%s&%s" % (s, qs) else: s = "%s?%s" % (s, qs) part.append_media_stream(s, b) return streams_found
def create_episode_item(self, result_set): """ Creates a new MediaItem for an episode. 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 """ # Logger.Trace(result_set) json = result_set title = json["name"] program_id = json["nid"] program_id = HtmlEntityHelper.url_encode(program_id) url = "https://api.tv4play.se/play/video_assets" \ "?platform=tablet&per_page=%s&is_live=false&type=episode&" \ "page=1&node_nids=%s&start=0" % (self.maxPageSize, program_id, ) if "channel" in json and json["channel"]: # noinspection PyTypeChecker channel_id = json["channel"]["nid"] Logger.trace("ChannelId found: %s", channel_id) else: channel_id = "tv4" Logger.warning("ChannelId NOT found. Assuming %s", channel_id) # match the exact channel or put them in TV4 is_match_for_channel = channel_id.startswith(self.__channelId) is_match_for_channel |= self.channelCode == "tv4se" and not channel_id.startswith( "sjuan") and not channel_id.startswith("tv12") if not is_match_for_channel: Logger.debug("Channel mismatch for '%s': %s vs %s", title, channel_id, self.channelCode) return None item = MediaItem(title, url) item.icon = self.icon item.thumb = result_set.get("program_image", self.noImage) item.fanart = result_set.get("program_image", self.fanart) item.isPaid = result_set.get("is_premium", False) return item
def create_api_tag(self, result_set): """ Creates a new MediaItem for tag listing items 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 str result_set: The result_set of the self.episodeItemRegex :return: A new MediaItem of type 'folder'. :rtype: MediaItem|None """ Logger.trace(result_set) query = 'query{programSearch(tag:"%s",per_page:1000){__typename,programs' \ '%s,' \ 'totalHits}}' % (result_set, self.__program_fields) query = HtmlEntityHelper.url_encode(query) url = "https://graphql.tv4play.se/graphql?query={}".format(query) item = MediaItem(result_set, url) return item
def create_category(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 """ Logger.trace(result_set) title = HtmlEntityHelper.url_encode(result_set['title']) url = "http://m.schooltv.nl/api/v1/categorieen/%s/afleveringen.json?sort=Nieuwste&age_filter=&size=%s" % (title, self.__PageSize) item = MediaItem(result_set['title'], url) item.thumb = result_set.get('image', self.noImage) item.description = "Totaal %(count)s videos" % result_set return item
def search_site(self, url=None): """ Creates an list of items by searching the site. This method is called when the URL of an item is "searchSite". The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. The %s the url will be replaced with an URL encoded representation of the text to search for. :param str url: Url to use to search with a %s for the search parameters. :return: A list with search results as MediaItems. :rtype: list[MediaItem] """ items = [] needle = XbmcWrapper.show_key_board() if not needle: return [] Logger.debug("Searching for '%s'", needle) # convert to HTML needle = HtmlEntityHelper.url_encode(needle) # Search Programma's url = "https://search.rtl.nl/?typeRestriction=tvabstract&search={}&page=1&pageSize=99" search_url = url.format(needle) temp = MediaItem("Search", search_url) items += self.process_folder_list(temp) or [] # Search Afleveringen -> no dates given, so this makes little sense # url = "https://search.rtl.nl/?typeRestriction=videoobject&uitzending=true&search={}&page=1&pageSize=99" # search_url = url.format(needle) # temp = MediaItem("Search", search_url) # items += self.process_folder_list(temp) or [] return items
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 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('starting FormatVideoItem for %s', self.channelName) # Logger.Trace(result_set) # the vmanProgramId (like 1019976) leads to http://anytime.tv4.se/webtv/metafileFlash.smil?p=1019976&bw=1000&emulate=true&sl=true program_id = result_set["id"] # Logger.Debug("ProgId = %s", programId) # We can either use M3u8 or Dash # url = "https://playback-api.b17g.net/media/%s?service=tv4&device=browser&protocol=hls" % (program_id,) url = "https://playback-api.b17g.net/media/%s?service=tv4&device=browser&protocol=dash" % ( program_id, ) name = result_set["title"] season = result_set.get("season", 0) episode = result_set.get("episode", 0) is_episodic = 0 < season < 1900 and not episode == 0 if is_episodic: episode_text = None if " del " in name: name, episode_text = name.split(" del ", 1) episode_text = episode_text.lstrip("0123456789") if episode_text: episode_text = episode_text.lstrip(" -") name = "{} - s{:02d}e{:02d} - {}".format( name, season, episode, episode_text) else: name = "{} - s{:02d}e{:02d}".format(name, season, episode) item = MediaItem(name, url) item.description = result_set["description"] if item.description is None: item.description = item.name if is_episodic: item.set_season_info(season, episode) # premium_expire_date_time=2099-12-31T00:00:00+01:00 expire_date = result_set.get("expire_date_time") if bool(expire_date): self.__set_expire_time(expire_date, item) date = result_set["broadcast_date_time"] (date_part, time_part) = date.split("T") (year, month, day) = date_part.split("-") (hour, minutes, rest1, zone) = time_part.split(":") item.set_date(year, month, day, hour, minutes, 00) broadcast_date = datetime.datetime(int(year), int(month), int(day), int(hour), int(minutes)) item.fanart = result_set.get("program_image", self.parentItem.fanart) thumb_url = result_set.get("image", result_set.get("program_image")) # some images need to come via a proxy: if thumb_url and "://img.b17g.net/" in thumb_url: item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ "&quality=90&resize=520x293&source={}"\ .format(HtmlEntityHelper.url_encode(thumb_url)) else: item.thumb = thumb_url availability = result_set["availability"] # noinspection PyTypeChecker free_period = availability["availability_group_free"] # noinspection PyTypeChecker premium_period = availability["availability_group_premium"] now = datetime.datetime.now() if False and not premium_period == "0": # always premium free_expired = now - datetime.timedelta(days=99 * 365) elif free_period == "30+" or free_period is None: free_expired = broadcast_date + datetime.timedelta(days=99 * 365) else: free_expired = broadcast_date + datetime.timedelta( days=int(free_period)) Logger.trace( "Premium info for: %s\nPremium state: %s\nFree State: %s\nBroadcast %s vs Expired %s", name, premium_period, free_period, broadcast_date, free_expired) if now > free_expired: item.isPaid = True item.type = "video" item.complete = False item.isGeoLocked = result_set["is_geo_restricted"] item.isDrmProtected = result_set["is_drm_protected"] item.isLive = result_set.get("is_live", False) if item.isLive: item.name = "{}:{} - {}".format(hour, minutes, name) item.url = "{0}&is_live=true".format(item.url) if item.isDrmProtected: item.url = "{}&drm=widevine&is_drm=true".format(item.url) item.set_info_label("duration", int(result_set.get("duration", 0))) return item
def create_api_video_asset_type(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 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('starting FormatVideoItem for %s', self.channelName) program_id = result_set["id"] url = "https://playback-api.b17g.net/media/{}?service=tv4&device=browser&protocol=dash".\ format(program_id) name = result_set["title"] season = result_set.get("season", 0) episode = result_set.get("episode", 0) is_episodic = 0 < season < 1900 and not episode == 0 if is_episodic: episode_text = None if " del " in name: name, episode_text = name.split(" del ", 1) episode_text = episode_text.lstrip("0123456789") if episode_text: episode_text = episode_text.lstrip(" -") name = "{} - s{:02d}e{:02d} - {}".format(name, season, episode, episode_text) else: name = "{} - s{:02d}e{:02d}".format(name, season, episode) item = MediaItem(name, url) item.description = result_set["description"] if item.description is None: item.description = item.name if is_episodic: item.set_season_info(season, episode) # premium_expire_date_time=2099-12-31T00:00:00+01:00 expire_in_days = result_set.get("daysLeftInService", 0) if 0 < expire_in_days < 10000: item.set_expire_datetime( timestamp=datetime.datetime.now() + datetime.timedelta(days=expire_in_days)) date = result_set["broadcastDateTime"] broadcast_date = DateHelper.get_datetime_from_string(date, "%Y-%m-%dT%H:%M:%SZ", "UTC") broadcast_date = broadcast_date.astimezone(self.__timezone) item.set_date(broadcast_date.year, broadcast_date.month, broadcast_date.day, broadcast_date.hour, broadcast_date.minute, 0) item.fanart = result_set.get("program_image", self.parentItem.fanart) thumb_url = result_set.get("image", result_set.get("program_image")) # some images need to come via a proxy: if thumb_url and "://img.b17g.net/" in thumb_url: item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ "&quality=70&resize=520x293&source={}" \ .format(HtmlEntityHelper.url_encode(thumb_url)) else: item.thumb = thumb_url item.type = "video" item.complete = False item.isGeoLocked = True # For now, none are paid. # item.isPaid = not result_set.get("freemium", False) if "drmProtected" in result_set: item.isDrmProtected = result_set["drmProtected"] elif "is_drm_protected" in result_set: item.isDrmProtected = result_set["is_drm_protected"] item.isLive = result_set.get("live", False) if item.isLive: item.name = "{:02d}:{:02d} - {}".format(broadcast_date.hour, broadcast_date.minute, name) item.url = "{0}&is_live=true".format(item.url) if item.isDrmProtected: item.url = "{}&drm=widevine&is_drm=true".format(item.url) item.set_info_label("duration", int(result_set.get("duration", 0))) return item
def __get_api_query_url(self, query, fields): result = "query{%s%s}" % (query, fields) return "https://graph.kijk.nl/graphql?query={}".format( HtmlEntityHelper.url_encode(result))
def set_input_stream_addon_input(strm, proxy=None, headers=None, addon="inputstream.adaptive", manifest_type=None, license_key=None, license_type=None, max_bit_rate=None, persist_storage=False, service_certificate=None, manifest_update=None): """ Parsers standard M3U8 lists and returns a list of tuples with streams and bitrates that can be used by other methods. :param strm: (MediaStream) the MediaStream to update :param proxy: (Proxy) The proxy to use for opening :param dict headers: Possible HTTP Headers :param str addon: Adaptive add-on to use :param str manifest_type: Type of manifest (hls/mpd) :param str license_key: The value of the license key request :param str license_type: The type of license key request used (see below) :param int max_bit_rate: The maximum bitrate to use (optional) :param bool persist_storage: Should we store certificates? And request server certificates? :param str service_certificate: Use the specified server certificate :param str manifest_update: How should the manifest be updated Can be used like this: part = item.create_new_empty_media_part() stream = part.append_media_stream(stream_url, 0) M3u8.set_input_stream_addon_input(stream, self.proxy, self.headers) item.complete = True if maxBitRate is not set, the bitrate will be configured via the normal generic Retrospect or channel settings. """ if manifest_type is None: raise ValueError("No manifest type set") strm.Adaptive = True # NOSONAR # See https://github.com/peak3d/inputstream.adaptive/blob/master/inputstream.adaptive/addon.xml.in strm.add_property("inputstreamaddon", addon) strm.add_property("inputstream.adaptive.manifest_type", manifest_type) if license_key: strm.add_property("inputstream.adaptive.license_key", license_key) if license_type: strm.add_property("inputstream.adaptive.license_type", license_type) if max_bit_rate: strm.add_property("inputstream.adaptive.max_bandwidth", max_bit_rate * 1000) if persist_storage: strm.add_property("inputstream.adaptive.license_flags", "persistent_storage") if service_certificate is not None: strm.add_property("inputstream.adaptive.server_certificate", service_certificate) if manifest_update: strm.add_property("inputstream.adaptive.manifest_update_parameter", manifest_update) if headers: header = "" for k, v in headers.items(): header = "{0}&{1}={2}".format(header, k, HtmlEntityHelper.url_encode(v)) strm.add_property("inputstream.adaptive.stream_headers", header.strip("&")) return strm
def __get_api_query(self, query): return "https://graphql.tv4play.se/graphql?query={}".format(HtmlEntityHelper.url_encode(query))
def add_categories_and_specials(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 = [] # TV4 Group specific items query = 'query{programSearch(per_page:1000){__typename,programs' \ '%s,' \ 'totalHits}}' % (self.__program_fields,) query = HtmlEntityHelper.url_encode(query) tv_shows_url = "https://graphql.tv4play.se/graphql?query={}".format( query) extras = { LanguageHelper.get_localized_string(LanguageHelper.Search): ("searchSite", None, False), LanguageHelper.get_localized_string(LanguageHelper.TvShows): (tv_shows_url, None, False), LanguageHelper.get_localized_string(LanguageHelper.Categories): ("https://graphql.tv4play.se/graphql?query=query%7Btags%7D", None, False), LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes): ("https://api.tv4play.se/play/video_assets/most_viewed?type=episode" "&platform=tablet&is_live=false&per_page=%s&start=0" % (self.__maxPageSize, ), None, False), } today = datetime.datetime.now() days = [ LanguageHelper.get_localized_string(LanguageHelper.Monday), LanguageHelper.get_localized_string(LanguageHelper.Tuesday), LanguageHelper.get_localized_string(LanguageHelper.Wednesday), LanguageHelper.get_localized_string(LanguageHelper.Thursday), LanguageHelper.get_localized_string(LanguageHelper.Friday), LanguageHelper.get_localized_string(LanguageHelper.Saturday), LanguageHelper.get_localized_string(LanguageHelper.Sunday) ] for i in range(0, 7, 1): start_date = today - datetime.timedelta(i) end_date = start_date + datetime.timedelta(1) day = days[start_date.weekday()] if i == 0: day = LanguageHelper.get_localized_string(LanguageHelper.Today) elif i == 1: day = LanguageHelper.get_localized_string( LanguageHelper.Yesterday) Logger.trace("Adding item for: %s - %s", start_date, end_date) url = "https://api.tv4play.se/play/video_assets?exclude_node_nids=" \ "&platform=tablet&is_live=false&product_groups=2&type=episode&per_page=100" url = "%s&broadcast_from=%s&broadcast_to=%s&" % ( url, start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")) extras[day] = (url, start_date, False) extras[LanguageHelper.get_localized_string( LanguageHelper.CurrentlyPlayingEpisodes )] = ( "https://api.tv4play.se/play/video_assets?exclude_node_nids=&platform=tablet&" "is_live=true&product_groups=2&type=episode&per_page=100", None, False) # Actually add the extra items for name in extras: title = name url, date, is_live = extras[name] item = MediaItem(title, url) item.dontGroup = True item.complete = True item.HttpHeaders = self.httpHeaders item.isLive = is_live if date is not None: item.set_date(date.year, date.month, date.day, 0, 0, 0, text=date.strftime("%Y-%m-%d")) items.append(item) Logger.debug("Pre-Processing finished") return data, items
def get_kodi_play_list_data(self, bitrate, proxy=None): """ Returns the playlist items for this MediaItem :param int bitrate: The bitrate of the streams that should be in the playlist. Given in kbps. :param ProxyInfo|None proxy: The proxy to set :return: A list of ListItems that should be added to a playlist with their selected stream url :rtype: list[tuple[xbmcgui.ListItem, str]] """ Logger.info("Creating playlist items for Bitrate: %s kbps\n%s", bitrate, self) if not bool(bitrate): raise ValueError("Bitrate not specified") play_list_data = [] for part in self.MediaItemParts: if len(part.MediaStreams) == 0: Logger.warning("Ignoring empty MediaPart: %s", part) continue kodi_item = self.get_kodi_item() stream = part.get_media_stream_for_bitrate(bitrate) Logger.info("Selected Stream: %s", stream) if stream.Adaptive: Adaptive.set_max_bitrate(stream, max_bit_rate=bitrate) # Set the actual stream path kodi_item.setProperty("path", stream.Url) # properties of the Part for prop in part.Properties + stream.Properties: Logger.trace("Adding property: %s", prop) kodi_item.setProperty(prop[0], prop[1]) # TODO: Apparently if we use the InputStream Adaptive, using the setSubtitles() causes sync issues. if part.Subtitle and False: Logger.debug("Adding subtitle to ListItem: %s", part.Subtitle) kodi_item.setSubtitles([ part.Subtitle, ]) # Set any custom Header header_params = dict() # set proxy information if present self.__set_kodi_proxy_info(kodi_item, stream, stream.Url, header_params, proxy) # Now add the actual HTTP headers for k in part.HttpHeaders: header_params[k] = HtmlEntityHelper.url_encode( part.HttpHeaders[k]) stream_url = stream.Url if header_params: kodi_query_string = reduce( lambda x, y: "%s&%s=%s" % (x, y, header_params[y]), header_params.keys(), "") kodi_query_string = kodi_query_string.lstrip("&") Logger.debug("Adding Kodi Stream parameters: %s\n%s", header_params, kodi_query_string) stream_url = "%s|%s" % (stream.Url, kodi_query_string) play_list_data.append((kodi_item, stream_url)) return play_list_data
def get_stream_info(self, item, mzid, live=False, hls_over_dash=False): # NOSONAR """ Updates an item with Vualto stream data. :param MediaItem item: The Mediaitem to update :param str mzid: The MZ ID of the stream :param bool live: Indicator if the stream is live or not :param bool hls_over_dash: Should we prefer HLS over Dash? :return: An updated MediaItem :rtype: MediaItem """ # We need a player token token_data = UriHandler.open("https://media-services-public.vrt.be/" "vualto-video-aggregator-web/rest/external/v1/tokens", data="", additional_headers={"Content-Type": "application/json"}) token = JsonHelper(token_data).get_value("vrtPlayerToken") asset_url = "https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/" \ "external/v1/videos/{0}?vrtPlayerToken={1}&client={2}" \ .format(mzid, HtmlEntityHelper.url_encode(token), self.client_id) asset_data = UriHandler.open(asset_url, no_cache=True) asset_data = JsonHelper(asset_data) drm_key = asset_data.get_value("drm") drm_protected = drm_key is not None adaptive_available = AddonSettings.use_adaptive_stream_add_on( with_encryption=drm_protected, channel=self.channel) part = item.create_new_empty_media_part() srt = None # see if we prefer hls over dash hls_prio = 2 if hls_over_dash else 0 for target_url in asset_data.get_value("targetUrls"): video_type = target_url["type"] video_url = target_url["url"] if video_type == "hls_aes" and drm_protected and adaptive_available: # no difference in encrypted or not. Logger.debug("Found HLS AES encrypted stream and a DRM key") stream = part.append_media_stream(video_url, hls_prio) M3u8.set_input_stream_addon_input(stream) elif video_type == "hls" and not drm_protected: # no difference in encrypted or not. if adaptive_available: Logger.debug("Found standard HLS stream and without DRM protection") stream = part.append_media_stream(video_url, hls_prio) M3u8.set_input_stream_addon_input(stream) else: m3u8_data = UriHandler.open(video_url) for s, b, a in M3u8.get_streams_from_m3u8(video_url, play_list_data=m3u8_data, map_audio=True): item.complete = True if a: audio_part = a.rsplit("-", 1)[-1] audio_part = "-%s" % (audio_part,) s = s.replace(".m3u8", audio_part) part.append_media_stream(s, b) srt = M3u8.get_subtitle(video_url, play_list_data=m3u8_data) if not srt or live: # If there is not SRT don't download it. If it a live stream with subs, # don't use it as it is not supported by Kodi continue srt = srt.replace(".m3u8", ".vtt") part.Subtitle = SubtitleHelper.download_subtitle(srt, format="webvtt") elif video_type == "mpeg_dash" and adaptive_available: if not drm_protected: Logger.debug("Found standard MPD stream and without DRM protection") stream = part.append_media_stream(video_url, 1) Mpd.set_input_stream_addon_input(stream) else: stream = part.append_media_stream(video_url, 1) encryption_json = '{{"token":"{0}","drm_info":[D{{SSM}}],"kid":"{{KID}}"}}' \ .format(drm_key) encryption_key = Mpd.get_license_key( key_url="https://widevine-proxy.drm.technology/proxy", key_type="D", key_value=encryption_json, key_headers={"Content-Type": "text/plain;charset=UTF-8"} ) Mpd.set_input_stream_addon_input(stream, license_key=encryption_key) if video_type.startswith("hls") and srt is None: srt = M3u8.get_subtitle(video_url) if not srt or live: # If there is not SRT don't download it. If it a live stream with subs, # don't use it as it is not supported by Kodi continue srt = srt.replace(".m3u8", ".vtt") part.Subtitle = SubtitleHelper.download_subtitle(srt, format="webvtt") item.complete = True return item
def log_on(self, username=None, password=None): """ Logs on to a website, using an url. :param username: If provided overrides the Kodi stored username :param password: If provided overrides the Kodi stored username :return: indication if the login was successful. :rtype: bool First checks if the channel requires log on. If so and it's not already logged on, it should handle the log on. That part should be implemented by the specific channel. More arguments can be passed on, but must be handled by custom code. After a successful log on the self.loggedOn property is set to True and True is returned. """ username = username or AddonSettings.get_setting("dplayse_username") if self.__is_already_logged_on(username): return True # Local import to not slow down any other stuff import os import binascii try: # If running on Leia import pyaes except: # If running on Pre-Leia from resources.lib import pyaes import random now = int(time.time()) b64_now = binascii.b2a_base64(str(now).encode()).decode().strip() user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \ "(KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36" device_id = AddonSettings.get_client_id().replace("-", "") window_id = "{}|{}".format( binascii.hexlify(os.urandom(16)).decode(), binascii.hexlify(os.urandom(16)).decode()) fe = [ "DNT:unknown", "L:en-US", "D:24", "PR:1", "S:1920,975", "AS:1920,935", "TO:-120", "SS:true", "LS:true", "IDB:true", "B:false", "ODB:true", "CPUC:unknown", "PK:Win32", "CFP:990181251", "FR:false", "FOS:false", "FB:false", "JSF:Arial", "P:Chrome PDF Plugin", "T:0,false,false", "H:4", "SWF:false" ] fs_murmur_hash = '48bf49e1796939175b0406859d00baec' data = [ { "key": "api_type", "value": "js" }, { "key": "p", "value": 1 }, # constant { "key": "f", "value": device_id }, # browser instance ID { "key": "n", "value": b64_now }, # base64 encoding of time.now() { "key": "wh", "value": window_id }, # WindowHandle ID { "key": "fe", "value": fe }, # browser properties { "key": "ife_hash", "value": fs_murmur_hash }, # hash of browser properties { "key": "cs", "value": 1 }, # canvas supported 0/1 { "key": "jsbd", "value": "{\"HL\":41,\"NCE\":true,\"DMTO\":1,\"DOTO\":1}" } ] data_value = JsonHelper.dump(data) stamp = now - (now % (60 * 60 * 6)) key_password = "******".format(user_agent, stamp) salt_bytes = os.urandom(8) key_iv = self.__evp_kdf(key_password.encode(), salt_bytes, key_size=8, iv_size=4, iterations=1, hash_algorithm="md5") key = key_iv["key"] iv = key_iv["iv"] encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) encrypted = encrypter.feed(data_value) # Again, make a final call to flush any remaining bytes and strip padding encrypted += encrypter.feed() salt_hex = binascii.hexlify(salt_bytes) iv_hex = binascii.hexlify(iv) encrypted_b64 = binascii.b2a_base64(encrypted) bda = { "ct": encrypted_b64.decode(), "iv": iv_hex.decode(), "s": salt_hex.decode() } bda_str = JsonHelper.dump(bda) bda_base64 = binascii.b2a_base64(bda_str.encode()) req_dict = { "bda": bda_base64.decode(), "public_key": "FE296399-FDEA-2EA2-8CD5-50F6E3157ECA", "site": "https://client-api.arkoselabs.com", "userbrowser": user_agent, "simulate_rate_limit": "0", "simulated": "0", "rnd": "{}".format(random.random()) } req_data = "" for k, v in req_dict.items(): req_data = "{}{}={}&".format(req_data, k, HtmlEntityHelper.url_encode(v)) req_data = req_data.rstrip("&") arkose_data = UriHandler.open( "https://client-api.arkoselabs.com/fc/gt2/public_key/FE296399-FDEA-2EA2-8CD5-50F6E3157ECA", proxy=self.proxy, data=req_data, additional_headers={"user-agent": user_agent}, no_cache=True) arkose_json = JsonHelper(arkose_data) arkose_token = arkose_json.get_value("token") if "rid=" not in arkose_token: Logger.error("Error logging in. Invalid Arkose token.") return False Logger.debug("Succesfully required a login token from Arkose.") UriHandler.open( "https://disco-api.dplay.se/token?realm=dplayse&deviceId={}&shortlived=true" .format(device_id), proxy=self.proxy, no_cache=True) if username is None or password is None: from resources.lib.vault import Vault v = Vault() password = v.get_setting("dplayse_password") dplay_username = username dplay_password = password creds = { "credentials": { "username": dplay_username, "password": dplay_password } } headers = { "x-disco-arkose-token": arkose_token, "Origin": "https://auth.dplay.se", "x-disco-client": "WEB:10:AUTH_DPLAY_V1:2.4.1", # is not specified a captcha is required # "Sec-Fetch-Site": "same-site", # "Sec-Fetch-Mode": "cors", # "Sec-Fetch-Dest": "empty", "Referer": "https://auth.dplay.se/login", "User-Agent": user_agent } result = UriHandler.open("https://disco-api.dplay.se/login", proxy=self.proxy, json=creds, additional_headers=headers) if UriHandler.instance().status.code > 200: Logger.error("Failed to log in: %s", result) return False Logger.debug("Succesfully logged in") return True