Ejemplo n.º 1
0
class ard_live(Plugin):
    swf_url = "http://live.daserste.de/lib/br-player/swf/main.swf"
    _url_re = re.compile(r"https?://(www.)?daserste.de/", re.I)
    _player_re = re.compile(r'''dataURL\s*:\s*(?P<q>['"])(?P<url>.*?)(?P=q)''')
    _player_url_schema = validate.Schema(
        validate.transform(_player_re.search),
        validate.any(None, validate.all(validate.get("url"), validate.text)))
    _livestream_schema = validate.Schema(
        validate.xml_findall(".//assets"),
        validate.filter(lambda x: x.attrib.get("type") != "subtitles"),
        validate.get(0), validate.xml_findall(".//asset"), [
            validate.union({
                "url": validate.xml_findtext("./fileName"),
                "bitrate": validate.xml_findtext("./bitrateVideo")
            })
        ])

    @classmethod
    def can_handle_url(cls, url):
        return cls._url_re.match(url) is not None

    def _get_streams(self):
        data_url = http.get(self.url, schema=self._player_url_schema)
        if data_url:
            res = http.get(urljoin(self.url, data_url))
            stream_info = http.xml(res, schema=self._livestream_schema)

            for stream in stream_info:
                url = stream["url"]
                try:
                    if ".m3u8" in url:
                        for s in HLSStream.parse_variant_playlist(
                                self.session, url, name_key="bitrate").items():
                            yield s
                    elif ".f4m" in url:
                        for s in HDSStream.parse_manifest(
                                self.session,
                                url,
                                pvswf=self.swf_url,
                                is_akamai=True).items():
                            yield s
                    elif ".mp4" in url:
                        yield "{0}k".format(stream["bitrate"]), HTTPStream(
                            self.session, url)
                except IOError as err:
                    self.logger.warning("Error parsing stream: {0}", err)
Ejemplo n.º 2
0
        /
        (?P<clip_name>[\w]+)
    )?
