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
Example #4
0
    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
Example #8
0
    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
Example #17
0
    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
Example #25
0
    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