code:
import base64
import hashlib
import itertools
import functools
import math
import re

from .common import InfoExtractor, SearchInfoExtractor
from ..compat import (
    compat_parse_qs,
    compat_urlparse,
    compat_urllib_parse_urlparse
)
from ..utils import (
    ExtractorError,
    InAdvancePagedList,
    OnDemandPagedList,
    filter_dict,
    float_or_none,
    int_or_none,
    mimetype2ext,
    parse_count,
    parse_iso8601,
    qualities,
    smuggle_url,
    srt_subtitles_timecode,
    str_or_none,
    strip_jsonp,
    traverse_obj,
    unified_timestamp,
    unsmuggle_url,
    urlencode_postdata,
    url_or_none,
)

class BiliBiliIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:(?:www|bangumi)\.)?
                        bilibili\.(?:tv|com)/
                        (?:
                            (?:
                                video/[aA][vV]|
                                anime/(?P<anime_id>\d+)/play\#
                            )(?P<id>\d+)|
                            (s/)?video/[bB][vV](?P<id_bv>[^/?#&]+)
                        )
                        (?:/?\?p=(?P<page>\d+))?
                    '''

    _TESTS = [{
        'url': 'http://www.bilibili.com/video/av1074402/',
        'md5': '7ac275ec84a99a6552c5d229659a0fe1',
        'info_dict': {
            'id': '1074402_part1',
            'ext': 'mp4',
            'title': '【金坷垃】金泡沫',
            'uploader_id': '156160',
            'uploader': '菊子桑',
            'upload_date': '20140420',
            'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
            'timestamp': 1398012678,
            'tags': ['顶上去报复社会', '该来的总会来的', '金克拉是检验歌曲的唯一标准', '坷垃教主', '金坷垃', '邓紫棋', '治愈系坷垃'],
            'bv_id': 'BV11x411K7CN',
            'cid': '1554319',
            'thumbnail': 'http://i2.hdslb.com/bfs/archive/c79a8cf0347cd7a897c53a2f756e96aead128e8c.jpg',
            'duration': 308.36,
        },
    }, {
        # Tested in BiliBiliBangumiIE
        'url': 'http://bangumi.bilibili.com/anime/1869/play#40062',
        'only_matching': True,
    }, {
        # bilibili.tv
        'url': 'http://www.bilibili.tv/video/av1074402/',
        'only_matching': True,
    }, {
        'url': 'http://bangumi.bilibili.com/anime/5802/play#100643',
        'md5': '3f721ad1e75030cc06faf73587cfec57',
        'info_dict': {
            'id': '100643_part1',
            'ext': 'mp4',
            'title': 'CHAOS;CHILD',
            'description': '如果你是神明，并且能够让妄想成为现实。那你会进行怎么样的妄想？是淫靡的世界？独裁社会？毁灭性的制裁？还是……2015年，涩谷。从6年前发生的大灾害“涩谷地震”之后复兴了的这个街区里新设立的私立高中...',
        },
        'skip': 'Geo-restricted to China',
    }, {
        'url': 'http://www.bilibili.com/video/av8903802/',
        'info_dict': {
            'id': '8903802_part1',
            'ext': 'mp4',
            'title': '阿滴英文｜英文歌分享#6 "Closer',
            'upload_date': '20170301',
            'description': '滴妹今天唱Closer給你聽! 有史以来，被推最多次也是最久的歌曲，其实歌词跟我原本想像差蛮多的，不过还是好听！ 微博@阿滴英文',
            'timestamp': 1488382634,
            'uploader_id': '65880958',
            'uploader': '阿滴英文',
            'thumbnail': 'http://i2.hdslb.com/bfs/archive/49267ce20bc246be6304bf369a3ded0256854c23.jpg',
            'cid': '14694589',
            'duration': 554.117,
            'bv_id': 'BV13x41117TL',
            'tags': ['人文', '英语', '文化', '公开课', '阿滴英文'],
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # new BV video id format
        'url': 'https://www.bilibili.com/video/BV1JE411F741',
        'only_matching': True,
    }, {
        # Anthology
        'url': 'https://www.bilibili.com/video/BV1bK411W797',
        'info_dict': {
            'id': 'BV1bK411W797',
            'title': '物语中的人物是如何吐槽自己的OP的'
        },
        'playlist_count': 17,
    }, {
        # Correct matching of single and double quotes in title
        'url': 'https://www.bilibili.com/video/BV1NY411E7Rx/',
        'info_dict': {
            'id': '255513412_part1',
            'ext': 'mp4',
            'title': 'Vid"eo" Te\'st',
            'cid': '570602418',
            'thumbnail': 'http://i2.hdslb.com/bfs/archive/0c0de5a90b6d5b991b8dcc6cde0afbf71d564791.jpg',
            'upload_date': '20220408',
            'timestamp': 1649436552,
            'description': 'Vid"eo" Te\'st',
            'uploader_id': '1630758804',
            'bv_id': 'BV1NY411E7Rx',
            'duration': 60.394,
            'uploader': 'bili_31244483705',
            'tags': ['VLOG'],
        },
        'params': {
            'skip_download': True,
        },
    }]

    _APP_KEY = 'iVGUTjsxvpLeuDCf'
    _BILIBILI_KEY = 'aHRmhWMLkdeMuILqORnYZocwMBpMEOdt'

    def _report_error(self, result):
        if 'message' in result:
            raise ExtractorError('%s said: %s' % (self.IE_NAME, result['message']), expected=True)
        elif 'code' in result:
            raise ExtractorError('%s returns error %d' % (self.IE_NAME, result['code']), expected=True)
        else:
            raise ExtractorError('Can\'t extract Bangumi episode ID')

    def _real_extract(self, url):
        url, smuggled_data = unsmuggle_url(url, {})

        mobj = self._match_valid_url(url)
        video_id = mobj.group('id_bv') or mobj.group('id')

        av_id, bv_id = self._get_video_id_set(video_id, mobj.group('id_bv') is not None)
        video_id = av_id

        info = {}
        anime_id = mobj.group('anime_id')
        page_id = mobj.group('page')
        webpage = self._download_webpage(url, video_id)

        # Bilibili anthologies are similar to playlists but all videos share the same video ID as the anthology itself.
        # If the video has no page argument, check to see if it's an anthology
        if page_id is None:
            if not self.get_param('noplaylist'):
                r = self._extract_anthology_entries(bv_id, video_id, webpage)
                if r is not None:
                    self.to_screen('Downloading anthology %s - add --no-playlist to just download video' % video_id)
                    return r
            else:
                self.to_screen('Downloading just video %s because of --no-playlist' % video_id)

        if 'anime/' not in url:
            cid = self._search_regex(
                r'\bcid(?:["\']:|=)(\d+),["\']page(?:["\']:|=)' + str(page_id), webpage, 'cid',
                default=None
            ) or self._search_regex(
                r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid',
                default=None
            ) or compat_parse_qs(self._search_regex(
                [r'EmbedPlayer\([^)]+,\s*"([^"]+)"\)',
                 r'EmbedPlayer\([^)]+,\s*\\"([^"]+)\\"\)',
                 r']+src="https://secure\.bilibili\.com/secure,([^"]+)"'],
                webpage, 'player parameters'))['cid'][0]
        else:
            if 'no_bangumi_tip' not in smuggled_data:
                self.to_screen('Downloading episode %s. To download all videos in anime %s, re-run yt-dlp with %s' % (
                    video_id, anime_id, compat_urlparse.urljoin(url, '//bangumi.bilibili.com/anime/%s' % anime_id)))
            headers = {
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'Referer': url
            }
            headers.update(self.geo_verification_headers())

            js = self._download_json(
                'http://bangumi.bilibili.com/web_api/get_source', video_id,
                data=urlencode_postdata({'episode_id': video_id}),
                headers=headers)
            if 'result' not in js:
                self._report_error(js)
            cid = js['result']['cid']

        headers = {
            'Accept': 'application/json',
            'Referer': url
        }
        headers.update(self.geo_verification_headers())

        video_info = self._parse_json(
            self._search_regex(r'window.__playinfo__\s*=\s*({.+?})</script>', webpage, 'video info', default=None) or '{}',
            video_id, fatal=False)
        video_info = video_info.get('data') or {}

        durl = traverse_obj(video_info, ('dash', 'video'))
        audios = traverse_obj(video_info, ('dash', 'audio')) or []
        flac_audio = traverse_obj(video_info, ('dash', 'flac', 'audio'))
        if flac_audio:
            audios.append(flac_audio)
        entries = []

        RENDITIONS = ('qn=80&quality=80&type=', 'quality=2&type=mp4')
        for num, rendition in enumerate(RENDITIONS, start=1):
            payload = 'appkey=%s&cid=%s&otype=json&%s' % (self._APP_KEY, cid, rendition)
            sign = hashlib.md5((payload + self._BILIBILI_KEY).encode('utf-8')).hexdigest()
            if not video_info:
                video_info = self._download_json(
                    'http://interface.bilibili.com/v2/playurl?%s&sign=%s' % (payload, sign),
                    video_id, note='Downloading video info page',
                    headers=headers, fatal=num == len(RENDITIONS))
                if not video_info:
                    continue

            if not durl and 'durl' not in video_info:
                if num < len(RENDITIONS):
                    continue
                self._report_error(video_info)

            formats = []
            for idx, durl in enumerate(durl or video_info['durl']):
                formats.append({
                    'url': durl.get('baseUrl') or durl.get('base_url') or durl.get('url'),
                    'ext': mimetype2ext(durl.get('mimeType') or durl.get('mime_type')),
                    'fps': int_or_none(durl.get('frameRate') or durl.get('frame_rate')),
                    'width': int_or_none(durl.get('width')),
                    'height': int_or_none(durl.get('height')),
                    'vcodec': durl.get('codecs'),
                    'acodec': 'none' if audios else None,
                    'tbr': float_or_none(durl.get('bandwidth'), scale=1000),
                    'filesize': int_or_none(durl.get('size')),
                })
                for backup_url in traverse_obj(durl, 'backup_url', expected_type=list) or []:
                    formats.append({
                        'url': backup_url,
                        'quality': -2 if 'hd.mp4' in backup_url else -3,
                    })

            for audio in audios:
                formats.append({
                    'url': audio.get('baseUrl') or audio.get('base_url') or audio.get('url'),
                    'ext': mimetype2ext(audio.get('mimeType') or audio.get('mime_type')),
                    'fps': int_or_none(audio.get('frameRate') or audio.get('frame_rate')),
                    'width': int_or_none(audio.get('width')),
                    'height': int_or_none(audio.get('height')),
                    'acodec': audio.get('codecs'),
                    'vcodec': 'none',
                    'tbr': float_or_none(audio.get('bandwidth'), scale=1000),
                    'filesize': int_or_none(audio.get('size'))
                })
                for backup_url in traverse_obj(audio, 'backup_url', expected_type=list) or []:
                    formats.append({
                        'url': backup_url,
                        # backup URLs have lower priorities
                        'quality': -3,
                    })

            info.update({
                'id': video_id,
                'duration': float_or_none(durl.get('length'), 1000),
                'formats': formats,
                'http_headers': {
                    'Referer': url,
                },
            })
            break

        self._sort_formats(formats)

        title = self._html_search_regex((
            r'
]+title=(["])(?P<content>[^"]+)',
            r'
]+title=([\'])(?P<content>[^\']+)',
            r'(?s)
]*>(?P<content>.+?)
',
            self._meta_regex('title')
        ), webpage, 'title', group='content', fatal=False)

        # Get part title for anthologies
        if page_id is not None:
            # TODO: The json is already downloaded by _extract_anthology_entries. Don't redownload for each video.
            part_info = traverse_obj(self._download_json(
                f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
                video_id, note='Extracting videos in anthology'), 'data', expected_type=list)
            title = title if len(part_info) == 1 else traverse_obj(part_info, (int(page_id) - 1, 'part')) or title

        description = self._html_search_meta('description', webpage)
        timestamp = unified_timestamp(self._html_search_regex(
            r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time',
            default=None) or self._html_search_meta(
            'uploadDate', webpage, 'timestamp', default=None))
        thumbnail = self._html_search_meta(['og:image', 'thumbnailUrl'], webpage)

        # TODO 'view_count' requires deobfuscating Javascript
        info.update({
            'id': f'{video_id}_part{page_id or 1}',
            'cid': cid,
            'title': title,
            'description': description,
            'timestamp': timestamp,
            'thumbnail': thumbnail,
            'duration': float_or_none(video_info.get('timelength'), scale=1000),
        })

        uploader_mobj = re.search(
            r']+href="(?:https?:)?//space\.bilibili\.com/(?P<id>\d+)"[^>]*>\s*(?P<name>[^<]+?)\s*<',
            webpage)
        if uploader_mobj:
            info.update({
                'uploader': uploader_mobj.group('name').strip(),
                'uploader_id': uploader_mobj.group('id'),
            })

        if not info.get('uploader'):
            info['uploader'] = self._html_search_meta(
                'author', webpage, 'uploader', default=None)

        top_level_info = {
            'tags': traverse_obj(self._download_json(
                f'https://api.bilibili.com/x/tag/archive/tags?aid={video_id}',
                video_id, fatal=False, note='Downloading tags'), ('data', ..., 'tag_name')),
        }

        info['subtitles'] = {
            'danmaku': [{
                'ext': 'xml',
                'url': f'https://comment.bilibili.com/{cid}.xml',
            }]
        }

        r'''
        # Requires https://github.com/m13253/danmaku2ass which is licenced under GPL3
        # See https://github.com/animelover1984/youtube-dl

        raw_danmaku = self._download_webpage(
            f'https://comment.bilibili.com/{cid}.xml', video_id, fatal=False, note='Downloading danmaku comments')
        danmaku = NiconicoIE.CreateDanmaku(raw_danmaku, commentType='Bilibili', x=1024, y=576)
        entries[0]['subtitles'] = {
            'danmaku': [{
                'ext': 'ass',
                'data': danmaku
            }]
        }
        '''

        top_level_info['__post_extractor'] = self.extract_comments(video_id)

        for entry in entries:
            entry.update(info)

        if len(entries) == 1:
            entries[0].update(top_level_info)
            return entries[0]

        for idx, entry in enumerate(entries):
            entry['id'] = '%s_part%d' % (video_id, (idx + 1))

        return {
            'id': str(video_id),
            'bv_id': bv_id,
            'title': title,
            'description': description,
            **info, **top_level_info
        }

    def _extract_anthology_entries(self, bv_id, video_id, webpage):
        title = self._html_search_regex(
            (r'
]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
             r'(?s)
]*>(?P<title>.+?)
',
             r'<title>(?P<title>.+?)</title>'), webpage, 'title',
            group='title')
        json_data = self._download_json(
            f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
            video_id, note='Extracting videos in anthology')

        if json_data['data']:
            return self.playlist_from_matches(
                json_data['data'], bv_id, title, ie=BiliBiliIE.ie_key(),
                getter=lambda entry: 'https://www.bilibili.com/video/%s?p=%d' % (bv_id, entry['page']))

    def _get_video_id_set(self, id, is_bv):
        query = {'bvid': id} if is_bv else {'aid': id}
        response = self._download_json(
            "http://api.bilibili.cn/x/web-interface/view",
            id, query=query,
            note='Grabbing original ID via API')

        if response['code'] == -400:
            raise ExtractorError('Video ID does not exist', expected=True, video_id=id)
        elif response['code'] != 0:
            raise ExtractorError(f'Unknown error occurred during API check (code {response["code"]})',
                                 expected=True, video_id=id)
        return response['data']['aid'], response['data']['bvid']

    def _get_comments(self, video_id, commentPageNumber=0):
        for idx in itertools.count(1):
            replies = traverse_obj(
                self._download_json(
                    f'https://api.bilibili.com/x/v2/reply?pn={idx}&oid={video_id}&type=1&jsonp=jsonp&sort=2&_=1567227301685',
                    video_id, note=f'Extracting comments from page {idx}', fatal=False),
                ('data', 'replies'))
            if not replies:
                return
            for children in map(self._get_all_children, replies):
                yield from children

    def _get_all_children(self, reply):
        yield {
            'author': traverse_obj(reply, ('member', 'uname')),
            'author_id': traverse_obj(reply, ('member', 'mid')),
            'id': reply.get('rpid'),
            'text': traverse_obj(reply, ('content', 'message')),
            'timestamp': reply.get('ctime'),
            'parent': reply.get('parent') or 'root',
        }
        for children in map(self._get_all_children, reply.get('replies') or []):
            yield from children

class BiliBiliBangumiIE(InfoExtractor):
    _VALID_URL = r'https?://bangumi\.bilibili\.com/anime/(?P<id>\d+)'

    IE_NAME = 'bangumi.bilibili.com'
    IE_DESC = 'BiliBili番剧'

    _TESTS = [{
        'url': 'http://bangumi.bilibili.com/anime/1869',
        'info_dict': {
            'id': '1869',
            'title': '混沌武士',
            'description': 'md5:6a9622b911565794c11f25f81d6a97d2',
        },
        'playlist_count': 26,
    }, {
        'url': 'http://bangumi.bilibili.com/anime/1869',
        'info_dict': {
            'id': '1869',
            'title': '混沌武士',
            'description': 'md5:6a9622b911565794c11f25f81d6a97d2',
        },
        'playlist': [{
            'md5': '91da8621454dd58316851c27c68b0c13',
            'info_dict': {
                'id': '40062',
                'ext': 'mp4',
                'title': '混沌武士',
                'description': '故事发生在日本的江户时代。风是一个小酒馆的打工女。一日，酒馆里来了一群恶霸，虽然他们的举动令风十分不满，但是毕竟风只是一届女流，无法对他们采取什么行动，只能在心里嘟哝。这时，酒家里又进来了个“不良份子...',
                'timestamp': 1414538739,
                'upload_date': '20141028',
                'episode': '疾风怒涛 Tempestuous Temperaments',
                'episode_number': 1,
            },
        }],
        'params': {
            'playlist_items': '1',
        },
    }]

    @classmethod
    def suitable(cls, url):
        return False if BiliBiliIE.suitable(url) else super(BiliBiliBangumiIE, cls).suitable(url)

    def _real_extract(self, url):
        bangumi_id = self._match_id(url)

        # Sometimes this API returns a JSONP response
        season_info = self._download_json(
            'http://bangumi.bilibili.com/jsonp/seasoninfo/%s.ver' % bangumi_id,
            bangumi_id, transform_source=strip_jsonp)['result']

        entries = [{
            '_type': 'url_transparent',
            'url': smuggle_url(episode['webplay_url'], {'no_bangumi_tip': 1}),
            'ie_key': BiliBiliIE.ie_key(),
            'timestamp': parse_iso8601(episode.get('update_time'), delimiter=' '),
            'episode': episode.get('index_title'),
            'episode_number': int_or_none(episode.get('index')),
        } for episode in season_info['episodes']]

        entries = sorted(entries, key=lambda entry: entry.get('episode_number'))

        return self.playlist_result(
            entries, bangumi_id,
            season_info.get('bangumi_title'), season_info.get('evaluate'))

class BilibiliSpaceBaseIE(InfoExtractor):
    def _extract_playlist(self, fetch_page, get_metadata, get_entries):
        first_page = fetch_page(1)
        metadata = get_metadata(first_page)

        paged_list = InAdvancePagedList(
            lambda idx: get_entries(fetch_page(idx) if idx > 1 else first_page),
            metadata['page_count'], metadata['page_size'])

        return metadata, paged_list

class BilibiliSpaceVideoIE(BilibiliSpaceBaseIE):
    _VALID_URL = r'https?://space\.bilibili\.com/(?P<id>\d+)(?P<video>/video)?/?(?:[?#]|$)'
    _TESTS = [{
        'url': 'https://space.bilibili.com/3985676/video',
        'info_dict': {
            'id': '3985676',
        },
        'playlist_mincount': 178,
    }]

    def _real_extract(self, url):
        playlist_id, is_video_url = self._match_valid_url(url).group('id', 'video')
        if not is_video_url:
            self.to_screen('A channel URL was given. Only the channel\'s videos will be downloaded. '
                           'To download audios, add a "/audio" to the URL')

        def fetch_page(page_idx):
            return self._download_json(
                'https://api.bilibili.com/x/space/arc/search', playlist_id,
                note=f'Downloading page {page_idx}',
                query={'mid': playlist_id, 'pn': page_idx, 'jsonp': 'jsonp'})['data']

        def get_metadata(page_data):
            page_size = page_data['page']['ps']
            entry_count = page_data['page']['count']
            return {
                'page_count': math.ceil(entry_count / page_size),
                'page_size': page_size,
            }

        def get_entries(page_data):
            for entry in traverse_obj(page_data, ('list', 'vlist')) or []:
                yield self.url_result(f'https://www.bilibili.com/video/{entry["bvid"]}', BiliBiliIE, entry['bvid'])

        metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
        return self.playlist_result(paged_list, playlist_id)

class BilibiliSpaceAudioIE(BilibiliSpaceBaseIE):
    _VALID_URL = r'https?://space\.bilibili\.com/(?P<id>\d+)/audio'
    _TESTS = [{
        'url': 'https://space.bilibili.com/3985676/audio',
        'info_dict': {
            'id': '3985676',
        },
        'playlist_mincount': 1,
    }]

    def _real_extract(self, url):
        playlist_id = self._match_id(url)

        def fetch_page(page_idx):
            return self._download_json(
                'https://api.bilibili.com/audio/music-service/web/song/upper', playlist_id,
                note=f'Downloading page {page_idx}',
                query={'uid': playlist_id, 'pn': page_idx, 'ps': 30, 'order': 1, 'jsonp': 'jsonp'})['data']

        def get_metadata(page_data):
            return {
                'page_count': page_data['pageCount'],
                'page_size': page_data['pageSize'],
            }

        def get_entries(page_data):
            for entry in page_data.get('data', []):
                yield self.url_result(f'https://www.bilibili.com/audio/au{entry["id"]}', BilibiliAudioIE, entry['id'])

        metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
        return self.playlist_result(paged_list, playlist_id)

class BilibiliSpacePlaylistIE(BilibiliSpaceBaseIE):
    _VALID_URL = r'https?://space.bilibili\.com/(?P<mid>\d+)/channel/collectiondetail\?sid=(?P<sid>\d+)'
    _TESTS = [{
        'url': 'https://space.bilibili.com/2142762/channel/collectiondetail?sid=57445',
        'info_dict': {
            'id': '2142762_57445',
            'title': '《底特律 变人》'
        },
        'playlist_mincount': 31,
    }]

    def _real_extract(self, url):
        mid, sid = self._match_valid_url(url).group('mid', 'sid')
        playlist_id = f'{mid}_{sid}'

        def fetch_page(page_idx):
            return self._download_json(
                'https://api.bilibili.com/x/polymer/space/seasons_archives_list',
                playlist_id, note=f'Downloading page {page_idx}',
                query={'mid': mid, 'season_id': sid, 'page_num': page_idx, 'page_size': 30})['data']

        def get_metadata(page_data):
            page_size = page_data['page']['page_size']
            entry_count = page_data['page']['total']
            return {
                'page_count': math.ceil(entry_count / page_size),
                'page_size': page_size,
                'title': traverse_obj(page_data, ('meta', 'name'))
            }

        def get_entries(page_data):
            for entry in page_data.get('archives', []):
                yield self.url_result(f'https://www.bilibili.com/video/{entry["bvid"]}',
                                      BiliBiliIE, entry['bvid'])

        metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
        return self.playlist_result(paged_list, playlist_id, metadata['title'])

class BilibiliCategoryIE(InfoExtractor):
    IE_NAME = 'Bilibili category extractor'
    _MAX_RESULTS = 1000000
    _VALID_URL = r'https?://www\.bilibili\.com/v/[a-zA-Z]+\/[a-zA-Z]+'
    _TESTS = [{
        'url': 'https://www.bilibili.com/v/kichiku/mad',
        'info_dict': {
            'id': 'kichiku: mad',
            'title': 'kichiku: mad'
        },
        'playlist_mincount': 45,
        'params': {
            'playlistend': 45
        }
    }]

    def _fetch_page(self, api_url, num_pages, query, page_num):
        parsed_json = self._download_json(
            api_url, query, query={'Search_key': query, 'pn': page_num},
            note='Extracting results from page %s of %s' % (page_num, num_pages))

        video_list = traverse_obj(parsed_json, ('data', 'archives'), expected_type=list)
        if not video_list:
            raise ExtractorError('Failed to retrieve video list for page %d' % page_num)

        for video in video_list:
            yield self.url_result(
                'https://www.bilibili.com/video/%s' % video['bvid'], 'BiliBili', video['bvid'])

    def _entries(self, category, subcategory, query):
        # map of categories : subcategories : RIDs
        rid_map = {
            'kichiku': {
                'mad': 26,
                'manual_vocaloid': 126,
                'guide': 22,
                'theatre': 216,
                'course': 127
            },
        }

        if category not in rid_map:
            raise ExtractorError(
                f'The category {category} isn\'t supported. Supported categories: {list(rid_map.keys())}')
        if subcategory not in rid_map[category]:
            raise ExtractorError(
                f'The subcategory {subcategory} isn\'t supported for this category. Supported subcategories: {list(rid_map[category].keys())}')
        rid_value = rid_map[category][subcategory]

        api_url = 'https://api.bilibili.com/x/web-interface/newlist?rid=%d&type=1&ps=20&jsonp=jsonp' % rid_value
        page_json = self._download_json(api_url, query, query={'Search_key': query, 'pn': '1'})
        page_data = traverse_obj(page_json, ('data', 'page'), expected_type=dict)
        count, size = int_or_none(page_data.get('count')), int_or_none(page_data.get('size'))
        if count is None or not size:
            raise ExtractorError('Failed to calculate either page count or size')

        num_pages = math.ceil(count / size)

        return OnDemandPagedList(functools.partial(
            self._fetch_page, api_url, num_pages, query), size)

    def _real_extract(self, url):
        u = compat_urllib_parse_urlparse(url)
        category, subcategory = u.path.split('/')[2:4]
        query = '%s: %s' % (category, subcategory)

        return self.playlist_result(self._entries(category, subcategory, query), query, query)

class BiliBiliSearchIE(SearchInfoExtractor):
    IE_DESC = 'Bilibili video search'
    _MAX_RESULTS = 100000
    _SEARCH_KEY = 'bilisearch'

    def _search_results(self, query):
        for page_num in itertools.count(1):
            videos = self._download_json(
                'https://api.bilibili.com/x/web-interface/search/type', query,
                note=f'Extracting results from page {page_num}', query={
                    'Search_key': query,
                    'keyword': query,
                    'page': page_num,
                    'context': '',
                    'duration': 0,
                    'tids_2': '',
                    '__refresh__': 'true',
                    'search_type': 'video',
                    'tids': 0,
                    'highlight': 1,
                })['data'].get('result')
            if not videos:
                break
            for video in videos:
                yield self.url_result(video['arcurl'], 'BiliBili', str(video['aid']))

class BilibiliAudioBaseIE(InfoExtractor):
    def _call_api(self, path, sid, query=None):
        if not query:
            query = {'sid': sid}
        return self._download_json(
            'https://www.bilibili.com/audio/music-service-c/web/' + path,
            sid, query=query)['data']

class BilibiliAudioIE(BilibiliAudioBaseIE):
    _VALID_URL = r'https?://(?:www\.)?bilibili\.com/audio/au(?P<id>\d+)'
    _TEST = {
        'url': 'https://www.bilibili.com/audio/au1003142',
        'md5': 'fec4987014ec94ef9e666d4d158ad03b',
        'info_dict': {
            'id': '1003142',
            'ext': 'm4a',
            'title': '【tsukimi】YELLOW / 神山羊',
            'artist': 'tsukimi',
            'comment_count': int,
            'description': 'YELLOW的mp3版！',
            'duration': 183,
            'subtitles': {
                'origin': [{
                    'ext': 'lrc',
                }],
            },
            'thumbnail': r're:^https?://.+\.jpg',
            'timestamp': 1564836614,
            'upload_date': '20190803',
            'uploader': 'tsukimi-つきみぐー',
            'view_count': int,
        },
    }

    def _real_extract(self, url):
        au_id = self._match_id(url)

        play_data = self._call_api('url', au_id)
        formats = [{
            'url': play_data['cdns'][0],
            'filesize': int_or_none(play_data.get('size')),
            'vcodec': 'none'
        }]

        for a_format in formats:
            a_format.setdefault('http_headers', {}).update({
                'Referer': url,
            })

        song = self._call_api('song/info', au_id)
        title = song['title']
        statistic = song.get('statistic') or {}

        subtitles = None
        lyric = song.get('lyric')
        if lyric:
            subtitles = {
                'origin': [{
                    'url': lyric,
                }]
            }

        return {
            'id': au_id,
            'title': title,
            'formats': formats,
            'artist': song.get('author'),
            'comment_count': int_or_none(statistic.get('comment')),
            'description': song.get('intro'),
            'duration': int_or_none(song.get('duration')),
            'subtitles': subtitles,
            'thumbnail': song.get('cover'),
            'timestamp': int_or_none(song.get('passtime')),
            'uploader': song.get('uname'),
            'view_count': int_or_none(statistic.get('play')),
        }

class BilibiliAudioAlbumIE(BilibiliAudioBaseIE):
    _VALID_URL = r'https?://(?:www\.)?bilibili\.com/audio/am(?P<id>\d+)'
    _TEST = {
        'url': 'https://www.bilibili.com/audio/am10624',
        'info_dict': {
            'id': '10624',
            'title': '每日新曲推荐（每日11:00更新）',
            'description': '每天11:00更新，为你推送最新音乐',
        },
        'playlist_count': 19,
    }

    def _real_extract(self, url):
        am_id = self._match_id(url)

        songs = self._call_api(
            'song/of-menu', am_id, {'sid': am_id, 'pn': 1, 'ps': 100})['data']

        entries = []
        for song in songs:
            sid = str_or_none(song.get('id'))
            if not sid:
                continue
            entries.append(self.url_result(
                'https://www.bilibili.com/audio/au' + sid,
                BilibiliAudioIE.ie_key(), sid))

        if entries:
            album_data = self._call_api('menu/info', am_id) or {}
            album_title = album_data.get('title')
            if album_title:
                for entry in entries:
                    entry['album'] = album_title
                return self.playlist_result(
                    entries, am_id, album_title, album_data.get('intro'))

        return self.playlist_result(entries, am_id)

class BiliBiliPlayerIE(InfoExtractor):
    _VALID_URL = r'https?://player\.bilibili\.com/player\.html\?.*?\baid=(?P<id>\d+)'
    _TEST = {
        'url': 'http://player.bilibili.com/player.html?aid=92494333&cid=157926707&page=1',
        'only_matching': True,
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        return self.url_result(
            'http://www.bilibili.tv/video/av%s/' % video_id,
            ie=BiliBiliIE.ie_key(), video_id=video_id)

class BiliIntlBaseIE(InfoExtractor):
    _API_URL = 'https://api.bilibili.tv/intl/gateway'
    _NETRC_MACHINE = 'biliintl'

    def _call_api(self, endpoint, *args, **kwargs):
        json = self._download_json(self._API_URL + endpoint, *args, **kwargs)
        if json.get('code'):
            if json['code'] in (10004004, 10004005, 10023006):
                self.raise_login_required()
            elif json['code'] == 10004001:
                self.raise_geo_restricted()
            else:
                if json.get('message') and str(json['code']) != json['message']:
                    errmsg = f'{kwargs.get("errnote", "Unable to download JSON metadata")}: {self.IE_NAME} said: {json["message"]}'
                else:
                    errmsg = kwargs.get('errnote', 'Unable to download JSON metadata')
                if kwargs.get('fatal'):
                    raise ExtractorError(errmsg)
                else:
                    self.report_warning(errmsg)
        return json.get('data')

    def json2srt(self, json):
        data = '\n\n'.join(
            f'{i + 1}\n{srt_subtitles_timecode(line["from"])} --> {srt_subtitles_timecode(line["to"])}\n{line["content"]}'
            for i, line in enumerate(traverse_obj(json, (
                'body', lambda _, l: l['content'] and l['from'] and l['to']))))
        return data

    def _get_subtitles(self, *, ep_id=None, aid=None):
        sub_json = self._call_api(
            '/web/v2/subtitle', ep_id or aid, fatal=False,
            note='Downloading subtitles list', errnote='Unable to download subtitles list',
            query=filter_dict({
                'platform': 'web',
                's_locale': 'en_US',
                'episode_id': ep_id,
                'aid': aid,
            })) or {}
        subtitles = {}
        for sub in sub_json.get('subtitles') or []:
            sub_url = sub.get('url')
            if not sub_url:
                continue
            sub_data = self._download_json(
                sub_url, ep_id or aid, errnote='Unable to download subtitles', fatal=False,
                note='Downloading subtitles%s' % f' for {sub["lang"]}' if sub.get('lang') else '')
            if not sub_data:
                continue
            subtitles.setdefault(sub.get('lang_key', 'en'), []).append({
                'ext': 'srt',
                'data': self.json2srt(sub_data)
            })
        return subtitles

    def _get_formats(self, *, ep_id=None, aid=None):
        video_json = self._call_api(
            '/web/playurl', ep_id or aid, note='Downloading video formats',
            errnote='Unable to download video formats', query=filter_dict({
                'platform': 'web',
                'ep_id': ep_id,
                'aid': aid,
            }))
        video_json = video_json['playurl']
        formats = []
        for vid in video_json.get('video') or []:
            video_res = vid.get('video_resource') or {}
            video_info = vid.get('stream_info') or {}
            if not video_res.get('url'):
                continue
            formats.append({
                'url': video_res['url'],
                'ext': 'mp4',
                'format_note': video_info.get('desc_words'),
                'width': video_res.get('width'),
                'height': video_res.get('height'),
                'vbr': video_res.get('bandwidth'),
                'acodec': 'none',
                'vcodec': video_res.get('codecs'),
                'filesize': video_res.get('size'),
            })
        for aud in video_json.get('audio_resource') or []:
            if not aud.get('url'):
                continue
            formats.append({
                'url': aud['url'],
                'ext': 'mp4',
                'abr': aud.get('bandwidth'),
                'acodec': aud.get('codecs'),
                'vcodec': 'none',
                'filesize': aud.get('size'),
            })

        self._sort_formats(formats)
        return formats

    def _extract_video_info(self, video_data, *, ep_id=None, aid=None):
        return {
            'id': ep_id or aid,
            'title': video_data.get('title_display') or video_data.get('title'),
            'thumbnail': video_data.get('cover'),
            'episode_number': int_or_none(self._search_regex(
                r'^E(\d+)(?:$| - )', video_data.get('title_display') or '', 'episode number', default=None)),
            'formats': self._get_formats(ep_id=ep_id, aid=aid),
            'subtitles': self._get_subtitles(ep_id=ep_id, aid=aid),
            'extractor_key': BiliIntlIE.ie_key(),
        }

    def _perform_login(self, username, password):
        try:
            from Cryptodome.PublicKey import RSA
            from Cryptodome.Cipher import PKCS1_v1_5
        except ImportError:
            try:
                from Crypto.PublicKey import RSA
                from Crypto.Cipher import PKCS1_v1_5
            except ImportError:
                raise ExtractorError('pycryptodomex not found. Please install', expected=True)

        key_data = self._download_json(
            'https://passport.bilibili.tv/x/intl/passport-login/web/key?lang=en-US', None,
            note='Downloading login key', errnote='Unable to download login key')['data']

        public_key = RSA.importKey(key_data['key'])
        password_hash = PKCS1_v1_5.new(public_key).encrypt((key_data['hash'] + password).encode('utf-8'))
        login_post = self._download_json(
            'https://passport.bilibili.tv/x/intl/passport-login/web/login/password?lang=en-US', None, data=urlencode_postdata({
                'username': username,
                'password': base64.b64encode(password_hash).decode('ascii'),
                'keep_me': 'true',
                's_locale': 'en_US',
                'isTrusted': 'true'
            }), note='Logging in', errnote='Unable to log in')
        if login_post.get('code'):
            if login_post.get('message'):
                raise ExtractorError(f'Unable to log in: {self.IE_NAME} said: {login_post["message"]}', expected=True)
            else:
                raise ExtractorError('Unable to log in')

class BiliIntlIE(BiliIntlBaseIE):
    _VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-zA-Z]{2}/)?(play/(?P<season_id>\d+)/(?P<ep_id>\d+)|video/(?P<aid>\d+))'
    _TESTS = [{
        # Bstation page
        'url': 'https://www.bilibili.tv/en/play/34613/341736',
        'info_dict': {
            'id': '341736',
            'ext': 'mp4',
            'title': 'E2 - The First Night',
            'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
            'episode_number': 2,
        }
    }, {
        # Non-Bstation page
        'url': 'https://www.bilibili.tv/en/play/1033760/11005006',
        'info_dict': {
            'id': '11005006',
            'ext': 'mp4',
            'title': 'E3 - Who?',
            'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
            'episode_number': 3,
        }
    }, {
        # Subtitle with empty content
        'url': 'https://www.bilibili.tv/en/play/1005144/10131790',
        'info_dict': {
            'id': '10131790',
            'ext': 'mp4',
            'title': 'E140 - Two Heartbeats: Kabuto\'s Trap',
            'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
            'episode_number': 140,
        },
        'skip': 'According to the copyright owner\'s request, you may only watch the video after you log in.'
    }, {
        'url': 'https://www.biliintl.com/en/play/34613/341736',
        'only_matching': True,
    }, {
        # User-generated content (as opposed to a series licensed from a studio)
        'url': 'https://bilibili.tv/en/video/2019955076',
        'only_matching': True,
    }, {
        # No language in URL
        'url': 'https://www.bilibili.tv/video/2019955076',
        'only_matching': True,
    }, {
        # Uppercase language in URL
        'url': 'https://www.bilibili.tv/EN/video/2019955076',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        season_id, ep_id, aid = self._match_valid_url(url).group('season_id', 'ep_id', 'aid')
        video_id = ep_id or aid
        webpage = self._download_webpage(url, video_id)
        # Bstation layout
        initial_data = (
            self._search_json(r'window\.__INITIAL_(?:DATA|STATE)__\s*=', webpage, 'preload state', video_id, default={})
            or self._search_nuxt_data(webpage, video_id, '__initialState', fatal=False, traverse=None))
        video_data = traverse_obj(
            initial_data, ('OgvVideo', 'epDetail'), ('UgcVideo', 'videoData'), ('ugc', 'archive'), expected_type=dict)

        if season_id and not video_data:
            # Non-Bstation layout, read through episode list
            season_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={season_id}&platform=web', video_id)
            video_data = traverse_obj(season_json,
                                      ('sections', ..., 'episodes', lambda _, v: str(v['episode_id']) == ep_id),
                                      expected_type=dict, get_all=False)
        return self._extract_video_info(video_data or {}, ep_id=ep_id, aid=aid)

class BiliIntlSeriesIE(BiliIntlBaseIE):
    _VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-zA-Z]{2}/)?play/(?P<id>\d+)/?(?:[?#]|$)'
    _TESTS = [{
        'url': 'https://www.bilibili.tv/en/play/34613',
        'playlist_mincount': 15,
        'info_dict': {
            'id': '34613',
            'title': 'Fly Me to the Moon',
            'description': 'md5:a861ee1c4dc0acfad85f557cc42ac627',
            'categories': ['Romance', 'Comedy', 'Slice of life'],
            'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
            'view_count': int,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://www.biliintl.com/en/play/34613',
        'only_matching': True,
    }, {
        'url': 'https://www.biliintl.com/EN/play/34613',
        'only_matching': True,
    }]

    def _entries(self, series_id):
        series_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={series_id}&platform=web', series_id)
        for episode in traverse_obj(series_json, ('sections', ..., 'episodes', ...), expected_type=dict, default=[]):
            episode_id = str(episode.get('episode_id'))
            yield self._extract_video_info(episode, ep_id=episode_id)

    def _real_extract(self, url):
        series_id = self._match_id(url)
        series_info = self._call_api(f'/web/v2/ogv/play/season_info?season_id={series_id}&platform=web', series_id).get('season') or {}
        return self.playlist_result(
            self._entries(series_id), series_id, series_info.get('title'), series_info.get('description'),
            categories=traverse_obj(series_info, ('styles', ..., 'title'), expected_type=str_or_none),
            thumbnail=url_or_none(series_info.get('horizontal_cover')), view_count=parse_count(series_info.get('view')))

class BiliLiveIE(InfoExtractor):
    _VALID_URL = r'https?://live.bilibili.com/(?P<id>\d+)'

    _TESTS = [{
        'url': 'https://live.bilibili.com/196',
        'info_dict': {
            'id': '33989',
            'description': "周六杂谈回，其他时候随机游戏。 | \n录播：@下播型泛式录播组。 | \n直播通知群（全员禁言）：666906670，902092584，59971⑧481 （功能一样，别多加）",
            'ext': 'flv',
            'title': "太空狼人杀联动，不被爆杀就算赢",
            'thumbnail': "https://i0.hdslb.com/bfs/live/new_room_cover/e607bc1529057ef4b332e1026e62cf46984c314d.jpg",
            'timestamp': 1650802769,
        },
        'skip': 'not live'
    }, {
        'url': 'https://live.bilibili.com/196?broadcast_type=0&is_room_feed=1?spm_id_from=333.999.space_home.strengthen_live_card.click',
        'only_matching': True
    }]

    _FORMATS = {
        80: {'format_id': 'low', 'format_note': '流畅'},
        150: {'format_id': 'high_res', 'format_note': '高清'},
        250: {'format_id': 'ultra_high_res', 'format_note': '超清'},
        400: {'format_id': 'blue_ray', 'format_note': '蓝光'},
        10000: {'format_id': 'source', 'format_note': '原画'},
        20000: {'format_id': '4K', 'format_note': '4K'},
        30000: {'format_id': 'dolby', 'format_note': '杜比'},
    }

    _quality = staticmethod(qualities(list(_FORMATS)))

    def _call_api(self, path, room_id, query):
        api_result = self._download_json(f'https://api.live.bilibili.com/{path}', room_id, query=query)
        if api_result.get('code') != 0:
            raise ExtractorError(api_result.get('message') or 'Unable to download JSON metadata')
        return api_result.get('data') or {}

    def _parse_formats(self, qn, fmt):
        for codec in fmt.get('codec') or []:
            if codec.get('current_qn') != qn:
                continue
            for url_info in codec['url_info']:
                yield {
                    'url': f'{url_info["host"]}{codec["base_url"]}{url_info["extra"]}',
                    'ext': fmt.get('format_name'),
                    'vcodec': codec.get('codec_name'),
                    'quality': self._quality(qn),
                    **self._FORMATS[qn],
                }

    def _real_extract(self, url):
        room_id = self._match_id(url)
        room_data = self._call_api('room/v1/Room/get_info', room_id, {'id': room_id})
        if room_data.get('live_status') == 0:
            raise ExtractorError('Streamer is not live', expected=True)

        formats = []
        for qn in self._FORMATS.keys():
            stream_data = self._call_api('xlive/web-room/v2/index/getRoomPlayInfo', room_id, {
                'room_id': room_id,
                'qn': qn,
                'codec': '0,1',
                'format': '0,2',
                'mask': '0',
                'no_playurl': '0',
                'platform': 'web',
                'protocol': '0,1',
            })
            for fmt in traverse_obj(stream_data, ('playurl_info', 'playurl', 'stream', ..., 'format', ...)) or []:
                formats.extend(self._parse_formats(qn, fmt))
        self._sort_formats(formats)

        return {
            'id': room_id,
            'title': room_data.get('title'),
            'description': room_data.get('description'),
            'thumbnail': room_data.get('user_cover'),
            'timestamp': stream_data.get('live_time'),
            'formats': formats,
            'http_headers': {
                'Referer': url,
            },
        }


code:
from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    remove_end,
)

class BioBioChileTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:tv|www)\.biobiochile\.cl/(?:notas|noticias)/(?:[^/]+/)+(?P<id>[^/]+)\.shtml'

    _TESTS = [{
        'url': 'http://tv.biobiochile.cl/notas/2015/10/21/sobre-camaras-y-camarillas-parlamentarias.shtml',
        'md5': '26f51f03cf580265defefb4518faec09',
        'info_dict': {
            'id': 'sobre-camaras-y-camarillas-parlamentarias',
            'ext': 'mp4',
            'title': 'Sobre Cámaras y camarillas parlamentarias',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'Fernando Atria',
        },
        'skip': 'URL expired and redirected to http://www.biobiochile.cl/portada/bbtv/index.html',
    }, {
        # different uploader layout
        'url': 'http://tv.biobiochile.cl/notas/2016/03/18/natalia-valdebenito-repasa-a-diputado-hasbun-paso-a-la-categoria-de-hablar-brutalidades.shtml',
        'md5': 'edc2e6b58974c46d5b047dea3c539ff3',
        'info_dict': {
            'id': 'natalia-valdebenito-repasa-a-diputado-hasbun-paso-a-la-categoria-de-hablar-brutalidades',
            'ext': 'mp4',
            'title': 'Natalia Valdebenito repasa a diputado Hasbún: Pasó a la categoría de hablar brutalidades',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'Piangella Obrador',
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'URL expired and redirected to http://www.biobiochile.cl/portada/bbtv/index.html',
    }, {
        'url': 'http://www.biobiochile.cl/noticias/bbtv/comentarios-bio-bio/2016/07/08/edecanes-del-congreso-figuras-decorativas-que-le-cuestan-muy-caro-a-los-chilenos.shtml',
        'info_dict': {
            'id': 'b4xd0LK3SK',
            'ext': 'mp4',
            # TODO: fix url_transparent information overriding
            # 'uploader': 'Juan Pablo Echenique',
            'title': 'Comentario Oscar Cáceres',
        },
        'params': {
            # empty m3u8 manifest
            'skip_download': True,
        },
    }, {
        'url': 'http://tv.biobiochile.cl/notas/2015/10/22/ninos-transexuales-de-quien-es-la-decision.shtml',
        'only_matching': True,
    }, {
        'url': 'http://tv.biobiochile.cl/notas/2015/10/21/exclusivo-hector-pinto-formador-de-chupete-revela-version-del-ex-delantero-albo.shtml',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)

        webpage = self._download_webpage(url, video_id)

        rudo_url = self._search_regex(
            r'<iframe[^>]+src=(?P<q1>[\'"])(?P<url>(?:https?:)?//rudo\.video/vod/[0-9a-zA-Z]+)(?P=q1)',
            webpage, 'embed URL', None, group='url')
        if not rudo_url:
            raise ExtractorError('No videos found')

        title = remove_end(self._og_search_title(webpage), ' - BioBioChile TV')

        thumbnail = self._og_search_thumbnail(webpage)
        uploader = self._html_search_regex(
            r'<a[^>]+href=["\'](?:https?://(?:busca|www)\.biobiochile\.cl)?/(?:lista/)?(?:author|autor)[^>]+>(.+?)</a>',
            webpage, 'uploader', fatal=False)

        return {
            '_type': 'url_transparent',
            'url': rudo_url,
            'id': video_id,
            'title': title,
            'thumbnail': thumbnail,
            'uploader': uploader,
        }

repo_name: Lesmiscore/ytdl


code:
from .common import InfoExtractor
from .vk import VKIE
from ..compat import compat_b64decode
from ..utils import (
    int_or_none,
    js_to_json,
    traverse_obj,
    unified_timestamp,
)

class BIQLEIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?biqle\.(?:com|org|ru)/watch/(?P<id>-?\d+_\d+)'
    _TESTS = [{
        'url': 'https://biqle.ru/watch/-2000421746_85421746',
        'md5': 'ae6ef4f04d19ac84e4658046d02c151c',
        'info_dict': {
            'id': '-2000421746_85421746',
            'ext': 'mp4',
            'title': 'Forsaken By Hope Studio Clip',
            'description': 'Forsaken By Hope Studio Clip — Смотреть онлайн',
            'upload_date': '19700101',
            'thumbnail': r're:https://[^/]+/impf/7vN3ACwSTgChP96OdOfzFjUCzFR6ZglDQgWsIw/KPaACiVJJxM\.jpg\?size=800x450&quality=96&keep_aspect_ratio=1&background=000000&sign=b48ea459c4d33dbcba5e26d63574b1cb&type=video_thumb',
            'timestamp': 0,
        },
    }, {
        'url': 'http://biqle.org/watch/-44781847_168547604',
        'md5': '7f24e72af1db0edf7c1aaba513174f97',
        'info_dict': {
            'id': '-44781847_168547604',
            'ext': 'mp4',
            'title': 'Ребенок в шоке от автоматической мойки',
            'description': 'Ребенок в шоке от автоматической мойки — Смотреть онлайн',
            'timestamp': 1396633454,
            'upload_date': '20140404',
            'thumbnail': r're:https://[^/]+/c535507/u190034692/video/l_b84df002\.jpg',
        },
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(url, video_id)

        title = self._html_search_meta('name', webpage, 'Title', fatal=False)
        timestamp = unified_timestamp(self._html_search_meta('uploadDate', webpage, 'Upload Date', default=None))
        description = self._html_search_meta('description', webpage, 'Description', default=None)

        global_embed_url = self._search_regex(
            r'<script[^<]+?window.globEmbedUrl\s*=\s*\'((?:https?:)?//(?:daxab\.com|dxb\.to|[^/]+/player)/[^\']+)\'',
            webpage, 'global Embed url')
        hash = self._search_regex(
            r'<script id="data-embed-video[^<]+?hash: "([^"]+)"[^<]*</script>', webpage, 'Hash')

        embed_url = global_embed_url + hash

        if VKIE.suitable(embed_url):
            return self.url_result(embed_url, VKIE.ie_key(), video_id)

        embed_page = self._download_webpage(
            embed_url, video_id, 'Downloading embed webpage', headers={'Referer': url})

        glob_params = self._parse_json(self._search_regex(
            r'<script id="globParams">[^<]*window.globParams = ([^;]+);[^<]+</script>',
            embed_page, 'Global Parameters'), video_id, transform_source=js_to_json)
        host_name = compat_b64decode(glob_params['server'][::-1]).decode()

        item = self._download_json(
            f'https://{host_name}/method/video.get/{video_id}', video_id,
            headers={'Referer': url}, query={
                'token': glob_params['video']['access_token'],
                'videos': video_id,
                'ckey': glob_params['c_key'],
                'credentials': glob_params['video']['credentials'],
            })['response']['items'][0]

        formats = []
        for f_id, f_url in item.get('files', {}).items():
            if f_id == 'external':
                return self.url_result(f_url)
            ext, height = f_id.split('_')
            height_extra_key = traverse_obj(glob_params, ('video', 'partial', 'quality', height))
            if height_extra_key:
                formats.append({
                    'format_id': f'{height}p',
                    'url': f'https://{host_name}/{f_url[8:]}&videos={video_id}&extra_key={height_extra_key}',
                    'height': int_or_none(height),
                    'ext': ext,
                })
        self._sort_formats(formats)

        thumbnails = []
        for k, v in item.items():
            if k.startswith('photo_') and v:
                width = k.replace('photo_', '')
                thumbnails.append({
                    'id': width,
                    'url': v,
                    'width': int_or_none(width),
                })

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'comment_count': int_or_none(item.get('comments')),
            'description': description,
            'duration': int_or_none(item.get('duration')),
            'thumbnails': thumbnails,
            'timestamp': timestamp,
            'view_count': int_or_none(item.get('views')),
        }


code:
import itertools
import re

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    GeoRestrictedError,
    orderedSet,
    unified_strdate,
    urlencode_postdata,
)

class BitChuteIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?bitchute\.com/(?:video|embed|torrent/[^/]+)/(?P<id>[^/?#&]+)'
    _EMBED_REGEX = [rf'<(?:script|iframe)[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL})']
    _TESTS = [{
        'url': 'https://www.bitchute.com/video/UGlrF9o9b-Q/',
        'md5': '7e427d7ed7af5a75b5855705ec750e2b',
        'info_dict': {
            'id': 'szoMrox2JEI',
            'ext': 'mp4',
            'title': 'This is the first video on #BitChute !',
            'description': 'md5:a0337e7b1fe39e32336974af8173a034',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'BitChute',
            'upload_date': '20170103',
        },
    }, {
        'url': 'https://www.bitchute.com/embed/lbb5G1hjPhw/',
        'only_matching': True,
    }, {
        'url': 'https://www.bitchute.com/torrent/Zee5BE49045h/szoMrox2JEI.webtorrent',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)

        webpage = self._download_webpage(
            'https://www.bitchute.com/video/%s' % video_id, video_id, headers={
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.57 Safari/537.36',
            })

        title = self._html_search_regex(
            (r'<[^>]+\bid=["\']video-title[^>]+>([^<]+)', r'<title>([^<]+)'),
            webpage, 'title', default=None) or self._html_search_meta(
            'description', webpage, 'title',
            default=None) or self._og_search_description(webpage)

        format_urls = []
        for mobj in re.finditer(
                r'addWebSeed\s*\(\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage):
            format_urls.append(mobj.group('url'))
        format_urls.extend(re.findall(r'as=(https?://[^&"\']+)', webpage))

        formats = [
            {'url': format_url}
            for format_url in orderedSet(format_urls)]

        if not formats:
            entries = self._parse_html5_media_entries(
                url, webpage, video_id)
            if not entries:
                error = self._html_search_regex(r'
([^<]+)
', webpage, 'error', default='Cannot find video')
                if error == 'Video Unavailable':
                    raise GeoRestrictedError(error)
                raise ExtractorError(error, expected=True)
            formats = entries[0]['formats']

        self._check_formats(formats, video_id)
        if not formats:
            raise self.raise_no_formats('Video is unavailable', expected=True, video_id=video_id)
        self._sort_formats(formats)

        description = self._html_search_regex(
            r'(?s)
]+\bclass=["\']full hidden[^>]+>(.+?)
',
            webpage, 'description', fatal=False)
        thumbnail = self._og_search_thumbnail(
            webpage, default=None) or self._html_search_meta(
            'twitter:image:src', webpage, 'thumbnail')
        uploader = self._html_search_regex(
            (r'(?s)]+>(.+?)
',
             r'(?s)
]+\bclass=["\']video-author[^>]+>(.+?)
'),
            webpage, 'uploader', fatal=False)

        upload_date = unified_strdate(self._search_regex(
            r'class=["\']video-publish-date[^>]+>[^<]+ at \d+:\d+ UTC on (.+?)\.',
            webpage, 'upload date', fatal=False))

        return {
            'id': video_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'uploader': uploader,
            'upload_date': upload_date,
            'formats': formats,
        }

class BitChuteChannelIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?bitchute\.com/channel/(?P<id>[^/?#&]+)'
    _TEST = {
        'url': 'https://www.bitchute.com/channel/victoriaxrave/',
        'playlist_mincount': 185,
        'info_dict': {
            'id': 'victoriaxrave',
        },
    }

    _TOKEN = 'zyG6tQcGPE5swyAEFLqKUwMuMMuF6IO2DZ6ZDQjGfsL0e4dcTLwqkTTul05Jdve7'

    def _entries(self, channel_id):
        channel_url = 'https://www.bitchute.com/channel/%s/' % channel_id
        offset = 0
        for page_num in itertools.count(1):
            data = self._download_json(
                '%sextend/' % channel_url, channel_id,
                'Downloading channel page %d' % page_num,
                data=urlencode_postdata({
                    'csrfmiddlewaretoken': self._TOKEN,
                    'name': '',
                    'offset': offset,
                }), headers={
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    'Referer': channel_url,
                    'X-Requested-With': 'XMLHttpRequest',
                    'Cookie': 'csrftoken=%s' % self._TOKEN,
                })
            if data.get('success') is False:
                break
            html = data.get('html')
            if not html:
                break
            video_ids = re.findall(
                r'class=["\']channel-videos-image-container[^>]+>\s*]+\bhref=["\']/video/([^"\'/]+)',
                html)
            if not video_ids:
                break
            offset += len(video_ids)
            for video_id in video_ids:
                yield self.url_result(
                    'https://www.bitchute.com/video/%s' % video_id,
                    ie=BitChuteIE.ie_key(), video_id=video_id)

    def _real_extract(self, url):
        channel_id = self._match_id(url)
        return self.playlist_result(
            self._entries(channel_id), playlist_id=channel_id)


code:
from .common import InfoExtractor

class BitwaveReplayIE(InfoExtractor):
    IE_NAME = 'bitwave:replay'
    _VALID_URL = r'https?://(?:www\.)?bitwave\.tv/(?P<user>\w+)/replay/(?P<id>\w+)/?$'
    _TEST = {
        'url': 'https://bitwave.tv/RhythmicCarnage/replay/z4P6eq5L7WDrM85UCrVr',
        'only_matching': True
    }

    def _real_extract(self, url):
        replay_id = self._match_id(url)
        replay = self._download_json(
            'https://api.bitwave.tv/v1/replays/' + replay_id,
            replay_id
        )

        return {
            'id': replay_id,
            'title': replay['data']['title'],
            'uploader': replay['data']['name'],
            'uploader_id': replay['data']['name'],
            'url': replay['data']['url'],
            'thumbnails': [
                {'url': x} for x in replay['data']['thumbnails']
            ],
        }

class BitwaveStreamIE(InfoExtractor):
    IE_NAME = 'bitwave:stream'
    _VALID_URL = r'https?://(?:www\.)?bitwave\.tv/(?P<id>\w+)/?$'
    _TEST = {
        'url': 'https://bitwave.tv/doomtube',
        'only_matching': True
    }

    def _real_extract(self, url):
        username = self._match_id(url)
        channel = self._download_json(
            'https://api.bitwave.tv/v1/channels/' + username,
            username)

        formats = self._extract_m3u8_formats(
            channel['data']['url'], username,
            'mp4')
        self._sort_formats(formats)

        return {
            'id': username,
            'title': channel['data']['title'],
            'uploader': username,
            'uploader_id': username,
            'formats': formats,
            'thumbnail': channel['data']['thumbnail'],
            'is_live': True,
            'view_count': channel['data']['viewCount']
        }


code:
from .common import InfoExtractor
from ..utils import parse_iso8601

class BlackboardCollaborateIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                        https?://
                        (?P<region>[a-z-]+)\.bbcollab\.com/
                        (?:
                            collab/ui/session/playback/load|
                            recording
                        )/
                        (?P<id>[^/]+)'''
    _TESTS = [
        {
            'url': 'https://us-lti.bbcollab.com/collab/ui/session/playback/load/0a633b6a88824deb8c918f470b22b256',
            'md5': 'bb7a055682ee4f25fdb5838cdf014541',
            'info_dict': {
                'id': '0a633b6a88824deb8c918f470b22b256',
                'title': 'HESI A2 Information Session - Thursday, May 6, 2021 - recording_1',
                'ext': 'mp4',
                'duration': 1896000,
                'timestamp': 1620331399,
                'upload_date': '20210506',
            },
        },
        {
            'url': 'https://us.bbcollab.com/collab/ui/session/playback/load/76761522adfe4345a0dee6794bbcabda',
            'only_matching': True,
        },
        {
            'url': 'https://ca.bbcollab.com/collab/ui/session/playback/load/b6399dcb44df4f21b29ebe581e22479d',
            'only_matching': True,
        },
        {
            'url': 'https://eu.bbcollab.com/recording/51ed7b50810c4444a106e48cefb3e6b5',
            'only_matching': True,
        },
        {
            'url': 'https://au.bbcollab.com/collab/ui/session/playback/load/2bccf7165d7c419ab87afc1ec3f3bb15',
            'only_matching': True,
        },
    ]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        region = mobj.group('region')
        video_id = mobj.group('id')
        info = self._download_json(
            'https://{}.bbcollab.com/collab/api/csa/recordings/{}/data'.format(region, video_id), video_id)
        duration = info.get('duration')
        title = info['name']
        upload_date = info.get('created')
        streams = info['streams']
        formats = [{'format_id': k, 'url': url} for k, url in streams.items()]

        return {
            'duration': duration,
            'formats': formats,
            'id': video_id,
            'timestamp': parse_iso8601(upload_date),
            'title': title,
        }


code:
from .common import InfoExtractor
from .amp import AMPIE
from ..utils import (
    ExtractorError,
    int_or_none,
    parse_iso8601,
)

class BleacherReportIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/articles/(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://bleacherreport.com/articles/2496438-fsu-stat-projections-is-jalen-ramsey-best-defensive-player-in-college-football',
        'md5': 'a3ffc3dc73afdbc2010f02d98f990f20',
        'info_dict': {
            'id': '2496438',
            'ext': 'mp4',
            'title': 'FSU Stat Projections: Is Jalen Ramsey Best Defensive Player in College Football?',
            'uploader_id': 3992341,
            'description': 'CFB, ACC, Florida State',
            'timestamp': 1434380212,
            'upload_date': '20150615',
            'uploader': 'Team Stream Now ',
        },
        'add_ie': ['Ooyala'],
    }, {
        'url': 'http://bleacherreport.com/articles/2586817-aussie-golfers-get-fright-of-their-lives-after-being-chased-by-angry-kangaroo',
        'md5': '6a5cd403418c7b01719248ca97fb0692',
        'info_dict': {
            'id': '2586817',
            'ext': 'webm',
            'title': 'Aussie Golfers Get Fright of Their Lives After Being Chased by Angry Kangaroo',
            'timestamp': 1446839961,
            'uploader': 'Sean Fay',
            'description': 'md5:b1601e2314c4d8eec23b6eafe086a757',
            'uploader_id': 6466954,
            'upload_date': '20151011',
        },
        'add_ie': ['Youtube'],
    }]

    def _real_extract(self, url):
        article_id = self._match_id(url)

        article_data = self._download_json('http://api.bleacherreport.com/api/v1/articles/%s' % article_id, article_id)['article']

        thumbnails = []
        primary_photo = article_data.get('primaryPhoto')
        if primary_photo:
            thumbnails = [{
                'url': primary_photo['url'],
                'width': primary_photo.get('width'),
                'height': primary_photo.get('height'),
            }]

        info = {
            '_type': 'url_transparent',
            'id': article_id,
            'title': article_data['title'],
            'uploader': article_data.get('author', {}).get('name'),
            'uploader_id': article_data.get('authorId'),
            'timestamp': parse_iso8601(article_data.get('createdAt')),
            'thumbnails': thumbnails,
            'comment_count': int_or_none(article_data.get('commentsCount')),
            'view_count': int_or_none(article_data.get('hitCount')),
        }

        video = article_data.get('video')
        if video:
            video_type = video['type']
            if video_type in ('cms.bleacherreport.com', 'vid.bleacherreport.com'):
                info['url'] = 'http://bleacherreport.com/video_embed?id=%s' % video['id']
            elif video_type == 'ooyala.com':
                info['url'] = 'ooyala:%s' % video['id']
            elif video_type == 'youtube.com':
                info['url'] = video['id']
            elif video_type == 'vine.co':
                info['url'] = 'https://vine.co/v/%s' % video['id']
            else:
                info['url'] = video_type + video['id']
            return info
        else:
            raise ExtractorError('no video in the article', expected=True)

class BleacherReportCMSIE(AMPIE):
    _VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36}|\d{5})'
    _TESTS = [{
        'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1&library=video-cms',
        'md5': '670b2d73f48549da032861130488c681',
        'info_dict': {
            'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
            'ext': 'mp4',
            'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
            'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
            'upload_date': '20150723',
            'timestamp': 1437679032,

        },
        'expected_warnings': [
            'Unable to download f4m manifest'
        ]
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        info = self._extract_feed_info('http://vid.bleacherreport.com/videos/%s.akamai' % video_id)
        info['id'] = video_id
        return info


code:
from ..utils import (
    mimetype2ext,
    parse_duration,
    parse_qs,
    str_or_none,
    traverse_obj,
)
from .common import InfoExtractor

class BloggerIE(InfoExtractor):
    IE_NAME = 'blogger.com'
    _VALID_URL = r'https?://(?:www\.)?blogger\.com/video\.g\?token=(?P<id>.+)'
    _EMBED_REGEX = [r'''<iframe[^>]+src=["'](?P<url>(?:https?:)?//(?:www\.)?blogger\.com/video\.g\?token=[^"']+)["']''']
    _TESTS = [{
        'url': 'https://www.blogger.com/video.g?token=AD6v5dzEe9hfcARr5Hlq1WTkYy6t-fXH3BBahVhGvVHe5szdEUBEloSEDSTA8-b111089KbfWuBvTN7fnbxMtymsHhXAXwVvyzHH4Qch2cfLQdGxKQrrEuFpC1amSl_9GuLWODjPgw',
        'md5': 'f1bc19b6ea1b0fd1d81e84ca9ec467ac',
        'info_dict': {
            'id': 'BLOGGER-video-3c740e3a49197e16-796',
            'title': 'BLOGGER-video-3c740e3a49197e16-796',
            'ext': 'mp4',
            'thumbnail': r're:^https?://.*',
            'duration': 76.068,
        }
    }]

    def _real_extract(self, url):
        token_id = self._match_id(url)
        webpage = self._download_webpage(url, token_id)
        data_json = self._search_regex(r'var\s+VIDEO_CONFIG\s*=\s*(\{.*)', webpage, 'JSON data')
        data = self._parse_json(data_json.encode('utf-8').decode('unicode_escape'), token_id)
        streams = data['streams']
        formats = [{
            'ext': mimetype2ext(traverse_obj(parse_qs(stream['play_url']), ('mime', 0))),
            'url': stream['play_url'],
            'format_id': str_or_none(stream.get('format_id')),
        } for stream in streams]

        return {
            'id': data.get('iframe_id', token_id),
            'title': data.get('iframe_id', token_id),
            'formats': formats,
            'thumbnail': data.get('thumbnail'),
            'duration': parse_duration(traverse_obj(parse_qs(streams[0]['play_url']), ('dur', 0))),
        }


code:
import re

from .common import InfoExtractor

class BloombergIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?bloomberg\.com/(?:[^/]+/)*(?P<id>[^/?#]+)'

    _TESTS = [{
        'url': 'https://www.bloomberg.com/news/videos/2021-09-14/apple-unveils-the-new-iphone-13-stock-doesn-t-move-much-video',
        'info_dict': {
            'id': 'V8cFcYMxTHaMcEiiYVr39A',
            'ext': 'flv',
            'title': 'Apple Unveils the New IPhone 13, Stock Doesn\'t Move Much',
        },
        'params': {
            'format': 'best[format_id^=hds]',
        },
    }, {
        # video ID in BPlayer(...)
        'url': 'http://www.bloomberg.com/features/2016-hello-world-new-zealand/',
        'info_dict': {
            'id': '938c7e72-3f25-4ddb-8b85-a9be731baa74',
            'ext': 'flv',
            'title': 'Meet the Real-Life Tech Wizards of Middle Earth',
            'description': 'Hello World, Episode 1: New Zealand’s freaky AI babies, robot exoskeletons, and a virtual you.',
        },
        'params': {
            'format': 'best[format_id^=hds]',
        },
    }, {
        # data-bmmrid=
        'url': 'https://www.bloomberg.com/politics/articles/2017-02-08/le-pen-aide-briefed-french-central-banker-on-plan-to-print-money',
        'only_matching': True,
    }, {
        'url': 'http://www.bloomberg.com/news/articles/2015-11-12/five-strange-things-that-have-been-happening-in-financial-markets',
        'only_matching': True,
    }, {
        'url': 'http://www.bloomberg.com/politics/videos/2015-11-25/karl-rove-on-jeb-bush-s-struggles-stopping-trump',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        name = self._match_id(url)
        webpage = self._download_webpage(url, name)
        video_id = self._search_regex(
            (r'["\']bmmrId["\']\s*:\s*(["\'])(?P<id>(?:(?!\1).)+)\1',
             r'videoId\s*:\s*(["\'])(?P<id>(?:(?!\1).)+)\1',
             r'data-bmmrid=(["\'])(?P<id>(?:(?!\1).)+)\1'),
            webpage, 'id', group='id', default=None)
        if not video_id:
            bplayer_data = self._parse_json(self._search_regex(
                r'BPlayer\(null,\s*({[^;]+})\);', webpage, 'id'), name)
            video_id = bplayer_data['id']
        title = re.sub(': Video$', '', self._og_search_title(webpage))

        embed_info = self._download_json(
            'http://www.bloomberg.com/multimedia/api/embed?id=%s' % video_id, video_id)
        formats = []
        for stream in embed_info['streams']:
            stream_url = stream.get('url')
            if not stream_url:
                continue
            if stream['muxing_format'] == 'TS':
                formats.extend(self._extract_m3u8_formats(
                    stream_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
            else:
                formats.extend(self._extract_f4m_formats(
                    stream_url, video_id, f4m_id='hds', fatal=False))
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'description': self._og_search_description(webpage),
            'thumbnail': self._og_search_thumbnail(webpage),
        }


code:
from .common import InfoExtractor
from ..compat import compat_parse_qs
from ..utils import ExtractorError

class BokeCCBaseIE(InfoExtractor):
    def _extract_bokecc_formats(self, webpage, video_id, format_id=None):
        player_params_str = self._html_search_regex(
            r'<(?:script|embed)[^>]+src=(?P<q>["\'])(?:https?:)?//p\.bokecc\.com/(?:player|flash/player\.swf)\?(?P<query>.+?)(?P=q)',
            webpage, 'player params', group='query')

        player_params = compat_parse_qs(player_params_str)

        info_xml = self._download_xml(
            'http://p.bokecc.com/servlet/playinfo?uid=%s&vid=%s&m=1' % (
                player_params['siteid'][0], player_params['vid'][0]), video_id)

        formats = [{
            'format_id': format_id,
            'url': quality.find('./copy').attrib['playurl'],
            'quality': int(quality.attrib['value']),
        } for quality in info_xml.findall('./video/quality')]

        self._sort_formats(formats)

        return formats

class BokeCCIE(BokeCCBaseIE):
    _IE_DESC = 'CC视频'
    _VALID_URL = r'https?://union\.bokecc\.com/playvideo\.bo\?(?P<query>.*)'

    _TESTS = [{
        'url': 'http://union.bokecc.com/playvideo.bo?vid=E0ABAE9D4F509B189C33DC5901307461&uid=FE644790DE9D154A',
        'info_dict': {
            'id': 'FE644790DE9D154A_E0ABAE9D4F509B189C33DC5901307461',
            'ext': 'flv',
            'title': 'BokeCC Video',
        },
    }]

    def _real_extract(self, url):
        qs = compat_parse_qs(self._match_valid_url(url).group('query'))
        if not qs.get('vid') or not qs.get('uid'):
            raise ExtractorError('Invalid URL', expected=True)

        video_id = '%s_%s' % (qs['uid'][0], qs['vid'][0])

        webpage = self._download_webpage(url, video_id)

        return {
            'id': video_id,
            'title': 'BokeCC Video',  # no title provided in the webpage
            'formats': self._extract_bokecc_formats(webpage, video_id),
        }


code:
from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    int_or_none,
    try_get,
    urlencode_postdata,
)

class BongaCamsIE(InfoExtractor):
    _VALID_URL = r'https?://(?P<host>(?:[^/]+\.)?bongacams\d*\.com)/(?P<id>[^/?&#]+)'
    _TESTS = [{
        'url': 'https://de.bongacams.com/azumi-8',
        'only_matching': True,
    }, {
        'url': 'https://cn.bongacams.com/azumi-8',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        host = mobj.group('host')
        channel_id = mobj.group('id')

        amf = self._download_json(
            'https://%s/tools/amf.php' % host, channel_id,
            data=urlencode_postdata((
                ('method', 'getRoomData'),
                ('args[]', channel_id),
                ('args[]', 'false'),
            )), headers={'X-Requested-With': 'XMLHttpRequest'})

        server_url = amf['localData']['videoServerUrl']

        uploader_id = try_get(
            amf, lambda x: x['performerData']['username'], compat_str) or channel_id
        uploader = try_get(
            amf, lambda x: x['performerData']['displayName'], compat_str)
        like_count = int_or_none(try_get(
            amf, lambda x: x['performerData']['loversCount']))

        formats = self._extract_m3u8_formats(
            '%s/hls/stream_%s/playlist.m3u8' % (server_url, uploader_id),
            channel_id, 'mp4', m3u8_id='hls', live=True)
        self._sort_formats(formats)

        return {
            'id': channel_id,
            'title': uploader or uploader_id,
            'uploader': uploader,
            'uploader_id': uploader_id,
            'like_count': like_count,
            'age_limit': 18,
            'is_live': True,
            'formats': formats,
        }


code:
import re

from .common import InfoExtractor

from ..utils import (
    extract_attributes,
)

class BostonGlobeIE(InfoExtractor):
    _VALID_URL = r'(?i)https?://(?:www\.)?bostonglobe\.com/.*/(?P<id>[^/]+)/\w+(?:\.html)?'
    _TESTS = [
        {
            'url': 'http://www.bostonglobe.com/metro/2017/02/11/tree-finally-succumbs-disease-leaving-hole-neighborhood/h1b4lviqzMTIn9sVy8F3gP/story.html',
            'md5': '0a62181079c85c2d2b618c9a738aedaf',
            'info_dict': {
                'title': 'A tree finally succumbs to disease, leaving a hole in a neighborhood',
                'id': '5320421710001',
                'ext': 'mp4',
                'description': 'It arrived as a sapling when the Back Bay was in its infancy, a spindly American elm tamped down into a square of dirt cut into the brick sidewalk of 1880s Marlborough Street, no higher than the first bay window of the new brownstone behind it.',
                'timestamp': 1486877593,
                'upload_date': '20170212',
                'uploader_id': '245991542',
            },
        },
        {
            # Embedded youtube video; we hand it off to the Generic extractor.
            'url': 'https://www.bostonglobe.com/lifestyle/names/2017/02/17/does-ben-affleck-play-matt-damon-favorite-version-batman/ruqkc9VxKBYmh5txn1XhSI/story.html',
            'md5': '582b40327089d5c0c949b3c54b13c24b',
            'info_dict': {
                'title': "Who Is Matt Damon's Favorite Batman?",
                'id': 'ZW1QCnlA6Qc',
                'ext': 'mp4',
                'upload_date': '20170217',
                'description': 'md5:3b3dccb9375867e0b4d527ed87d307cb',
                'uploader': 'The Late Late Show with James Corden',
                'uploader_id': 'TheLateLateShow',
            },
            'expected_warnings': ['404'],
        },
    ]

    def _real_extract(self, url):
        page_id = self._match_id(url)
        webpage = self._download_webpage(url, page_id)

        page_title = self._og_search_title(webpage, default=None)

        # <video data-brightcove-video-id="5320421710001" data-account="245991542" data-player="SJWAiyYWg" data-embed="default" class="video-js" controls itemscope itemtype="http://schema.org/VideoObject">
        entries = []
        for video in re.findall(r'(?i)(<video[^>]+>)', webpage):
            attrs = extract_attributes(video)

            video_id = attrs.get('data-brightcove-video-id')
            account_id = attrs.get('data-account')
            player_id = attrs.get('data-player')
            embed = attrs.get('data-embed')

            if video_id and account_id and player_id and embed:
                entries.append(
                    'http://players.brightcove.net/%s/%s_%s/index.html?videoId=%s'
                    % (account_id, player_id, embed, video_id))

        if len(entries) == 0:
            return self.url_result(url, 'Generic')
        elif len(entries) == 1:
            return self.url_result(entries[0], 'BrightcoveNew')
        else:
            return self.playlist_from_matches(entries, page_id, page_title, ie='BrightcoveNew')


code:
import json

from .common import InfoExtractor
from ..utils import (
    determine_ext,
    parse_iso8601,
    # try_get,
    update_url_query,
)

class BoxIE(InfoExtractor):
    _VALID_URL = r'https?://(?:[^.]+\.)?app\.box\.com/s/(?P<shared_name>[^/]+)/file/(?P<id>\d+)'
    _TEST = {
        'url': 'https://mlssoccer.app.box.com/s/0evd2o3e08l60lr4ygukepvnkord1o1x/file/510727257538',
        'md5': '1f81b2fd3960f38a40a3b8823e5fcd43',
        'info_dict': {
            'id': '510727257538',
            'ext': 'mp4',
            'title': 'Garber   St. Louis will be 28th MLS team  +scarving.mp4',
            'uploader': 'MLS Video',
            'timestamp': 1566320259,
            'upload_date': '20190820',
            'uploader_id': '235196876',
        }
    }

    def _real_extract(self, url):
        shared_name, file_id = self._match_valid_url(url).groups()
        webpage = self._download_webpage(url, file_id)
        request_token = self._parse_json(self._search_regex(
            r'Box\.config\s*=\s*({.+?});', webpage,
            'Box config'), file_id)['requestToken']
        access_token = self._download_json(
            'https://app.box.com/app-api/enduserapp/elements/tokens', file_id,
            'Downloading token JSON metadata',
            data=json.dumps({'fileIDs': [file_id]}).encode(), headers={
                'Content-Type': 'application/json',
                'X-Request-Token': request_token,
                'X-Box-EndUser-API': 'sharedName=' + shared_name,
            })[file_id]['read']
        shared_link = 'https://app.box.com/s/' + shared_name
        f = self._download_json(
            'https://api.box.com/2.0/files/' + file_id, file_id,
            'Downloading file JSON metadata', headers={
                'Authorization': 'Bearer ' + access_token,
                'BoxApi': 'shared_link=' + shared_link,
                'X-Rep-Hints': '[dash]',  # TODO: extract `hls` formats
            }, query={
                'fields': 'authenticated_download_url,created_at,created_by,description,extension,is_download_available,name,representations,size'
            })
        title = f['name']

        query = {
            'access_token': access_token,
            'shared_link': shared_link
        }

        formats = []

        # for entry in (try_get(f, lambda x: x['representations']['entries'], list) or []):
        #     entry_url_template = try_get(
        #         entry, lambda x: x['content']['url_template'])
        #     if not entry_url_template:
        #         continue
        #     representation = entry.get('representation')
        #     if representation == 'dash':
        #         TODO: append query to every fragment URL
        #         formats.extend(self._extract_mpd_formats(
        #             entry_url_template.replace('{+asset_path}', 'manifest.mpd'),
        #             file_id, query=query))

        authenticated_download_url = f.get('authenticated_download_url')
        if authenticated_download_url and f.get('is_download_available'):
            formats.append({
                'ext': f.get('extension') or determine_ext(title),
                'filesize': f.get('size'),
                'format_id': 'download',
                'url': update_url_query(authenticated_download_url, query),
            })

        self._sort_formats(formats)

        creator = f.get('created_by') or {}

        return {
            'id': file_id,
            'title': title,
            'formats': formats,
            'description': f.get('description') or None,
            'uploader': creator.get('name'),
            'timestamp': parse_iso8601(f.get('created_at')),
            'uploader_id': creator.get('id'),
        }


code:
import re

from .common import InfoExtractor
from ..utils import (
    js_to_json,
    determine_ext,
)

class BpbIE(InfoExtractor):
    IE_DESC = 'Bundeszentrale für politische Bildung'
    _VALID_URL = r'https?://(?:www\.)?bpb\.de/mediathek/(?P<id>[0-9]+)/'

    _TEST = {
        'url': 'http://www.bpb.de/mediathek/297/joachim-gauck-zu-1989-und-die-erinnerung-an-die-ddr',
        'md5': 'c4f84c8a8044ca9ff68bb8441d300b3f',
        'info_dict': {
            'id': '297',
            'ext': 'mp4',
            'title': 'Joachim Gauck zu 1989 und die Erinnerung an die DDR',
            'description': 'Joachim Gauck, erster Beauftragter für die Stasi-Unterlagen, spricht auf dem Geschichtsforum über die friedliche Revolution 1989 und eine "gewisse Traurigkeit" im Umgang mit der DDR-Vergangenheit.'
        }
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(url, video_id)

        title = self._html_search_regex(
            r'
(.*?)
', webpage, 'title')
        video_info_dicts = re.findall(
            r"({\s*src\s*:\s*'https?://film\.bpb\.de/[^}]+})", webpage)

        formats = []
        for video_info in video_info_dicts:
            video_info = self._parse_json(
                video_info, video_id, transform_source=js_to_json, fatal=False)
            if not video_info:
                continue
            video_url = video_info.get('src')
            if not video_url:
                continue
            quality = 'high' if '_high' in video_url else 'low'
            formats.append({
                'url': video_url,
                'quality': 10 if quality == 'high' else 0,
                'format_note': quality,
                'format_id': '%s-%s' % (quality, determine_ext(video_url)),
            })

        self._sort_formats(formats)

        return {
            'id': video_id,
            'formats': formats,
            'title': title,
            'description': self._og_search_description(webpage),
        }


code:
import json

from .common import InfoExtractor
from ..utils import (
    determine_ext,
    ExtractorError,
    int_or_none,
    parse_duration,
    parse_iso8601,
    xpath_element,
    xpath_text,
)

class BRIE(InfoExtractor):
    IE_DESC = 'Bayerischer Rundfunk'
    _VALID_URL = r'(?P<base_url>https?://(?:www\.)?br(?:-klassik)?\.de)/(?:[a-z0-9\-_]+/)+(?P<id>[a-z0-9\-_]+)\.html'

    _TESTS = [
        {
            'url': 'http://www.br.de/mediathek/video/sendungen/abendschau/betriebliche-altersvorsorge-104.html',
            'md5': '83a0477cf0b8451027eb566d88b51106',
            'info_dict': {
                'id': '48f656ef-287e-486f-be86-459122db22cc',
                'ext': 'mp4',
                'title': 'Die böse Überraschung',
                'description': 'md5:ce9ac81b466ce775b8018f6801b48ac9',
                'duration': 180,
                'uploader': 'Reinhard Weber',
                'upload_date': '20150422',
            },
            'skip': '404 not found',
        },
        {
            'url': 'http://www.br.de/nachrichten/oberbayern/inhalt/muenchner-polizeipraesident-schreiber-gestorben-100.html',
            'md5': 'af3a3a4aa43ff0ce6a89504c67f427ef',
            'info_dict': {
                'id': 'a4b83e34-123d-4b81-9f4e-c0d3121a4e05',
                'ext': 'flv',
                'title': 'Manfred Schreiber ist tot',
                'description': 'md5:b454d867f2a9fc524ebe88c3f5092d97',
                'duration': 26,
            },
            'skip': '404 not found',
        },
        {
            'url': 'https://www.br-klassik.de/audio/peeping-tom-premierenkritik-dance-festival-muenchen-100.html',
            'md5': '8b5b27c0b090f3b35eac4ab3f7a73d3d',
            'info_dict': {
                'id': '74c603c9-26d3-48bb-b85b-079aeed66e0b',
                'ext': 'aac',
                'title': 'Kurzweilig und sehr bewegend',
                'description': 'md5:0351996e3283d64adeb38ede91fac54e',
                'duration': 296,
            },
            'skip': '404 not found',
        },
        {
            'url': 'http://www.br.de/radio/bayern1/service/team/videos/team-video-erdelt100.html',
            'md5': 'dbab0aef2e047060ea7a21fc1ce1078a',
            'info_dict': {
                'id': '6ba73750-d405-45d3-861d-1ce8c524e059',
                'ext': 'mp4',
                'title': 'Umweltbewusster Häuslebauer',
                'description': 'md5:d52dae9792d00226348c1dbb13c9bae2',
                'duration': 116,
            }
        },
        {
            'url': 'http://www.br.de/fernsehen/br-alpha/sendungen/kant-fuer-anfaenger/kritik-der-reinen-vernunft/kant-kritik-01-metaphysik100.html',
            'md5': '23bca295f1650d698f94fc570977dae3',
            'info_dict': {
                'id': 'd982c9ce-8648-4753-b358-98abb8aec43d',
                'ext': 'mp4',
                'title': 'Folge 1 - Metaphysik',
                'description': 'md5:bb659990e9e59905c3d41e369db1fbe3',
                'duration': 893,
                'uploader': 'Eva Maria Steimle',
                'upload_date': '20170208',
            }
        },
    ]

    def _real_extract(self, url):
        base_url, display_id = self._match_valid_url(url).groups()
        page = self._download_webpage(url, display_id)
        xml_url = self._search_regex(
            r"return BRavFramework\.register\(BRavFramework\('avPlayer_(?:[a-f0-9-]{36})'\)\.setup\({dataURL:'(/(?:[a-z0-9\-]+/)+[a-z0-9/~_.-]+)'}\)\);", page, 'XMLURL')
        xml = self._download_xml(base_url + xml_url, display_id)

        medias = []

        for xml_media in xml.findall('video') + xml.findall('audio'):
            media_id = xml_media.get('externalId')
            media = {
                'id': media_id,
                'title': xpath_text(xml_media, 'title', 'title', True),
                'duration': parse_duration(xpath_text(xml_media, 'duration')),
                'formats': self._extract_formats(xpath_element(
                    xml_media, 'assets'), media_id),
                'thumbnails': self._extract_thumbnails(xpath_element(
                    xml_media, 'teaserImage/variants'), base_url),
                'description': xpath_text(xml_media, 'desc'),
                'webpage_url': xpath_text(xml_media, 'permalink'),
                'uploader': xpath_text(xml_media, 'author'),
            }
            broadcast_date = xpath_text(xml_media, 'broadcastDate')
            if broadcast_date:
                media['upload_date'] = ''.join(reversed(broadcast_date.split('.')))
            medias.append(media)

        if len(medias) > 1:
            self.report_warning(
                'found multiple medias; please '
                'report this with the video URL to http://yt-dl.org/bug')
        if not medias:
            raise ExtractorError('No media entries found')
        return medias[0]

    def _extract_formats(self, assets, media_id):
        formats = []
        for asset in assets.findall('asset'):
            format_url = xpath_text(asset, ['downloadUrl', 'url'])
            asset_type = asset.get('type')
            if asset_type.startswith('HDS'):
                formats.extend(self._extract_f4m_formats(
                    format_url + '?hdcore=3.2.0', media_id, f4m_id='hds', fatal=False))
            elif asset_type.startswith('HLS'):
                formats.extend(self._extract_m3u8_formats(
                    format_url, media_id, 'mp4', 'm3u8_native', m3u8_id='hds', fatal=False))
            else:
                format_info = {
                    'ext': xpath_text(asset, 'mediaType'),
                    'width': int_or_none(xpath_text(asset, 'frameWidth')),
                    'height': int_or_none(xpath_text(asset, 'frameHeight')),
                    'tbr': int_or_none(xpath_text(asset, 'bitrateVideo')),
                    'abr': int_or_none(xpath_text(asset, 'bitrateAudio')),
                    'vcodec': xpath_text(asset, 'codecVideo'),
                    'acodec': xpath_text(asset, 'codecAudio'),
                    'container': xpath_text(asset, 'mediaType'),
                    'filesize': int_or_none(xpath_text(asset, 'size')),
                }
                format_url = self._proto_relative_url(format_url)
                if format_url:
                    http_format_info = format_info.copy()
                    http_format_info.update({
                        'url': format_url,
                        'format_id': 'http-%s' % asset_type,
                    })
                    formats.append(http_format_info)
                server_prefix = xpath_text(asset, 'serverPrefix')
                if server_prefix:
                    rtmp_format_info = format_info.copy()
                    rtmp_format_info.update({
                        'url': server_prefix,
                        'play_path': xpath_text(asset, 'fileName'),
                        'format_id': 'rtmp-%s' % asset_type,
                    })
                    formats.append(rtmp_format_info)
        self._sort_formats(formats)
        return formats

    def _extract_thumbnails(self, variants, base_url):
        thumbnails = [{
            'url': base_url + xpath_text(variant, 'url'),
            'width': int_or_none(xpath_text(variant, 'width')),
            'height': int_or_none(xpath_text(variant, 'height')),
        } for variant in variants.findall('variant') if xpath_text(variant, 'url')]
        thumbnails.sort(key=lambda x: x['width'] * x['height'], reverse=True)
        return thumbnails

class BRMediathekIE(InfoExtractor):
    IE_DESC = 'Bayerischer Rundfunk Mediathek'
    _VALID_URL = r'https?://(?:www\.)?br\.de/mediathek//?video/(?:[^/?&#]+?-)?(?P<id>av:[0-9a-f]{24})'

    _TESTS = [{
        'url': 'https://www.br.de/mediathek/video/gesundheit-die-sendung-vom-28112017-av:5a1e6a6e8fce6d001871cc8e',
        'md5': 'fdc3d485835966d1622587d08ba632ec',
        'info_dict': {
            'id': 'av:5a1e6a6e8fce6d001871cc8e',
            'ext': 'mp4',
            'title': 'Die Sendung vom 28.11.2017',
            'description': 'md5:6000cdca5912ab2277e5b7339f201ccc',
            'timestamp': 1511942766,
            'upload_date': '20171129',
        }
    }, {
        'url': 'https://www.br.de/mediathek//video/av:61b0db581aed360007558c12',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        clip_id = self._match_id(url)

        clip = self._download_json(
            'https://proxy-base.master.mango.express/graphql',
            clip_id, data=json.dumps({
                "query": """{
  viewer {
    clip(id: "%s") {
      title
      description
      duration
      createdAt
      ageRestriction
      videoFiles {
        edges {
          node {
            publicLocation
            fileSize
            videoProfile {
              width
              height
              bitrate
              encoding
            }
          }
        }
      }
      captionFiles {
        edges {
          node {
            publicLocation
          }
        }
      }
      teaserImages {
        edges {
          node {
            imageFiles {
              edges {
                node {
                  publicLocation
                  width
                  height
                }
              }
            }
          }
        }
      }
    }
  }
}""" % clip_id}).encode(), headers={
                'Content-Type': 'application/json',
            })['data']['viewer']['clip']
        title = clip['title']

        formats = []
        for edge in clip.get('videoFiles', {}).get('edges', []):
            node = edge.get('node', {})
            n_url = node.get('publicLocation')
            if not n_url:
                continue
            ext = determine_ext(n_url)
            if ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    n_url, clip_id, 'mp4', 'm3u8_native',
                    m3u8_id='hls', fatal=False))
            else:
                video_profile = node.get('videoProfile', {})
                tbr = int_or_none(video_profile.get('bitrate'))
                format_id = 'http'
                if tbr:
                    format_id += '-%d' % tbr
                formats.append({
                    'format_id': format_id,
                    'url': n_url,
                    'width': int_or_none(video_profile.get('width')),
                    'height': int_or_none(video_profile.get('height')),
                    'tbr': tbr,
                    'filesize': int_or_none(node.get('fileSize')),
                })
        self._sort_formats(formats)

        subtitles = {}
        for edge in clip.get('captionFiles', {}).get('edges', []):
            node = edge.get('node', {})
            n_url = node.get('publicLocation')
            if not n_url:
                continue
            subtitles.setdefault('de', []).append({
                'url': n_url,
            })

        thumbnails = []
        for edge in clip.get('teaserImages', {}).get('edges', []):
            for image_edge in edge.get('node', {}).get('imageFiles', {}).get('edges', []):
                node = image_edge.get('node', {})
                n_url = node.get('publicLocation')
                if not n_url:
                    continue
                thumbnails.append({
                    'url': n_url,
                    'width': int_or_none(node.get('width')),
                    'height': int_or_none(node.get('height')),
                })

        return {
            'id': clip_id,
            'title': title,
            'description': clip.get('description'),
            'duration': int_or_none(clip.get('duration')),
            'timestamp': parse_iso8601(clip.get('createdAt')),
            'age_limit': int_or_none(clip.get('ageRestriction')),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnails': thumbnails,
        }