""", re.VERBOSE)

_access_token_schema = validate.Schema(
    {
        "token": validate.text,
        "sig": validate.text
    }, validate.union((validate.get("sig"), validate.get("token"))))
_token_schema = validate.Schema(
    {
        "chansub": {
            "restricted_bitrates":
            validate.all([validate.text],
                         validate.filter(lambda n: not re.match(
                             r"(.+_)?archives|live|chunked", n)))
        }
    }, validate.get("chansub"))
_user_schema = validate.Schema(
    {validate.optional("display_name"): validate.text},
    validate.get("display_name"))
_video_schema = validate.Schema({
    "chunks": {
        validate.text: [{
            "length": int,
            "url": validate.any(None, validate.url(scheme="http")),
            "upkeep": validate.any("pass", "fail", None)
        }]
    },
    "restrictions": {
        validate.text: validate.text
Ejemplo n.º 3
0
class Looch(Plugin):
    url_re = re.compile(
        r"https?://(?:www\.)?looch\.tv/channel/(?P<name>[^/]+)(/videos/(?P<video_id>\d+))?"
    )

    api_base = "https://api.looch.tv"
    channel_api = api_base + "/channels/{name}"
    video_api = api_base + "/videos/{id}"

    playback_schema = validate.Schema({"weight": int, "uri": validate.url()})
    data_schema = validate.Schema({
        "type": validate.text,
        "attributes": {
            validate.optional("playback"): [playback_schema],
            validate.optional("resolution"): {
                "width": int,
                "height": int
            }
        }
    })
    channel_schema = validate.Schema(
        validate.transform(parse_json), {
            "included":
            validate.all(
                [data_schema],
                validate.filter(lambda x: x["type"] == "active_streams"),
                validate.map(lambda x: x["attributes"].get("playback")),
                validate.transform(lambda x: list(itertools.chain(*x)))),
        }, validate.get("included"))
    video_schema = validate.Schema(validate.transform(parse_json),
                                   {"data": data_schema}, validate.get("data"),
                                   validate.get("attributes"))

    @classmethod
    def can_handle_url(cls, url):
        return cls.url_re.match(url) is not None

    def _get_live_stream(self, channel):
        url = self.channel_api.format(name=channel)
        self.logger.debug("Channel API call: {0}", url)
        data = http.get(url, schema=self.channel_schema)
        self.logger.debug("Got {0} channel playback items", len(data))
        for playback in data:
            for s in HLSStream.parse_variant_playlist(self.session,
                                                      playback["uri"]).items():
                yield s

    def _get_video_stream(self, video_id):
        url = self.video_api.format(id=video_id)
        self.logger.debug("Video API call: {0}", url)
        data = http.get(url, schema=self.video_schema)
        self.logger.debug("Got video {0} playback items",
                          len(data["playback"]))
        res = data["resolution"]["height"]
        for playback in data["playback"]:
            yield "{0}p".format(res), HTTPStream(self.session, playback["uri"])

    def _get_streams(self):
        match = self.url_re.match(self.url)
        self.logger.debug("Matched URL: name={name}, video_id={video_id}",
                          **match.groupdict())

        if match.group("video_id"):
            return self._get_video_stream(match.group("video_id"))
        elif match.group("name"):
            return self._get_live_stream(match.group("name"))
Ejemplo n.º 4
0
 def test_filter(self):
     assert validate(filter(lambda i: i > 5),
                     [10, 5, 4, 6, 7]) == [10, 6, 7]
Ejemplo n.º 5
0
class Schoolism(Plugin):
    url_re = re.compile(r"https?://(?:www\.)?schoolism\.com/watchLesson.php")
    login_url = "https://www.schoolism.com/index.php"
    key_time_url = "https://www.schoolism.com/video-html/key-time.php"
    playlist_re = re.compile(r"var allVideos=(\[\{.*\}]);", re.DOTALL)
    js_to_json = partial(re.compile(r'(?!<")(\w+):(?!/)').sub, r'"\1":')
    playlist_schema = validate.Schema(
        validate.transform(playlist_re.search),
        validate.any(
            None,
            validate.all(
                validate.get(1),
                validate.transform(js_to_json),
                validate.transform(
                    lambda x: x.replace(",}", "}")),  # remove invalid ,
                validate.transform(parse_json),
                [{
                    "sources":
                    validate.all(
                        [{
                            "playlistTitle": validate.text,
                            "title": validate.text,
                            "src": validate.text,
                            "type": validate.text,
                        }],
                        # only include HLS streams
                        validate.filter(
                            lambda s: s["type"] == "application/x-mpegurl"))
                }])))

    options = PluginOptions({"email": None, "password": None, "part": 1})

    @classmethod
    def can_handle_url(cls, url):
        return cls.url_re.match(url) is not None

    def login(self, email, password):
        """
        Login to the schoolism account and return the users account
        :param email: (str) email for account
        :param password: (str) password for account
        :return: (str) users email
        """
        if self.options.get("email") and self.options.get("password"):
            res = http.post(self.login_url,
                            data={
                                "email": email,
                                "password": password,
                                "redirect": None,
                                "submit": "Login"
                            })

            if res.cookies.get("password") and res.cookies.get("email"):
                return res.cookies.get("email")
            else:
                self.logger.error(
                    "Failed to login to Schoolism, incorrect email/password combination"
                )
        else:
            self.logger.error(
                "An email and password are required to access Schoolism streams"
            )

    def _get_streams(self):
        user = self.login(self.options.get("email"),
                          self.options.get("password"))
        if user:
            self.logger.debug("Logged in to Schoolism as {0}", user)
            res = http.get(self.url,
                           headers={"User-Agent": useragents.SAFARI_8})
            lesson_playlist = self.playlist_schema.validate(res.text)

            part = self.options.get("part")

            self.logger.info("Attempting to play lesson Part {0}", part)
            found = False

            # make request to key-time api, to get key specific headers
            res = http.get(self.key_time_url,
                           headers={"User-Agent": useragents.SAFARI_8})

            for i, video in enumerate(lesson_playlist, 1):
                if video["sources"] and i == part:
                    found = True
                    for source in video["sources"]:
                        for s in HLSStream.parse_variant_playlist(
                                self.session,
                                source["src"],
                                headers={
                                    "User-Agent": useragents.SAFARI_8,
                                    "Referer": self.url
                                }).items():
                            yield s

            if not found:
                self.logger.error("Could not find lesson Part {0}", part)
Ejemplo n.º 6
0
METADATA_URL = "http://aftonbladet-play-metadata.cdn.drvideo.aptoma.no/video/{0}.json"

_embed_re = re.compile(r"<iframe src=\"(http://tv.aftonbladet.se[^\"]+)\"")
_aptoma_id_re = re.compile(r"<div id=\"drvideo\".+data-aptomaId=\"([^\"]+)\"")
_live_re = re.compile(r"data-isLive=\"true\"")
_url_re = re.compile(r"http(s)?://(\w+.)?.aftonbladet.se")

_video_schema = validate.Schema({
    "formats":
    validate.all(
        {
            validate.text: {
                validate.text:
                validate.all(
                    dict,
                    validate.filter(lambda k, v: k in STREAM_FORMATS),
                    {
                        validate.text: [{
                            "address": validate.text,
                            "filename": validate.text,
                            "path": validate.text
                        }]
                    },
                )
            }
        }, validate.filter(lambda k, v: k in STREAM_TYPES))
})


class Aftonbladet(Plugin):
    @classmethod
Ejemplo n.º 7
0
        /(?P<channel_name>[A-Za-z0-9-_]+)
    )
""", re.VERBOSE)
chromecast_re = re.compile(r'''stream_chromecast_url"\s*:\s*(?P<url>".*?")''')

