Beispiel #1
0
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))
Beispiel #2
0
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))
Beispiel #3
0
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
Beispiel #4
0
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))
Beispiel #5
0
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("&quot;", "\"")
        txt = txt.replace("&amp;", "&")
        txt = txt.replace("&#38;", "&")
        txt = txt.replace("&nbsp;", "")

        return txt
Beispiel #6
0
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
Beispiel #7
0
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))
Beispiel #8
0
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
Beispiel #9
0
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))
Beispiel #13
0
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)
Beispiel #15
0
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))
Beispiel #17
0
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))
Beispiel #18
0
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))
Beispiel #19
0
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()
Beispiel #20
0
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"])
Beispiel #21
0
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)
Beispiel #22
0
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
Beispiel #23
0
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,
                                            "&quot;webSocketUrl&quot;:&quot;",
                                            "&quot;")
            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,
                                            "&quot;frontendId&quot;:",
                                            ",&quot;")
        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
Beispiel #24
0
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
Beispiel #26
0
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")
Beispiel #27
0
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')
Beispiel #28
0
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
Beispiel #29
0
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
Beispiel #30
0
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")