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)
/ (?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
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"))
def test_filter(self): assert validate(filter(lambda i: i > 5), [10, 5, 4, 6, 7]) == [10, 6, 7]
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)
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
/(?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": [{
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")
} 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\.)? (?:
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)
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
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
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)) }]