_media_inner_schema = validate.Schema([{
    "layerList": [{
        "name":
        validate.text,
        validate.optional("sequenceList"): [{
            "layerList":
            validate.all([{
                "name": validate.text,
                validate.optional("param"): dict
            }],
                         validate.filter(lambda l: l["name"] in
                                         ("video", "reporting", "message")))
        }]
    }]
}])
_media_schema = validate.Schema(
    validate.any(
        _media_inner_schema,
        validate.all({"sequence": _media_inner_schema},
                     validate.get("sequence"))))
_vod_playlist_schema = validate.Schema({
    "duration": float,
    "fragments": [[int, float]],
    "template": validate.text
})
_vod_manifest_schema = validate.Schema({
    "alternates": [{
Ejemplo n.º 8
0
class UHSClient(object):
    """
    API Client, reverse engineered by observing the interactions
    between the web browser and the ustream servers.
    """
    API_URL = "http://r{0}-1-{1}-{2}-{3}.ums.ustream.tv"
    APP_ID, APP_VERSION = 2, 1
    api_schama = validate.Schema([{"args": [object], "cmd": validate.text}])
    connect_schama = validate.Schema([{
        "args":
        validate.all([{
            "host": validate.text,
            "connectionId": validate.text
        }], validate.length(1)),
        "cmd":
        "tracking"
    }], validate.length(1), validate.get(0), validate.get("args"),
                                     validate.get(0))
    module_info_schema = validate.Schema(
        [validate.get("stream")], validate.filter(lambda r: r is not None))

    def __init__(self, session, media_id, application, **options):
        self.session = session
        http.headers.update({"User-Agent": useragents.IPHONE_6})
        self.logger = session.logger.new_module("plugin.ustream.apiclient")
        self.media_id = media_id
        self.application = application
        self.referrer = options.pop("referrer", None)
        self._host = None
        self.rsid = self.generate_rsid()
        self.rpin = self.generate_rpin()
        self._connection_id = None
        self._app_id = options.pop("app_id", self.APP_ID)
        self._app_version = options.pop("app_version", self.APP_VERSION)
        self._cluster = options.pop("cluster", "live")
        self._password = options.pop("password")

    def connect(self, **options):
        result = self.send_command(type="viewer",
                                   appId=self._app_id,
                                   appVersion=self._app_version,
                                   rsid=self.rsid,
                                   rpin=self.rpin,
                                   referrer=self.referrer,
                                   media=str(self.media_id),
                                   application=self.application,
                                   schema=self.connect_schama,
                                   password=self._password)

        self._host = "http://{0}".format(result["host"])
        self._connection_id = result["connectionId"]
        self.logger.debug("Got new host={0}, and connectionId={1}", self._host,
                          self._connection_id)
        return True

    def poll(self, schema=None, retries=5, timeout=5.0):
        stime = time.time()
        try:
            r = self.send_command(connectionId=self._connection_id,
                                  schema=schema,
                                  retries=retries,
                                  timeout=timeout)
        except PluginError as err:
            self.logger.debug("poll took {0:.2f}s: {1}",
                              time.time() - stime, err)
        else:
            self.logger.debug("poll took {0:.2f}s", time.time() - stime)
            return r

    def generate_rsid(self):
        return "{0:x}:{1:x}".format(randint(0, 1e10), randint(0, 1e10))

    def generate_rpin(self):
        return "_rpin.{0}".format(randint(0, 1e15))

    def send_command(self, schema=None, retries=5, timeout=5.0, **args):
        res = http.get(self.host,
                       params=args,
                       headers={
                           "Referer": self.referrer,
                           "User-Agent": useragents.IPHONE_6
                       },
                       retries=retries,
                       timeout=timeout,
                       retry_max_backoff=0.5)
        return http.json(res, schema=schema or self.api_schama)

    @property
    def host(self):
        host = self._host or self.API_URL.format(randint(
            0, 0xffffff), self.media_id, self.application,
                                                 "lp-" + self._cluster)
        return urljoin(host, "/1/ustream")
Ejemplo n.º 9
0
}

STREAM_API_URL = "https://playapi.mtgx.tv/v3/videos/stream/{0}"

_swf_url_re = re.compile(r"data-flashplayer-url=\"([^\"]+)\"")
_player_data_re = re.compile(r"window.fluxData\s*=\s*JSON.parse\(\"(.+)\"\);")

_stream_schema = validate.Schema(
    validate.any(
        None, validate.all({"msg": validate.text}),
        validate.all(
            {
                "streams":
                validate.all(
                    {validate.text: validate.any(validate.text, int, None)},
                    validate.filter(lambda k, v: isinstance(v, validate.text)))
            }, validate.get("streams"))))


class Viasat(Plugin):
    """Livecli Plugin for Viasat"""

    _iframe_re = re.compile(
        r"""<iframe.+src=["'](?P<url>[^"']+)["'].+allowfullscreen""")
    _image_re = re.compile(
        r"""<meta\sproperty=["']og:image["']\scontent=".+/(?P<stream_id>\d+)/[^/]+\.jpg"""
    )

    _url_re = re.compile(
        r"""https?://(?:www\.)?
        (?:
Ejemplo n.º 10
0
class Mixer(Plugin):
    api_url = "https://mixer.com/api/v1/{type}/{id}"

    _vod_schema = validate.Schema(
        {
            "state":
            "AVAILABLE",
            "vods": [{
                "baseUrl": validate.url(),
                "data": validate.any(None, {"Height": int}),
                "format": validate.text
            }]
        }, validate.get("vods"),
        validate.filter(lambda x: x["format"] in ("raw", "hls")), [
            validate.union({
                "url":
                validate.get("baseUrl"),
                "format":
                validate.get("format"),
                "height":
                validate.all(validate.get("data"), validate.get("Height"))
            })
        ])

    @classmethod
    def can_handle_url(cls, url):
        return _url_re.match(url)

    def _get_api_res(self, api_type, api_id):
        try:
            res = http.get(self.api_url.format(type=api_type, id=api_id))
            return res
        except Exception as e:
            if "404" in str(e):
                self.logger.error("invalid {0} - {1}".format(api_type, api_id))
            elif "429" in str(e):
                self.logger.error(
                    "Too Many Requests, API rate limit exceeded.")
            raise NoStreamsError(self.url)

    def _get_vod_stream(self, vod_id):
        res = self._get_api_res("recordings", vod_id)

        for sdata in http.json(res, schema=self._vod_schema):
            if sdata["format"] == "hls":
                hls_url = urljoin(sdata["url"], "manifest.m3u8")
                yield "{0}p".format(sdata["height"]), HLSStream(
                    self.session, hls_url)

    def _get_live_stream(self, channel):
        res = self._get_api_res("channels", channel)

        channel_info = http.json(res)
        if not channel_info["online"]:
            return

        user_id = channel_info["id"]
        hls_url = self.api_url.format(type="channels",
                                      id="{0}/manifest.m3u8".format(user_id))
        for s in HLSStream.parse_variant_playlist(self.session,
                                                  hls_url).items():
            yield s

    def _get_streams(self):
        params = dict(parse_qsl(urlparse(self.url).query))
        vod_id = params.get("vod")
        match = _url_re.match(self.url)
        channel = match.group("channel")

        if vod_id:
            self.logger.debug("Looking for VOD {0} from channel: {1}", vod_id,
                              channel)
            return self._get_vod_stream(vod_id)
        else:
            self.logger.debug("Looking for channel: {0}", channel)
            return self._get_live_stream(channel)
Ejemplo n.º 11
0
            validate.optional("media_hosted_media"):
            object,
            "media_is_live":
            validate.all(validate.text, validate.transform(int),
                         validate.transform(bool)),
            "media_id":
            validate.text
        }],
    }, validate.get("livestream"), validate.length(1), validate.get(0))
_player_schema = validate.Schema({
    "clip": {
        "baseUrl":
        validate.any(None, validate.text),
        "bitrates":
        validate.all(
            validate.filter(lambda b: b.get("url") and b.get("label")),
            [{
                "label": validate.text,
                "url": validate.text,
            }],
        )
    },
    validate.optional("playlist"): [{
        validate.optional("connectionProvider"):
        validate.text,
        validate.optional("netConnectionUrl"):
        validate.text,
        validate.optional("bitrates"): [{
            "label": validate.text,
            "url": validate.text,
            "provider": validate.text
Ejemplo n.º 12
0
class BBCiPlayer(Plugin):
    """
    Allows streaming of live channels from bbc.co.uk/iplayer/live/* and of iPlayer programmes from
    bbc.co.uk/iplayer/episode/*
    """
    url_re = re.compile(
        r"""https?://(?:www\.)?bbc.co.uk/iplayer/
        (
            episode/(?P<episode_id>\w+)|
            live/(?P<channel_name>\w+)
        )
    """, re.VERBOSE)
    mediator_re = re.compile(
        r'window\.mediatorDefer\s*=\s*page\([^,]*,\s*({.*?})\);', re.DOTALL)
    tvip_re = re.compile(r'event_master_brand=(\w+?)&')
    account_locals_re = re.compile(r'window.bbcAccount.locals\s*=\s*({.*?});')
    swf_url = "http://emp.bbci.co.uk/emp/SMPf/1.18.3/StandardMediaPlayerChromelessFlash.swf"
    hash = base64.b64decode(
        b"N2RmZjc2NzFkMGM2OTdmZWRiMWQ5MDVkOWExMjE3MTk5MzhiOTJiZg==")
    api_url = (
        "http://open.live.bbc.co.uk/mediaselector/6/select/"
        "version/2.0/mediaset/{platform}/vpid/{vpid}/format/json/atk/{vpid_hash}/asn/1/"
    )
    platforms = ("pc", "iptv-all")
    session_url = "https://session.bbc.com/session"
    auth_url = "https://account.bbc.com/signin"

    mediator_schema = validate.Schema(
        {"episode": {
            "versions": [{
                "id": validate.text
            }]
        }}, validate.get("episode"), validate.get("versions"), validate.get(0),
        validate.get("id"))
    mediaselector_schema = validate.Schema(
        validate.transform(parse_json), {
            "media": [{
                "connection":
                [{
                    validate.optional("href"): validate.url(),
                    validate.optional("transferFormat"): validate.text
                }],
                "kind":
                validate.text
            }]
        }, validate.get("media"),
        validate.filter(lambda x: x["kind"] == "video"))
    options = PluginOptions({"password": None, "username": None})

    @classmethod
    def can_handle_url(cls, url):
        """ Confirm plugin can handle URL """
        return cls.url_re.match(url) is not None

    @classmethod
    def _hash_vpid(cls, vpid):
        return sha1(cls.hash + str(vpid).encode("utf8")).hexdigest()

    @classmethod
    def _extract_nonce(cls, http_result):
        """
        Given an HTTP response from the sessino endpoint, extract the nonce, so we can "sign" requests with it.
        We don't really sign the requests in the traditional sense of a nonce, we just incude them in the auth requests.

        :param http_result: HTTP response from the bbc session endpoint.
        :type http_result: requests.Response
        :return: nonce to "sign" url requests with
        :rtype: string
        """

        # Extract the redirect URL from the last call
        last_redirect_url = urlparse(http_result.history[-1].request.url)
        last_redirect_query = dict(parse_qsl(last_redirect_url.query))
        # Extract the nonce from the query string in the redirect URL
        final_url = urlparse(last_redirect_query['goto'])
        goto_url = dict(parse_qsl(final_url.query))
        goto_url_query = parse_json(goto_url['state'])

        # Return the nonce we can use for future queries
        return goto_url_query['nonce']

    def find_vpid(self, url, res=None):
        """
        Find the Video Packet ID in the HTML for the provided URL

        :param url: URL to download, if res is not provided.
        :param res: Provide a cached version of the HTTP response to search
        :type url: string
        :type res: requests.Response
        :return: Video Packet ID for a Programme in iPlayer
        :rtype: string
        """
        self.logger.debug("Looking for vpid on {0}", url)
        # Use pre-fetched page if available
        res = res or http.get(url)
        m = self.mediator_re.search(res.text)
        vpid = m and parse_json(m.group(1), schema=self.mediator_schema)
        return vpid

    def find_tvip(self, url):
        self.logger.debug("Looking for tvip on {0}", url)
        res = http.get(url)
        m = self.tvip_re.search(res.text)
        return m and m.group(1)

    def mediaselector(self, vpid):
        for platform in self.platforms:
            url = self.api_url.format(vpid=vpid,
                                      vpid_hash=self._hash_vpid(vpid),
                                      platform=platform)
            self.logger.debug("Info API request: {0}", url)
            stream_urls = http.get(url, schema=self.mediaselector_schema)
            for media in stream_urls:
                for connection in media["connection"]:
                    if connection.get("transferFormat") == "hds":
                        for s in HDSStream.parse_manifest(
                                self.session, connection["href"]).items():
                            yield s
                    if connection.get("transferFormat") == "hls":
                        for s in HLSStream.parse_variant_playlist(
                                self.session, connection["href"]).items():
                            yield s

    def login(self, ptrt_url):
        """
        Create session using BBC ID. See https://www.bbc.co.uk/usingthebbc/account/

        :param ptrt_url: The snapback URL to redirect to after successful authentication
        :type ptrt_url: string
        :return: Whether authentication was successful
        :rtype: bool
        """
        session_res = http.get(self.session_url, params=dict(ptrt=ptrt_url))

        http_nonce = self._extract_nonce(session_res)

        res = http.post(self.auth_url,
                        params=dict(ptrt=ptrt_url, nonce=http_nonce),
                        data=dict(jsEnabled=True,
                                  username=self.get_option("username"),
                                  password=self.get_option('password'),
                                  attempts=0),
                        headers={"Referer": self.url})

        return len(res.history) != 0

    def _get_streams(self):
        if not self.get_option("username"):
            self.logger.error(
                "BBC iPlayer requires an account you must login using "
                "--bbciplayer-username and --bbciplayer-password")
            return
        self.logger.info(
            "A TV License is required to watch BBC iPlayer streams, see the BBC website for more "
            "information: https://www.bbc.co.uk/iplayer/help/tvlicence")
        if not self.login(self.url):
            self.logger.error(
                "Could not authenticate, check your username and password")
            return

        m = self.url_re.match(self.url)
        episode_id = m.group("episode_id")
        channel_name = m.group("channel_name")

        if episode_id:
            self.logger.debug("Loading streams for episode: {0}", episode_id)
            vpid = self.find_vpid(self.url)
            if vpid:
                self.logger.debug("Found VPID: {0}", vpid)
                for s in self.mediaselector(vpid):
                    yield s
            else:
                self.logger.error("Could not find VPID for episode {0}",
                                  episode_id)
        elif channel_name:
            self.logger.debug("Loading stream for live channel: {0}",
                              channel_name)
            tvip = self.find_tvip(self.url)
            if tvip:
                self.logger.debug("Found TVIP: {0}", tvip)
                for s in self.mediaselector(tvip):
                    yield s
Ejemplo n.º 13
0
            validate.all([{
                "LinkType":
                validate.text,
                "Qualities": [
                    validate.all(
                        {
                            "Streams":
                            validate.all([
                                validate.all({"Stream": validate.text},
                                             validate.get("Stream"))
                            ], validate.get(0))
                        }, validate.get("Streams"))
                ],
                "Server":
                validate.text
            }], validate.filter(lambda s: s["LinkType"] in STREAMING_TYPES))
        }]
    }, validate.get("Data", {}))

_video_schema = validate.Schema(
    {
        "Data": [{
            "Assets":
            validate.all([{
                validate.optional("Links"):
                validate.all([{
                    "Target": validate.text,
                    "Uri": validate.text
                }], validate.filter(lambda l: l["Target"] in STREAMING_TYPES))
            }], validate.filter(lambda a: "Links" in a))
        }]