def _get_streams(self): channelid = urlparse( self.url).path.rstrip("/").rpartition("/")[-1].lower() self.logger.debug("Fetching stream info") headers = {"Referer": self.url} options = dict(id=channelid) res = urlget(self.StreamInfoURL, headers=headers, params=options) json = res_json(res, "stream info JSON") if not isinstance(json, dict): raise PluginError("Invalid JSON response") if not ("rtmp" in json and "streamname" in json): raise NoStreamsError(self.url) if not RTMPStream.is_usable(self.session): raise PluginError( "rtmpdump is not usable and required by Owncast plugin") rtmp = json["rtmp"] playpath = json["streamname"] streams = {} streams["live"] = RTMPStream( self.session, { "rtmp": rtmp, "pageUrl": self.url, "swfUrl": self.SWFURL, "playpath": playpath, "live": True }) return streams
def _get_hls_streams(self): url = self.HLSStreamTokenURL.format(self.channelname) try: res = urlget(url, params=dict(type="any", connection="wifi"), exception=IOError) except IOError: self.logger.debug("HLS streams not available") return {} json = res_json(res, "stream token JSON") if not isinstance(json, list): raise PluginError("Invalid JSON response") if len(json) == 0: raise PluginError("No stream token in JSON") token = verifyjson(json[0], "token") hashed = hmac.new(self.HLSStreamTokenKey, bytes(token, "utf8"), sha1) fulltoken = hashed.hexdigest() + ":" + token url = self.HLSSPlaylistURL.format(self.channelname) try: params = dict(token=fulltoken, hd="true") playlist = HLSStream.parse_variant_playlist(self.session, url, params=params) except IOError as err: raise PluginError(err) return playlist
def _get_player_params(self): res = urlopen(self.url) match = re.search("<param name=\"playerKey\" value=\"(.+)\" />", res.text) if not match: raise PluginError("Missing key 'playerKey' in player params") key = match.group(1) match = re.search("<param name=\"@videoPlayer\" value=\"(\d+)\" />", res.text) if not match: raise PluginError("Missing key 'videoPlayer' in player params") video_player = match.group(1) match = re.search("<param name=\"playerID\" value=\"(\d+)\" />", res.text) if not match: raise PluginError("Missing key 'playerID' in player params") player_id = match.group(1) match = re.search("<!-- live on -->", res.text) is_live = not not match return key, video_player, player_id, is_live
def _get_streams(self): parsed = urlparse(self.url) if parsed.fragment: channelid = parsed.fragment else: channelid = parsed.path.rpartition("view/")[-1] if not channelid: raise NoStreamsError(self.url) channelid = channelid.lower().replace("/", "_") self.logger.debug("Fetching stream info") res = urlget(self.APIURL.format(channelid)) json = res_json(res) if not isinstance(json, dict): raise PluginError("Invalid JSON response") elif not ("success" in json and "payload" in json): raise PluginError("Invalid JSON response") elif json["success"] == False: raise NoStreamsError(self.url) streams = {} streams["live"] = HTTPStream(self.session, json["payload"]) return streams
def _get_player_params(self, retries=3): res = http.get(self.url) match = re.search("<param name=\"playerKey\" value=\"(.+)\" />", res.text) if not match: # The HTML returned sometimes doesn't contain the parameters if not retries: raise PluginError("Missing key 'playerKey' in player params") else: return self._get_player_params(retries - 1) key = match.group(1) match = re.search("<param.+name=\"@videoPlayer\" value=\"(.+)\" />", res.text) if not match: raise PluginError("Missing key 'videoPlayer' in player params") video_player = match.group(1) match = re.search("<param name=\"playerID\" value=\"(\d+)\" />", res.text) if not match: raise PluginError("Missing key 'playerID' in player params") player_id = match.group(1) match = re.search("<!-- live on -->", res.text) if not match: match = re.search("<div id=\"channel_live\">", res.text) is_live = not not match return key, video_player, player_id, is_live
def _create_flv_playlist(self, template): res = http.get(template) json = http.json(res) if not isinstance(json, dict): raise PluginError("Invalid JSON response") parsed = urlparse(template) try: url_template = '{0}://{1}{2}'.format(parsed.scheme, parsed.netloc, json['template']) segment_max = reduce(lambda i, j: i + j[0], json['fragments'], 0) duration = json['duration'] except KeyError: raise PluginError('Unexpected JSON response') substreams = [ HTTPStream(self.session, url_template.replace('$fragment$', str(i))) for i in range(1, segment_max + 1) ] return FLVPlaylist(self.session, streams=substreams, duration=duration, skip_header=True, flatten_timestamps=True)
def _get_vod_streams(self, channelname): res = http.get(self.url) match = re.search('autoURL%22%3A%22(.*?)%22', res.text) if not match: raise PluginError('Error retrieving manifest url') manifest_url = unquote(match.group(1)).replace('\\', '') try: res = http.get(manifest_url) manifest = http.json(res) except: raise PluginError('Error retrieving manifest') # A fallback host (http://proxy-xx...) is sometimes provided # that we could make us of. streams = {} for params in manifest.get('alternates', []): name = params.get('name') template = params.get('template') if not (name and template): continue name = '{0}p'.format(name) streams[name] = self._create_flv_playlist(template) return streams
def _get_player_params(self): res = urlopen(self.url) match = re.search("<param name=\"playerKey\" value=\"(.+)\" />", res.text) if not match: raise PluginError("Missing key 'playerKey' in player params") key = match.group(1) match = re.search("<param name=\"@videoPlayer\" value=\"(\d+)\" />", res.text) if not match: raise PluginError("Missing key 'videoPlayer' in player params") video_player = match.group(1) match = re.search("<param name=\"playerID\" value=\"(\d+)\" />", res.text) if not match: raise PluginError("Missing key 'playerID' in player params") player_id = match.group(1) match = re.search( "<img src=\".+/static/images/channels/live_check.png\" />", res.text) is_live = not not match return key, video_player, player_id, is_live
def _get_rtmp_streams(self, text): match = re.search("streamer=(rtmp://.+?)&", text) if not match: raise PluginError( ("No RTMP streamer found on URL {0}").format(self.url)) rtmp = match.group(1) match = re.search("<meta content=\"(http://.+?\.swf)\?", text) if not match: self.logger.warning( "Failed to get player SWF URL location on URL {0}", self.url) else: self.SWFURL = match.group(1) self.logger.debug("Found player SWF URL location {0}", self.SWFURL) match = re.search("<meta content=\"(.+)\" name=\"item-id\" />", text) if not match: raise PluginError( ("Missing channel item-id on URL {0}").format(self.url)) res = http.get(self.APIURL.format(match.group(1), time()), params=dict(output="json")) json = http.json(res) if not isinstance(json, list): raise PluginError("Invalid JSON response") rtmplist = {} for jdata in json: if "stream_name" not in jdata or "type" not in jdata: continue if "rtmp" not in jdata["type"]: continue playpath = jdata["stream_name"] if "token" in jdata and jdata["token"]: playpath += jdata["token"] if len(json) == 1: stream_name = "live" else: stream_name = jdata["stream_name"] rtmplist[stream_name] = RTMPStream( self.session, { "rtmp": rtmp, "pageUrl": self.url, "swfVfy": self.SWFURL, "playpath": playpath, "live": True }) return rtmplist
def _get_streams(self): # If email option given, try to login if self.options.get("email"): res = http.get(self.LOGINPAGEURL) match = re.search('<meta content="([^"]+)" name="csrf-token"', res.text) if not match: raise PluginError("Missing CSRF Token: " + self.LOGINPAGEURL) csrf_token = match.group(1) email = self.options.get("email") password = self.options.get("password") res = http.post( self.LOGINPOSTURL, data={ 'authenticity_token': csrf_token, 'channel_id': '', 'commit': 'Login', 'plan_id': '', 'session[email]': email, 'session[password]': password, 'utf8': "\xE2\x9C\x93", # Check Mark Character }) self.logger.debug("Login account info: {0}", res.text) result = http.json(res) if result.get('email', 'no-mail') != email: raise PluginError("Invalid account") res = http.get(self.url) streams = {} if RTMPStream.is_usable(self.session): try: rtmpstreams = self._get_rtmp_streams(res.text) streams.update(rtmpstreams) except PluginError as err: self.logger.error("Error when fetching RTMP stream info: {0}", str(err)) else: self.logger.warning( "rtmpdump is not usable, only HLS streams will be available") try: hlsstreams = self._get_hls_streams(res.text) streams.update(hlsstreams) except PluginError as err: self.logger.error("Error when fetching HLS stream info: {0}", str(err)) return streams
def _get_streams(self): self.logger.debug("Fetching stream info") res = urlget(self.url) match = re.search("var current_channel = (.*);", res.text) if match: json = parse_json(match.group(1)) else: raise NoStreamsError(self.url) if not isinstance(json, dict): raise PluginError("Invalid JSON response") elif not "streams" in json: raise NoStreamsError(self.url) if not RTMPStream.is_usable(self.session): raise PluginError( "rtmpdump is not usable and required by Filmon plugin") match = re.search("var flash_config = (.*);", res.text) if match: config = parse_json(match.group(1)) if "streamer" in config: self.SWFURL = urljoin(self.SWFURL, config["streamer"]) streams = {} for stream in json["streams"]: if not ("url" in stream and "name" in stream): continue parsed = urlparse(stream["url"]) if not parsed.scheme.startswith("rtmp"): continue if parsed.query: app = "{0}?{1}".format(parsed.path[1:], parsed.query) else: app = parsed.path[1:] name = stream["quality"] streams[name] = RTMPStream( self.session, { "rtmp": stream["url"], "pageUrl": self.url, "swfUrl": self.SWFURL, "playpath": stream["name"], "app": app, "live": True }) return streams
def _get_streams(self): country_code = urlparse(self.url).netloc.split(".")[0] self.logger.debug("Fetching stream info") res = urlget(self.APIURL) json = res_json(res) if not isinstance(json, dict): raise PluginError("Invalid JSON response") elif not ("primary" in json or "secondary" in json): raise PluginError("Invalid JSON response") if not RTMPStream.is_usable(self.session): raise PluginError( "rtmpdump is not usable and required by Euronews plugin") streams = {} self.logger.debug("Euronews Countries:{0}", " ".join(json["primary"].keys())) if not (country_code in json["primary"] or country_code in json["secondary"]): res = urlget(self.GEOIPURL) geo = res_json(res) if isinstance(json, dict) and "country_code" in geo: country_code = geo["country_code"].lower() if not (country_code in json["primary"] or country_code in json["secondary"]): country_code = "en" else: country_code = "en" for site in ("primary", "secondary"): for quality in json[site][country_code]["rtmp_flash"]: stream = json[site][country_code]["rtmp_flash"][quality] name = quality + "k" if site == "secondary": name += "_alt" streams[name] = RTMPStream( self.session, { "rtmp": stream["server"], "playpath": stream["name"], "swfUrl": self.SWFURL, "live": True }) if len(streams) == 0: raise NoStreamsError(self.url) return streams
def _get_player_params(self, retries=5): try: res = http.get(self.url, headers=HTTP_HEADERS) except PluginError as err: # The server sometimes gives us 404 for no reason if "404" in str(err) and retries: sleep(1) return self._get_player_params(retries - 1) else: raise match = re.search("<param name=\"playerKey\" value=\"(.+)\" />", res.text) if not match: # The HTML returned sometimes doesn't contain the parameters if not retries: raise PluginError("Missing key 'playerKey' in player params") else: sleep(1) return self._get_player_params(retries - 1) key = match.group(1) match = re.search("AZUBU.setVar\(\"firstVideoRefId\", \"(.+)\"\);", res.text) if not match: # The HTML returned sometimes doesn't contain the parameters if not retries: raise PluginError("Unable to find video reference") else: sleep(1) return self._get_player_params(retries - 1) video_player = "ref:" + match.group(1) match = re.search("<param name=\"playerID\" value=\"(\d+)\" />", res.text) if not match: # The HTML returned sometimes doesn't contain the parameters if not retries: raise PluginError("Missing key 'playerID' in player params") else: sleep(1) return self._get_player_params(retries - 1) player_id = match.group(1) match = re.search("<!-- live on -->", res.text) if not match: match = re.search("<div id=\"channel_live\">", res.text) is_live = not not match return key, video_player, player_id, is_live
def _get_streams_from_amf(self): if not RTMPStream.is_usable(self.session): raise NoStreamsError(self.url) res = urlget(AMF_URL.format(self.channel_id)) try: packet = AMFPacket.deserialize(BytesIO(res.content)) except (IOError, AMFError) as err: raise PluginError("Failed to parse AMF packet: {0}".format(err)) for message in packet.messages: if message.target_uri == "/1/onResult": result = message.value break else: raise PluginError("No result found in AMF packet") streams = {} stream_name = result.get("streamName") if stream_name: cdn = result.get("cdnUrl") or result.get("fmsUrl") if cdn: stream = self._create_rtmp_stream(cdn, stream_name) if "videoCodec" in result and result["videoCodec"][ "height"] > 0: stream_name = "{0}p".format( int(result["videoCodec"]["height"])) else: stream_name = "live" streams[stream_name] = stream else: self.logger.warning("Missing cdnUrl and fmsUrl from result") stream_versions = result.get("streamVersions") if stream_versions: for version, info in stream_versions.items(): stream_version_cdn = info.get("streamVersionCdn", {}) for name, cdn in filter(valid_cdn, stream_version_cdn.items()): stream = self._create_rtmp_stream(cdn["cdnStreamUrl"], cdn["cdnStreamName"]) stream_name = "live_alt_{0}".format(name) streams[stream_name] = stream return streams
def _get_rtmp_streams(self, channelname): options = dict(l="info", a="xmlClipPath", clip_id=channelname, rid=time()) res = urlget(self.APIURL, params=options) dom = res_xml(res) rtmpurl = dom.getElementsByTagName("url") rtmp = None if len(rtmpurl) > 0: rtmp = get_node_text(rtmpurl[0]) else: raise PluginError( ("No RTMP Streams found on URL {0}").format(self.url)) rtmplist = {} rtmplist["live"] = RTMPStream(self.session, { "rtmp": rtmp, "swfVfy": self.SWFURL, "live": True }) return rtmplist
def _get_live_streams(self): self._authenticate() sig, token = self._access_token() url = self.usher.select(self.channel, password=self.options.get("password"), nauthsig=sig, nauth=token) try: streams = HLSStream.parse_variant_playlist(self.session, url) 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) chansub = verifyjson(token, "chansub") restricted_bitrates = verifyjson(chansub, "restricted_bitrates") for name in filter( lambda n: not re.match(r"(.+_)?archives|live", n), restricted_bitrates): self.logger.warning( "The quality '{0}' is not available " "since it requires a subscription.", name) except PluginError: pass return dict(starmap(self._check_stream_name, streams.items()))
def _get_live_streams(self): self._authenticate() sig, token = self._access_token() url = self.usher.select(self.channel, password=self.options.get("password"), nauthsig=sig, nauth=token) try: streams = HLSStream.parse_variant_playlist(self.session, url) 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 dict(starmap(self._check_stream_name, streams.items()))
def _get_streams(self): if not CAN_DECRYPT: raise PluginError("Need pyCrypto installed to decrypt streams") json = self._get_stream_info(self.url) if not json: return if json.get("status", 0) == 0: return parts = [] for media in json.get("media", []): if media.get("id") < 0: continue for part in media.get("parts", []): if part.get("duration") < 0: continue parts.append(part) if not parts: return streams = {} for quality in QUALITY_MAP: streams[quality] = BeatStream(self.session, parts, quality) return streams
def _get_streams(self): if not RTMPStream.is_usable(self.session): raise PluginError( "rtmpdump is not usable and required by Filmon plugin") self.logger.debug("Fetching stream info") res = http.get(self.url) match = re.search("movie_id=(\d+)", res.text) if match: return self._get_vod_stream(match.group(1)) match = re.search("/channels/(\d+)/extra_big_logo.png", res.text) if not match: return channel_id = match.group(1) streams = {} for quality in ("low", "high"): try: streams[quality] = self._get_stream(channel_id, quality) except NoStreamsError: pass return streams
def _get_hls_streams(self, type="live"): self._authenticate() sig, token = self._access_token(type) if type == "live": url = self.usher.channel(self.channel, sig=sig, token=token) elif type == "video": url = self.usher.video(self.video_id, nauthsig=sig, nauth=token) try: streams = HLSStream.parse_variant_playlist(self.session, url) 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_swf_url(self): res = http.get(self.url) match = _swf_url_re.search(res.text) if not match: raise PluginError("Unable to find SWF URL in the HTML") return match.group(1)
def _create_gox_params(self, flashvars, level): flashvars["adstate"] = "0" flashvars["goxkey"] = self.GOXHashKey flashvars["level"] = str(level) keys = [ "leagueid", "conid", "goxkey", "level", "uno", "uip", "adstate" ] if "playmode" in flashvars and flashvars["playmode"] == "vod": keys += ["vjoinid", "nid"] goxkey = hashlib.md5() params = {} for key in keys: if not key in flashvars: raise PluginError( ("Missing key '{0}' in flashvars").format(key)) goxkey.update(bytes(flashvars[key], "ascii")) params[key] = flashvars[key] params["goxkey"] = goxkey.hexdigest() return params
def _get_streams(self): channelname = urlparse( self.url).path.rstrip("/").rpartition("/")[-1].lower() self.logger.debug("Fetching stream info") headers = {"Referer": self.url} res = urlget(self.PlayerURL.format(channelname), headers=headers) match = re.search("'FlashVars', '(id=\d+)&", res.text) if not match: raise NoStreamsError(self.url) channelname += "?" + match.group(1) res = urlget(self.BalancerURL, headers=headers) match = re.search("redirect=(.+)", res.text) if not match: raise PluginError( "Error retrieving RTMP address from loadbalancer") rtmp = match.group(1) streams = {} streams["live"] = RTMPStream( self.session, { "rtmp": "rtmp://{0}/live/{1}".format(rtmp, channelname), "pageUrl": self.url, "swfVfy": self.SWFURL, "conn": "S:OK", "live": True }) return streams
def _parse_event_url(self, prefix, data): match = re.search(' \"(.*)\";', data) if not match: raise PluginError("Event live page URL not found") return urljoin(prefix, match.group(1))
def _authenticate(self, username=None, password=None, cookies=None): if (username is None or password is None) and cookies is None: raise PluginError( "GOMTV.net requires a username and password or a cookie") if cookies is not None: for cookie in cookies.split(";"): try: name, value = cookie.split("=") except ValueError: continue self.rsession.cookies[name.strip()] = value.strip() self.logger.info("Attempting to authenticate with cookies") else: form = dict(cmd="login", rememberme="1", mb_username=username, mb_password=password) self.logger.info( "Attempting to authenticate with username and password") urlopen(self.LoginURL, data=form, headers=self.LoginHeaders, session=self.rsession) res = urlget(self.LoginCheckURL, session=self.rsession) if "Please need login" in res.text: raise PluginError("Authentication failed") if "SES_USERNICK" in self.rsession.cookies: username = self.rsession.cookies["SES_USERNICK"] self.logger.info( ("Successfully logged in as {0}").format(username)) if username and password: cookie = "" for v in ("SES_MEMBERNO", "SES_STATE", "SES_MEMBERNICK", "SES_USERNICK"): if v in self.rsession.cookies: cookie += "{0}={1}; ".format(v, self.rsession.cookies[v]) self.logger.info("Cookie for reusing this session: {0}", cookie)
def _get_hls_streams(self, text): match = re.search("\"(http://.+\.m3u8)\"", text) if not match: raise PluginError( ("No HLS playlist found on URL {0}").format(self.url)) playlisturl = match.group(1) self.logger.debug("Playlist URL is {0}", playlisturl) playlist = {} try: playlist = HLSStream.parse_variant_playlist( self.session, playlisturl) except IOError as err: raise PluginError(err) return playlist
def _parse_result(self, res): streams = {} if not hasattr(res, "programmedContent"): raise PluginError("Invalid result") player = res.programmedContent["videoPlayer"] if not hasattr(player, "mediaDTO"): raise PluginError("Invalid result") for i, rendition in player.mediaDTO.renditions.items(): stream = AkamaiHDStream(self.session, rendition.defaultURL) streamname = "{0}p".format(rendition.frameHeight) streams[streamname] = stream return streams
def _get_hls_streams(self): url = self.HLSStreamTokenURL.format(self.channelname) try: res = urlget(url, params=dict(type="iphone", connection="wifi", allow_cdn="true"), exception=IOError) except IOError: self.logger.debug("HLS streams not available") return {} json = res_json(res, "stream token JSON") if not isinstance(json, list): raise PluginError("Invalid JSON response") if len(json) == 0: raise PluginError("No stream token in JSON") token = verifyjson(json[0], "token") hashed = hmac.new(self.HLSStreamTokenKey, bytes(token, "utf8"), sha1) fulltoken = hashed.hexdigest() + ":" + token url = self.HLSSPlaylistURL.format(self.channelname) try: params = dict(token=fulltoken, hd="true", allow_cdn="true") playlist = HLSStream.parse_variant_playlist(self.session, url, params=params) except IOError as err: if "404" in err: raise PluginError(err) else: self.logger.debug("Requesting mobile transcode") payload = dict(channel=self.channelname, type="iphone") urlopen("http://usher.twitch.tv/stream/transcode_iphone.json", data=payload) return {} return playlist
def _parse_result(self, res): streams = {} if not hasattr(res, "programmedContent"): raise PluginError("Invalid result") player = res.programmedContent["videoPlayer"] if not hasattr(player, "mediaDTO"): raise PluginError("Invalid result") renditions = sorted(player.mediaDTO.renditions.values(), key=attrgetter("encodingRate")) for stream_name, rendition in zip(STREAM_NAMES, renditions): stream = AkamaiHDStream(self.session, rendition.defaultURL) streams[stream_name] = stream return streams
def _check_channel_live(self, channelname): url = self.MetadataURL.format(channelname) res = urlget(url, params=dict(fields="mode")) json = res_json(res) if not isinstance(json, dict): raise PluginError("Invalid JSON response") mode = verifyjson(json, "mode") return mode == "live"