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: return {} if not isinstance(res.json, list): raise PluginError("Stream info response is not JSON") if len(res.json) == 0: raise PluginError("No stream token in JSON") streams = {} token = verifyjson(res.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_streams(self): channelid = self._get_channel_id(self.url) if not channelid: raise NoStreamsError(self.url) self.logger.debug("Fetching stream info") res = urlget(self.AMFURL.format(channelid)) try: packet = AMF0Packet.deserialize(BytesIO(res.content)) except (IOError, AMFError) as err: raise PluginError( ("Failed to parse AMF packet: {0}").format(str(err))) result = None for message in packet.messages: if message.target_uri == "/1/onResult": result = message.value break if not result: raise PluginError("No result found in AMF packet") streams = {} if "liveHttpUrl" in result: try: hlsstreams = HLSStream.parse_variant_playlist( self.session, result["liveHttpUrl"]) streams.update(hlsstreams) except IOError as err: self.logger.warning("Failed to get variant playlist: {0}", err) if "streamName" in result: if "cdnUrl" in result: cdn = result["cdnUrl"] elif "fmsUrl" in result: cdn = result["fmsUrl"] else: self.logger.warning("Missing cdnUrl and fmsUrl from result") return streams if "videoCodec" in result and result["videoCodec"]["height"] > 0: streamname = "{0}p".format(int(result["videoCodec"]["height"])) else: streamname = "live" streams[streamname] = self._create_stream(cdn, result["streamName"]) if "streamVersions" in result: for version, info in result["streamVersions"].items(): if "streamVersionCdn" in info: for name, cdn in info["streamVersionCdn"].items(): if "cdnStreamUrl" in cdn and "cdnStreamName" in cdn: streams["cdn_" + name] = self._create_stream( cdn["cdnStreamUrl"], cdn["cdnStreamName"]) return streams
def _parse_smil(self, url): res = urlget(url) try: dom = xml.dom.minidom.parseString(res.text) except Exception as err: raise PluginError(("Unable to parse config XML: {0})").format(err)) httpbase = None streams = {} for meta in dom.getElementsByTagName("meta"): if meta.getAttribute("name") == "httpBase": httpbase = meta.getAttribute("content") break if not httpbase: raise PluginError("Missing HTTP base in SMIL") for video in dom.getElementsByTagName("video"): url = "{0}/{1}".format(httpbase, video.getAttribute("src")) bitrate = int(video.getAttribute("system-bitrate")) streams[bitrate] = AkamaiHDStream(self.session, url) return streams
def _check_vod_key(self, nodeip, nodeid, userno, userip): try: conn = socket.create_connection((nodeip, self.KeyCheckPort), timeout=15) except socket.error as err: raise PluginError( ("Failed to connect to key check server: {0}").format( str(err))) msg = "Login,0,{userno},{nodeid},{userip}\n".format(nodeid=nodeid, userno=userno, userip=userip) try: conn.sendall(bytes(msg, "ascii")) res = conn.recv(4096) except IOError as err: raise PluginError( ("Failed to communicate with key check server: {0}").format( str(err))) if len(res) == 0: raise PluginError("Empty response from key check server") conn.close() res = str(res, "ascii").strip().split(",") return res[-1]
def _find_gox_url(self, data): url = None # Parsing through the live page for a link to the gox XML file. # Quality is simply passed as a URL parameter e.g. HQ, SQ, SQTest try: patternhtml = "[^/]+var.+(http://www.gomtv.net/gox[^;]+;)" url = re.search(patternhtml, data).group(1) url = re.sub('\" \+ playType \+ \"', "{quality}", url) except AttributeError: raise PluginError( "Unable to find the majority of the GOMTV.net XML URL on the Live page" ) # Finding the title of the stream, probably not necessary but # done for completeness try: patterntitle = "this\.title[^;]+;" title = re.search(patterntitle, data).group(0) title = re.search('\"(.*)\"', title).group(0) title = re.sub('"', "", title) url = re.sub('"\+ tmpThis.title[^;]+;', title, url) except AttributeError: raise PluginError( "Unable to find the stream title on the Live page") return url
def _get_vod_streams(self): res = urlget(self.url) flashvars = re.search("FlashVars=\"(.+?)\"", res.text) if not flashvars: raise PluginError("Unable to find flashvars on page") flashvars = parse_qs(flashvars.group(1)) for var in ("vjoinid", "conid", "leagueid"): if not var in flashvars: raise PluginError( ("Missing key '{0}' in flashvars").format(var)) streams = {} qualities = ["SQ", "HQ"] for quality in qualities: params = dict(leagueid=flashvars["leagueid"][0], vjoinid=flashvars["vjoinid"][0], conid=flashvars["conid"][0], title="", ref="", tmpstamp=int(time.time()), strLevel=quality) res = urlget(self.GOXVODURL, params=params, session=self.rsession) if res.text != "1002" and len(res.text) > 0: gox = self._parse_gox_file(res.text) entry = gox[0] nokey = False for var in ("NODEIP", "NODEID", "UNO", "USERIP"): if not var in entry: nokey = True if nokey: self.logger.warning( "Unable to fetch key, make sure that you have access to this VOD" ) continue key = self._check_vod_key(entry["NODEIP"], entry["NODEID"], entry["UNO"], entry["USERIP"]) streams[quality.lower()] = HTTPStream( self.session, gox[0]["REF"], params=dict(key=key), headers=self.StreamHeaders) return streams
def _get_rtmp_streams(self): def clean_tag(tag): if tag[0] == "_": return tag[1:] else: return tag chansub = self._authenticate() url = self.StreamInfoURL.format(self.channelname) params = dict(b_id="true", group="", private_code="null", p=int(random.random() * 999999), channel_subscription=chansub, type="any") self.logger.debug("Fetching stream info") res = urlget(url, params=params) data = res.text # fix invalid xml data = re.sub("<(\d+)", "<_\g<1>", data) data = re.sub("</(\d+)", "</_\g<1>", data) streams = {} try: dom = xml.dom.minidom.parseString(data) except Exception as err: raise PluginError(("Unable to parse config XML: {0})").format(err)) nodes = dom.getElementsByTagName("nodes")[0] if len(nodes.childNodes) == 0: return streams swfurl = urlresolve(self.SWFURL) for node in nodes.childNodes: info = {} for child in node.childNodes: info[child.tagName] = self._get_node_text(child) if not ("connect" in info and "play" in info): continue stream = RTMPStream(self.session, { "rtmp": ("{0}/{1}").format(info["connect"], info["play"]), "swfVfy": swfurl, "live": True }) sname = clean_tag(node.tagName) if "token" in info: stream.params["jtv"] = info["token"] else: self.logger.warning("No token found for stream {0}, this stream may fail to play", sname) streams[sname] = stream return streams
def _get_streams(self): self.logger.debug("Fetching stream info") res = urlget(self.url, params=dict(output="json")) if res.json is None: raise PluginError("No JSON data in stream info") streams = {} video = verifyjson(res.json, "video") videos = verifyjson(video, "videoReferences") for video in videos: if not ("url" in video and "playerType" in video): continue if video["playerType"] == "flash": if video["url"].startswith("rtmp"): stream = RTMPStream( self.session, { "rtmp": video["url"], "pageUrl": self.PageURL, "swfVfy": self.SWFURL, "live": True }) streams[str(video["bitrate"]) + "k"] = stream elif video["playerType"] == "ios": try: hlsstreams = HLSStream.parse_variant_playlist( self.session, video["url"]) streams.update(hlsstreams) except IOError as err: self.logger.warning("Failed to get variant playlist: {0}", err) return streams
def _parse_gox_file(self, data): try: dom = xml.dom.minidom.parseString(data) except Exception as err: raise PluginError(("Unable to parse gox file: {0})").format(err)) entries = [] for xentry in dom.getElementsByTagName("ENTRY"): entry = {} for child in xentry.childNodes: if isinstance(child, xml.dom.minidom.Element): if child.tagName == "REF": href = child.getAttribute("href") # SQ and SQTest streams can be gomp2p links, with actual stream address passed as a parameter. if href.startswith("gomp2p://"): href, n = re.subn("^.*LiveAddr=", "", href) href = unquote(href) entry[child.tagName] = href else: entry[child.tagName] = self._get_node_text(child) entries.append(entry) return entries
def _get_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 _find_stream_urls(self, data): url = None # Parsing through the live page for a link to the gox XML file. # Quality is simply passed as a URL parameter e.g. HQ, SQ, SQTest try: patternhtml = "[^/]+var.+(http://www.gomtv.net/gox[^;]+;)" url = re.search(patternhtml, data).group(1) url = re.sub('\" \+ playType \+ \"', "{quality}", url) except AttributeError: raise PluginError( "Unable to find the majority of the GOMTV.net XML URL on the Live page" ) # Finding the title of the stream, probably not necessary but # done for completeness try: patterntitle = "this\.title[^;]+;" title = re.search(patterntitle, data).group(0) title = re.search('\"(.*)\"', title).group(0) title = re.sub('"', "", title) url = re.sub('"\+ tmpThis.title[^;]+;', title, url) except AttributeError: raise PluginError( "Unable to find the stream title on the Live page") # Check for multiple streams going at the same time, and extract the conid and the title # Those streams have the class "live_now" patternlive = '<a\shref=\"/live/index.gom\?conid=(?P<conid>\d+)\"\sclass=\"live_now\"\stitle=\"(?P<title>[^\"]+)' streams = re.findall(patternlive, data) if len(streams) > 1: urls = [] for stream in streams: # Modify the urlFromHTML according to the user singleurl = re.sub("conid=\d+", "conid=" + stream[0], url) singletitlehtml = "+".join(stream[0].split(" ")) singleurl = re.sub("title=[\w|.|+]*", "title=" + singletitlehtml, singleurl) urls.append(singleurl) return urls else: if url is None: return [] else: return [url]
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_USERNO", "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 _check_channel_live(self, channelname): url = self.MetadataURL.format(channelname) res = urlget(url, params=dict(fields="mode")) if len(res.json) == 0: raise PluginError("Error retrieving stream live status") mode = verifyjson(res.json, "mode") return mode == "live"
def _get_streams(self): (channelid, swfurl) = self._get_channel_info(self.url) if not (channelid and swfurl): raise NoStreamsError(self.url) self.logger.debug("Fetching stream info") res = urlget(self.ConfigURL.format(channelid)) try: dom = xml.dom.minidom.parseString(res.text) except Exception as err: raise PluginError(("Unable to parse config XML: {0})").format(err)) streams = {} channels = dom.getElementsByTagName("channels")[0] clip = channels.getElementsByTagName("clip")[0] self.logger.debug("Verifying SWF: {0}", swfurl) swfhash, swfsize = swfverify(swfurl) for item in clip.getElementsByTagName("item"): base = item.getAttribute("base") if not base: continue if base[0] == "$": ref = re.match("\${(.+)}", base).group(1) base = self.CDN[ref] for streamel in item.getElementsByTagName("stream"): altcount = 1 name = streamel.getAttribute("label").lower().replace(" ", "_") playpath = streamel.getAttribute("name") stream = RTMPStream(self.session, { "rtmp": ("{0}/{1}").format(base, playpath), "live": True, "swfhash": swfhash, "swfsize": swfsize, "pageUrl": self.url }) if not name in streams: streams[name] = stream else: if altcount == 1: streams[name + "_alt"] = stream else: streams[name + "_alt" + str(altcount)] = stream altcount += 1 return streams
def _get_stream_info(self): res = urlget(self.url) match = re.search("var initialData = ({.+})", res.text) if match: config = match.group(1) try: parsed = json.loads(config) except ValueError as err: raise PluginError( ("Unable to parse config JSON: {0})").format(err)) return parsed
def _parse_gox_file(self, data): # Grabbing the gomcmd URL try: patternstream = '<REF href="([^"]*)"\s*/>' match = re.search(patternstream, data).group(1) except AttributeError: raise PluginError( "Unable to find the gomcmd URL in the GOX XML file") match = match.replace("&", "&") match = unquote(match) # SQ and SQTest streams can be gomp2p links, with actual stream address passed as a parameter. if match.startswith("gomp2p://"): match, n = re.subn("^.*LiveAddr=", "", match) # Cosmetics, getting rid of the HTML entity, we don't # need either of the " character or " match = match.replace(""", "") return match
def _get_stream_info(self, url): res = urlget(url) data = res.text config = None match = re.search("'PLAYER_CONFIG': (.+)\n.+}\);", data) if match: config = match.group(1) match = re.search("yt.playerConfig = (.+)\;\n", data) if match: config = match.group(1) if config: try: parsed = json.loads(config) except ValueError as err: raise PluginError( ("Unable to parse config JSON: {0})").format(err)) return parsed
def _get_live_streams(self): streams = {} qualities = ["HQ", "SQ", "HQTest", "SQTest"] res = self._get_live_page(self.url) goxurl = self._find_gox_url(res.text) if not goxurl: raise PluginError("Unable to find GOX URL") for quality in qualities: # Grab the response of the URL listed on the Live page for a stream url = goxurl.format(quality=quality) res = urlget(url, session=self.rsession) # The response for the GOX XML if an incorrect stream quality is chosen is 1002. if res.text != "1002" and len(res.text) > 0: gox = self._parse_gox_file(res.text) streams[quality.lower()] = HTTPStream( self.session, gox[0]["REF"], headers=self.StreamHeaders) return streams
def _get_streams(self): channelname = self.url.rstrip("/").rpartition("/")[2].lower() streams = {} try: options = dict(l="info", a="ajax_video_info", file=channelname, rid=time()) res = urlget(self.HLSStreamURL, params=options, exception=IOError) match = re.search("'(http://.+\.m3u8)'", res.text) if not match: raise PluginError("Failed to find HLS playlist in result") playlisturl = match.group(1) self.logger.debug("Playlist URL is {0}", playlisturl) streams["live"] = HLSStream(self.session, playlisturl) except IOError: raise NoStreamsError(self.url) return streams
def _get_metadata(self): url = self.MetadataURL.format(self.channelname) headers = {} cookie = self.options.get("cookie") if cookie: headers["Cookie"] = cookie res = urlget(url, headers=headers) try: dom = xml.dom.minidom.parseString(res.text) except Exception as err: raise PluginError(("Unable to parse config XML: {0})").format(err)) meta = dom.getElementsByTagName("meta")[0] metadata = {} metadata["title"] = self._get_node_if_exists(dom, "title") metadata["access_guid"] = self._get_node_if_exists(dom, "access_guid") metadata["login"] = self._get_node_if_exists(dom, "login") return metadata
def _get_rtmp_streams(self, channelname): self.logger.debug("Fetching stream info") url = self.StreamInfoURL.format(channelname) self.logger.debug("JSON data url: {0}", url) res = urlget(url) if not isinstance(res.json, dict): raise PluginError("Stream info response is not JSON") if len(res.json) == 0: raise PluginError("JSON is empty") chan_info_json = res.json # This is ugly, not sure how to fix it. back_json_node = chan_info_json["sequence"][0]["layerList"][0] if back_json_node["name"] != "background": raise PluginError("JSON data has unexpected structure") rep_node = self._get_node_by_name(back_json_node["sequenceList"], "reporting")["layerList"] main_node = self._get_node_by_name(back_json_node["sequenceList"], "main")["layerList"] if not (rep_node and main_node): raise PluginError("Error parsing stream RTMP url") swfurl = self._get_node_by_name( rep_node, "reporting")["param"]["extraParams"]["videoSwfURL"] feeds_params = self._get_node_by_name(main_node, "video")["param"] if not (swfurl and feeds_params): raise PluginError("Error parsing stream RTMP url") streams = {} # Different feed qualities are available are a dict under "live" # In some cases where there's only 1 quality available, # it seems the "live" is absent. We use the single stream available # under the "customURL" key. if "live" in feeds_params and len(feeds_params["live"]) > 0: quals = feeds_params["live"] for key, quality in quals.items(): info = {} try: res = urlget(quality, exception=IOError) except IOError: continue rtmpurl = res.text stream = RTMPStream(self.session, { "rtmp": rtmpurl, "swfVfy": swfurl, "live": True }) self.logger.debug("Adding URL: {0}", rtmpurl) if key in self.QualityMap: sname = self.QualityMap[key] else: sname = key streams[sname] = stream else: res = urlget(feeds_params["customURL"]) rtmpurl = res.text stream = RTMPStream(self.session, { "rtmp": rtmpurl, "swfVfy": swfurl, "live": True }) self.logger.debug("Adding URL: {0}", feeds_params["customURL"]) streams["live"] = stream return streams