class StarGr(Plugin): _url_re = re.compile(r'https?://www\.star\.gr/tv/live-stream/') arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} res = self.session.http.get(self.url, headers=headers) script = [i.text for i in list(itertags(res.text, 'script'))][16] stream = re.search(r"'(?P<url>.+?\.m3u8)'", script).group('url') if self.session.http.head(stream).status_code == 404: raise NoStreamsError( 'Live stream is disabled due to 3rd party broacasts with no rights for web streams' ) headers.update({"Referer": self.url}) parse_hls = bool(strtobool(self.get_option('parse_hls'))) if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(live=HTTPStream(self.session, stream, headers=headers))
class OmegaCy(Plugin): _url_re = re.compile(r'https?://www\.omegatv\.com\.cy/live/') arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} cookie = urlencode(dict(self.session.http.head(self.url, headers={'User-Agent': CHROME}).cookies.items())) headers.update({'Cookie': cookie}) res = self.session.http.get(self.url, headers=headers) tags = list(itertags(res.text, 'script')) m3u8 = [i for i in tags if i.text.startswith(u'var playerInstance')][0].text stream = re.findall('"(.+?)"', m3u8)[1] headers.update({"Referer": self.url}) del headers['Cookie'] parse_hls = bool(strtobool(self.get_option('parse_hls'))) if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(live=HTTPStream(self.session, stream, headers=headers))
class TestPlugin(Plugin): arguments = PluginArguments( PluginArgument("bool", action="store_true"), PluginArgument("password", metavar="PASSWORD", sensitive=True)) options = Options({"a_option": "default"}) @classmethod def can_handle_url(self, url): return "test.se" in url def get_title(self): return "Test Title" def get_author(self): return "Tѥst Āuƭhǿr" def get_category(self): return None def _get_streams(self): if "empty" in self.url: return if "UnsortableStreamNames" in self.url: def gen(): for i in range(3): yield "vod", HTTPStream(self.session, "http://test.se/stream") return gen() if "NoStreamsError" in self.url: raise NoStreamsError(self.url) streams = {} streams["test"] = TestStream(self.session) streams["rtmp"] = RTMPStream(self.session, dict(rtmp="rtmp://test.se")) streams["hls"] = HLSStream(self.session, "http://test.se/playlist.m3u8") streams["http"] = HTTPStream(self.session, "http://test.se/stream") streams["akamaihd"] = AkamaiHDStream(self.session, "http://test.se/stream") streams["240p"] = HTTPStream(self.session, "http://test.se/stream") streams["360p"] = HTTPStream(self.session, "http://test.se/stream") streams["1080p"] = HTTPStream(self.session, "http://test.se/stream") streams["350k"] = HTTPStream(self.session, "http://test.se/stream") streams["800k"] = HTTPStream(self.session, "http://test.se/stream") streams["1500k"] = HTTPStream(self.session, "http://test.se/stream") streams["3000k"] = HTTPStream(self.session, "http://test.se/stream") streams["480p"] = [ HTTPStream(self.session, "http://test.se/stream"), RTMPStream(self.session, dict(rtmp="rtmp://test.se")) ] return streams
class Star(Plugin): _url_re = re.compile( r'https?://www\.starx?\.gr/(?:tv|show)/(?:live-stream/|(?:psychagogia|enimerosi|[\w-]+)/[\w-]+/(?:[\w-]+-\d+/|\d+))' ) _player_url = 'https://cdnapisec.kaltura.com/p/713821/sp/0/playManifest/entryId/{0}/format/applehttp/protocol/https/flavorParamId/0/manifest.m3u8' arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} res = self.session.http.get(self.url, headers=headers) if 'live-stream' in self.url: html = [i.text for i in list(itertags(res.text, 'script'))] html = [i for i in html if 'm3u8' in i][0] stream = re.search(r"(?P<url>http.+?\.m3u8)", html) elif 'starx' in self.url: try: vid = re.search(r"kalturaPlayer\('(?P<id>\w+)'", res.text).group('id') stream = self._player_url.format(vid) except Exception: stream = None else: stream = re.search(r"(?P<url>http.+?\.m3u8)", res.text) if not stream: raise PluginError('Did not find the playable url') elif 'starx' not in self.url: stream = stream.group('url') headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict( stream=HTTPStream(self.session, stream, headers=headers))
class AlphaGr(Plugin): arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return _url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} live = False res = self.session.http.get(self.url, headers=headers) if '/live' in self.url: stream = [i for i in list(itertags(res.text, 'div')) if 'data-liveurl' in i.attributes] stream = stream[0].attributes['data-liveurl'] live = True else: if 'vtype' in self.url: vid = _url_re.match(self.url).group('vid') show_id = _url_re.match(self.url).group('show_id') res = self.session.http.get(_api_url.format(vid=vid, show_id=show_id), headers=headers) vid = [i for i in list(itertags(res.text, 'div')) if 'data-plugin-player' in i.attributes][0].attributes['data-plugin-player'] try: stream = json.loads(self._replace_html_codes(vid.decode('utf-8')))['Url'] except Exception: stream = json.loads(self._replace_html_codes(vid))['Url'] headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if parse_hls and live: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(stream=HTTPStream(self.session, stream, headers=headers)) def _replace_html_codes(self, txt): try: from HTMLParser import HTMLParser unescape = HTMLParser().unescape except Exception: from html import unescape txt = re.sub("(&#[0-9]+)([^;^0-9]+)", "\\1;\\2", txt) txt = unescape(txt) txt = txt.replace(""", "\"") txt = txt.replace("&", "&") txt = txt.replace("&", "&") txt = txt.replace(" ", "") return txt
class TestPlugin(Plugin): arguments = PluginArguments( PluginArgument( "bool", action="store_true" ), PluginArgument( "password", metavar="PASSWORD", sensitive=True ) ) options = Options({ "a_option": "default" }) id = "test-id-1234-5678" author = "Tѥst Āuƭhǿr" category = None title = "Test Title" def _get_streams(self): if "empty" in self.url: return if "UnsortableStreamNames" in self.url: def gen(): for i in range(3): yield "vod", HTTPStream(self.session, "http://test.se/stream") return gen() if "NoStreamsError" in self.url: raise NoStreamsError(self.url) streams = {} streams["test"] = TestStream(self.session) streams["hls"] = HLSStream(self.session, "http://test.se/playlist.m3u8") streams["http"] = HTTPStream(self.session, "http://test.se/stream") streams["240p"] = HTTPStream(self.session, "http://test.se/stream") streams["360p"] = HTTPStream(self.session, "http://test.se/stream") streams["1080p"] = HTTPStream(self.session, "http://test.se/stream") streams["350k"] = HTTPStream(self.session, "http://test.se/stream") streams["800k"] = HTTPStream(self.session, "http://test.se/stream") streams["1500k"] = HTTPStream(self.session, "http://test.se/stream") streams["3000k"] = HTTPStream(self.session, "http://test.se/stream") streams["480p"] = [ HTTPStream(self.session, "http://test.se/stream"), HLSStream(self.session, "http://test.se/playlist.m3u8") ] return streams
class Ant1Gr(Plugin): _url_re = re.compile( r'(?P<scheme>https?)://www\.(?P<domain>antenna|netwix)\.gr/(?P<path>Live|watch)(?:/\d+/[\w-]+)?' ) _param_re = re.compile( r"\$.getJSON\(\'(?P<param>.+?)[?'](?:.+?cid: '(?P<id>\d+)')?") _base_link = '{0}://www.{1}.gr' arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} if self.url.endswith('/Live'): live = True else: live = False res = self.session.http.get(self.url, headers=headers) match = self._param_re.search(res.text) domain = self._url_re.match(self.url).group('domain') scheme = self._url_re.match(self.url).group('scheme') param = match.group('param') if not live: param = '?'.join([param, 'cid={0}'.format(match.group('id'))]) _json_url = urljoin(self._base_link.format(scheme, domain), param) _json_object = self.session.http.get(_json_url, headers=headers).json() stream = _json_object.get('url') headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict( stream=HTTPStream(self.session, stream, headers=headers))
class HLSKeyUriPlugin(Plugin): _url_re = re.compile(r'(hlskeyuri://)(.+(?:\.m3u8)?.*)') arguments = PluginArguments( PluginArgument('key-uri', argument_name='hls-key-uri', required=True, metavar='KEY-URI', help=''' Repair a broken Key-URI. You can reuse the none broken items: ${scheme} ${netloc} ${path} ${query} streamlink --hls-key-uri '${scheme}${netloc}${path}${query}' Replace the broken part, like: streamlink --hls-key-uri 'https://${netloc}${path}${query}' '''), ) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): self.session.http.headers.update({'User-Agent': useragents.FIREFOX}) log.debug('Version 2018-07-12') log.info('This is a custom plugin. ' 'For support visit https://github.com/back-to/plugins') url, params = parse_url_params(self.url) urlnoproto = self._url_re.match(url).group(2) urlnoproto = update_scheme('http://', urlnoproto) streams = self.session.streams(urlnoproto, stream_types=['hls']) if not streams: log.debug('No stream found for hls-key-uri,' ' stream is not available.') return stream = streams['best'] urlnoproto = stream.url log.debug('URL={0}; params={1}', urlnoproto, params) streams = KeyUriHLSStream.parse_variant_playlist( self.session, urlnoproto, **params) if not streams: return { 'live': KeyUriHLSStream(self.session, urlnoproto, **params) } else: return streams
class Ert(Plugin): _url_re = re.compile(r'https?://(?:webtv|archive|www)\.ert(?:flix)?\.gr/(?:\d+/|\w+-live/|[\w-]+/[\w-]+/[\w-]+/)') arguments = PluginArguments( PluginArgument("parse_hls", default='true'), PluginArgument("force_gr_stream", default='false') ) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} res = self.session.http.get(self.url, headers=headers) iframe = list(itertags(res.text, 'iframe'))[0].attributes['src'] res = self.session.http.get(iframe, headers=headers) streams = re.findall(r'var (?:HLSLink|stream)(?:ww)?\s+=\s+[\'"](.+?)[\'"]', res.text) try: force_gr = bool(strtobool(self.get_option('force_gr_stream'))) except AttributeError: force_gr = True if (len(streams) == 2 and self._geo_detect()) or force_gr: stream = streams[0] else: stream = streams[-1] headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(vod=HTTPStream(self.session, stream, headers=headers)) def _geo_detect(self): _json = self.session.http.get('https://geoip.siliconweb.com/geo.json').text _json = json.loads(_json) if 'GR' in _json['country']: return True
class BTV(Plugin): arguments = PluginArguments( PluginArgument( "username", help=argparse.SUPPRESS ), PluginArgument( "password", sensitive=True, help=argparse.SUPPRESS ) ) url_re = re.compile(r"https?://(?:www\.)?btvplus\.bg/live/?") api_url = "https://btvplus.bg/lbin/v3/btvplus/player_config.php" 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 get_hls_url(self, media_id): res = self.session.http.get(self.api_url, params=dict(media_id=media_id)) return parse_json(res.text, schema=self.api_schema) def _get_streams(self): res = self.session.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: log.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)
class AlphaCy(Plugin): arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return _url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} res = self.session.http.get(self.url, headers=headers, verify=False) if urlparse(self.url).path == '/live': stream = [ i for i in list(itertags(res.text, 'script')) if "hls" in i.text ] try: stream = re.search(r'''['"](http.+?)['"]''', stream[0].text).group(1) except Exception: stream = None live = True else: stream = [ i for i in list(itertags(res.text, 'a')) if "mp4" in i.attributes.get('href', '') ] stream = stream[0].attributes.get('href') live = False headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if stream: if parse_hls and live: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict( vod=HTTPStream(self.session, stream, headers=headers))
class Rik(Plugin): _url_re = re.compile(r'https?://cybc\.com\.cy/(?:live-tv|video-on-demand)/(?:\u03c1\u03b9\u03ba|\xcf\x81\xce\xb9\xce\xba|%CF%81%CE%B9%CE%BA)-\w+/(?:.+?episodes.+?/)?', re.U) arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} self.url = self.url.replace(u'ρικ', quote(u'ρικ'.encode('utf-8'))) get_page = self.session.http.get(self.url, headers=headers) tags = list(itertags(get_page.text, 'script')) if 'live-tv' in self.url: tag = [i for i in tags if 'm3u8' in i.text][0].text try: stream = re.search(r'''["'](http.+?\.m3u8)['"]''', tag).group(1) except IndexError: raise NoStreamsError('RIK Broadcast is currently disabled') else: tag = [i for i in tags if '.mp4' in i.text and 'sources' in i.text][0].text stream = re.search(r'''file: ['"](.+?\.mp4)['"]''', tag).group(1) headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if parse_hls and 'live-tv' in self.url: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(stream=HTTPStream(self.session, stream, headers=headers))
class SkaiGr(Plugin): _url_re = re.compile(r'https?://www\.skai(?:tv)?\.gr/(?:episode|videos|live)/?(?:\S+|\w+/[\w-]+/[\d-]+)?') _player_url = 'http://videostream.skai.gr/' arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} res = self.session.http.get(self.url, headers=headers) if '/videos' not in self.url: json_ = re.search(r'var data = ({.+?});', res.text).group(1) json_ = json.loads(json_) if '/live' not in self.url: stream = ''.join([self._player_url, json_['episode'][0]['media_item_file'], '.m3u8']) else: stream = json_['now']['livestream'] else: stream = [ i for i in list(itertags(res.text, 'meta')) if 'videostream' in i.attributes.get('content', '') ][0].attributes.get('content') headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(vod=HTTPStream(self.session, stream, headers=headers))
class Sigma(Plugin): arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return _url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} res = self.session.http.get(self.url, headers=headers) if 'page/live' in self.url: stream = ''.join([ 'https:', [i for i in list(itertags(res.text, 'source')) ][0].attributes['src'] ]) live = True else: stream = [(i.attributes['type'], ''.join(['https:', i.attributes['src']])) for i in list(itertags(res.text, 'source'))[:-1]] live = False headers.update({"Referer": self.url}) try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if live: if parse_hls: yield HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: yield dict( live=HTTPStream(self.session, stream, headers=headers)) else: for q, s in stream: yield q, HTTPStream(self.session, s, headers=headers)
class SS365(Plugin): arguments = PluginArguments( PluginArgument( "bw", argument_name="ss365-bandwidth", metavar="BANDWIDTH", default=1000000, help=""" The bandwidth in bit/sec. Default is 1Mbit/sec. """ ) ) _url_re = re.compile(r"http(s)?://sportstream-365.com/viewer\?gameId=(?P<channel>\d+)(?:&tagz=)?", re.VERBOSE) _STREAM_INFO_URL = "http://sportstream-365.com/viewer\?gameId={channel}&tagz=" _STREAM_REAL_URL = "{proto}://{host}/xsport{movie_id}_smooth_1?b={mode}" def __init__(self, url): Plugin.__init__(self, url) match = self._url_re.match(url).groupdict() self.channel = match.get("channel") self.session.http.headers.update({'User-Agent': useragents.CHROME}) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) is not None def _get_streams(self): #wss://edge1.tvbetstream.com:4433/xsport1049_smooth_1?b=597620 proto = "wss" host = "edge1.tvbetstream.com:4433" movie_id = self.channel bw = self.options.get("bw") if (proto == '') or (host == '') or (not movie_id): raise PluginError("No stream available for {}".format(self.channel)) real_stream_url = self._STREAM_REAL_URL.format(proto=proto, host=host, movie_id=movie_id, mode=bw) log.debug("SS365 stream url: {}".format(real_stream_url)) return {"live": SS365Stream(session=self.session, url=real_stream_url)}
class OmegaCy(Plugin): _url_re = re.compile(r'https?://www\.omegatv\.com\.cy/live/') arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} cookie = urlencode( dict( self.session.http.head(self.url, headers={ 'User-Agent': CHROME }).cookies.items())) headers.update({'Cookie': cookie}) res = self.session.http.get(self.url, headers=headers) tags = list(itertags(res.text, 'script')) text = [i for i in tags if 'OmegaTvLive' in i.text][0].text stream = json.loads(re.search('({.+})', text).group(1))['video']['source']['src'] headers.update({"Referer": self.url}) del headers['Cookie'] try: parse_hls = bool(strtobool(self.get_option('parse_hls'))) except AttributeError: parse_hls = True if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(live=HTTPStream(self.session, stream, headers=headers))
class Ant1Gr(Plugin): _url_re = re.compile(r'https?://www\.antenna\.gr/Live') _param_re = re.compile(r'\$.getJSON\(\'(?P<param>.+?)\?') _base_link = 'http://www.antenna.gr' arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} res = self.session.http.get(self.url, headers=headers) param = self._param_re.search(res.text).group('param') _json_url = urljoin(self._base_link, param) _json_object = self.session.http.get(_json_url, headers=headers).json() stream = _json_object.get('url') if stream.endswith('.mp4'): raise NoStreamsError('Stream is probably geo-locked to Greece') headers.update({"Referer": self.url}) parse_hls = bool(strtobool(self.get_option('parse_hls'))) if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(live=HTTPStream(self.session, stream, headers=headers))
class Ant1Cy(Plugin): _url_re = re.compile(r'https?://www\.ant1\.com\.cy/web-tv-live/') _live_api_url = 'https://www.ant1.com.cy/ajax.aspx?m=Atcom.Sites.Ant1iwo.Modules.TokenGenerator&videoURL={0}' arguments = PluginArguments(PluginArgument("parse_hls", default='true')) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): headers = {'User-Agent': CHROME} get_page = self.session.http.get(self.url, headers=headers) try: m3u8 = re.findall("'(.+?)'", list(itertags(get_page.text, 'script'))[-2].text)[1] except IndexError: raise NoStreamsError stream = self.session.http.post(self._live_api_url.format(m3u8), headers=headers).text headers.update({"Referer": self.url}) parse_hls = bool(strtobool(self.get_option('parse_hls'))) if parse_hls: return HLSStream.parse_variant_playlist(self.session, stream, headers=headers) else: return dict(live=HTTPStream(self.session, stream, headers=headers))
class Zattoo(Plugin): API_CHANNELS = '{0}/zapi/v2/cached/channels/{1}?details=False' API_HELLO = '{0}/zapi/session/hello' API_HELLO_V3 = '{0}/zapi/v3/session/hello' API_LOGIN = '******' API_LOGIN_V3 = '{0}/zapi/v3/account/login' API_SESSION = '{0}/zapi/v2/session' API_WATCH = '{0}/zapi/watch' API_WATCH_REC = '{0}/zapi/watch/recording/{1}' API_WATCH_VOD = '{0}/zapi/avod/videos/{1}/watch' STREAMS_ZATTOO = ['dash', 'hls', 'hls5'] TIME_CONTROL = 60 * 60 * 2 TIME_SESSION = 60 * 60 * 24 * 30 _url_re = re.compile(r'''(?x) https?:// (?P<base_url> (?:(?: iptv\.glattvision|www\.(?:myvisiontv|saktv|vtxtv) )\.ch )|(?:(?: mobiltv\.quickline|www\.quantum-tv|zattoo )\.com )|(?:(?: tvonline\.ewe|nettv\.netcologne|tvplus\.m-net )\.de )|(?:(?: player\.waly|www\.(?:1und1|netplus) )\.tv) |www\.bbv-tv\.net |www\.meinewelt\.cc )/ (?: (?: recording(?:s\?recording=|/) | (?:ondemand/)?(?:watch/(?:[^/\s]+)(?:/[^/]+/)) )(?P<recording_id>\d+) | (?: (?:live/|watch/)|(?:channels(?:/\w+)?|guide)\?channel= )(?P<channel>[^/\s]+) | ondemand(?:\?video=|/watch/)(?P<vod_id>[^-]+) ) ''') _app_token_re = re.compile(r"""window\.appToken\s+=\s+'([^']+)'""") _channels_schema = validate.Schema( { 'success': bool, 'channel_groups': [{ 'channels': [ { 'display_alias': validate.text, 'cid': validate.text }, ] }] }, validate.get('channel_groups'), ) _session_schema = validate.Schema( { 'success': bool, 'session': { 'loggedin': bool } }, validate.get('session')) arguments = PluginArguments( PluginArgument("email", requires=["password"], metavar="EMAIL", help=""" The email associated with your zattoo account, required to access any zattoo stream. """), PluginArgument("password", sensitive=True, metavar="PASSWORD", help=""" A zattoo account password to use with --zattoo-email. """), PluginArgument("purge-credentials", action="store_true", help=""" Purge cached zattoo credentials to initiate a new session and reauthenticate. """), PluginArgument('stream-types', metavar='TYPES', type=comma_list_filter(STREAMS_ZATTOO), default=['hls'], help=''' A comma-delimited list of stream types which should be used, the following types are allowed: - {0} Default is "hls". '''.format('\n - '.join(STREAMS_ZATTOO)))) def __init__(self, url): super(Zattoo, self).__init__(url) self.domain = self._url_re.match(url).group('base_url') self._session_attributes = Cache( filename='plugin-cache.json', key_prefix='zattoo:attributes:{0}'.format(self.domain)) self._uuid = self._session_attributes.get('uuid') self._authed = (self._session_attributes.get('power_guide_hash') and self._uuid and self.session.http.cookies.get( 'pzuid', domain=self.domain) and self.session.http.cookies.get('beaker.session.id', domain=self.domain)) self._session_control = self._session_attributes.get( 'session_control', False) self.base_url = 'https://{0}'.format(self.domain) 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 cls._url_re.match(url) is not None def _hello(self): log.debug('_hello ...') # a new session is required for the app_token self.session.http.cookies = cookiejar_from_dict({}) if self.base_url == 'https://zattoo.com': app_token_url = 'https://zattoo.com/int/' elif self.base_url == 'https://www.quantum-tv.com': app_token_url = 'https://www.quantum-tv.com/token-4d0d61d4ce0bf8d9982171f349d19f34.json' else: app_token_url = self.base_url res = self.session.http.get(app_token_url) match = self._app_token_re.search(res.text) if self.base_url == 'https://www.quantum-tv.com': app_token = self.session.http.json(res)["session_token"] hello_url = self.API_HELLO_V3.format(self.base_url) else: 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=self.TIME_SESSION) params = { 'client_app_token': app_token, 'uuid': __uuid, } if self.base_url == 'https://www.quantum-tv.com': params['app_version'] = '3.2028.3' else: params['lang'] = 'en' params['format'] = 'json' res = self.session.http.post(hello_url, headers=self.headers, data=params) def _login(self, email, password): log.debug('_login ... Attempting login as {0}'.format(email)) params = {'login': email, 'password': password, 'remember': 'true'} if self.base_url == 'https://quantum-tv.com': login_url = self.API_LOGIN_V3.format(self.base_url) else: login_url = self.API_LOGIN.format(self.base_url) try: res = self.session.http.post(login_url, headers=self.headers, data=params) except Exception as e: if '400 Client Error' in str(e): raise PluginError( 'Failed to login, check your username/password') raise e data = self.session.http.json(res) self._authed = data['success'] log.debug('New Session Data') self.save_cookies(default_expires=self.TIME_SESSION) self._session_attributes.set('power_guide_hash', data['session']['power_guide_hash'], expires=self.TIME_SESSION) self._session_attributes.set('session_control', True, expires=self.TIME_CONTROL) def _watch(self): log.debug('_watch ...') match = self._url_re.match(self.url) if not match: log.debug('_watch ... no match') return channel = match.group('channel') vod_id = match.group('vod_id') recording_id = match.group('recording_id') params = {'https_watch_urls': True} if channel: watch_url = self.API_WATCH.format(self.base_url) params_cid = self._get_params_cid(channel) if not params_cid: return params.update(params_cid) elif vod_id: log.debug('Found vod_id: {0}'.format(vod_id)) watch_url = self.API_WATCH_VOD.format(self.base_url, vod_id) elif recording_id: log.debug('Found recording_id: {0}'.format(recording_id)) watch_url = self.API_WATCH_REC.format(self.base_url, recording_id) else: log.debug('Missing watch_url') return zattoo_stream_types = self.get_option('stream-types') or ['hls'] for stream_type in zattoo_stream_types: params_stream_type = {'stream_type': stream_type} params.update(params_stream_type) try: res = self.session.http.post(watch_url, headers=self.headers, data=params) except Exception as e: if '404 Client Error' in str(e): log.error('Unfortunately streaming is not permitted in ' 'this country or this channel does not exist.') elif '402 Client Error: Payment Required' in str(e): log.error('Paid subscription required for this channel.') log.info('If paid subscription exist, use --zattoo-purge' '-credentials to start a new session.') elif '403 Client Error' in str(e): log.debug('Force session reset for watch_url') self.reset_session() else: log.error(str(e)) return data = self.session.http.json(res) log.debug('Found data for {0}'.format(stream_type)) if data['success'] and stream_type in ['hls', 'hls5']: for url in data['stream']['watch_urls']: for s in HLSStream.parse_variant_playlist( self.session, url['url']).items(): yield s elif data['success'] and stream_type == 'dash': for url in data['stream']['watch_urls']: for s in DASHStream.parse_manifest(self.session, url['url']).items(): yield s def _get_params_cid(self, channel): log.debug('get channel ID for {0}'.format(channel)) channels_url = self.API_CHANNELS.format( self.base_url, self._session_attributes.get('power_guide_hash')) try: res = self.session.http.get(channels_url, headers=self.headers) except Exception: log.debug('Force session reset for _get_params_cid') self.reset_session() return False data = self.session.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'] log.debug('Available zattoo channels in this country: {0}'.format( ', '.join(sorted(zattoo_list)))) if not cid: cid = channel log.debug('CHANNEL ID: {0}'.format(cid)) return {'cid': cid} def reset_session(self): self._session_attributes.set('power_guide_hash', None, expires=0) self._session_attributes.set('uuid', None, expires=0) self.clear_cookies() self._authed = False def _get_streams(self): email = self.get_option('email') password = self.get_option('password') if self.options.get('purge_credentials'): self.reset_session() log.info('All credentials were successfully removed.') elif (self._authed and not self._session_control): # check every two hours, if the session is actually valid log.debug('Session control for {0}'.format(self.domain)) res = self.session.http.get(self.API_SESSION.format(self.base_url)) res = self.session.http.json(res, schema=self._session_schema) if res['loggedin']: self._session_attributes.set('session_control', True, expires=self.TIME_CONTROL) else: log.debug('User is not logged in') self._authed = False if not self._authed and (not email and not password): log.error( 'A login for Zattoo is required, use --zattoo-email EMAIL' ' --zattoo-password PASSWORD to set them') return if not self._authed: self._hello() self._login(email, password) return self._watch()
class Pixiv(Plugin): """Plugin for https://sketch.pixiv.net/lives""" _url_re = re.compile(r"https?://sketch\.pixiv\.net/@?(?P<user>[^/]+)") _post_key_re = re.compile( r"""name=["']post_key["']\svalue=["'](?P<data>[^"']+)["']""") _user_dict_schema = validate.Schema({ "user": { "unique_name": validate.text, "name": validate.all(validate.text, validate.transform(maybe_decode)) }, validate.optional("hls_movie"): { "url": validate.text } }) _user_schema = validate.Schema({ "owner": _user_dict_schema, "performers": [validate.any(_user_dict_schema, None)] }) _data_lives_schema = validate.Schema({"data": { "lives": [_user_schema] }}, validate.get("data"), validate.get("lives")) api_lives = "https://sketch.pixiv.net/api/lives.json" login_url_get = "https://accounts.pixiv.net/login" login_url_post = "https://accounts.pixiv.net/api/login" arguments = PluginArguments( PluginArgument("username", requires=["password"], metavar="USERNAME", help=""" The email/username used to register with pixiv.net """), PluginArgument("password", sensitive=True, metavar="PASSWORD", help=""" A pixiv.net account password to use with --pixiv-username """), PluginArgument("sessionid", requires=["devicetoken"], sensitive=True, metavar="SESSIONID", help=""" The pixiv.net sessionid that's used in pixivs PHPSESSID cookie. can be used instead of the username/password login process. """), PluginArgument("devicetoken", sensitive=True, metavar="DEVICETOKEN", help=""" The pixiv.net device token that's used in pixivs device_token cookie. can be used instead of the username/password login process. """), PluginArgument("purge-credentials", action="store_true", help=""" Purge cached Pixiv credentials to initiate a new session and reauthenticate. """), PluginArgument("performer", metavar="USER", help=""" Select a co-host stream instead of the owner stream. """)) def __init__(self, url): super(Pixiv, self).__init__(url) self._authed = (self.session.http.cookies.get("PHPSESSID") and self.session.http.cookies.get("device_token")) self.session.http.headers.update({ "User-Agent": useragents.FIREFOX, "Referer": self.url }) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) is not None def _login(self, username, password): res = self.session.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 = self.session.http.post(self.login_url_post, data=data) res = self.session.http.json(res) log.trace("{0!r}".format(res)) if res["body"].get("success"): self.save_cookies() log.info("Successfully logged in") else: log.error("Failed to log in.") def _login_using_session_id_and_device_token(self, session_id, device_token): res = self.session.http.get(self.login_url_get) self.session.http.cookies.set('PHPSESSID', session_id, domain='.pixiv.net', path='/') self.session.http.cookies.set('device_token', device_token, domain='.pixiv.net', path='/') self.save_cookies() log.info("Successfully set sessionId and deviceToken") def hls_stream(self, hls_url): log.debug("URL={0}".format(hls_url)) for s in HLSStream.parse_variant_playlist(self.session, hls_url).items(): yield s def get_streamer_data(self): res = self.session.http.get(self.api_lives) data = self.session.http.json(res, schema=self._data_lives_schema) log.debug("Found {0} streams".format(len(data))) m = self._url_re.match(self.url) for item in data: if item["owner"]["user"]["unique_name"] == m.group("user"): return item raise NoStreamsError(self.url) def _get_streams(self): login_username = self.get_option("username") login_password = self.get_option("password") login_session_id = self.get_option("sessionid") login_device_token = self.get_option("devicetoken") if self.options.get("purge_credentials"): self.clear_cookies() self._authed = False log.info("All credentials were successfully removed.") if self._authed: log.debug("Attempting to authenticate using cached cookies") elif not self._authed and login_username and login_password: self._login(login_username, login_password) elif not self._authed and login_session_id and login_device_token: self._login_using_session_id_and_device_token( login_session_id, login_device_token) streamer_data = self.get_streamer_data() performers = streamer_data.get("performers") log.trace("{0!r}".format(streamer_data)) if performers: co_hosts = [] # create a list of all available performers for p in performers: co_hosts += [(p["user"]["unique_name"], p["user"]["name"])] log.info("Available hosts: {0}".format(", ".join( ["{0} ({1})".format(k, v) for k, v in co_hosts]))) # control if the host from --pixiv-performer is valid, # if not let the User select a different host if (self.get_option("performer") and not self.get_option("performer") in [v[0] for v in co_hosts]): # print the owner as 0 log.info("0 - {0} ({1})".format( streamer_data["owner"]["user"]["unique_name"], streamer_data["owner"]["user"]["name"])) # print all other performer for i, item in enumerate(co_hosts, start=1): log.info("{0} - {1} ({2})".format(i, item[0], item[1])) try: number = int( self.input_ask("Enter the number you'd like to watch"). split(" ")[0]) if number == 0: # default stream self.set_option("performer", None) else: # other co-hosts self.set_option("performer", co_hosts[number - 1][0]) except FatalPluginError: raise PluginError("Selected performer is invalid.") except (IndexError, ValueError, TypeError): raise PluginError("Input is invalid") # ignore the owner stream, if a performer is selected # or use it when there are no other performers if not self.get_option("performer") or not performers: return self.hls_stream(streamer_data["owner"]["hls_movie"]["url"]) # play a co-host stream if performers and self.get_option("performer"): for p in performers: if p["user"]["unique_name"] == self.get_option("performer"): # if someone goes online at the same time as Streamlink # was used, the hls URL might not be in the JSON data hls_movie = p.get("hls_movie") if hls_movie: return self.hls_stream(hls_movie["url"])
class SteamBroadcastPlugin(Plugin): _url_re = re.compile(r"https?://steamcommunity.com/broadcast/watch/(\d+)") _get_broadcast_url = "https://steamcommunity.com/broadcast/getbroadcastmpd/" _user_agent = "streamlink/{}".format(streamlink.__version__) _broadcast_schema = Schema({ "success": validate.any("ready", "unavailable", "waiting", "waiting_to_start", "waiting_for_start"), "retry": int, "broadcastid": validate.any(validate.text, int), validate.optional("url"): validate.url(), validate.optional("viewertoken"): validate.text }) _get_rsa_key_url = "https://steamcommunity.com/login/getrsakey/" _rsa_key_schema = validate.Schema({ "publickey_exp": validate.all(validate.text, validate.transform(lambda x: int(x, 16))), "publickey_mod": validate.all(validate.text, validate.transform(lambda x: int(x, 16))), "success": True, "timestamp": validate.text, "token_gid": validate.text }) _dologin_url = "https://steamcommunity.com/login/dologin/" _dologin_schema = validate.Schema({ "success": bool, "requires_twofactor": bool, validate.optional("message"): validate.text, validate.optional("emailauth_needed"): bool, validate.optional("emaildomain"): validate.text, validate.optional("emailsteamid"): validate.text, validate.optional("login_complete"): bool, validate.optional("captcha_needed"): bool, validate.optional("captcha_gid"): validate.any(validate.text, int) }) _captcha_url = "https://steamcommunity.com/public/captcha.php?gid={}" arguments = PluginArguments( PluginArgument( "email", metavar="EMAIL", requires=["password"], help=""" A Steam account email address to access friends/private streams """ ), PluginArgument( "password", metavar="PASSWORD", sensitive=True, help=""" A Steam account password to use with --steam-email. """ )) def __init__(self, url): super(SteamBroadcastPlugin, self).__init__(url) http.headers["User-Agent"] = self._user_agent @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) is not None @property def donotcache(self): return str(int(time.time() * 1000)) def encrypt_password(self, email, password): """ Get the RSA key for the user and encrypt the users password :param email: steam account :param password: password for account :return: encrypted password """ res = http.get(self._get_rsa_key_url, params=dict(username=email, donotcache=self.donotcache)) rsadata = http.json(res, schema=self._rsa_key_schema) rsa = RSA.construct((rsadata["publickey_mod"], rsadata["publickey_exp"])) cipher = PKCS1_v1_5.new(rsa) return base64.b64encode(cipher.encrypt(password.encode("utf8"))), rsadata["timestamp"] def dologin(self, email, password, emailauth="", emailsteamid="", captchagid="-1", captcha_text="", twofactorcode=""): """ Logs in to Steam """ epassword, rsatimestamp = self.encrypt_password(email, password) login_data = { 'username': email, "password": epassword, "emailauth": emailauth, "loginfriendlyname": "Streamlink", "captchagid": captchagid, "captcha_text": captcha_text, "emailsteamid": emailsteamid, "rsatimestamp": rsatimestamp, "remember_login": True, "donotcache": self.donotcache, "twofactorcode": twofactorcode } res = http.post(self._dologin_url, data=login_data) resp = http.json(res, schema=self._dologin_schema) if not resp[u"success"]: if resp.get(u"captcha_needed"): # special case for captcha captchagid = resp[u"captcha_gid"] log.error("Captcha result required, open this URL to see the captcha: {}".format( self._captcha_url.format(captchagid))) try: captcha_text = self.input_ask("Captcha text") except FatalPluginError: captcha_text = None if not captcha_text: return False else: # If the user must enter the code that was emailed to them if resp.get(u"emailauth_needed"): if not emailauth: try: emailauth = self.input_ask("Email auth code required") except FatalPluginError: emailauth = None if not emailauth: return False else: raise SteamLoginFailed("Email auth key error") # If the user must enter a two factor auth code if resp.get(u"requires_twofactor"): try: twofactorcode = self.input_ask("Two factor auth code required") except FatalPluginError: twofactorcode = None if not twofactorcode: return False if resp.get(u"message"): raise SteamLoginFailed(resp[u"message"]) return self.dologin(email, password, emailauth=emailauth, emailsteamid=resp.get(u"emailsteamid", u""), captcha_text=captcha_text, captchagid=captchagid, twofactorcode=twofactorcode) elif resp.get("login_complete"): return True else: log.error("Something when wrong when logging in to Steam") return False def login(self, email, password): log.info("Attempting to login to Steam as {}".format(email)) return self.dologin(email, password) def _get_broadcast_stream(self, steamid, viewertoken=0): res = http.get(self._get_broadcast_url, params=dict(broadcastid=0, steamid=steamid, viewertoken=viewertoken)) return http.json(res, schema=self._broadcast_schema) def _get_streams(self): streamdata = None if self.get_option("email"): if self.login(self.get_option("email"), self.get_option("password")): log.info("Logged in as {0}".format(self.get_option("email"))) self.save_cookies(lambda c: "steamMachineAuth" in c.name) # extract the steam ID from the URL steamid = self._url_re.match(self.url).group(1) while streamdata is None or streamdata[u"success"] in ("waiting", "waiting_for_start"): streamdata = self._get_broadcast_stream(steamid) if streamdata[u"success"] == "ready": return DASHStream.parse_manifest(self.session, streamdata["url"]) elif streamdata[u"success"] == "unavailable": log.error("This stream is currently unavailable") return else: r = streamdata[u"retry"] / 1000.0 log.info("Waiting for stream, will retry again in {} seconds...".format(r)) time.sleep(r)
class FunimationNow(Plugin): arguments = PluginArguments( PluginArgument("email", argument_name="funimation-email", requires=["password"], help="Email address for your Funimation account."), PluginArgument("password", argument_name="funimation-password", sensitive=True, help="Password for your Funimation account."), PluginArgument("language", argument_name="funimation-language", choices=["en", "ja", "english", "japanese"], default="english", help=""" The audio language to use for the stream; japanese or english. Default is "english". """), PluginArgument("mux-subtitles", argument_name="funimation-mux-subtitles", action="store_true", help=""" Enable automatically including available subtitles in to the output stream. """)) url_re = re.compile( r""" https?://(?:www\.)funimation(.com|now.uk) """, re.VERBOSE) experience_id_re = re.compile(r"/player/(\d+)") mp4_quality = "480p" @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None def _get_streams(self): self.session.http.headers = {"User-Agent": useragents.CHROME} res = self.session.http.get(self.url) # remap en to english, and ja to japanese rlanguage = { "en": "english", "ja": "japanese" }.get( self.get_option("language").lower(), self.get_option("language").lower()) if "_Incapsula_Resource" in res.text: self.bypass_incapsula(res) res = self.session.http.get(self.url) id_m = self.experience_id_re.search(res.text) experience_id = id_m and int(id_m.group(1)) if experience_id: log.debug("Found experience ID: {0}", experience_id) exp = Experience(experience_id) if self.get_option("email") and self.get_option("password"): if exp.login(self.get_option("email"), self.get_option("password")): log.info("Logged in to Funimation as {0}", self.get_option("email")) else: log.warning("Failed to login") log.debug("Found episode: {0}", exp.episode_info["episodeTitle"]) log.debug(" has languages: {0}", ", ".join(exp.episode_info["languages"].keys())) log.debug(" requested language: {0}", rlanguage) log.debug(" current language: {0}", exp.language) if rlanguage != exp.language: log.debug("switching language to: {0}", rlanguage) exp.set_language(rlanguage) if exp.language != rlanguage: log.warning( "Requested language {0} is not available, continuing with {1}", rlanguage, exp.language) else: log.debug("New experience ID: {0}", exp.experience_id) subtitles = None stream_metadata = {} disposition = {} for subtitle in exp.subtitles(): log.debug("Subtitles: {0}", subtitle["src"]) if subtitle["src"].endswith( ".vtt") or subtitle["src"].endswith(".srt"): sub_lang = {"en": "eng", "ja": "jpn"}[subtitle["language"]] # pick the first suitable subtitle stream subtitles = subtitles or HTTPStream( self.session, subtitle["src"]) stream_metadata["s:s:0"] = [ "language={0}".format(sub_lang) ] stream_metadata["s:a:0"] = [ "language={0}".format(exp.language_code) ] sources = exp.sources() if 'errors' in sources: for error in sources['errors']: log.error("{0} : {1}".format(error['title'], error['detail'])) return for item in sources["items"]: url = item["src"] if ".m3u8" in url: for q, s in HLSStream.parse_variant_playlist( self.session, url).items(): if self.get_option("mux_subtitles") and subtitles: yield q, MuxedStream(self.session, s, subtitles, metadata=stream_metadata, disposition=disposition) else: yield q, s elif ".mp4" in url: # TODO: fix quality s = HTTPStream(self.session, url) if self.get_option("mux_subtitles") and subtitles: yield self.mp4_quality, MuxedStream( self.session, s, subtitles, metadata=stream_metadata, disposition=disposition) else: yield self.mp4_quality, s else: log.error("Could not find experience ID?!") def bypass_incapsula(self, res): log.info("Attempting to by-pass Incapsula...") self.clear_cookies(lambda c: "incap" in c.name) for m in re.finditer(r'''"([A-Z0-9]+)"''', res.text): d = m.group(1) # decode the encoded blob to text js = "".join( map(lambda i: chr(int(i, 16)), [d[x:x + 2] for x in range(0, len(d), 2)])) jsm = re.search(r'''"GET","([^"]+)''', js) url = jsm and jsm.group(1) if url: log.debug("Found Incapsula auth URL: {0}", url) res = self.session.http.get(urljoin(self.url, url)) success = res.status_code == 200 if success: self.save_cookies(lambda c: "incap" in c.name) return success
class NicoLive(Plugin): arguments = PluginArguments( PluginArgument("email", argument_name="niconico-email", sensitive=True, metavar="EMAIL", help="The email or phone number associated with your " "Niconico account"), PluginArgument("password", argument_name="niconico-password", sensitive=True, metavar="PASSWORD", help="The password of your Niconico account"), PluginArgument( "user-session", argument_name="niconico-user-session", sensitive=True, metavar="VALUE", help="Value of the user-session token \n(can be used in " "case you do not want to put your password here)")) is_stream_ready = False is_stream_ended = False watching_interval = 30 watching_interval_worker_thread = None stream_reader = None _ws = None frontend_id = None @classmethod def can_handle_url(cls, url): return _url_re.match(url) is not None def _get_streams(self): self.url = self.url.split("?")[0] self.session.http.headers.update({ "User-Agent": useragents.CHROME, }) if not self.get_wss_api_url(): _log.debug("Coundn't extract wss_api_url. Attempting login...") if not self.niconico_web_login(): return None if not self.get_wss_api_url(): _log.error("Failed to get wss_api_url.") _log.error( "Please check if the URL is correct, " "and make sure your account has access to the video.") return None self.api_connect(self.wss_api_url) i = 0 while not self.is_stream_ready: if i % 10 == 0: _log.debug("Waiting for permit...") if i == 600: _log.error("Waiting for permit timed out.") return None if self.is_stream_ended: return None time.sleep(0.1) i += 1 streams = HLSStream.parse_variant_playlist(self.session, self.hls_stream_url) nico_streams = {} for s in streams: nico_stream = NicoHLSStream(streams[s], self) nico_streams[s] = nico_stream return nico_streams def get_wss_api_url(self): _log.debug("Getting video page: {0}".format(self.url)) resp = self.session.http.get(self.url) try: self.wss_api_url = extract_text(resp.text, ""webSocketUrl":"", """) if not self.wss_api_url: return False except Exception as e: _log.debug(e) _log.debug("Failed to extract wss api url") return False try: self.frontend_id = extract_text(resp.text, ""frontendId":", ","") except Exception as e: _log.debug(e) _log.warning("Failed to extract frontend id") self.wss_api_url = "{0}&frontend_id={1}".format( self.wss_api_url, self.frontend_id) _log.debug("Video page response code: {0}".format(resp.status_code)) _log.trace(u"Video page response body: {0}".format(resp.text)) _log.debug("Got wss_api_url: {0}".format(self.wss_api_url)) _log.debug("Got frontend_id: {0}".format(self.frontend_id)) return self.wss_api_url.startswith("wss://") def api_on_open(self): self.send_playerversion() require_new_stream = not self.is_stream_ready self.send_getpermit(require_new_stream=require_new_stream) def api_on_error(self, ws, error=None): if error: _log.warning(error) _log.warning("wss api disconnected.") _log.warning("Attempting to reconnect in 5 secs...") time.sleep(5) self.api_connect(self.wss_api_url) def api_connect(self, url): # Proxy support adapted from the UStreamTV plugin (ustreamtv.py) proxy_url = self.session.get_option("https-proxy") if proxy_url is None: proxy_url = self.session.get_option("http-proxy") proxy_options = parse_proxy_url(proxy_url) if proxy_options.get('http_proxy_host'): _log.debug("Using proxy ({0}://{1}:{2})".format( proxy_options.get('proxy_type') or "http", proxy_options.get('http_proxy_host'), proxy_options.get('http_proxy_port') or 80)) _log.debug("Connecting: {0}".format(url)) self._ws = websocket.WebSocketApp( url, header=["User-Agent: {0}".format(useragents.CHROME)], on_open=self.api_on_open, on_message=self.handle_api_message, on_error=self.api_on_error) self.ws_worker_thread = threading.Thread(target=self._ws.run_forever, args=proxy_options) self.ws_worker_thread.daemon = True self.ws_worker_thread.start() def send_message(self, type_, body): msg = {"type": type_, "body": body} msg_json = json.dumps(msg) _log.debug(u"Sending: {0}".format(msg_json)) if self._ws and self._ws.sock.connected: self._ws.send(msg_json) else: _log.warning("wss api is not connected.") def send_no_body_message(self, type_): msg = {"type": type_} msg_json = json.dumps(msg) _log.debug(u"Sending: {0}".format(msg_json)) if self._ws and self._ws.sock.connected: self._ws.send(msg_json) else: _log.warning("wss api is not connected.") def send_custom_message(self, msg): msg_json = json.dumps(msg) _log.debug(u"Sending: {0}".format(msg_json)) if self._ws and self._ws.sock.connected: self._ws.send(msg_json) else: _log.warning("wss api is not connected.") def send_playerversion(self): body = { "type": "startWatching", "data": { "stream": { "quality": "abr", "protocol": "hls", "latency": "high", "chasePlay": False }, "room": { "protocol": "webSocket", "commentable": True }, "reconnect": False } } self.send_custom_message(body) def send_getpermit(self, require_new_stream=True): body = {"type": "getAkashic", "data": {"chasePlay": False}} self.send_custom_message(body) def send_watching(self): body = { "command": "watching", "params": [self.broadcast_id, "-1", "0"] } self.send_message("watch", body) def send_pong(self): self.send_no_body_message("pong") self.send_no_body_message("keepSeat") def handle_api_message(self, message): _log.debug(u"Received: {0}".format(message)) message_parsed = json.loads(message) if message_parsed["type"] == "stream": data = message_parsed["data"] self.hls_stream_url = data["uri"] self.is_stream_ready = True if message_parsed["type"] == "watch": body = message_parsed["body"] command = body["command"] if command == "currentstream": current_stream = body["currentStream"] self.hls_stream_url = current_stream["uri"] self.is_stream_ready = True elif command == "watchinginterval": self.watching_interval = int(body["params"][0]) _log.debug("Got watching_interval: {0}".format( self.watching_interval)) if self.watching_interval_worker_thread is None: _log.debug("send_watching_scheduler starting.") self.watching_interval_worker_thread = threading.Thread( target=self.send_watching_scheduler) self.watching_interval_worker_thread.daemon = True self.watching_interval_worker_thread.start() else: _log.debug("send_watching_scheduler already running.") elif command == "disconnect": _log.info("Websocket API closed.") _log.info("Stream ended.") self.is_stream_ended = True if self.stream_reader is not None: self.stream_reader.close() _log.info("Stream reader closed.") elif message_parsed["type"] == "ping": self.send_pong() def send_watching_scheduler(self): """ Periodically send "watching" command to the API. This is necessary to keep the session alive. """ while not self.is_stream_ended: self.send_watching() time.sleep(self.watching_interval) def niconico_web_login(self): user_session = self.get_option("user-session") email = self.get_option("email") password = self.get_option("password") if user_session is not None: _log.info("User session cookie is provided. Using it.") self.session.http.cookies.set("user_session", user_session, path="/", domain="nicovideo.jp") self.save_cookies() return True elif email is not None and password is not None: _log.info("Email and password are provided. Attemping login.") payload = {"mail_tel": email, "password": password} resp = self.session.http.post(_login_url, data=payload, params=_login_url_params) _log.debug("Login response code: {0}".format(resp.status_code)) _log.trace(u"Login response body: {0}".format(resp.text)) _log.debug("Cookies: {0}".format( self.session.http.cookies.get_dict())) if self.session.http.cookies.get("user_session") is None: try: msg = extract_text(resp.text, '<p class="notice__text">', "</p>") except Exception as e: _log.debug(e) msg = "unknown reason" _log.warn("Login failed. {0}".format(msg)) return False else: _log.info("Logged in.") self.save_cookies() return True else: _log.warn( "Neither a email and password combination nor a user session " "token is provided. Cannot attempt login.") return False
class Vimeo(Plugin): _url_re = re.compile(r"https?://(player\.vimeo\.com/video/\d+|(www\.)?vimeo\.com/.+)") _config_url_re = re.compile(r'(?:"config_url"|\bdata-config-url)\s*[:=]\s*(".+?")') _config_re = re.compile(r"var\s+config\s*=\s*({.+?})\s*;") _config_url_schema = validate.Schema( validate.transform(_config_url_re.search), validate.any( None, validate.Schema( validate.get(1), validate.transform(parse_json), validate.transform(html_unescape), validate.url(), ), ), ) _config_schema = validate.Schema( validate.transform(parse_json), { "request": { "files": { validate.optional("dash"): {"cdns": {validate.text: {"url": validate.url()}}}, validate.optional("hls"): {"cdns": {validate.text: {"url": validate.url()}}}, validate.optional("progressive"): validate.all( [{"url": validate.url(), "quality": validate.text}] ), }, validate.optional("text_tracks"): validate.all( [{"url": validate.text, "lang": validate.text}] ), } }, ) _player_schema = validate.Schema( validate.transform(_config_re.search), validate.any(None, validate.Schema(validate.get(1), _config_schema)), ) arguments = PluginArguments( PluginArgument("mux-subtitles", is_global=True) ) @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) def _get_streams(self): if "player.vimeo.com" in self.url: data = self.session.http.get(self.url, schema=self._player_schema) else: api_url = self.session.http.get(self.url, schema=self._config_url_schema) if not api_url: return data = self.session.http.get(api_url, schema=self._config_schema) videos = data["request"]["files"] streams = [] for stream_type in ("hls", "dash"): if stream_type not in videos: continue for _, video_data in videos[stream_type]["cdns"].items(): log.trace("{0!r}".format(video_data)) url = video_data.get("url") if stream_type == "hls": for stream in HLSStream.parse_variant_playlist(self.session, url).items(): streams.append(stream) elif stream_type == "dash": p = urlparse(url) if p.path.endswith("dash.mpd"): # LIVE url = self.session.http.get(url).json()["url"] elif p.path.endswith("master.json"): # VOD url = url.replace("master.json", "master.mpd") else: log.error("Unsupported DASH path: {0}".format(p.path)) continue for stream in DASHStream.parse_manifest(self.session, url).items(): streams.append(stream) for stream in videos.get("progressive", []): streams.append((stream["quality"], HTTPStream(self.session, stream["url"]))) if self.get_option("mux_subtitles") and data["request"].get("text_tracks"): substreams = { s["lang"]: HTTPStream(self.session, "https://vimeo.com" + s["url"]) for s in data["request"]["text_tracks"] } for quality, stream in streams: yield quality, MuxedStream(self.session, stream, subtitles=substreams) else: for stream in streams: yield stream
class UStreamTV(Plugin): url_re = re.compile(r"""(?x) https?://(www\.)?ustream\.tv (?: (/embed/|/channel/id/)(?P<channel_id>\d+) )? (?: (/embed)?/recorded/(?P<video_id>\d+) )? """) media_id_re = re.compile(r'"ustream:channel_id"\s+content\s*=\s*"(\d+)"') arguments = PluginArguments( PluginArgument("password", argument_name="ustream-password", sensitive=True, metavar="PASSWORD", help=""" A password to access password protected UStream.tv channels. """)) STREAM_WEIGHTS = { "original": 65535, } @classmethod def can_handle_url(cls, url): return cls.url_re.match(url) is not None @classmethod def stream_weight(cls, stream): if stream in cls.STREAM_WEIGHTS: return cls.STREAM_WEIGHTS[stream], "ustreamtv" return Plugin.stream_weight(stream) def handle_module_info(self, args): res = {} for arg in args: if "cdnConfig" in arg: parts = [ # scheme arg["cdnConfig"]["protocol"], # netloc arg["cdnConfig"]["data"][0]["data"][0]["sites"][0]["host"], # path arg["cdnConfig"]["data"][0]["data"][0]["sites"][0]["path"], "", "", "", # params, query, fragment ] # Example: # LIVE: http://uhs-akamai.ustream.tv/ # VOD: http://vod-cdn.ustream.tv/ res["cdn_url"] = urlunparse(parts) if "stream" in arg and bool(arg["stream"].get("streamFormats")): data = arg["stream"] if data["streamFormats"].get("flv/segmented"): flv_segmented = data["streamFormats"]["flv/segmented"] path = flv_segmented["contentAccess"]["accessList"][0][ "data"]["path"] res["streams"] = [] for stream in flv_segmented["streams"]: res["streams"] += [ dict( stream_name=stream["preset"], path=urljoin( path, stream["segmentUrl"].replace("%", "%s")), hashes=flv_segmented["hashes"], first_chunk=flv_segmented["chunkId"], chunk_time=flv_segmented["chunkTime"], ) ] elif bool(data["streamFormats"]): # supported formats: # - flv/segmented # unsupported formats: # - flv # - mp4 # - mp4/segmented raise PluginError( "Stream format is not supported: {0}".format(", ".join( data["streamFormats"].keys()))) elif "stream" in arg and arg["stream"]["contentAvailable"] is False: log.error("This stream is currently offline") raise ModuleInfoNoStreams return res def handle_reject(self, api, args): for arg in args: if "cluster" in arg: api.cluster = arg["cluster"]["name"] if "referrerLock" in arg: api.referrer = arg["referrerLock"]["redirectUrl"] if "nonexistent" in arg: log.error("This channel does not exist") raise ModuleInfoNoStreams if "geoLock" in arg: log.error("This content is not available in your area") raise ModuleInfoNoStreams def _get_streams(self): media_id, application = self._get_media_app() if media_id: api = UHSClient(media_id, application, referrer=self.url, cluster="live", password=self.get_option("password")) log.debug( "Connecting to UStream API: media_id={0}, application={1}, referrer={2}, cluster={3}", media_id, application, self.url, "live") api.connect() streams_data = {} streams = {} for _ in range(5): # do not use to many tries, it might take longer for a timeout # when streamFormats is {} and contentAvailable is True data = api.recv() try: if data["cmd"] == "moduleInfo": r = self.handle_module_info(data["args"]) if r: streams_data.update(r) elif data["cmd"] == "reject": self.handle_reject(api, data["args"]) else: log.debug("Unexpected `{0}` command".format( data["cmd"])) log.trace("{0!r}".format(data)) except ModuleInfoNoStreams: return None if streams_data.get("streams") and streams_data.get("cdn_url"): for s in streams_data["streams"]: streams[s["stream_name"]] = UHSStream( session=self.session, api=api, first_chunk_data=ChunkData( s["first_chunk"], s["chunk_time"], s["hashes"], datetime.datetime.now(tz=utc)), template_url=urljoin(streams_data["cdn_url"], s["path"]), ) return streams def _get_media_app(self): 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: res = self.session.http.get( self.url, headers={"User-Agent": useragents.CHROME}) m = self.media_id_re.search(res.text) media_id = m and m.group(1) return media_id, application
class Twitch(Plugin): arguments = PluginArguments( PluginArgument("oauth-token", sensitive=True, metavar="TOKEN", help=""" An OAuth token to use for Twitch authentication. Use --twitch-oauth-authenticate to create a token. """), PluginArgument("cookie", sensitive=True, metavar="COOKIES", help=""" Twitch cookies to authenticate to allow access to subscription channels. Example: "_twitch_session_id=xxxxxx; persistent=xxxxx" Note: This method is the old and clunky way of authenticating with Twitch, using --twitch-oauth-authenticate is the recommended and simpler way of doing it now. """), PluginArgument("disable-hosting", action="store_true", help=""" Do not open the stream if the target channel is hosting another channel. """), PluginArgument("disable-ads", action="store_true", help=""" Skip embedded advertisement segments at the beginning or during a stream. Will cause these segments to be missing from the stream. """)) @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 _get_metadata(self): if self.video_id: api_res = self.api.videos(self.video_id) self.title = api_res["title"] self.author = api_res["channel"]["display_name"] self.category = api_res["game"] elif self.clip_name: api_res = self.api.clips(self.clip_name) self.title = api_res["title"] self.author = api_res["broadcaster"]["display_name"] self.category = api_res["game"] elif self._channel: api_res = self.api.streams(self.channel_id)["stream"]["channel"] self.title = api_res["status"] self.author = api_res["display_name"] self.category = api_res["game"] def get_title(self): if self.title is None: self._get_metadata() return self.title def get_author(self): if self.author is None: self._get_metadata() return self.author def get_category(self): if self.category is None: self._get_metadata() return self.category def __init__(self, url): Plugin.__init__(self, url) self._hosted_chain = [] match = _url_re.match(url).groupdict() parsed = urlparse(url) self.params = parse_query(parsed.query) self.subdomain = match.get("subdomain") self.video_id = None self.video_type = None self._channel_id = None self._channel = None self.clip_name = None self.title = None self.author = None self.category = None if self.subdomain == "player": # pop-out player if self.params.get("video"): try: self.video_type = self.params["video"][0] self.video_id = self.params["video"][1:] except IndexError: self.logger.debug("Invalid video param: {0}", self.params["video"]) self._channel = self.params.get("channel") elif self.subdomain == "clips": # clip share URL self.clip_name = match.get("channel") else: self._channel = match.get("channel") and match.get( "channel").lower() 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.api = TwitchAPI(beta=self.subdomain == "beta", session=self.session, version=5) self.usher = UsherService(session=self.session) @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 {0} (type={1})".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: try: time_offset = hours_minutes_seconds(time_offset) except ValueError: time_offset = 0 videos["start_offset"] += time_offset 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 {0} HLS streams for {1}".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 {0}", 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. ({0})".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, fast_bread=True) 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: {0}".format(stream_type)) return {} time_offset = self.params.get("t", 0) if time_offset: try: time_offset = hours_minutes_seconds(time_offset) except ValueError: time_offset = 0 try: # If the stream is a VOD that is still being recorded the stream should start at the # beginning of the recording streams = TwitchHLSStream.parse_variant_playlist( self.session, url, start_offset=time_offset, 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() elif self._channel: return self._get_hls_streams("live")
class Pluzz(Plugin): GEO_URL = 'http://geo.francetv.fr/ws/edgescape.json' API_URL = 'http://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion={0}' TOKEN_URL = 'http://hdfauthftv-a.akamaihd.net/esi/TA?url={0}' SWF_PLAYER_URL = 'https://staticftv-a.akamaihd.net/player/bower_components/player_flash/dist/' \ 'FranceTVNVPVFlashPlayer.akamai-7301b6035a43c4e29b7935c9c36771d2.swf' _pluzz_video_id_re = re.compile( r'''(?P<q>["']*)videoId(?P=q):\s*["'](?P<video_id>[^"']+)["']''') _jeunesse_video_id_re = re.compile( r'playlist: \[{.*?,"identity":"(?P<video_id>.+?)@(?P<catalogue>Ludo|Zouzous)"' ) _sport_video_id_re = re.compile(r'data-video="(?P<video_id>.+?)"') _embed_video_id_re = re.compile( r'href="http://videos\.francetv\.fr/video/(?P<video_id>.+?)(?:@.+?)?"') _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()}) arguments = PluginArguments(PluginArgument("mux-subtitles", is_global=True)) def _get_streams(self): # Retrieve geolocation data res = self.session.http.get(self.GEO_URL) geo = self.session.http.json(res, schema=self._geo_schema) country_code = geo['reponse']['geo_info']['country_code'] log.debug('Country: {0}'.format(country_code)) # Retrieve URL page and search for video ID res = self.session.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 'sport.francetvinfo.fr' in self.url: match = self._sport_video_id_re.search(res.text) else: match = self._embed_video_id_re.search(res.text) if match is None: return video_id = match.group('video_id') log.debug('Video ID: {0}'.format(video_id)) res = self.session.http.get(self.API_URL.format(video_id)) videos = self.session.http.json(res, schema=self._api_schema) now = time.time() offline = False geolocked = False drm = False expired = False streams = [] for video in videos['videos']: log.trace('{0!r}'.format(video)) 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 res = self.session.http.get(self.TOKEN_URL.format(video_url)) video_url = res.text if '.mpd' in video_url: # Get redirect video URL res = self.session.http.get(res.text) video_url = res.url for bitrate, stream in DASHStream.parse_manifest( self.session, video_url).items(): streams.append((bitrate, stream)) elif '.f4m' in video_url: for bitrate, stream in HDSStream.parse_manifest( self.session, video_url, is_akamai=True, pvswf=self.SWF_PLAYER_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: log.error('Failed to access stream, may be due to offline content') if geolocked: log.error( 'Failed to access stream, may be due to geo-restricted content' ) if drm: log.error( 'Failed to access stream, may be due to DRM-protected content') if expired: log.error('Failed to access stream, may be due to expired content')
class YouTube(Plugin): arguments = PluginArguments( PluginArgument( "apihost", metavar="APIHOST", default=API_HOST, help="Use custom api host url to bypass bilibili's cloud blocking") ) _re_url = re.compile( r""" https?://(?:\w+\.)?youtube\.com/ (?: (?: (?: watch\?(?:.*&)*v= | (?P<embed>embed)/(?!live_stream) | v/ )(?P<video_id>[0-9A-z_-]{11}) ) | embed/live_stream\?channel=(?P<embed_live>[^/?&]+) | (?:c(?:hannel)?/|user/)?[^/?]+/live/?$ ) | https?://youtu\.be/(?P<video_id_short>[0-9A-z_-]{11}) """, re.VERBOSE) _re_ytInitialPlayerResponse = re.compile( r"""var\s+ytInitialPlayerResponse\s*=\s*({.*?});\s*var\s+meta\s*=""", re.DOTALL) _re_mime_type = re.compile( r"""^(?P<type>\w+)/(?P<container>\w+); codecs="(?P<codecs>.+)"$""") _url_canonical = "https://www.youtube.com/watch?v={video_id}" _url_channelid_live = "https://www.youtube.com/channel/{channel_id}/live" # There are missing itags adp_video = { 137: "1080p", 299: "1080p60", # HFR 264: "1440p", 308: "1440p60", # HFR 266: "2160p", 315: "2160p60", # HFR 138: "2160p", 302: "720p60", # HFR 135: "480p", 133: "240p", 160: "144p", } adp_audio = { 140: 128, 141: 256, 171: 128, 249: 48, 250: 64, 251: 160, 256: 256, 258: 258, } def __init__(self, url): match = self._re_url.match(url) parsed = urlparse(url) # translate input URLs to be able to find embedded data and to avoid unnecessary HTTP redirects if parsed.netloc == "gaming.youtube.com": url = urlunparse( parsed._replace(scheme="https", netloc="www.youtube.com")) elif match.group("video_id_short") is not None: url = self._url_canonical.format( video_id=match.group("video_id_short")) elif match.group("embed") is not None: url = self._url_canonical.format(video_id=match.group("video_id")) elif match.group("embed_live") is not None: url = self._url_channelid_live.format( channel_id=match.group("embed_live")) else: url = urlunparse(parsed._replace(scheme="https")) super().__init__(url) self.author = None self.title = None self.session.http.headers.update({'User-Agent': useragents.CHROME}) self.ori_request = self.session.http.request self.session.http.request = self.http_request def http_request(self, *args, **kwargs): args = list(args) args[1] = args[1] \ .replace("https://www.youtube.com", self.options.get("apihost")) \ .replace("http://www.youtube.com", self.options.get("apihost")) return self.ori_request(*args, **kwargs) def get_author(self): return self.author def get_title(self): return self.title @classmethod def can_handle_url(cls, url): return cls._re_url.match(url) @classmethod def stream_weight(cls, stream): match_3d = re.match(r"(\w+)_3d", stream) match_hfr = re.match(r"(\d+p)(\d+)", stream) if match_3d: weight, group = Plugin.stream_weight(match_3d.group(1)) weight -= 1 group = "youtube_3d" elif match_hfr: weight, group = Plugin.stream_weight(match_hfr.group(1)) weight += 1 group = "high_frame_rate" else: weight, group = Plugin.stream_weight(stream) return weight, group @classmethod def _schema_playabilitystatus(cls, data): schema = validate.Schema( { "playabilityStatus": { "status": str, validate.optional("reason"): str } }, validate.get("playabilityStatus"), validate.union_get("status", "reason")) return validate.validate(schema, data) @classmethod def _schema_videodetails(cls, data): schema = validate.Schema( { "videoDetails": { "videoId": str, "author": str, "title": str, validate.optional("isLiveContent"): validate.transform(bool) } }, validate.get("videoDetails"), validate.union_get("videoId", "author", "title", "isLiveContent")) return validate.validate(schema, data) @classmethod def _schema_streamingdata(cls, data): schema = validate.Schema( { "streamingData": { validate.optional("hlsManifestUrl"): str, validate.optional("formats"): [ validate.all( { "itag": int, "qualityLabel": str, validate.optional("url"): validate.url(scheme="http") }, validate.union_get("url", "qualityLabel")) ], validate.optional("adaptiveFormats"): [ validate.all( { "itag": int, "mimeType": validate.all( str, validate.transform( cls._re_mime_type.search), validate.union_get("type", "codecs"), ), validate.optional("url"): validate.url(scheme="http"), validate.optional("qualityLabel"): str }, validate.union_get("url", "qualityLabel", "itag", "mimeType")) ] } }, validate.get("streamingData"), validate.union_get("hlsManifestUrl", "formats", "adaptiveFormats")) hls_manifest, formats, adaptive_formats = validate.validate( schema, data) return hls_manifest, formats or [], adaptive_formats or [] def _create_adaptive_streams(self, adaptive_formats): streams = {} adaptive_streams = {} best_audio_itag = None # Extract audio streams from the adaptive format list for url, label, itag, mimeType in adaptive_formats: if url is None: continue # extract any high quality streams only available in adaptive formats adaptive_streams[itag] = url stream_type, stream_codecs = mimeType if stream_type == "audio": streams[f"audio_{stream_codecs}"] = HTTPStream( self.session, url) # find the best quality audio stream m4a, opus or vorbis if best_audio_itag is None or self.adp_audio[ itag] > self.adp_audio[best_audio_itag]: best_audio_itag = itag if best_audio_itag and adaptive_streams and MuxedStream.is_usable( self.session): aurl = adaptive_streams[best_audio_itag] for itag, name in self.adp_video.items(): if itag not in adaptive_streams: continue vurl = adaptive_streams[itag] log.debug( f"MuxedStream: v {itag} a {best_audio_itag} = {name}") streams[name] = MuxedStream(self.session, HTTPStream(self.session, vurl), HTTPStream(self.session, aurl)) return streams def _get_res(self, url): res = self.session.http.get(url) if urlparse(res.url).netloc == "consent.youtube.com": c_data = {} for _i in itertags(res.text, "input"): if _i.attributes.get("type") == "hidden": c_data[_i.attributes.get("name")] = unescape( _i.attributes.get("value")) log.debug(f"c_data_keys: {', '.join(c_data.keys())}") res = self.session.http.post("https://consent.youtube.com/s", data=c_data) return res def _get_data(self, res): match = re.search(self._re_ytInitialPlayerResponse, res.text) if not match: log.debug("Missing initial player response data") return return parse_json(match.group(1)) def _get_data_from_api(self, res): _i_video_id = self._re_url.match(self.url).group("video_id") if _i_video_id is None: for link in itertags(res.text, "link"): if link.attributes.get("rel") == "canonical": try: _i_video_id = self._re_url.match( link.attributes.get("href")).group("video_id") except AttributeError: return break else: return try: _i_api_key = re.search(r'"INNERTUBE_API_KEY":\s*"([^"]+)"', res.text).group(1) except AttributeError: _i_api_key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" try: _i_version = re.search( r'"INNERTUBE_CLIENT_VERSION":\s*"([\d\.]+)"', res.text).group(1) except AttributeError: _i_version = "1.20210616.1.0" res = self.session.http.post( "https://www.youtube.com/youtubei/v1/player", headers={"Content-Type": "application/json"}, params={"key": _i_api_key}, data=json.dumps({ "videoId": _i_video_id, "context": { "client": { "clientName": "WEB_EMBEDDED_PLAYER", "clientVersion": _i_version, "platform": "DESKTOP", "clientFormFactor": "UNKNOWN_FORM_FACTOR", "browserName": "Chrome", }, "user": { "lockedSafetyMode": "false" }, "request": { "useSsl": "true" }, } }), ) return parse_json(res.text) def _data_status(self, data): if not data: return False status, reason = self._schema_playabilitystatus(data) if status != "OK": log.error(f"Could not get video info - {status}: {reason}") return False return True def _get_streams(self): res = self._get_res(self.url) data = self._get_data(res) if not self._data_status(data): data = self._get_data_from_api(res) if not self._data_status(data): return video_id, self.author, self.title, is_live = self._schema_videodetails( data) log.debug(f"Using video ID: {video_id}") if is_live: log.debug("This video is live.") streams = {} hls_manifest, formats, adaptive_formats = self._schema_streamingdata( data) protected = next( (True for url, *_ in formats + adaptive_formats if url is None), False) if protected: log.debug("This video may be protected.") for url, label in formats: if url is None: continue streams[label] = HTTPStream(self.session, url) if not is_live: streams.update(self._create_adaptive_streams(adaptive_formats)) if hls_manifest: streams.update( HLSStream.parse_variant_playlist(self.session, hls_manifest, name_key="pixels")) if not streams and protected: raise PluginError( "This plugin does not support protected videos, try youtube-dl instead" ) return streams
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) ) ) ) }) ) arguments = PluginArguments( PluginArgument( "email", requires=["password"], metavar="EMAIL", help="The email address used to register with animelab.com." ), PluginArgument( "password", sensitive=True, metavar="PASSWORD", help="A animelab.com account password to use with --animelab-email." ) ) @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 USTVNow(Plugin): _url_re = re.compile( r"https?://(?:www\.)?ustvnow\.com/live/(?P<scode>\w+)/-(?P<id>\d+)") _main_js_re = re.compile(r"""src=['"](main\..*\.js)['"]""") _enc_key_re = re.compile( r'(?P<key>AES_(?:Key|IV))\s*:\s*"(?P<value>[^"]+)"') TENANT_CODE = "ustvnow" _api_url = "https://teleupapi.revlet.net/service/api/v1/" _token_url = _api_url + "get/token" _signin_url = "https://www.ustvnow.com/signin" arguments = PluginArguments( PluginArgument("username", metavar="USERNAME", required=True, help="Your USTV Now account username"), PluginArgument("password", sensitive=True, metavar="PASSWORD", required=True, help="Your USTV Now account password", prompt="Enter USTV Now account password")) def __init__(self, url): super(USTVNow, self).__init__(url) self._encryption_config = {} self._token = None @classmethod def can_handle_url(cls, url): return cls._url_re.match(url) is not None @classmethod def encrypt_data(cls, data, key, iv): rkey = "".join(reversed(key)).encode('utf8') riv = "".join(reversed(iv)).encode('utf8') fkey = SHA256.new(rkey).hexdigest()[:32].encode("utf8") cipher = AES.new(fkey, AES.MODE_CBC, riv) encrypted = cipher.encrypt(pad(data, 16, 'pkcs7')) return base64.b64encode(encrypted) @classmethod def decrypt_data(cls, data, key, iv): rkey = "".join(reversed(key)).encode('utf8') riv = "".join(reversed(iv)).encode('utf8') fkey = SHA256.new(rkey).hexdigest()[:32].encode("utf8") cipher = AES.new(fkey, AES.MODE_CBC, riv) decrypted = cipher.decrypt(base64.b64decode(data)) if decrypted: return unpad(decrypted, 16, 'pkcs7') else: return decrypted def _get_encryption_config(self, url): # find the path to the main.js # load the main.js and extract the config if not self._encryption_config: res = self.session.http.get(url) m = self._main_js_re.search(res.text) main_js_path = m and m.group(1) if main_js_path: res = self.session.http.get(urljoin(url, main_js_path)) self._encryption_config = dict( self._enc_key_re.findall(res.text)) return self._encryption_config.get( "AES_Key"), self._encryption_config.get("AES_IV") @property def box_id(self): if not self.cache.get("box_id"): self.cache.set("box_id", str(uuid4())) return self.cache.get("box_id") def get_token(self): """ Get the token for USTVNow :return: a valid token """ if not self._token: log.debug("Getting new session token") res = self.session.http.get(self._token_url, params={ "tenant_code": self.TENANT_CODE, "box_id": self.box_id, "product": self.TENANT_CODE, "device_id": 5, "display_lang_code": "ENG", "device_sub_type": "", "timezone": "UTC" }) data = res.json() if data['status']: self._token = data['response']['sessionId'] log.debug("New token: {}".format(self._token)) else: log.error( "Token acquisition failed: {details} ({detail})".format( **data['error'])) raise PluginError("could not obtain token") return self._token def api_request(self, path, data, metadata=None): key, iv = self._get_encryption_config(self._signin_url) post_data = { "data": self.encrypt_data(json.dumps(data).encode('utf8'), key, iv).decode("utf8"), "metadata": self.encrypt_data(json.dumps(metadata).encode('utf8'), key, iv).decode("utf8") } headers = { "box-id": self.box_id, "session-id": self.get_token(), "tenant-code": self.TENANT_CODE, "content-type": "application/json" } res = self.session.http.post(self._api_url + path, data=json.dumps(post_data), headers=headers).json() data = dict((k, v and json.loads(self.decrypt_data(v, key, iv))) for k, v in res.items()) return data def login(self, username, password): log.debug("Trying to login...") resp = self.api_request( "send", { "login_id": username, "login_key": password, "login_mode": "1", "manufacturer": "123" }, {"request": "signin"}) return resp['data']['status'] def _get_streams(self): """ Finds the streams from ustvnow.com. """ if self.login(self.get_option("username"), self.get_option("password")): path = urlparse(self.url).path.strip("/") resp = self.api_request("send", {"path": path}, {"request": "page/stream"}) if resp['data']['status']: for stream in resp['data']['response']['streams']: if stream['keys']['licenseKey']: log.warning("Stream possibly protected by DRM") yield from HLSStream.parse_variant_playlist( self.session, stream['url']).items() else: log.error( "Could not find any streams: {code}: {message}".format( **resp['data']['error'])) else: log.error("Failed to login, check username and password")