def setup_plugin_args(streamlink): """Sets Streamlink plugin options.""" plugin_args = PARSER.add_argument_group("Plugin options") for pname, plugin in streamlink.plugins.items(): defaults = {} for parg in plugin.arguments: if not parg.is_global: plugin_args.add_argument(parg.argument_name(pname), **parg.options) defaults[parg.dest] = parg.default else: pargdest = parg.dest for action in PARSER._actions: # find matching global argument if pargdest != action.dest: continue defaults[pargdest] = action.default # add plugin to global argument plugins = getattr(action, "plugins", []) plugins.append(pname) setattr(action, "plugins", plugins) plugin.options = PluginOptions(defaults) return True
def setup_plugin_args(session, parser): """Sets Streamlink plugin options.""" plugin_args = parser.add_argument_group("Plugin options") for pname, plugin in session.plugins.items(): defaults = {} group = plugin_args.add_argument_group(pname.capitalize()) for parg in plugin.arguments: if not parg.is_global: group.add_argument(parg.argument_name(pname), **parg.options) defaults[parg.dest] = parg.default else: pargdest = parg.dest for action in parser._actions: # find matching global argument if pargdest != action.dest: continue defaults[pargdest] = action.default # add plugin to global argument plugins = getattr(action, "plugins", []) plugins.append(pname) setattr(action, "plugins", plugins) plugin.options = PluginOptions(defaults)
class BTV(Plugin): options = PluginOptions({"username": None, "password": None}) url_re = re.compile(r"https?://(?:www\.)?btv\.bg/live/?") api_url = "http://www.btv.bg/lbin/global/player_config.php" check_login_url = "http://www.btv.bg/lbin/userRegistration/check_user_login.php" login_url = "http://www.btv.bg/bin/registration2/login.php?action=login&settings=0" media_id_re = re.compile(r"media_id=(\d+)") src_re = re.compile(r"src: \"(http.*?)\"") api_schema = validate.Schema( validate.all({ "status": "ok", "config": validate.text }, validate.get("config"), validate.all( validate.transform(src_re.search), validate.any(None, validate.get(1), validate.url())))) @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def login(self, username, password): res = http.post(self.login_url, data={ "username": username, "password": password }) return res.text.startswith("success") def get_hls_url(self, media_id): res = http.get(self.api_url, params=dict(media_id=media_id)) try: return parse_json(res.text, schema=self.api_schema) except PluginError: return def _get_streams(self): if not self.options.get("username") or not self.options.get( "username"): self.logger.error( "BTV requires registration, set the username and password" " with --btv-username and --btv-password") elif self.login(self.options.get("username"), self.options.get("password")): res = http.get(self.url) media_match = self.media_id_re.search(res.text) media_id = media_match and media_match.group(1) if media_id: self.logger.debug("Found media id: {0}", media_id) stream_url = self.get_hls_url(media_id) if stream_url: return HLSStream.parse_variant_playlist( self.session, stream_url) else: self.logger.error( "Login failed, a valid username and password is required")
def setup_plugin_args(session, parser): """Set Streamlink plugin options.""" plugin_args = parser.add_argument_group("Plugin options") for pname, plugin in session.plugins.items(): defaults = {} for parg in plugin.arguments: plugin_args.add_argument(parg.argument_name(pname), **parg.options) defaults[parg.dest] = parg.default plugin.options = PluginOptions(defaults)
class PCYourFreeTV(Plugin): _login_url = 'http://pc-yourfreetv.com/home.php' _url_re = re.compile( r'http://pc-yourfreetv\.com/index_player\.php\?channel=.+?&page_id=\d+' ) _video_url_re = re.compile( r"jwplayer\('.+?'\)\.setup\({.+?file: \"(?P<video_url>[^\"]+?)\".+?}\);", re.DOTALL) options = PluginOptions({'username': None, 'password': None}) @classmethod def can_handle_url(cls, url): return PCYourFreeTV._url_re.match(url) def login(self, username, password): res = http.post(self._login_url, data={ 'user_name': username, 'user_pass': password, 'login': '******' }) return username in res.text def _get_streams(self): username = self.get_option('username') password = self.get_option('password') if username is None or password is None: self.logger.error( "PC-YourFreeTV requires authentication, use --pcyourfreetv-username" "and --pcyourfreetv-password to set your username/password combination" ) return if self.login(username, password): self.logger.info("Successfully logged in as {0}", username) # Retrieve URL page and search for stream data res = http.get(self.url) match = self._video_url_re.search(res.text) if match is None: return video_url = match.group('video_url') if '.m3u8' in video_url: streams = HLSStream.parse_variant_playlist(self.session, video_url) if len(streams) != 0: for stream in streams.items(): yield stream else: # Not a HLS playlist yield 'live', HLSStream(self.session, video_url)
def setup_plugin_args(streamlink): """Sets Streamlink plugin options.""" plugin_args = PARSER.add_argument_group("Plugin options") for pname, plugin in streamlink.plugins.items(): defaults = {} for parg in plugin.arguments: plugin_args.add_argument(parg.argument_name(pname), **parg.options) defaults[parg.dest] = parg.default plugin.options = PluginOptions(defaults) return True
class Livestation(Plugin): options = PluginOptions({"email": "", "password": ""}) @classmethod def can_handle_url(self, url): return _url_re.match(url) def _authenticate(self, email, password): csrf_token = http.get(LOGIN_PAGE_URL, schema=_csrf_token_schema) if not csrf_token: raise PluginError("Unable to find CSRF token") data = { "authenticity_token": csrf_token, "channel_id": "", "commit": "Login", "plan_id": "", "session[email]": email, "session[password]": password, "utf8": "\xE2\x9C\x93", # Check Mark Character } res = http.post(LOGIN_POST_URL, data=data, acceptable_status=(200, 422)) result = http.json(res, schema=_login_schema) errors = result.get("errors") if errors: errors = ", ".join(errors) raise PluginError("Unable to authenticate: {0}".format(errors)) self.logger.info("Successfully logged in as {0}", result["email"]) def _get_streams(self): login_email = self.options.get("email") login_password = self.options.get("password") if login_email and login_password: self._authenticate(login_email, login_password) hls_playlist = http.get(self.url, schema=_hls_playlist_schema) if not hls_playlist: return return HLSStream.parse_variant_playlist(self.session, hls_playlist)
class AfreecaTV(Plugin): login_url = "https://member.afreecatv.com:8111/login/LoginAction.php" options = PluginOptions({"username": None, "password": None}) @classmethod def can_handle_url(self, url): return _url_re.match(url) @classmethod def stream_weight(cls, key): weight = QUALITY_WEIGHTS.get(key) if weight: return weight, "afreeca" return Plugin.stream_weight(key) def _get_channel_info(self, username): data = {"bid": username, "mode": "landing", "player_type": "html5"} res = http.post(CHANNEL_API_URL, data=data) return http.json(res, schema=_channel_schema) def _get_hls_key(self, broadcast, username, quality): headers = {"Referer": self.url} data = { "bid": username, "bno": broadcast, "pwd": "", "quality": quality, "type": "pwd" } res = http.post(CHANNEL_API_URL, data=data, headers=headers) return http.json(res, schema=_channel_schema) def _get_stream_info(self, broadcast, quality, cdn, rmd): params = { "return_type": cdn, "broad_key": "{broadcast}-flash-{quality}-hls".format(**locals()) } res = http.get(STREAM_INFO_URLS.format(rmd=rmd), params=params) return http.json(res, schema=_stream_schema) def _get_hls_stream(self, broadcast, username, quality, cdn, rmd): keyjson = self._get_hls_key(broadcast, username, quality) if keyjson["RESULT"] != CHANNEL_RESULT_OK: return key = keyjson["AID"] info = self._get_stream_info(broadcast, quality, cdn, rmd) if "view_url" in info: return HLSStream(self.session, info["view_url"], params=dict(aid=key)) def _login(self, username, password): data = { "szWork": "login", "szType": "json", "szUid": username, "szPassword": password, "isSaveId": "true", "isSavePw": "false", "isSaveJoin": "false" } res = http.post(self.login_url, data=data) res = http.json(res) if res["RESULT"] == 1: return True else: return False def _get_streams(self): if not self.session.get_option("hls-segment-ignore-names"): ignore_segment = ["_0", "_1", "_2"] self.session.set_option("hls-segment-ignore-names", ignore_segment) login_username = self.get_option("username") login_password = self.get_option("password") if login_username and login_password: self.logger.debug("Attempting login as {0}".format(login_username)) if self._login(login_username, login_password): self.logger.info( "Successfully logged in as {0}".format(login_username)) else: self.logger.info( "Failed to login as {0}".format(login_username)) match = _url_re.match(self.url) username = match.group("username") channel = self._get_channel_info(username) if channel.get("BPWD") == "Y": self.logger.error("Stream is Password-Protected") return elif channel.get("RESULT") == -6: self.logger.error("Login required") return elif channel.get("RESULT") != CHANNEL_RESULT_OK: return (broadcast, rmd, cdn) = (channel["BNO"], channel["RMD"], channel["CDN"]) if not (broadcast and rmd and cdn): return for qkey in QUALITYS: hls_stream = self._get_hls_stream(broadcast, username, qkey, cdn, rmd) if hls_stream: yield qkey, hls_stream
class Daisuki(Plugin): options = PluginOptions({ "mux_subtitles": False }) @classmethod def can_handle_url(cls, url): return _url_re.match(url) def _get_streams(self): page = http.get(self.url, schema=_schema) if not page: return pubkey_pem = get_public_key(self.cache, urljoin(self.url, page["clientlibs"])) if not pubkey_pem: raise PluginError("Unable to get public key") flashvars = page["flashvars"] params = { "cashPath": int(time.time() * 1000) } res = http.get(urljoin(self.url, flashvars["country"]), params=params) if not res: return language = http.xml(res, schema=_language_schema) api_params = {} for key in ("ss_id", "mv_id", "device_cd", "ss1_prm", "ss2_prm", "ss3_prm"): if flashvars.get(key, ""): api_params[key] = flashvars[key] aeskey = number.long_to_bytes(random.getrandbits(8 * 32), 32) params = { "s": flashvars["s"], "c": language, "e": self.url, "d": aes_encrypt(aeskey, json.dumps(api_params)), "a": rsa_encrypt(pubkey_pem, aeskey) } res = http.get(urljoin(self.url, flashvars["init"]), params=params) if not res: return rtn = http.json(res, schema=_init_schema) if not rtn: return init_data = parse_json(aes_decrypt(aeskey, rtn)) parsed = urlparse(init_data["play_url"]) if parsed.scheme != "https" or not parsed.path.startswith("/i/") or not parsed.path.endswith("/master.m3u8"): return hlsstream_url = init_data["play_url"] streams = HLSStream.parse_variant_playlist(self.session, hlsstream_url) if "caption_url" in init_data: if self.get_option("mux_subtitles") and FFMPEGMuxer.is_usable(self.session): res = http.get(init_data["caption_url"]) srt = http.xml(res, ignore_ns=True, schema=_xml_to_srt_schema) subfiles = [] metadata = {} for i, lang, srt in ((i, s[0], s[1]) for i, s in enumerate(srt)): subfile = tempfile.TemporaryFile() subfile.write(srt.encode("utf8")) subfile.seek(0) subfiles.append(FileStream(self.session, fileobj=subfile)) metadata["s:s:{0}".format(i)] = ["language={0}".format(lang)] for n, s in streams.items(): yield n, MuxedStream(self.session, s, *subfiles, maps=list(range(0, len(metadata) + 1)), metadata=metadata) return else: self.logger.info("Subtitles: {0}".format(init_data["caption_url"])) for s in streams.items(): yield s
class Crunchyroll(Plugin): options = PluginOptions({ "username": None, "password": None, "purge_credentials": None, "locale": API_DEFAULT_LOCALE }) @classmethod def can_handle_url(self, url): return _url_re.match(url) @classmethod def stream_weight(cls, key): weight = STREAM_WEIGHTS.get(key) if weight: return weight, "crunchyroll" return Plugin.stream_weight(key) def _get_streams(self): api = self._create_api() match = _url_re.match(self.url) media_id = int(match.group("media_id")) try: info = api.get_info(media_id, fields=["media.stream_data"], schema=_media_schema) except CrunchyrollAPIError as err: raise PluginError(u"Media lookup error: {0}".format(err.msg)) if not info: return streams = {} # The adaptive quality stream sometimes a subset of all the other streams listed, ultra is no included has_adaptive = any( [s[u"quality"] == u"adaptive" for s in info[u"streams"]]) if has_adaptive: self.logger.debug(u"Loading streams from adaptive playlist") for stream in filter(lambda x: x[u"quality"] == u"adaptive", info[u"streams"]): for q, s in HLSStream.parse_variant_playlist( self.session, stream[u"url"]).items(): # rename the bitrates to low, mid, or high. ultra doesn't seem to appear in the adaptive streams name = STREAM_NAMES.get(q, q) streams[name] = s # If there is no adaptive quality stream then parse each individual result for stream in info[u"streams"]: if stream[u"quality"] != u"adaptive": # the video_encode_id indicates that the stream is not a variant playlist if u"video_encode_id" in stream: streams[stream[u"quality"]] = HLSStream( self.session, stream[u"url"]) else: # otherwise the stream url is actually a list of stream qualities for q, s in HLSStream.parse_variant_playlist( self.session, stream[u"url"]).items(): # rename the bitrates to low, mid, or high. ultra doesn't seem to appear in the adaptive streams name = STREAM_NAMES.get(q, q) streams[name] = s return streams def _get_device_id(self): """Returns the saved device id or creates a new one and saves it.""" device_id = self.cache.get("device_id") if not device_id: # Create a random device id and cache it for a year char_set = string.ascii_letters + string.digits device_id = "".join(random.sample(char_set, 32)) self.cache.set("device_id", device_id, 365 * 24 * 60 * 60) return device_id def _create_api(self): """Creates a new CrunchyrollAPI object, initiates it's session and tries to authenticate it either by using saved credentials or the user's username and password. """ if self.options.get("purge_credentials"): self.cache.set("session_id", None, 0) self.cache.set("auth", None, 0) current_time = datetime.datetime.utcnow() device_id = self._get_device_id() locale = self.options.get("locale") api = CrunchyrollAPI(self.cache.get("session_id"), self.cache.get("auth"), locale) self.logger.debug("Creating session") try: api.session_id = api.start_session(device_id, schema=_session_schema) except CrunchyrollAPIError as err: if err.code == "bad_session": self.logger.debug( "Current session has expired, creating a new one") api = CrunchyrollAPI(locale=locale) api.session_id = api.start_session(device_id, schema=_session_schema) else: raise err # Save session and hope it lasts for a few hours self.cache.set("session_id", api.session_id, 4 * 60 * 60) self.logger.debug("Session created") if api.auth: self.logger.debug("Using saved credentials") elif self.options.get("username"): try: self.logger.info( "Attempting to login using username and password") login = api.login(self.options.get("username"), self.options.get("password"), schema=_login_schema) api.auth = login["auth"] self.logger.info( "Successfully logged in as '{0}'", login["user"]["username"] or login["user"]["email"]) expires = (login["expires"] - current_time).total_seconds() self.cache.set("auth", login["auth"], expires) except CrunchyrollAPIError as err: raise PluginError(u"Authentication error: {0}".format(err.msg)) else: self.logger.warning( "No authentication provided, you won't be able to access " "premium restricted content") return api
class BTSports(Plugin): url_re = re.compile(r"https?://sport.bt.com") options = PluginOptions({"username": None, "password": None}) content_re = re.compile(r"CONTENT_(\w+)\s*=\s*'(\w+)'") saml_re = re.compile(r'''name="SAMLResponse" value="(.*?)"''', re.M | re.DOTALL) api_url = "https://be.avs.bt.com/AVS/besc" saml_url = "https://samlfed.bt.com/sportgetfedwebhls" login_url = "https://signin1.bt.com/siteminderagent/forms/login.fcc" def __init__(self, url): super().__init__(url) http.headers = {"User-Agent": useragents.FIREFOX} @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def login(self, username, password): self.logger.debug("Logging in as {0}".format(username)) redirect_to = "https://home.bt.com/ss/Satellite/secure/loginforward?redirectURL={0}".format( quote(self.url)) data = { "cookieExpp": "30", "Switch": "yes", "SMPostLoginUrl": "/appsyouraccount/secure/postlogin", "loginforward": "https://home.bt.com/ss/Satellite/secure/loginforward", "smauthreason": "0", "TARGET": redirect_to, "USER": username, "PASSWORD": password } res = http.post(self.login_url, data=data) self.logger.debug("Redirected to: {0}".format(res.url)) if url_equal(res.url, self.url, ignore_scheme=True): self.logger.debug("Login successful, getting SAML token") res = http.get( "https://samlfed.bt.com/sportgetfedwebhls?bt.cid={0}".format( self.acid())) d = self.saml_re.search(res.text) if d: saml_data = d.group(1) self.logger.debug("BT Sports federated login...") res = http.post(self.api_url, params={ "action": "LoginBT", "channel": "WEBHLS", "bt.cid": self.acid }, data={"SAMLResponse": saml_data}) fed_json = http.json(res) success = fed_json['resultCode'] == "OK" if not success: self.logger.error("Failed to login: {0} - {1}".format( fed_json['errorDescription'], fed_json['message'])) return success return False def device_id(self): device_id = self.cache.get("device_id") or str(uuid4()) self.cache.set("device_id", device_id) return device_id def acid(self): acid = self.cache.get("acid") or "{cid}-B-{timestamp}".format( cid=self.device_id(), timestamp=int(time.time())) self.cache.set("acid", acid) return acid def _get_cdn(self, channel_id, channel_type="LIVE"): d = { "action": "GetCDN", "type": channel_type, "id": channel_id, "channel": "WEBHLS", "asJson": "Y", "bt.cid": self.acid(), "_": int(time.time()) } res = http.get(self.api_url, params=d, headers={"Accept": "application/json"}) return http.json(res) def _get_streams(self): if self.options.get("email") and self.options.get("password"): if self.login(self.options.get("email"), self.options.get("password")): self.logger.debug( "Logged in and authenticated with BT Sports.") res = http.get(self.url) m = self.content_re.findall(res.text) if m: info = dict(m) data = self._get_cdn(info.get("ID"), info.get("TYPE")) if data['resultCode'] == 'OK': return HLSStream.parse_variant_playlist( self.session, data['resultObj']['src']) else: self.logger.error( "Failed to get stream with error: {0} - {1}". format(data['errorDescription'], data['message'])) else: self.logger.error( "A username and password is required to use BT Sports")
class TVPlayer(Plugin): api_url = "http://api.tvplayer.com/api/v2/stream/live" login_url = "https://tvplayer.com/account/login" update_url = "https://tvplayer.com/account/update-detail" dummy_postcode = "SE1 9LT" # location of ITV HQ in London url_re = re.compile( r"https?://(?:www.)?tvplayer.com/(:?watch/?|watch/(.+)?)") stream_attrs_re = re.compile( r'var\s+(validate|platform|resourceId|token)\s+=\s*(.*?);', re.S) login_token_re = re.compile(r'input.*?name="token".*?value="(\w+)"') stream_schema = validate.Schema( { "tvplayer": validate.Schema({ "status": u'200 OK', "response": validate.Schema({ "stream": validate.url(scheme=validate.any("http")), validate.optional("drmToken"): validate.any(None, validate.text) }) }) }, validate.get("tvplayer"), validate.get("response")) options = PluginOptions({"email": None, "password": None}) @classmethod def can_handle_url(cls, url): match = TVPlayer.url_re.match(url) return match is not None def __init__(self, url): super(TVPlayer, self).__init__(url) http.headers.update({"User-Agent": useragents.CHROME}) def authenticate(self, username, password): res = http.get(self.login_url) match = self.login_token_re.search(res.text) token = match and match.group(1) res2 = http.post(self.login_url, data=dict(email=username, password=password, token=token), allow_redirects=False) # there is a 302 redirect on a successful login return res2.status_code == 302 def _get_streams(self): if self.get_option("email") and self.get_option("password"): self.authenticate(self.get_option("email"), self.get_option("password")) # find the list of channels from the html in the page self.url = self.url.replace("https", "http") # https redirects to http res = http.get(self.url) if "enter your postcode" in res.text: self.logger.info( "Setting your postcode to: {0}. " "This can be changed in the settings on tvplayer.com", self.dummy_postcode) res = http.post(self.update_url, data=dict(postcode=self.dummy_postcode), params=dict(return_url=self.url)) stream_attrs = dict((k, v.strip('"')) for k, v in self.stream_attrs_re.findall(res.text)) if "resourceId" in stream_attrs and "validate" in stream_attrs and "platform" in stream_attrs: # get the stream urls res = http.post(self.api_url, data=dict(service=1, id=stream_attrs["resourceId"], validate=stream_attrs["validate"], platform=stream_attrs["platform"], token=stream_attrs.get("token"))) stream_data = http.json(res, schema=self.stream_schema) if stream_data.get("drmToken"): self.logger.error( "This stream is protected by DRM can cannot be played") return else: return HLSStream.parse_variant_playlist( self.session, stream_data["stream"]) else: if "need to login" in res.text: self.logger.error( "You need to login using --tvplayer-email/--tvplayer-password to view this stream" )
class BBCiPlayer(Plugin): 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") config_url = "http://www.bbc.co.uk/idcta/config" auth_url = "https://account.bbc.com/signin" config_schema = validate.Schema( validate.transform(parse_json), { "signin_url": validate.url(), "identity": { "cookieAgeDays": int, "accessTokenCookieName": validate.text, "idSignedInCookieName": validate.text } }) 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): return cls.url_re.match(url) is not None @classmethod def _hash_vpid(cls, vpid): return sha1(cls.hash + str(vpid).encode("utf8")).hexdigest() def find_vpid(self, url, res=None): 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, context="tvandiplayer"): # get the site config, to find the signin url config = http.get(self.config_url, params=dict(ptrt=ptrt_url), schema=self.config_schema) res = http.get(config["signin_url"], params=dict(userOrigin=context, context=context), headers={"Referer": self.url}) m = self.account_locals_re.search(res.text) if m: auth_data = parse_json(m.group(1)) res = http.post(self.auth_url, params=dict(context=auth_data["userOrigin"], ptrt=auth_data["ptrt"]["value"], userOrigin=auth_data["userOrigin"], nonce=auth_data["nonce"]), data=dict(jsEnabled="false", attempts=0, username=self.get_option("username"), password=self.get_option("password"))) # redirects to ptrt_url on successful login if res.url == ptrt_url: return res else: self.logger.error( "Could not authenticate, could not find the authentication nonce" ) 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") page_res = self.login(self.url) if not page_res: 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, res=page_res) 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
class AnimeLab(Plugin): url_re = re.compile(r"https?://(?:www\.)?animelab\.com/player/") login_url = "https://www.animelab.com/login" video_collection_re = re.compile(r"VideoCollection\((\[.*?\])\);") playlist_position_re = re.compile(r"playlistPosition\s*=\s*(\d+);") video_collection_schema = validate.Schema( validate.union({ "position": validate.all( validate.transform(playlist_position_re.search), validate.any( None, validate.all(validate.get(1), validate.transform(int)) ) ), "playlist": validate.all( validate.transform(video_collection_re.search), validate.any( None, validate.all( validate.get(1), validate.transform(parse_json) ) ) ) }) ) options = PluginOptions({ "email": None, "password": None }) @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def login(self, email, password): self.logger.debug("Attempting to log in as {0}", email) res = http.post(self.login_url, data=dict(email=email, password=password), allow_redirects=False, raise_for_status=False) loc = res.headers.get("Location", "") if "geoblocked" in loc.lower(): self.logger.error("AnimeLab is not available in your territory") elif res.status_code >= 400: self.logger.error("Failed to login to AnimeLab, check your email/password combination") else: return True return False def _get_streams(self): email, password = self.get_option("email"), self.get_option("password") if not email or not password: self.logger.error("AnimeLab requires authentication, use --animelab-email " "and --animelab-password to set your email/password combination") return if self.login(email, password): self.logger.info("Successfully logged in as {0}", email) video_collection = http.get(self.url, schema=self.video_collection_schema) if video_collection["playlist"] is None or video_collection["position"] is None: return data = video_collection["playlist"][video_collection["position"]] self.logger.debug("Found {0} version {1} hard-subs", data["language"]["name"], "with" if data["hardSubbed"] else "without") for video in data["videoInstances"]: if video["httpUrl"]: q = video["videoQuality"]["description"] s = HTTPStream(self.session, video["httpUrl"]) yield q, s
class Zattoo(Plugin): API_HELLO = '{0}/zapi/session/hello' API_LOGIN = '******' API_CHANNELS = '{0}/zapi/v2/cached/channels/{1}?details=False' API_WATCH = '{0}/zapi/watch' API_WATCH_VOD = '{0}/zapi/avod/videos/{1}/watch' _url_re = re.compile( r''' https?:// (?P<base_url> zattoo\.com | tvonline\.ewe\.de | nettv\.netcologne\.de )/(?:watch/(?P<channel>[^/\s]+) | ondemand/watch/(?P<vod_id>[^-]+)-) ''', re.VERBOSE) _app_token_re = re.compile(r"""window\.appToken\s+=\s+'([^']+)'""") _channels_schema = validate.Schema( { 'success': int, 'channel_groups': [{ 'channels': [ { 'display_alias': validate.text, 'cid': validate.text }, ] }] }, validate.get('channel_groups'), ) options = PluginOptions({ 'email': None, 'password': None, 'purge_credentials': None }) def __init__(self, url): super(Zattoo, self).__init__(url) self._session_attributes = Cache(filename='plugin-cache.json', key_prefix='zattoo:attributes') self._authed = self._session_attributes.get( 'beaker.session.id') and self._session_attributes.get( 'pzuid') and self._session_attributes.get('power_guide_hash') self._uuid = self._session_attributes.get('uuid') self._expires = self._session_attributes.get('expires') self.base_url = 'https://{0}'.format( Zattoo._url_re.match(url).group('base_url')) self.headers = { 'User-Agent': useragents.CHROME, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': self.base_url } @classmethod def can_handle_url(cls, url): return Zattoo._url_re.match(url) def _hello(self): self.logger.debug('_hello ...') res = http.get(self.base_url) match = self._app_token_re.search(res.text) app_token = match.group(1) hello_url = self.API_HELLO.format(self.base_url) if self._uuid: __uuid = self._uuid else: __uuid = str(uuid.uuid4()) self._session_attributes.set('uuid', __uuid, expires=3600 * 24) params = { 'client_app_token': app_token, 'uuid': __uuid, 'lang': 'en', 'format': 'json' } res = http.post(hello_url, headers=self.headers, data=params) return res def _login(self, email, password, _hello): self.logger.debug('_login ... Attempting login as {0}'.format(email)) login_url = self.API_LOGIN.format(self.base_url) params = {'login': email, 'password': password, 'remember': 'true'} res = http.post(login_url, headers=self.headers, data=params, cookies=_hello.cookies) data = http.json(res) self._authed = data['success'] if self._authed: self.logger.debug('New Session Data') self._session_attributes.set('beaker.session.id', res.cookies.get('beaker.session.id'), expires=3600 * 24) self._session_attributes.set('pzuid', res.cookies.get('pzuid'), expires=3600 * 24) self._session_attributes.set('power_guide_hash', data['session']['power_guide_hash'], expires=3600 * 24) return self._authed else: return None def _watch(self): self.logger.debug('_watch ...') match = self._url_re.match(self.url) if not match: return channel = match.group('channel') vod_id = match.group('vod_id') cookies = { 'beaker.session.id': self._session_attributes.get('beaker.session.id'), 'pzuid': self._session_attributes.get('pzuid') } watch_url = [] if channel: params, watch_url = self._watch_live(channel, cookies) elif vod_id: params, watch_url = self._watch_vod(vod_id) if not watch_url: return res = [] try: res = http.post(watch_url, headers=self.headers, data=params, cookies=cookies) except Exception as e: if '404 Client Error' in str(e): self.logger.error( 'Unfortunately streaming is not permitted in this country or this channel does not exist.' ) elif '402 Client Error: Payment Required' in str(e): self.logger.error( 'Paid subscription required for this channel.') self.logger.info( 'If paid subscription exist, use --zattoo-purge-credentials to start a new session.' ) else: self.logger.error(str(e)) return data = http.json(res) if data['success']: for hls_url in data['stream']['watch_urls']: for s in HLSStream.parse_variant_playlist( self.session, hls_url['url']).items(): yield s def _watch_live(self, channel, cookies): self.logger.debug('_watch_live ... Channel: {0}'.format(channel)) watch_url = self.API_WATCH.format(self.base_url) channels_url = self.API_CHANNELS.format( self.base_url, self._session_attributes.get('power_guide_hash')) res = http.get(channels_url, headers=self.headers, cookies=cookies) data = http.json(res, schema=self._channels_schema) c_list = [] for d in data: for c in d['channels']: c_list.append(c) cid = [] zattoo_list = [] for c in c_list: zattoo_list.append(c['display_alias']) if c['display_alias'] == channel: cid = c['cid'] self.logger.debug( 'Available zattoo channels in this country: {0}'.format(', '.join( sorted(zattoo_list)))) if not cid: cid = channel self.logger.debug('CHANNEL ID: {0}'.format(cid)) params = {'cid': cid, 'https_watch_urls': True, 'stream_type': 'hls'} return params, watch_url def _watch_vod(self, vod_id): self.logger.debug('_watch_vod ...') watch_url = self.API_WATCH_VOD.format(self.base_url, vod_id) params = {'https_watch_urls': True, 'stream_type': 'hls'} return params, watch_url def _get_streams(self): email = self.get_option('email') password = self.get_option('password') if self.options.get('purge_credentials'): self._session_attributes.set('beaker.session.id', None, expires=0) self._session_attributes.set('expires', None, expires=0) self._session_attributes.set('power_guide_hash', None, expires=0) self._session_attributes.set('pzuid', None, expires=0) self._session_attributes.set('uuid', None, expires=0) self._authed = False self.logger.info('All credentials were successfully removed.') if not self._authed and (not email and not password): self.logger.error( 'A login for Zattoo is required, use --zattoo-email EMAIL --zattoo-password PASSWORD to set them' ) return if self._authed: if self._expires < time.time(): # login after 24h expires = time.time() + 3600 * 24 self._session_attributes.set('expires', expires, expires=3600 * 24) self._authed = False if not self._authed: __hello = self._hello() if not self._login(email, password, __hello): self.logger.error( 'Failed to login, check your username/password') return return self._watch()
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
class WWENetwork(Plugin): url_re = re.compile(r"https?://network.wwe.com") content_id_re = re.compile(r'''"content_id" : "(\d+)"''') playback_scenario = "HTTP_CLOUD_WIRED" login_url = "https://secure.net.wwe.com/workflow.do" login_page_url = "https://secure.net.wwe.com/enterworkflow.do?flowId=account.login&forwardUrl=http%3A%2F%2Fnetwork.wwe.com" api_url = "https://ws.media.net.wwe.com/ws/media/mf/op-findUserVerifiedEvent/v-2.3" _info_schema = validate.Schema( validate.union({ "status": validate.union({ "code": validate.all(validate.xml_findtext(".//status-code"), validate.transform(int)), "message": validate.xml_findtext(".//status-message"), }), "urls": validate.all(validate.xml_findall(".//url"), [validate.getattr("text")]), validate.optional("fingerprint"): validate.xml_findtext(".//updated-fingerprint"), validate.optional("session_key"): validate.xml_findtext(".//session-key"), "session_attributes": validate.all(validate.xml_findall(".//session-attribute"), [ validate.getattr("attrib"), validate.union({ "name": validate.get("name"), "value": validate.get("value") }) ]) })) options = PluginOptions({ "email": None, "password": None, }) def __init__(self, url): super(WWENetwork, self).__init__(url) http.headers.update({"User-Agent": useragents.CHROME}) self._session_attributes = Cache(filename="plugin-cache.json", key_prefix="wwenetwork:attributes") self._session_key = self.cache.get("session_key") self._authed = self._session_attributes.get( "ipid") and self._session_attributes.get("fprt") @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def login(self, email, password): self.logger.debug("Attempting login as {0}", email) # sets some required cookies to login http.get(self.login_page_url) # login res = http.post(self.login_url, data=dict(registrationAction='identify', emailAddress=email, password=password, submitButton=""), headers={"Referer": self.login_page_url}, allow_redirects=False) self._authed = "Authentication Error" not in res.text if self._authed: self._session_attributes.set("ipid", res.cookies.get("ipid"), expires=3600 * 1.5) self._session_attributes.set("fprt", res.cookies.get("fprt"), expires=3600 * 1.5) return self._authed def _update_session_attribute(self, key, value): if value: self._session_attributes.set(key, value, expires=3600 * 1.5) # 1h30m expiry http.cookies.set(key, value) @property def session_key(self): return self._session_key @session_key.setter def session_key(self, value): self.cache.set("session_key", value) self._session_key = value def _get_media_info(self, content_id): """ Get the info about the content, based on the ID :param content_id: :return: """ params = { "identityPointId": self._session_attributes.get("ipid"), "fingerprint": self._session_attributes.get("fprt"), "contentId": content_id, "playbackScenario": self.playback_scenario, "platform": "WEB_MEDIAPLAYER_5", "subject": "LIVE_EVENT_COVERAGE", "frameworkURL": "https://ws.media.net.wwe.com", "_": int(time.time()) } if self.session_key: params["sessionKey"] = self.session_key url = self.api_url.format(id=content_id) res = http.get(url, params=params) return http.xml(res, ignore_ns=True, schema=self._info_schema) def _get_content_id(self): # check the page to find the contentId res = http.get(self.url) m = self.content_id_re.search(res.text) if m: return m.group(1) def _get_streams(self): email = self.get_option("email") password = self.get_option("password") if not self._authed and (not email and not password): self.logger.error( "A login for WWE Network is required, use --wwenetwork-email/" "--wwenetwork-password to set them") return if not self._authed: if not self.login(email, password): self.logger.error( "Failed to login, check your username/password") return content_id = self._get_content_id() if content_id: self.logger.debug("Found content ID: {0}", content_id) info = self._get_media_info(content_id) if info["status"]["code"] == 1: # update the session attributes self._update_session_attribute("fprt", info.get("fingerprint")) for attr in info["session_attributes"]: self._update_session_attribute(attr["name"], attr["value"]) if info.get("session_key"): self.session_key = info.get("session_key") for url in info["urls"]: for s in HLSStream.parse_variant_playlist( self.session, url, name_fmt="{pixels}_{bitrate}").items(): yield s else: raise PluginError( "Could not load streams: {message} ({code})".format( **info["status"]))
class UFCTV(Plugin): url_re = re.compile(r"https?://(?:www\.)?ufc\.tv/(channel|video)/.+") video_info_re = re.compile(r"""program\s*=\s*(\{.*?});""", re.DOTALL) channel_info_re = re.compile(r"""g_channel\s*=\s(\{.*?});""", re.DOTALL) stream_api_url = "https://www.ufc.tv/service/publishpoint" auth_url = "https://www.ufc.tv/secure/authenticate" auth_schema = validate.Schema(validate.xml_findtext("code")) options = PluginOptions({"username": None, "password": None}) @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def _get_stream_url(self, video_id, vtype="video"): res = http.post(self.stream_api_url, data={ "id": video_id, "type": vtype, "format": "json" }, headers={"User-Agent": useragents.IPHONE_6}) data = http.json(res) return data.get("path") def _get_info(self, url): res = http.get(url) # try to find video info first m = self.video_info_re.search(res.text) if not m: # and channel info if that fails m = self.channel_info_re.search(res.text) return m and js_to_json(m.group(1)) def _login(self, username, password): res = http.post(self.auth_url, data={ "username": username, "password": password, "cookielink": False }) login_status = http.xml(res, schema=self.auth_schema) self.logger.debug("Login status for {0}: {1}", username, login_status) if login_status == "loginlocked": self.logger.error( "The account {0} has been locked, the password needs to be reset" ) return login_status == "loginsuccess" def _get_streams(self): if self.get_option("username") and self.get_option("password"): self.logger.debug("Attempting login as {0}", self.get_option("username")) if self._login(self.get_option("username"), self.get_option("password")): self.logger.info("Successfully logged in as {0}", self.get_option("username")) else: self.logger.info("Failed to login as {0}", self.get_option("username")) video = self._get_info(self.url) if video: self.logger.debug("Found {type}: {name}", **video) surl = self._get_stream_url(video['id'], video.get('type', "video")) surl = surl.replace("_iphone", "") if surl: return HLSStream.parse_variant_playlist(self.session, surl) else: self.logger.error( "Could not get stream URL for video: {name} ({id})", **video) else: self.logger.error("Could not find any video info on the page")
class Crunchyroll(Plugin): options = PluginOptions({ "username": None, "password": None, "purge_credentials": None, "locale": API_DEFAULT_LOCALE }) @classmethod def can_handle_url(self, url): return _url_re.match(url) @classmethod def stream_weight(cls, key): weight = STREAM_WEIGHTS.get(key) if weight: return weight, "crunchyroll" return Plugin.stream_weight(key) def _get_streams(self): api = self._create_api() match = _url_re.match(self.url) media_id = int(match.group("media_id")) try: info = api.get_info(media_id, fields=["media.stream_data"], schema=_media_schema) except CrunchyrollAPIError as err: raise PluginError(u"Media lookup error: {0}".format(err.msg)) if not info: return # TODO: Use dict comprehension here after dropping Python 2.6 support. return dict((stream["quality"], HLSStream(self.session, stream["url"])) for stream in info["streams"]) def _get_device_id(self): """Returns the saved device id or creates a new one and saves it.""" device_id = self.cache.get("device_id") if not device_id: # Create a random device id and cache it for a year char_set = string.ascii_letters + string.digits device_id = "".join(random.sample(char_set, 32)) self.cache.set("device_id", device_id, 365 * 24 * 60 * 60) return device_id def _create_api(self): """Creates a new CrunchyrollAPI object, initiates it's session and tries to authenticate it either by using saved credentials or the user's username and password. """ if self.options.get("purge_credentials"): self.cache.set("session_id", None, 0) self.cache.set("auth", None, 0) current_time = datetime.datetime.utcnow() device_id = self._get_device_id() locale = self.options.get("locale") api = CrunchyrollAPI(self.cache.get("session_id"), self.cache.get("auth"), locale) self.logger.debug("Creating session") try: api.session_id = api.start_session(device_id, schema=_session_schema) except CrunchyrollAPIError as err: if err.code == "bad_session": self.logger.debug( "Current session has expired, creating a new one") api = CrunchyrollAPI(locale=locale) api.session_id = api.start_session(device_id, schema=_session_schema) else: raise err # Save session and hope it lasts for a few hours self.cache.set("session_id", api.session_id, 4 * 60 * 60) self.logger.debug("Session created") if api.auth: self.logger.debug("Using saved credentials") elif self.options.get("username"): try: self.logger.info( "Attempting to login using username and password") login = api.login(self.options.get("username"), self.options.get("password"), schema=_login_schema) api.auth = login["auth"] self.logger.info("Successfully logged in as '{0}'", login["user"]["username"]) expires = (login["expires"] - current_time).total_seconds() self.cache.set("auth", login["auth"], expires) except CrunchyrollAPIError as err: raise PluginError(u"Authentication error: {0}".format(err.msg)) else: self.logger.warning( "No authentication provided, you won't be able to access " "premium restricted content") return api
class Rtve(Plugin): secret_key = base64.b64decode("eWVMJmRhRDM=") content_id_re = re.compile(r'data-id\s*=\s*"(\d+)"') url_re = re.compile( r""" https?://(?:www\.)?rtve\.es/(?:directo|noticias|television|deportes|alacarta|drmn)/.*?/? """, re.VERBOSE) cdn_schema = validate.Schema( validate.transform(partial(parse_xml, invalid_char_entities=True)), validate.xml_findall(".//preset"), [ validate.union({ "quality": validate.all(validate.getattr("attrib"), validate.get("type")), "urls": validate.all(validate.xml_findall(".//url"), [validate.getattr("text")]) }) ]) subtitles_api = "http://www.rtve.es/api/videos/{id}/subtitulos.json" subtitles_schema = validate.Schema( {"page": { "items": [{ "src": validate.url(), "lang": validate.text }] }}, validate.get("page"), validate.get("items")) video_api = "http://www.rtve.es/api/videos/{id}.json" video_schema = validate.Schema( { "page": { "items": [{ "qualities": [{ "preset": validate.text, "height": int }] }] } }, validate.get("page"), validate.get("items"), validate.get(0)) options = PluginOptions({"mux_subtitles": False}) @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def __init__(self, url): Plugin.__init__(self, url) self.zclient = ZTNRClient(self.secret_key) http.headers = {"User-Agent": useragents.SAFARI_8} def _get_content_id(self): res = http.get(self.url) m = self.content_id_re.search(res.text) return m and int(m.group(1)) def _get_subtitles(self, content_id): res = http.get(self.subtitles_api.format(id=content_id)) return http.json(res, schema=self.subtitles_schema) def _get_quality_map(self, content_id): res = http.get(self.video_api.format(id=content_id)) data = http.json(res, schema=self.video_schema) qmap = {} for item in data["qualities"]: qname = { "MED": "Media", "HIGH": "Alta", "ORIGINAL": "Original" }.get(item["preset"], item["preset"]) qmap[qname] = u"{0}p".format(item["height"]) return qmap def _get_streams(self): streams = [] content_id = self._get_content_id() if content_id: self.logger.debug("Found content with id: {0}", content_id) stream_data = self.zclient.get_cdn_list(content_id, schema=self.cdn_schema) quality_map = None for stream in stream_data: for url in stream["urls"]: if url.endswith("m3u8"): try: streams.extend( HLSStream.parse_variant_playlist( self.session, url).items()) except (IOError, OSError): self.logger.debug("Failed to load m3u8 url: {0}", url) elif ((url.endswith("mp4") or url.endswith("mov") or url.endswith("avi")) and http.head( url, raise_for_status=False).status_code == 200): if quality_map is None: # only make the request when it is necessary quality_map = self._get_quality_map(content_id) # rename the HTTP sources to match the HLS sources quality = quality_map.get(stream["quality"], stream["quality"]) streams.append((quality, HTTPStream(self.session, url))) subtitles = None if self.get_option("mux_subtitles"): subtitles = self._get_subtitles(content_id) if subtitles: substreams = {} for i, subtitle in enumerate(subtitles): substreams[subtitle["lang"]] = HTTPStream( self.session, subtitle["src"]) for q, s in streams: yield q, MuxedStream(self.session, s, subtitles=substreams) else: for s in streams: yield s
class NPO(Plugin): api_url = "http://ida.omroep.nl/app.php/{endpoint}" url_re = re.compile(r"https?://(\w+\.)?(npo\.nl|zapp\.nl|zappelin\.nl)/") media_id_re = re.compile(r'''<npo-player\smedia-id=["'](?P<media_id>[^"']+)["']''') prid_re = re.compile(r'''(?:data(-alt)?-)?prid\s*[=:]\s*(?P<q>["'])(\w+)(?P=q)''') react_re = re.compile(r'''data-react-props\s*=\s*(?P<q>["'])(?P<data>.*?)(?P=q)''') auth_schema = validate.Schema({"token": validate.text}, validate.get("token")) streams_schema = validate.Schema({ "items": [ [{ "label": validate.text, "contentType": validate.text, "url": validate.url(), "format": validate.text }] ] }, validate.get("items"), validate.get(0)) stream_info_schema = validate.Schema(validate.any( validate.url(), validate.all({"errorcode": 0, "url": validate.url()}, validate.get("url")) )) options = PluginOptions({ "subtitles": False }) @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def __init__(self, url): super(NPO, self).__init__(url) self._token = None http.headers.update({"User-Agent": useragents.CHROME}) def api_call(self, endpoint, schema=None, params=None): url = self.api_url.format(endpoint=endpoint) res = http.get(url, params=params) return http.json(res, schema=schema) @property def token(self): if not self._token: self._token = self.api_call("auth", schema=self.auth_schema) return self._token def _get_prid(self, subtitles=False): res = http.get(self.url) bprid = None # Locate the asset id for the content on the page for alt, _, prid in self.prid_re.findall(res.text): if alt and subtitles: bprid = prid elif bprid is None: bprid = prid if bprid is None: m = self.react_re.search(res.text) if m: data = parse_json(m.group("data").replace(""", '"')) bprid = data.get("mid") if bprid is None: m = self.media_id_re.search(res.text) if m: bprid = m.group('media_id') return bprid def _get_streams(self): asset_id = self._get_prid(self.get_option("subtitles")) if asset_id: self.logger.debug("Found asset id: {0}", asset_id) streams = self.api_call(asset_id, params=dict(adaptive="yes", token=self.token), schema=self.streams_schema) for stream in streams: if stream["format"] in ("adaptive", "hls", "mp4"): if stream["contentType"] == "url": stream_url = stream["url"] else: # using type=json removes the javascript function wrapper info_url = stream["url"].replace("type=jsonp", "type=json") # find the actual stream URL stream_url = http.json(http.get(info_url), schema=self.stream_info_schema) if stream["format"] in ("adaptive", "hls"): for s in HLSStream.parse_variant_playlist(self.session, stream_url).items(): yield s elif stream["format"] in ("mp3", "mp4"): yield "vod", HTTPStream(self.session, stream_url)
class TVPlayer(Plugin): context_url = "http://tvplayer.com/watch/context" api_url = "http://api.tvplayer.com/api/v2/stream/live" login_url = "https://tvplayer.com/account/login" update_url = "https://tvplayer.com/account/update-detail" dummy_postcode = "SE1 9LT" # location of ITV HQ in London url_re = re.compile( r"https?://(?:www.)?tvplayer.com/(:?watch/?|watch/(.+)?)") stream_attrs_re = re.compile( r'data-(resource|token|channel-id)\s*=\s*"(.*?)"', re.S) data_id_re = re.compile(r'data-id\s*=\s*"(.*?)"', re.S) login_token_re = re.compile(r'input.*?name="token".*?value="(\w+)"') stream_schema = validate.Schema( { "tvplayer": validate.Schema({ "status": u'200 OK', "response": validate.Schema({ "stream": validate.url(scheme=validate.any("http", "https")), validate.optional("drmToken"): validate.any(None, validate.text) }) }) }, validate.get("tvplayer"), validate.get("response")) context_schema = validate.Schema({ "validate": validate.text, validate.optional("token"): validate.text, "platform": { "key": validate.text } }) options = PluginOptions({"email": None, "password": None}) @classmethod def can_handle_url(cls, url): match = TVPlayer.url_re.match(url) return match is not None def __init__(self, url): super(TVPlayer, self).__init__(url) http.headers.update({"User-Agent": useragents.CHROME}) def authenticate(self, username, password): res = http.get(self.login_url) match = self.login_token_re.search(res.text) token = match and match.group(1) res2 = http.post(self.login_url, data=dict(email=username, password=password, token=token), allow_redirects=False) # there is a 302 redirect on a successful login return res2.status_code == 302 def _get_stream_data(self, resource, channel_id, token, service=1): # Get the context info (validation token and platform) self.logger.debug( "Getting stream information for resource={0}".format(resource)) context_res = http.get(self.context_url, params={ "resource": resource, "gen": token }) context_data = http.json(context_res, schema=self.context_schema) self.logger.debug("Context data: {0}", str(context_data)) # get the stream urls res = http.post(self.api_url, data=dict(service=service, id=channel_id, validate=context_data["validate"], token=context_data.get("token"), platform=context_data["platform"]["key"]), raise_for_status=False) return http.json(res, schema=self.stream_schema) def _get_stream_attrs(self, page): stream_attrs = dict( (k.replace("-", "_"), v.strip('"')) for k, v in self.stream_attrs_re.findall(page.text)) if not stream_attrs.get("channel_id"): m = self.data_id_re.search(page.text) stream_attrs["channel_id"] = m and m.group(1) self.logger.debug("Got stream attributes: {0}", str(stream_attrs)) valid = True for a in ("channel_id", "resource", "token"): if a not in stream_attrs: self.logger.debug("Missing '{0}' from stream attributes", a) valid = False return stream_attrs if valid else {} def _get_streams(self): if self.get_option("email") and self.get_option("password"): if not self.authenticate(self.get_option("email"), self.get_option("password")): self.logger.warning("Failed to login as {0}".format( self.get_option("email"))) # find the list of channels from the html in the page self.url = self.url.replace("https", "http") # https redirects to http res = http.get(self.url) if "enter your postcode" in res.text: self.logger.info( "Setting your postcode to: {0}. " "This can be changed in the settings on tvplayer.com", self.dummy_postcode) res = http.post(self.update_url, data=dict(postcode=self.dummy_postcode), params=dict(return_url=self.url)) stream_attrs = self._get_stream_attrs(res) if stream_attrs: stream_data = self._get_stream_data(**stream_attrs) if stream_data: if stream_data.get("drmToken"): self.logger.error( "This stream is protected by DRM can cannot be played") return else: return HLSStream.parse_variant_playlist( self.session, stream_data["stream"]) else: if "need to login" in res.text: self.logger.error( "You need to login using --tvplayer-email/--tvplayer-password to view this stream" )
class UStreamTV(Plugin): url_re = re.compile( r""" https?://(www\.)?ustream\.tv (?: (/embed/|/channel/id/)(?P<channel_id>\d+) )? (?: (/embed)?/recorded/(?P<video_id>\d+) )? """, re.VERBOSE) media_id_re = re.compile(r'"ustream:channel_id"\s+content\s*=\s*"(\d+)"') options = PluginOptions({"password": None}) @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def _api_get_streams(self, media_id, application, cluster="live", referrer=None, retries=3): if retries > 0: app_id = 11 app_ver = 2 referrer = referrer or self.url self.api = UHSClient(self.session, media_id, application, referrer=referrer, cluster=cluster, app_id=app_id, app_version=app_ver, password=self.get_option("password")) self.logger.debug( "Connecting to UStream API: media_id={0}, application={1}, referrer={2}, cluster={3}, " "app_id={4}, app_ver={5}", media_id, application, referrer, cluster, app_id, app_ver) if self.api.connect(): for i in range( 5): # make at most five requests to get the moduleInfo try: for s in self._do_poll(media_id, application, cluster, referrer, retries): yield s except ModuleInfoNoStreams: self.logger.debug("Retrying moduleInfo request") time.sleep(1) else: break def _do_poll(self, media_id, application, cluster="live", referrer=None, retries=3): res = self.api.poll() if res: for result in res: if result["cmd"] == "moduleInfo": for s in self.handle_module_info(result["args"], media_id, application, cluster, referrer, retries): yield s elif result["cmd"] == "reject": for s in self.handle_reject(result["args"], media_id, application, cluster, referrer, retries): yield s else: self.logger.debug("Unknown command: {0}({1})", result["cmd"], result["args"]) def handle_module_info(self, args, media_id, application, cluster="live", referrer=None, retries=3): has_results = False for streams in UHSClient.module_info_schema.validate(args): has_results = True if isinstance(streams, list): for stream in streams: for q, s in HLSStream.parse_variant_playlist( self.session, stream["url"]).items(): yield q, UStreamHLSStream(self.session, s.url, self.api) elif isinstance(streams, dict): for stream in streams.get("streams", []): name = "{0}k".format(stream["bitrate"]) for surl in stream["streamName"]: yield name, HTTPStream(self.session, surl) elif streams == "offline": self.logger.warning("This stream is currently offline") if not has_results: raise ModuleInfoNoStreams def handle_reject(self, args, media_id, application, cluster="live", referrer=None, retries=3): for arg in args: if "cluster" in arg: self.logger.debug("Switching cluster to {0}", arg["cluster"]["name"]) cluster = arg["cluster"]["name"] if "referrerLock" in arg: referrer = arg["referrerLock"]["redirectUrl"] return self._api_get_streams(media_id, application, cluster=cluster, referrer=referrer, retries=retries - 1) def _get_streams(self): # establish a mobile non-websockets api connection umatch = self.url_re.match(self.url) application = "channel" channel_id = umatch.group("channel_id") video_id = umatch.group("video_id") if channel_id: application = "channel" media_id = channel_id elif video_id: application = "recorded" media_id = video_id else: media_id = self._find_media_id() if media_id: for s in self._api_get_streams(media_id, application): yield s else: self.logger.error("Cannot find a media_id on this page") def _find_media_id(self): self.logger.debug("Searching for media ID on the page") res = http.get(self.url, headers={"User-Agent": useragents.CHROME}) m = self.media_id_re.search(res.text) return m and m.group(1)
class ABweb(Plugin): '''BIS Livestreams of french AB Groupe http://www.abweb.com/BIS-TV-Online/ ''' login_url = 'http://www.abweb.com/BIS-TV-Online/Default.aspx' _url_re = re.compile( r'https?://(?:www\.)?abweb\.com/BIS-TV-Online/bistvo-tele-universal.aspx', re.IGNORECASE) _hls_re = re.compile( r'''["']file["']:\s?["'](?P<url>[^"']+\.m3u8[^"']+)["']''') _iframe_re = re.compile(r'''<iframe[^>]+src=["'](?P<url>[^"']+)["']''') _input_re = re.compile(r'''(<input[^>]+>)''') _name_re = re.compile(r'''name=["']([^"']*)["']''') _value_re = re.compile(r'''value=["']([^"']*)["']''') expires_time = 3600 * 24 options = PluginOptions({ 'username': None, 'password': None, 'purge_credentials': None }) def __init__(self, url): super(ABweb, self).__init__(url) self._session_attributes = Cache(filename='plugin-cache.json', key_prefix='abweb:attributes') self._authed = self._session_attributes.get( 'ASP.NET_SessionId') and self._session_attributes.get( '.abportail1') self._expires = self._session_attributes.get( 'expires', time.time() + self.expires_time) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) is not None def set_expires_time_cache(self): expires = time.time() + self.expires_time self._session_attributes.set('expires', expires, expires=self.expires_time) def get_iframe_url(self): self.logger.debug('search for an iframe') res = http.get(self.url) m = self._iframe_re.search(res.text) if not m: raise PluginError('No iframe found.') iframe_url = m.group('url') iframe_url = update_scheme('http://', iframe_url) self.logger.debug('IFRAME URL={0}'.format(iframe_url)) return iframe_url def get_hls_url(self, iframe_url): self.logger.debug('search for hls url') res = http.get(iframe_url) m = self._hls_re.search(res.text) if not m: raise PluginError('No playlist found.') return m and m.group('url') def _login(self, username, password): '''login and update cached cookies''' self.logger.debug('login ...') res = http.get(self.login_url) input_list = self._input_re.findall(res.text) if not input_list: raise PluginError('Missing input data on login website.') data = {} for _input_data in input_list: try: _input_name = self._name_re.search(_input_data).group(1) except AttributeError: continue try: _input_value = self._value_re.search(_input_data).group(1) except AttributeError: _input_value = '' data[_input_name] = _input_value login_data = { 'ctl00$Login1$UserName': username, 'ctl00$Login1$Password': password, 'ctl00$Login1$LoginButton.x': '0', 'ctl00$Login1$LoginButton.y': '0' } data.update(login_data) res = http.post(self.login_url, data=data) for cookie in http.cookies: self._session_attributes.set(cookie.name, cookie.value, expires=3600 * 24) if self._session_attributes.get( 'ASP.NET_SessionId') and self._session_attributes.get( '.abportail1'): self.logger.debug('New session data') self.set_expires_time_cache() return True else: self.logger.error('Failed to login, check your username/password') return False def _get_streams(self): http.headers.update({ 'User-Agent': useragents.CHROME, 'Referer': 'http://www.abweb.com/BIS-TV-Online/bistvo-tele-universal.aspx' }) login_username = self.get_option('username') login_password = self.get_option('password') if self.options.get('purge_credentials'): self._session_attributes.set('ASP.NET_SessionId', None, expires=0) self._session_attributes.set('.abportail1', None, expires=0) self._authed = False self.logger.info('All credentials were successfully removed.') if not self._authed and not (login_username and login_password): self.logger.error( 'A login for ABweb is required, use --abweb-username USERNAME --abweb-password PASSWORD' ) return if self._authed: if self._expires < time.time(): self.logger.debug('get new cached cookies') # login after 24h self.set_expires_time_cache() self._authed = False else: self.logger.info( 'Attempting to authenticate using cached cookies') http.cookies.set( 'ASP.NET_SessionId', self._session_attributes.get('ASP.NET_SessionId')) http.cookies.set('.abportail1', self._session_attributes.get('.abportail1')) if not self._authed and not self._login(login_username, login_password): return iframe_url = self.get_iframe_url() http.headers.update({'Referer': iframe_url}) hls_url = self.get_hls_url(iframe_url) hls_url = update_scheme(self.url, hls_url) self.logger.debug('URL={0}'.format(hls_url)) variant = HLSStream.parse_variant_playlist(self.session, hls_url) if variant: for q, s in variant.items(): yield q, s else: yield 'live', HLSStream(self.session, hls_url)
class Pluzz(Plugin): GEO_URL = 'http://geo.francetv.fr/ws/edgescape.json' API_URL = 'http://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion={0}' PLAYER_GENERATOR_URL = 'https://sivideo.webservices.francetelevisions.fr/assets/staticmd5/getUrl?id=jquery.player.7.js' TOKEN_URL = 'http://hdfauthftv-a.akamaihd.net/esi/TA?url={0}' _url_re = re.compile( r'https?://((?:www)\.france\.tv/.+\.html|www\.(ludo|zouzous)\.fr/heros/[\w-]+|(sport|france3-regions)\.francetvinfo\.fr/.+?/(tv/direct)?)' ) _pluzz_video_id_re = re.compile(r'data-main-video="(?P<video_id>.+?)"') _jeunesse_video_id_re = re.compile( r'playlist: \[{.*?,"identity":"(?P<video_id>.+?)@(?P<catalogue>Ludo|Zouzous)"' ) _f3_regions_video_id_re = re.compile( r'"http://videos\.francetv\.fr/video/(?P<video_id>.+)@Regions"') _sport_video_id_re = re.compile(r'data-video="(?P<video_id>.+?)"') _player_re = re.compile( r'src="(?P<player>//staticftv-a\.akamaihd\.net/player/jquery\.player.+?-[0-9a-f]+?\.js)"></script>' ) _swf_re = re.compile( r'//staticftv-a\.akamaihd\.net/player/bower_components/player_flash/dist/FranceTVNVPVFlashPlayer\.akamai-[0-9a-f]+\.swf' ) _hds_pv_data_re = re.compile(r"~data=.+?!") _mp4_bitrate_re = re.compile(r'.*-(?P<bitrate>[0-9]+k)\.mp4') _geo_schema = validate.Schema( {'reponse': { 'geo_info': { 'country_code': validate.text } }}) _api_schema = validate.Schema({ 'videos': validate.all([{ 'format': validate.any(None, validate.text), 'url': validate.any( None, validate.url(), ), 'statut': validate.text, 'drm': bool, 'geoblocage': validate.any(None, [validate.all(validate.text)]), 'plages_ouverture': validate.all([{ 'debut': validate.any(None, int), 'fin': validate.any(None, int) }]) }]), 'subtitles': validate.any([], validate.all([{ 'type': validate.text, 'url': validate.url(), 'format': validate.text }])) }) _player_schema = validate.Schema({'result': validate.url()}) options = PluginOptions({"mux_subtitles": False}) @classmethod def can_handle_url(cls, url): return Pluzz._url_re.match(url) def _get_streams(self): # Retrieve geolocation data res = http.get(self.GEO_URL) geo = http.json(res, schema=self._geo_schema) country_code = geo['reponse']['geo_info']['country_code'] # Retrieve URL page and search for video ID res = http.get(self.url) if 'france.tv' in self.url: match = self._pluzz_video_id_re.search(res.text) elif 'ludo.fr' in self.url or 'zouzous.fr' in self.url: match = self._jeunesse_video_id_re.search(res.text) elif 'france3-regions.francetvinfo.fr' in self.url: match = self._f3_regions_video_id_re.search(res.text) elif 'sport.francetvinfo.fr' in self.url: match = self._sport_video_id_re.search(res.text) if match is None: return video_id = match.group('video_id') # Retrieve SWF player URL swf_url = None res = http.get(self.PLAYER_GENERATOR_URL) player_url = update_scheme( self.url, http.json(res, schema=self._player_schema)['result']) res = http.get(player_url) match = self._swf_re.search(res.text) if match is not None: swf_url = update_scheme(self.url, match.group(0)) res = http.get(self.API_URL.format(video_id)) videos = http.json(res, schema=self._api_schema) now = time.time() offline = False geolocked = False drm = False expired = False streams = [] for video in videos['videos']: video_url = video['url'] # Check whether video format is available if video['statut'] != 'ONLINE': offline = offline or True continue # Check whether video format is geo-locked if video['geoblocage'] is not None and country_code not in video[ 'geoblocage']: geolocked = geolocked or True continue # Check whether video is DRM-protected if video['drm']: drm = drm or True continue # Check whether video format is expired available = False for interval in video['plages_ouverture']: available = (interval['debut'] or 0) <= now <= (interval['fin'] or sys.maxsize) if available: break if not available: expired = expired or True continue # TODO: add DASH streams once supported if '.mpd' in video_url: continue if '.f4m' in video_url or 'france.tv' in self.url: res = http.get(self.TOKEN_URL.format(video_url)) video_url = res.text if '.f4m' in video_url and swf_url is not None: for bitrate, stream in HDSStream.parse_manifest( self.session, video_url, is_akamai=True, pvswf=swf_url).items(): # HDS videos with data in their manifest fragment token # doesn't seem to be supported by HDSStream. Ignore such # stream (but HDS stream having only the hdntl parameter in # their manifest token will be provided) pvtoken = stream.request_params['params'].get( 'pvtoken', '') match = self._hds_pv_data_re.search(pvtoken) if match is None: streams.append((bitrate, stream)) elif '.m3u8' in video_url: for stream in HLSStream.parse_variant_playlist( self.session, video_url).items(): streams.append(stream) # HBB TV streams are not provided anymore by France Televisions elif '.mp4' in video_url and '/hbbtv/' not in video_url: match = self._mp4_bitrate_re.match(video_url) if match is not None: bitrate = match.group('bitrate') else: # Fallback bitrate (seems all France Televisions MP4 videos # seem have such bitrate) bitrate = '1500k' streams.append((bitrate, HTTPStream(self.session, video_url))) if self.get_option("mux_subtitles") and videos['subtitles'] != []: substreams = {} for subtitle in videos['subtitles']: # TTML subtitles are available but not supported by FFmpeg if subtitle['format'] == 'ttml': continue substreams[subtitle['type']] = HTTPStream( self.session, subtitle['url']) for quality, stream in streams: yield quality, MuxedStream(self.session, stream, subtitles=substreams) else: for stream in streams: yield stream if offline: self.logger.error( 'Failed to access stream, may be due to offline content') if geolocked: self.logger.error( 'Failed to access stream, may be due to geo-restricted content' ) if drm: self.logger.error( 'Failed to access stream, may be due to DRM-protected content') if expired: self.logger.error( 'Failed to access stream, may be due to expired content')
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)
class Twitch(Plugin): options = PluginOptions({ "cookie": None, "oauth_token": None, "disable_hosting": False, }) @classmethod def stream_weight(cls, key): weight = QUALITY_WEIGHTS.get(key) if weight: return weight, "twitch" return Plugin.stream_weight(key) @classmethod def can_handle_url(cls, url): return _url_re.match(url) def __init__(self, url): Plugin.__init__(self, url) match = _url_re.match(url).groupdict() self._channel = match.get("channel") and match.get("channel").lower() self._channel_id = None self.subdomain = match.get("subdomain") self.video_type = match.get("video_type") if match.get("videos_id"): self.video_type = "v" self.video_id = match.get("video_id") or match.get("videos_id") self.clip_name = match.get("clip_name") self._hosted_chain = [] parsed = urlparse(url) self.params = parse_query(parsed.query) self.api = TwitchAPI(beta=self.subdomain == "beta", version=5) self.usher = UsherService() @property def channel(self): if not self._channel: if self.video_id: cdata = self._channel_from_video_id(self.video_id) self._channel = cdata["name"].lower() self._channel_id = cdata["_id"] return self._channel @channel.setter def channel(self, channel): self._channel = channel # channel id becomes unknown self._channel_id = None @property def channel_id(self): if not self._channel_id: # If the channel name is set, use that to look up the ID if self._channel: cdata = self._channel_from_login(self._channel) self._channel_id = cdata["_id"] # If the channel name is not set but the video ID is, # use that to look up both ID and name elif self.video_id: cdata = self._channel_from_video_id(self.video_id) self._channel = cdata["name"].lower() self._channel_id = cdata["_id"] return self._channel_id def _channel_from_video_id(self, video_id): vdata = self.api.videos(video_id) if "channel" not in vdata: raise PluginError("Unable to find video: {0}".format(video_id)) return vdata["channel"] def _channel_from_login(self, channel): cdata = self.api.users(login=channel) if len(cdata["users"]): return cdata["users"][0] else: raise PluginError("Unable to find channel: {0}".format(channel)) def _authenticate(self): if self.api.oauth_token: return oauth_token = self.options.get("oauth_token") cookies = self.options.get("cookie") if oauth_token: self.logger.info("Attempting to authenticate using OAuth token") self.api.oauth_token = oauth_token user = self.api.user(schema=_user_schema) if user: self.logger.info("Successfully logged in as {0}", user) else: self.logger.error("Failed to authenticate, the access token " "is invalid or missing required scope") elif cookies: self.logger.info("Attempting to authenticate using cookies") self.api.add_cookies(cookies) self.api.oauth_token = self.api.token(schema=_viewer_token_schema) login = self.api.viewer_info(schema=_viewer_info_schema) if login: self.logger.info("Successfully logged in as {0}", login) else: self.logger.error("Failed to authenticate, your cookies " "may have expired") def _create_playlist_streams(self, videos): start_offset = int(videos.get("start_offset", 0)) stop_offset = int(videos.get("end_offset", 0)) streams = {} for quality, chunks in videos.get("chunks").items(): if not chunks: if videos.get("restrictions", {}).get(quality) == "chansub": self.logger.warning("The quality '{0}' is not available " "since it requires a subscription.", quality) continue # Rename 'live' to 'source' if quality == "live": quality = "source" chunks_filtered = list(filter(lambda c: c["url"], chunks)) if len(chunks) != len(chunks_filtered): self.logger.warning("The video '{0}' contains invalid chunks. " "There will be missing data.", quality) chunks = chunks_filtered chunks_duration = sum(c.get("length") for c in chunks) # If it's a full broadcast we just use all the chunks if start_offset == 0 and chunks_duration == stop_offset: # No need to use the FLV concat if it's just one chunk if len(chunks) == 1: url = chunks[0].get("url") stream = HTTPStream(self.session, url) else: chunks = [HTTPStream(self.session, c.get("url")) for c in chunks] stream = FLVPlaylist(self.session, chunks, duration=chunks_duration) else: try: stream = self._create_video_clip(chunks, start_offset, stop_offset) except StreamError as err: self.logger.error("Error while creating video '{0}': {1}", quality, err) continue streams[quality] = stream return streams def _create_video_clip(self, chunks, start_offset, stop_offset): playlist_duration = stop_offset - start_offset playlist_offset = 0 playlist_streams = [] playlist_tags = [] for chunk in chunks: chunk_url = chunk["url"] chunk_length = chunk["length"] chunk_start = playlist_offset chunk_stop = chunk_start + chunk_length chunk_stream = HTTPStream(self.session, chunk_url) if chunk_start <= start_offset <= chunk_stop: try: headers = extract_flv_header_tags(chunk_stream) except IOError as err: raise StreamError("Error while parsing FLV: {0}", err) if not headers.metadata: raise StreamError("Missing metadata tag in the first chunk") metadata = headers.metadata.data.value keyframes = metadata.get("keyframes") if not keyframes: if chunk["upkeep"] == "fail": raise StreamError("Unable to seek into muted chunk, try another timestamp") else: raise StreamError("Missing keyframes info in the first chunk") keyframe_offset = None keyframe_offsets = keyframes.get("filepositions") keyframe_times = [playlist_offset + t for t in keyframes.get("times")] for time, offset in zip(keyframe_times, keyframe_offsets): if time > start_offset: break keyframe_offset = offset if keyframe_offset is None: raise StreamError("Unable to find a keyframe to seek to " "in the first chunk") chunk_headers = dict(Range="bytes={0}-".format(int(keyframe_offset))) chunk_stream = HTTPStream(self.session, chunk_url, headers=chunk_headers) playlist_streams.append(chunk_stream) for tag in headers: playlist_tags.append(tag) elif start_offset <= chunk_start < stop_offset: playlist_streams.append(chunk_stream) playlist_offset += chunk_length return FLVPlaylist(self.session, playlist_streams, tags=playlist_tags, duration=playlist_duration) def _get_video_streams(self): self.logger.debug("Getting video steams for {} (type={})".format(self.video_id, self.video_type)) self._authenticate() if self.video_type == "b": self.video_type = "a" try: videos = self.api.videos(self.video_type + self.video_id, schema=_video_schema) except PluginError as err: if "HTTP/1.1 0 ERROR" in str(err): raise NoStreamsError(self.url) else: raise # Parse the "t" query parameter on broadcasts and adjust # start offset if needed. time_offset = self.params.get("t") if time_offset: videos["start_offset"] += time_to_offset(self.params.get("t")) return self._create_playlist_streams(videos) def _access_token(self, type="live"): try: if type == "live": endpoint = "channels" value = self.channel elif type == "video": endpoint = "vods" value = self.video_id sig, token = self.api.access_token(endpoint, value, schema=_access_token_schema) except PluginError as err: if "404 Client Error" in str(err): raise NoStreamsError(self.url) else: raise return sig, token def _check_for_host(self): host_info = self.api.hosted_channel(include_logins=1, host=self.channel_id).json()["hosts"][0] if "target_login" in host_info and host_info["target_login"].lower() != self.channel.lower(): self.logger.info("{0} is hosting {1}".format(self.channel, host_info["target_login"])) return host_info["target_login"] def _get_hls_streams(self, stream_type="live"): self.logger.debug("Getting {} HLS streams for {}".format(stream_type, self.channel)) self._authenticate() self._hosted_chain.append(self.channel) if stream_type == "live": hosted_channel = self._check_for_host() if hosted_channel and self.options.get("disable_hosting"): self.logger.info("hosting was disabled by command line option") elif hosted_channel: self.logger.info("switching to {}", hosted_channel) if hosted_channel in self._hosted_chain: self.logger.error(u"A loop of hosted channels has been detected, " "cannot find a playable stream. ({})".format(u" -> ".join(self._hosted_chain + [hosted_channel]))) return {} self.channel = hosted_channel return self._get_hls_streams(stream_type) # only get the token once the channel has been resolved sig, token = self._access_token(stream_type) url = self.usher.channel(self.channel, sig=sig, token=token) elif stream_type == "video": sig, token = self._access_token(stream_type) url = self.usher.video(self.video_id, nauthsig=sig, nauth=token) else: self.logger.debug("Unknown HLS stream type: {}".format(stream_type)) return {} try: # If the stream is a VOD that is still being recorded the stream should start at the # beginning of the recording streams = HLSStream.parse_variant_playlist(self.session, url, force_restart=not stream_type == "live") except IOError as err: err = str(err) if "404 Client Error" in err or "Failed to parse playlist" in err: return else: raise PluginError(err) try: token = parse_json(token, schema=_token_schema) for name in token["restricted_bitrates"]: if name not in streams: self.logger.warning("The quality '{0}' is not available " "since it requires a subscription.", name) except PluginError: pass return streams def _get_clips(self): quality_options = self.api.clip_status(self.channel, self.clip_name, schema=_quality_options_schema) streams = {} for quality_option in quality_options: streams[quality_option["quality"]] = HTTPStream(self.session, quality_option["source"]) return streams def _get_streams(self): if self.video_id: if self.video_type == "v": return self._get_hls_streams("video") else: return self._get_video_streams() elif self.clip_name: return self._get_clips() else: return self._get_hls_streams("live")
class Pixiv(Plugin): """Plugin for https://sketch.pixiv.net/lives""" _url_re = re.compile( r"https?://sketch\.pixiv\.net/[^/]+(?P<videopage>/lives/\d+)?") _videopage_re = re.compile( r"""["']live-button["']><a\shref=["'](?P<path>[^"']+)["']""") _data_re = re.compile( r"""<script\sid=["']state["']>[^><{]+(?P<data>{[^><]+})</script>""") _post_key_re = re.compile( r"""name=["']post_key["']\svalue=["'](?P<data>[^"']+)["']""") _data_schema = validate.Schema( validate.all( validate.transform(_data_re.search), validate.any( None, validate.all( validate.get("data"), validate.transform(parse_json), validate.get("context"), validate.get("dispatcher"), validate.get("stores"), )))) login_url_get = "https://accounts.pixiv.net/login" login_url_post = "https://accounts.pixiv.net/api/login" options = PluginOptions({"username": None, "password": None}) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) is not None def find_videopage(self): self.logger.debug("Not a videopage") res = http.get(self.url) m = self._videopage_re.search(res.text) if not m: self.logger.debug( "No stream path, stream might be offline or invalid url.") raise NoStreamsError(self.url) path = m.group("path") self.logger.debug("Found new path: {0}".format(path)) return urljoin(self.url, path) def _login(self, username, password): res = http.get(self.login_url_get) m = self._post_key_re.search(res.text) if not m: raise PluginError("Missing post_key, no login posible.") post_key = m.group("data") data = { "lang": "en", "source": "sketch", "post_key": post_key, "pixiv_id": username, "password": password, } res = http.post(self.login_url_post, data=data) res = http.json(res) if res["body"].get("success"): return True else: return False def _get_streams(self): http.headers = {"User-Agent": useragents.FIREFOX} login_username = self.get_option("username") login_password = self.get_option("password") if login_username and login_password: self.logger.debug("Attempting login as {0}".format(login_username)) if self._login(login_username, login_password): self.logger.info( "Successfully logged in as {0}".format(login_username)) else: self.logger.info( "Failed to login as {0}".format(login_username)) videopage = self._url_re.match(self.url).group("videopage") if not videopage: self.url = self.find_videopage() data = http.get(self.url, schema=self._data_schema) if not data.get("LiveStore"): self.logger.debug("No video url found, stream might be offline.") return data = data["LiveStore"]["lives"] # get the unknown user-id for _key in data.keys(): video_data = data.get(_key) owner = video_data["owner"] self.logger.info("Owner ID: {0}".format(owner["user_id"])) self.logger.debug("HLS URL: {0}".format(owner["hls_movie"])) for n, s in HLSStream.parse_variant_playlist( self.session, owner["hls_movie"]).items(): yield n, s performers = video_data.get("performers") if performers: for p in performers: self.logger.info("CO-HOST ID: {0}".format(p["user_id"])) hls_url = p["hls_movie"] self.logger.debug("HLS URL: {0}".format(hls_url)) for n, s in HLSStream.parse_variant_playlist( self.session, hls_url).items(): _n = "{0}_{1}".format(n, p["user_id"]) yield _n, s
class Neulion(Plugin): """Streamlink Plugin for websites based on Neulion Example urls can be found in tests/test_plugin_neulion.py """ url_re = re.compile( r"""https?:// (?P<domain> www\.(?: ufc\.tv | elevensports\.(?:be|lu|pl|sg|tw) | tennischanneleverywhere\.com ) | watch\.(?: nba\.com | rugbypass\.com ) | fanpass\.co\.nz ) /(?P<vtype>channel|game|video)/.+""", re.VERBOSE) video_info_re = re.compile(r"""program\s*=\s*(\{.*?});""", re.DOTALL) channel_info_re = re.compile(r"""g_channel\s*=\s(\{.*?});""", re.DOTALL) current_video_re = re.compile( r"""(?:currentVideo|video)\s*=\s*(\{[^;]+});""", re.DOTALL) info_fallback_re = re.compile( r""" var\s? (?: currentGameId | programId ) \s?=\s?["']?(?P<id>\d+)["']?; """, re.VERBOSE) stream_api_url = "https://{0}/service/publishpoint" auth_url = "https://{0}/secure/authenticate" auth_schema = validate.Schema(validate.xml_findtext("code")) options = PluginOptions({"username": None, "password": None}) @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None @property def _domain(self): match = self.url_re.match(self.url) return match.group("domain") @property def _vtype(self): match = self.url_re.match(self.url) return match.group("vtype") def _get_stream_url(self, video_id, vtype): try: res = http.post(self.stream_api_url.format(self._domain), data={ "id": video_id, "type": vtype, "format": "json" }, headers={"User-Agent": useragents.IPHONE_6}) except Exception as e: if "400 Client Error" in str(e): self.logger.error("Login required") return else: raise e data = http.json(res) return data.get("path") def _get_info(self, text): # try to find video info first m = self.video_info_re.search(text) if not m: m = self.current_video_re.search(text) if not m: # and channel info if that fails m = self.channel_info_re.search(text) if m: js_data = m.group(1) try: return_data = js_to_json(js_data) self.logger.debug("js_to_json") except Exception as e: self.logger.debug("js_to_json_regex_fallback") return_data = js_to_json_regex_fallback(js_data) finally: return return_data def _get_info_fallback(self, text): info_id = self.info_fallback_re.search(text) if info_id: self.logger.debug("Found id from _get_info_fallback") return {"id": info_id.group("id")} def _login(self, username, password): res = http.post(self.auth_url.format(self._domain), data={ "username": username, "password": password, "cookielink": False }) login_status = http.xml(res, schema=self.auth_schema) self.logger.debug("Login status for {0}: {1}", username, login_status) if login_status == "loginlocked": self.logger.error( "The account {0} has been locked, the password needs to be reset" ) return login_status == "loginsuccess" def _get_streams(self): login_username = self.get_option("username") login_password = self.get_option("password") if login_username and login_password: self.logger.debug("Attempting login as {0}", login_username) if self._login(login_username, login_password): self.logger.info("Successfully logged in as {0}", login_username) else: self.logger.info("Failed to login as {0}", login_username) res = http.get(self.url) video = self._get_info(res.text) if not video: video = self._get_info_fallback(res.text) if video: self.logger.debug("Found {type}: {name}".format( type=video.get("type", self._vtype), name=video.get("name", "???"))) surl = self._get_stream_url(video["id"], video.get("type", self._vtype)) if surl: surl = surl.replace("_iphone", "") return HLSStream.parse_variant_playlist(self.session, surl) else: self.logger.error( "Could not get stream URL for video: {name} ({id})".format( id=video.get("id", "???"), name=video.get("name", "???"), )) else: self.logger.error("Could not find any video info on the page")
class UStreamTV(Plugin): options = PluginOptions({"password": ""}) @classmethod def can_handle_url(cls, url): return _url_re.match(url) @classmethod def stream_weight(cls, stream): match = re.match("mobile_(\w+)", stream) if match: weight, group = Plugin.stream_weight(match.group(1)) weight -= 1 group = "mobile_ustream" elif stream == "recorded": weight, group = 720, "ustream" else: weight, group = Plugin.stream_weight(stream) return weight, group def _get_channel_id(self): res = http.get(self.url) match = _channel_id_re.search(res.text) if match: return int(match.group(1)) def _get_hls_streams(self, channel_id, wait_for_transcode=False): # HLS streams are created on demand, so we may have to wait # for a transcode to be started. attempts = wait_for_transcode and 10 or 1 playlist_url = HLS_PLAYLIST_URL.format(channel_id) streams = {} while attempts and not streams: try: streams = HLSStream.parse_variant_playlist( self.session, playlist_url, nameprefix="mobile_") except IOError: # Channel is probably offline break attempts -= 1 sleep(3) return streams def _create_rtmp_stream(self, cdn, stream_name): parsed = urlparse(cdn) params = { "rtmp": cdn, "app": parsed.path[1:], "playpath": stream_name, "pageUrl": self.url, "swfUrl": SWF_URL, "live": True } return RTMPStream(self.session, params) def _get_module_info(self, app, media_id, password="", schema=None): self.logger.debug("Waiting for moduleInfo invoke") conn = create_ums_connection(app, media_id, self.url, password) attempts = 3 while conn.connected and attempts: try: result = conn.process_packets(invoked_method="moduleInfo", timeout=10) except (IOError, librtmp.RTMPError) as err: raise PluginError("Failed to get stream info: {0}".format(err)) try: result = _module_info_schema.validate(result) break except PluginError: attempts -= 1 conn.close() if schema: result = schema.validate(result) return result def _get_desktop_streams(self, channel_id): password = self.options.get("password") channel = self._get_module_info("channel", channel_id, password, schema=_channel_schema) if not isinstance(channel.get("stream"), list): raise NoStreamsError(self.url) streams = {} for provider in channel["stream"]: try: provider_url = provider["url"] provider_name = provider["name"] except KeyError: continue for stream_index, stream_info in enumerate(provider["streams"]): if not isinstance(stream_info, dict): continue stream = None stream_height = int(stream_info.get("height", 0)) stream_name = stream_info.get("description") if not stream_name: if stream_height > 0: if not stream_info.get("isTranscoded"): stream_name = "{0}p+".format(stream_height) else: stream_name = "{0}p".format(stream_height) else: stream_name = "live" if stream_name in streams: provider_name_clean = provider_name.replace("uhs_", "") stream_name += "_alt_{0}".format(provider_name_clean) if provider_name.startswith("uhs_"): stream = UHSStream(self.session, channel_id, self.url, provider_name, stream_index, password) elif provider_url.startswith("rtmp"): playpath = stream_info["streamName"] stream = self._create_rtmp_stream(provider_url, playpath) if stream: streams[stream_name] = stream return streams def _get_live_streams(self, channel_id): has_desktop_streams = False if HAS_LIBRTMP: try: streams = self._get_desktop_streams(channel_id) # TODO: Replace with "yield from" when dropping Python 2. for stream in streams.items(): has_desktop_streams = True yield stream except PluginError as err: self.logger.error("Unable to fetch desktop streams: {0}", err) except NoStreamsError: pass else: self.logger.warning( "python-librtmp is not installed, but is needed to access " "the desktop streams") try: streams = self._get_hls_streams( channel_id, wait_for_transcode=not has_desktop_streams) # TODO: Replace with "yield from" when dropping Python 2. for stream in streams.items(): yield stream except PluginError as err: self.logger.error("Unable to fetch mobile streams: {0}", err) except NoStreamsError: pass def _get_recorded_streams(self, video_id): if HAS_LIBRTMP: recording = self._get_module_info("recorded", video_id, schema=_recorded_schema) if not isinstance(recording.get("stream"), list): return for provider in recording["stream"]: base_url = provider.get("url") for stream_info in provider["streams"]: bitrate = int(stream_info.get("bitrate", 0)) stream_name = (bitrate > 0 and "{0}k".format(bitrate) or "recorded") url = stream_info["streamName"] if base_url: url = base_url + url if url.startswith("http"): yield stream_name, HTTPStream(self.session, url) elif url.startswith("rtmp"): params = dict(rtmp=url, pageUrl=self.url) yield stream_name, RTMPStream(self.session, params) else: self.logger.warning( "The proper API could not be used without python-librtmp " "installed. Stream URL is not guaranteed to be valid") url = RECORDED_URL.format(video_id) random_hash = "{0:02x}{1:02x}".format(randint(0, 255), randint(0, 255)) params = dict(hash=random_hash) stream = HTTPStream(self.session, url, params=params) yield "recorded", stream def _get_streams(self): match = _url_re.match(self.url) video_id = match.group("video_id") if video_id: return self._get_recorded_streams(video_id) channel_id = match.group("channel_id") or self._get_channel_id() if channel_id: return self._get_live_streams(channel_id)