code:
import re
import json
import uuid
import random
import urllib.parse

from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    ExtractorError,
    int_or_none,
    strip_or_none,
    try_get,
    smuggle_url,
    unsmuggle_url,
    url_or_none,
)

class ViuBaseIE(InfoExtractor):
    def _call_api(self, path, *args, headers={}, **kwargs):
        response = self._download_json(
            f'https://www.viu.com/api/{path}', *args, **kwargs,
            headers={**self.geo_verification_headers(), **headers})['response']
        if response.get('status') != 'success':
            raise ExtractorError(f'{self.IE_NAME} said: {response["message"]}', expected=True)
        return response

class ViuIE(ViuBaseIE):
    _VALID_URL = r'(?:viu:|https?://[^/]+\.viu\.com/[a-z]{2}/media/)(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://www.viu.com/en/media/1116705532?containerId=playlist-22168059',
        'info_dict': {
            'id': '1116705532',
            'ext': 'mp4',
            'title': 'Citizen Khan - Ep 1',
            'description': 'md5:d7ea1604f49e5ba79c212c551ce2110e',
        },
        'params': {
            'skip_download': 'm3u8 download',
        },
        'skip': 'Geo-restricted to India',
    }, {
        'url': 'https://www.viu.com/en/media/1130599965',
        'info_dict': {
            'id': '1130599965',
            'ext': 'mp4',
            'title': 'Jealousy Incarnate - Episode 1',
            'description': 'md5:d3d82375cab969415d2720b6894361e9',
        },
        'params': {
            'skip_download': 'm3u8 download',
        },
        'skip': 'Geo-restricted to Indonesia',
    }, {
        'url': 'https://india.viu.com/en/media/1126286865',
        'only_matching': True,
    }]

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

        video_data = self._call_api(
            'clip/load', video_id, 'Downloading video data', query={
                'appid': 'viu_desktop',
                'fmt': 'json',
                'id': video_id
            })['item'][0]

        title = video_data['title']

        m3u8_url = None
        url_path = video_data.get('urlpathd') or video_data.get('urlpath')
        tdirforwhole = video_data.get('tdirforwhole')
        # #EXT-X-BYTERANGE is not supported by native hls downloader
        # and ffmpeg (#10955)
        # FIXME: It is supported in yt-dlp
        # hls_file = video_data.get('hlsfile')
        hls_file = video_data.get('jwhlsfile')
        if url_path and tdirforwhole and hls_file:
            m3u8_url = '%s/%s/%s' % (url_path, tdirforwhole, hls_file)
        else:
            # m3u8_url = re.sub(
            #     r'(/hlsc_)[a-z]+(\d+\.m3u8)',
            #     r'\1whe\2', video_data['href'])
            m3u8_url = video_data['href']
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4')
        self._sort_formats(formats)

        for key, value in video_data.items():
            mobj = re.match(r'^subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key)
            if not mobj:
                continue
            subtitles.setdefault(mobj.group('lang'), []).append({
                'url': value,
                'ext': mobj.group('ext')
            })

        return {
            'id': video_id,
            'title': title,
            'description': video_data.get('description'),
            'series': video_data.get('moviealbumshowname'),
            'episode': title,
            'episode_number': int_or_none(video_data.get('episodeno')),
            'duration': int_or_none(video_data.get('duration')),
            'formats': formats,
            'subtitles': subtitles,
        }

class ViuPlaylistIE(ViuBaseIE):
    IE_NAME = 'viu:playlist'
    _VALID_URL = r'https?://www\.viu\.com/[^/]+/listing/playlist-(?P<id>\d+)'
    _TEST = {
        'url': 'https://www.viu.com/en/listing/playlist-22461380',
        'info_dict': {
            'id': '22461380',
            'title': 'The Good Wife',
        },
        'playlist_count': 16,
        'skip': 'Geo-restricted to Indonesia',
    }

    def _real_extract(self, url):
        playlist_id = self._match_id(url)
        playlist_data = self._call_api(
            'container/load', playlist_id,
            'Downloading playlist info', query={
                'appid': 'viu_desktop',
                'fmt': 'json',
                'id': 'playlist-' + playlist_id
            })['container']

        entries = []
        for item in playlist_data.get('item', []):
            item_id = item.get('id')
            if not item_id:
                continue
            item_id = compat_str(item_id)
            entries.append(self.url_result(
                'viu:' + item_id, 'Viu', item_id))

        return self.playlist_result(
            entries, playlist_id, playlist_data.get('title'))

class ViuOTTIE(InfoExtractor):
    IE_NAME = 'viu:ott'
    _NETRC_MACHINE = 'viu'
    _VALID_URL = r'https?://(?:www\.)?viu\.com/ott/(?P<country_code>[a-z]{2})/(?P<lang_code>[a-z]{2}-[a-z]{2})/vod/(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://www.viu.com/ott/sg/en-us/vod/3421/The%20Prime%20Minister%20and%20I',
        'info_dict': {
            'id': '3421',
            'ext': 'mp4',
            'title': 'A New Beginning',
            'description': 'md5:1e7486a619b6399b25ba6a41c0fe5b2c',
        },
        'params': {
            'skip_download': 'm3u8 download',
            'noplaylist': True,
        },
        'skip': 'Geo-restricted to Singapore',
    }, {
        'url': 'http://www.viu.com/ott/hk/zh-hk/vod/7123/%E5%A4%A7%E4%BA%BA%E5%A5%B3%E5%AD%90',
        'info_dict': {
            'id': '7123',
            'ext': 'mp4',
            'title': '這就是我的生活之道',
            'description': 'md5:4eb0d8b08cf04fcdc6bbbeb16043434f',
        },
        'params': {
            'skip_download': 'm3u8 download',
            'noplaylist': True,
        },
        'skip': 'Geo-restricted to Hong Kong',
    }, {
        'url': 'https://www.viu.com/ott/hk/zh-hk/vod/68776/%E6%99%82%E5%B0%9A%E5%AA%BD%E5%92%AA',
        'playlist_count': 12,
        'info_dict': {
            'id': '3916',
            'title': '時尚媽咪',
        },
        'params': {
            'skip_download': 'm3u8 download',
            'noplaylist': False,
        },
        'skip': 'Geo-restricted to Hong Kong',
    }]

    _AREA_ID = {
        'HK': 1,
        'SG': 2,
        'TH': 4,
        'PH': 5,
    }
    _LANGUAGE_FLAG = {
        'zh-hk': 1,
        'zh-cn': 2,
        'en-us': 3,
    }

    _user_token = None
    _auth_codes = {}

    def _detect_error(self, response):
        code = try_get(response, lambda x: x['status']['code'])
        if code and code > 0:
            message = try_get(response, lambda x: x['status']['message'])
            raise ExtractorError(f'{self.IE_NAME} said: {message} ({code})', expected=True)
        return response.get('data') or {}

    def _login(self, country_code, video_id):
        if self._user_token is None:
            username, password = self._get_login_info()
            if username is None:
                return
            headers = {
                'Authorization': f'Bearer {self._auth_codes[country_code]}',
                'Content-Type': 'application/json'
            }
            data = self._download_json(
                'https://api-gateway-global.viu.com/api/account/validate',
                video_id, 'Validating email address', headers=headers,
                data=json.dumps({
                    'principal': username,
                    'provider': 'email'
                }).encode())
            if not data.get('exists'):
                raise ExtractorError('Invalid email address')

            data = self._download_json(
                'https://api-gateway-global.viu.com/api/auth/login',
                video_id, 'Logging in', headers=headers,
                data=json.dumps({
                    'email': username,
                    'password': password,
                    'provider': 'email',
                }).encode())
            self._detect_error(data)
            self._user_token = data.get('identity')
            # need to update with valid user's token else will throw an error again
            self._auth_codes[country_code] = data.get('token')
        return self._user_token

    def _get_token(self, country_code, video_id):
        rand = ''.join(random.choice('0123456789') for _ in range(10))
        return self._download_json(
            f'https://api-gateway-global.viu.com/api/auth/token?v={rand}000', video_id,
            headers={'Content-Type': 'application/json'}, note='Getting bearer token',
            data=json.dumps({
                'countryCode': country_code.upper(),
                'platform': 'browser',
                'platformFlagLabel': 'web',
                'language': 'en',
                'uuid': str(uuid.uuid4()),
                'carrierId': '0'
            }).encode('utf-8'))['token']

    def _real_extract(self, url):
        url, idata = unsmuggle_url(url, {})
        country_code, lang_code, video_id = self._match_valid_url(url).groups()

        query = {
            'r': 'vod/ajax-detail',
            'platform_flag_label': 'web',
            'product_id': video_id,
        }

        area_id = self._AREA_ID.get(country_code.upper())
        if area_id:
            query['area_id'] = area_id

        product_data = self._download_json(
            f'http://www.viu.com/ott/{country_code}/index.php', video_id,
            'Downloading video info', query=query)['data']

        video_data = product_data.get('current_product')
        if not video_data:
            self.raise_geo_restricted()

        series_id = video_data.get('series_id')
        if self._yes_playlist(series_id, video_id, idata):
            series = product_data.get('series') or {}
            product = series.get('product')
            if product:
                entries = []
                for entry in sorted(product, key=lambda x: int_or_none(x.get('number', 0))):
                    item_id = entry.get('product_id')
                    if not item_id:
                        continue
                    entries.append(self.url_result(
                        smuggle_url(f'http://www.viu.com/ott/{country_code}/{lang_code}/vod/{item_id}/',
                                    {'force_noplaylist': True}),
                        ViuOTTIE, str(item_id), entry.get('synopsis', '').strip()))

                return self.playlist_result(entries, series_id, series.get('name'), series.get('description'))

        duration_limit = False
        query = {
            'ccs_product_id': video_data['ccs_product_id'],
            'language_flag_id': self._LANGUAGE_FLAG.get(lang_code.lower()) or '3',
        }

        def download_playback():
            stream_data = self._download_json(
                'https://api-gateway-global.viu.com/api/playback/distribute',
                video_id=video_id, query=query, fatal=False, note='Downloading stream info',
                headers={
                    'Authorization': f'Bearer {self._auth_codes[country_code]}',
                    'Referer': url,
                    'Origin': url
                })
            return self._detect_error(stream_data).get('stream')

        if not self._auth_codes.get(country_code):
            self._auth_codes[country_code] = self._get_token(country_code, video_id)

        stream_data = None
        try:
            stream_data = download_playback()
        except (ExtractorError, KeyError):
            token = self._login(country_code, video_id)
            if token is not None:
                query['identity'] = token
            else:
                # The content is Preview or for VIP only.
                # We can try to bypass the duration which is limited to 3mins only
                duration_limit, query['duration'] = True, '180'
            try:
                stream_data = download_playback()
            except (ExtractorError, KeyError):
                if token is not None:
                    raise
                self.raise_login_required(method='password')
        if not stream_data:
            raise ExtractorError('Cannot get stream info', expected=True)

        formats = []
        for vid_format, stream_url in (stream_data.get('url') or {}).items():
            height = int(self._search_regex(r's(\d+)p', vid_format, 'height', default=None))

            # bypass preview duration limit
            if duration_limit:
                old_stream_url = urllib.parse.urlparse(stream_url)
                query = dict(urllib.parse.parse_qsl(old_stream_url.query, keep_blank_values=True))
                query.update({
                    'duration': video_data.get('time_duration') or '9999999',
                    'duration_start': '0',
                })
                stream_url = old_stream_url._replace(query=urllib.parse.urlencode(query)).geturl()

            formats.append({
                'format_id': vid_format,
                'url': stream_url,
                'height': height,
                'ext': 'mp4',
                'filesize': try_get(stream_data, lambda x: x['size'][vid_format], int)
            })
        self._sort_formats(formats)

        subtitles = {}
        for sub in video_data.get('subtitle') or []:
            sub_url = sub.get('url')
            if not sub_url:
                continue
            subtitles.setdefault(sub.get('name'), []).append({
                'url': sub_url,
                'ext': 'srt',
            })

        title = strip_or_none(video_data.get('synopsis'))
        return {
            'id': video_id,
            'title': title,
            'description': video_data.get('description'),
            'series': try_get(product_data, lambda x: x['series']['name']),
            'episode': title,
            'episode_number': int_or_none(video_data.get('number')),
            'duration': int_or_none(stream_data.get('duration')),
            'thumbnail': url_or_none(video_data.get('cover_image_url')),
            'formats': formats,
            'subtitles': subtitles,
        }


code:
import collections
import hashlib
import re

from .common import InfoExtractor
from .dailymotion import DailymotionIE
from .odnoklassniki import OdnoklassnikiIE
from .pladform import PladformIE
from .vimeo import VimeoIE
from .youtube import YoutubeIE
from ..compat import compat_urlparse
from ..utils import (
    ExtractorError,
    clean_html,
    get_element_by_class,
    int_or_none,
    orderedSet,
    str_or_none,
    str_to_int,
    unescapeHTML,
    unified_timestamp,
    update_url_query,
    url_or_none,
    urlencode_postdata,
)

class VKBaseIE(InfoExtractor):
    _NETRC_MACHINE = 'vk'

    def _download_webpage_handle(self, url_or_request, video_id, *args, fatal=True, **kwargs):
        response = super()._download_webpage_handle(url_or_request, video_id, *args, fatal=fatal, **kwargs)
        challenge_url, cookie = response[1].geturl() if response else '', None
        if challenge_url.startswith('https://vk.com/429.html?'):
            cookie = self._get_cookies(challenge_url).get('hash429')
        if not cookie:
            return response

        hash429 = hashlib.md5(cookie.value.encode('ascii')).hexdigest()
        self._request_webpage(
            update_url_query(challenge_url, {'key': hash429}), video_id, fatal=fatal,
            note='Resolving WAF challenge', errnote='Failed to bypass WAF challenge')
        return super()._download_webpage_handle(url_or_request, video_id, *args, fatal=True, **kwargs)

    def _perform_login(self, username, password):
        login_page, url_handle = self._download_webpage_handle(
            'https://vk.com', None, 'Downloading login page')

        login_form = self._hidden_inputs(login_page)

        login_form.update({
            'email': username.encode('cp1251'),
            'pass': password.encode('cp1251'),
        })

        # vk serves two same remixlhk cookies in Set-Cookie header and expects
        # first one to be actually set
        self._apply_first_set_cookie_header(url_handle, 'remixlhk')

        login_page = self._download_webpage(
            'https://vk.com/login', None,
            note='Logging in',
            data=urlencode_postdata(login_form))

        if re.search(r'onLoginFailed', login_page):
            raise ExtractorError(
                'Unable to login, incorrect username and/or password', expected=True)

    def _download_payload(self, path, video_id, data, fatal=True):
        endpoint = f'https://vk.com/{path}.php'
        data['al'] = 1
        code, payload = self._download_json(
            endpoint, video_id, data=urlencode_postdata(data), fatal=fatal,
            headers={
                'Referer': endpoint,
                'X-Requested-With': 'XMLHttpRequest',
            })['payload']
        if code == '3':
            self.raise_login_required()
        elif code == '8':
            raise ExtractorError(clean_html(payload[0][1:-1]), expected=True)
        return payload

class VKIE(VKBaseIE):
    IE_NAME = 'vk'
    IE_DESC = 'VK'
    _EMBED_REGEX = [r']+?src=(["\'])(?P<url>https?://vk\.com/video_ext\.php.+?)\1']
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            (?:
                                (?:(?:m|new)\.)?vk\.com/video_|
                                (?:www\.)?daxab.com/
                            )
                            ext\.php\?(?P<embed_query>.*?\boid=(?P<oid>-?\d+).*?\bid=(?P<id>\d+).*)|
                            (?:
                                (?:(?:m|new)\.)?vk\.com/(?:.+?\?.*?z=)?(?:video|clip)|
                                (?:www\.)?daxab.com/embed/
                            )
                            (?P<videoid>-?\d+_\d+)(?:.*\blist=(?P<list_id>([\da-f]+)|(ln-[\da-zA-Z]+)))?
                        )
                    '''
    # https://help.sibnet.ru/?sibnet_video_embed
    _EMBED_REGEX = [r']+\bsrc=(["\'])(?P<url>(?:https?:)?//video\.sibnet\.ru/shell\.php\?.*?\bvideoid=\d+.*?)\1']
    _TESTS = [
        {
            'url': 'http://vk.com/videos-77521?z=video-77521_162222515%2Fclub77521',
            'info_dict': {
                'id': '-77521_162222515',
                'ext': 'mp4',
                'title': 'ProtivoGunz - Хуёвая песня',
                'uploader': 're:(?:Noize MC|Alexander Ilyashenko).*',
                'uploader_id': '39545378',
                'duration': 195,
                'timestamp': 1329049880,
                'upload_date': '20120212',
                'comment_count': int,
                'like_count': int,
                'thumbnail': r're:https?://.+\.jpg$',
            },
            'params': {'skip_download': 'm3u8'},
        },
        {
            'url': 'http://vk.com/video205387401_165548505',
            'info_dict': {
                'id': '205387401_165548505',
                'ext': 'mp4',
                'title': 'No name',
                'uploader': 'Tom Cruise',
                'uploader_id': '205387401',
                'duration': 9,
                'timestamp': 1374364108,
                'upload_date': '20130720',
                'comment_count': int,
                'like_count': int,
                'thumbnail': r're:https?://.+\.jpg$',
            }
        },
        {
            'note': 'Embedded video',
            'url': 'https://vk.com/video_ext.php?oid=-77521&id=162222515&hash=87b046504ccd8bfa',
            'info_dict': {
                'id': '-77521_162222515',
                'ext': 'mp4',
                'uploader': 're:(?:Noize MC|Alexander Ilyashenko).*',
                'title': 'ProtivoGunz - Хуёвая песня',
                'duration': 195,
                'upload_date': '20120212',
                'timestamp': 1329049880,
                'uploader_id': '39545378',
                'thumbnail': r're:https?://.+\.jpg$',
            },
            'params': {'skip_download': 'm3u8'},
        },
        {
            # VIDEO NOW REMOVED
            # please update if you find a video whose URL follows the same pattern
            'url': 'http://vk.com/video-8871596_164049491',
            'md5': 'a590bcaf3d543576c9bd162812387666',
            'note': 'Only available for registered users',
            'info_dict': {
                'id': '-8871596_164049491',
                'ext': 'mp4',
                'uploader': 'Триллеры',
                'title': '► Бойцовский клуб / Fight Club 1999 [HD 720]',
                'duration': 8352,
                'upload_date': '20121218',
                'view_count': int,
            },
            'skip': 'Removed',
        },
        {
            'url': 'http://vk.com/hd_kino_mania?z=video-43215063_168067957%2F15c66b9b533119788d',
            'info_dict': {
                'id': '-43215063_168067957',
                'ext': 'mp4',
                'uploader': 'Bro Mazter',
                'title': ' ',
                'duration': 7291,
                'upload_date': '20140328',
                'uploader_id': '223413403',
                'timestamp': 1396018030,
            },
            'skip': 'Requires vk account credentials',
        },
        {
            'url': 'http://m.vk.com/video-43215063_169084319?list=125c627d1aa1cebb83&from=wall-43215063_2566540',
            'md5': '0c45586baa71b7cb1d0784ee3f4e00a6',
            'note': 'ivi.ru embed',
            'info_dict': {
                'id': '-43215063_169084319',
                'ext': 'mp4',
                'title': 'Книга Илая',
                'duration': 6771,
                'upload_date': '20140626',
                'view_count': int,
            },
            'skip': 'Removed',
        },
        {
            'url': 'https://vk.com/video-93049196_456239755?list=ln-cBjJ7S4jYYx3ADnmDT',
            'info_dict': {
                'id': '-93049196_456239755',
                'ext': 'mp4',
                'title': '8 серия (озвучка)',
                'duration': 8383,
                'comment_count': int,
                'uploader': 'Dizi2021',
                'like_count': int,
                'timestamp': 1640162189,
                'upload_date': '20211222',
                'uploader_id': '-93049196',
                'thumbnail': r're:https?://.+\.jpg$',
            },
        },
        {
            # video (removed?) only available with list id
            'url': 'https://vk.com/video30481095_171201961?list=8764ae2d21f14088d4',
            'md5': '091287af5402239a1051c37ec7b92913',
            'info_dict': {
                'id': '30481095_171201961',
                'ext': 'mp4',
                'title': 'ТюменцевВВ_09.07.2015',
                'uploader': 'Anton Ivanov',
                'duration': 109,
                'upload_date': '20150709',
                'view_count': int,
            },
            'skip': 'Removed',
        },
        {
            # youtube embed
            'url': 'https://vk.com/video276849682_170681728',
            'info_dict': {
                'id': 'V3K4mi0SYkc',
                'ext': 'mp4',
                'title': "DSWD Awards 'Children's Joy Foundation, Inc.' Certificate of Registration and License to Operate",
                'description': 'md5:bf9c26cfa4acdfb146362682edd3827a',
                'duration': 178,
                'upload_date': '20130117',
                'uploader': "Children's Joy Foundation Inc.",
                'uploader_id': 'thecjf',
                'view_count': int,
                'channel_id': 'UCgzCNQ11TmR9V97ECnhi3gw',
                'availability': 'public',
                'like_count': int,
                'live_status': 'not_live',
                'playable_in_embed': True,
                'channel': 'Children\'s Joy Foundation Inc.',
                'uploader_url': 'http://www.youtube.com/user/thecjf',
                'thumbnail': r're:https?://.+\.jpg$',
                'tags': 'count:27',
                'start_time': 0.0,
                'categories': ['Nonprofits & Activism'],
                'channel_url': 'https://www.youtube.com/channel/UCgzCNQ11TmR9V97ECnhi3gw',
                'age_limit': 0,
            },
        },
        {
            # dailymotion embed
            'url': 'https://vk.com/video-37468416_456239855',
            'info_dict': {
                'id': 'k3lz2cmXyRuJQSjGHUv',
                'ext': 'mp4',
                'title': 'md5:d52606645c20b0ddbb21655adaa4f56f',
                'description': 'md5:424b8e88cc873217f520e582ba28bb36',
                'uploader': 'AniLibria.Tv',
                'upload_date': '20160914',
                'uploader_id': 'x1p5vl5',
                'timestamp': 1473877246,
            },
            'skip': 'Removed'
        },
        {
            # video key is extra_data not url\d+
            'url': 'http://vk.com/video-110305615_171782105',
            'md5': 'e13fcda136f99764872e739d13fac1d1',
            'info_dict': {
                'id': '-110305615_171782105',
                'ext': 'mp4',
                'title': 'S-Dance, репетиции к The way show',
                'uploader': 'THE WAY SHOW | 17 апреля',
                'uploader_id': '-110305615',
                'timestamp': 1454859345,
                'upload_date': '20160207',
            },
            'skip': 'Removed',
        },
        {
            # finished live stream, postlive_mp4
            'url': 'https://vk.com/videos-387766?z=video-387766_456242764%2Fpl_-387766_-2',
            'info_dict': {
                'id': '-387766_456242764',
                'ext': 'mp4',
                'title': 'ИгроМир 2016 День 1 — Игромания Утром',
                'uploader': 'Игромания',
                'duration': 5239,
                'upload_date': '20160929',
                'uploader_id': '-387766',
                'timestamp': 1475137527,
                'thumbnail': r're:https?://.+\.jpg$',
                'comment_count': int,
                'like_count': int,
            },
            'params': {
                'skip_download': True,
            },
        },
        {
            # live stream, hls and rtmp links, most likely already finished live
            # stream by the time you are reading this comment
            'url': 'https://vk.com/video-140332_456239111',
            'only_matching': True,
        },
        {
            # removed video, just testing that we match the pattern
            'url': 'http://vk.com/feed?z=video-43215063_166094326%2Fbb50cacd3177146d7a',
            'only_matching': True,
        },
        {
            # age restricted video, requires vk account credentials
            'url': 'https://vk.com/video205387401_164765225',
            'only_matching': True,
        },
        {
            # pladform embed
            'url': 'https://vk.com/video-76116461_171554880',
            'only_matching': True,
        },
        {
            'url': 'http://new.vk.com/video205387401_165548505',
            'only_matching': True,
        },
        {
            # This video is no longer available, because its author has been blocked.
            'url': 'https://vk.com/video-10639516_456240611',
            'only_matching': True,
        },
        {
            # The video is not available in your region.
            'url': 'https://vk.com/video-51812607_171445436',
            'only_matching': True,
        },
        {
            'url': 'https://vk.com/clip30014565_456240946',
            'only_matching': True,
        }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        video_id = mobj.group('videoid')

        mv_data = {}
        if video_id:
            data = {
                'act': 'show',
                'video': video_id,
            }
            # Some videos (removed?) can only be downloaded with list id specified
            list_id = mobj.group('list_id')
            if list_id:
                data['list'] = list_id

            payload = self._download_payload('al_video', video_id, data)
            info_page = payload[1]
            opts = payload[-1]
            mv_data = opts.get('mvData') or {}
            player = opts.get('player') or {}
        else:
            video_id = '%s_%s' % (mobj.group('oid'), mobj.group('id'))

            info_page = self._download_webpage(
                'http://vk.com/video_ext.php?' + mobj.group('embed_query'), video_id)

            error_message = self._html_search_regex(
                [r'(?s)<!>
]+class="video_layer_message"[^>]*>(.+?)
',
                    r'(?s)
]+id="video_ext_msg"[^>]*>(.+?)
'],
                info_page, 'error message', default=None)
            if error_message:
                raise ExtractorError(error_message, expected=True)

            if re.search(r'<!>/login\.php\?.*\bact=security_check', info_page):
                raise ExtractorError(
                    'You are trying to log in from an unusual location. You should confirm ownership at vk.com to log in with this IP.',
                    expected=True)

            ERROR_COPYRIGHT = 'Video %s has been removed from public access due to rightholder complaint.'

            ERRORS = {
                r'>Видеозапись .*? была изъята из публичного доступа в связи с обращением правообладателя.<':
                ERROR_COPYRIGHT,

                r'>The video .*? was removed from public access by request of the copyright holder.<':
                ERROR_COPYRIGHT,

                r'<!>Please log in or <':
                'Video %s is only available for registered users, '
                'use --username and --password options to provide account credentials.',

                r'<!>Unknown error':
                'Video %s does not exist.',

                r'<!>Видео временно недоступно':
                'Video %s is temporarily unavailable.',

                r'<!>Access denied':
                'Access denied to video %s.',

                r'<!>Видеозапись недоступна, так как её автор был заблокирован.':
                'Video %s is no longer available, because its author has been blocked.',

                r'<!>This video is no longer available, because its author has been blocked.':
                'Video %s is no longer available, because its author has been blocked.',

                r'<!>This video is no longer available, because it has been deleted.':
                'Video %s is no longer available, because it has been deleted.',

                r'<!>The video .+? is not available in your region.':
                'Video %s is not available in your region.',
            }

            for error_re, error_msg in ERRORS.items():
                if re.search(error_re, info_page):
                    raise ExtractorError(error_msg % video_id, expected=True)

            player = self._parse_json(self._search_regex(
                r'var\s+playerParams\s*=\s*({.+?})\s*;\s*\n',
                info_page, 'player params'), video_id)

        youtube_url = YoutubeIE._extract_url(info_page)
        if youtube_url:
            return self.url_result(youtube_url, YoutubeIE.ie_key())

        vimeo_url = VimeoIE._extract_url(url, info_page)
        if vimeo_url is not None:
            return self.url_result(vimeo_url, VimeoIE.ie_key())

        pladform_url = PladformIE._extract_url(info_page)
        if pladform_url:
            return self.url_result(pladform_url, PladformIE.ie_key())

        m_rutube = re.search(
            r'\ssrc="((?:https?:)?//rutube\.ru\\?/(?:video|play)\\?/embed(?:.*?))\\?"', info_page)
        if m_rutube is not None:
            rutube_url = self._proto_relative_url(
                m_rutube.group(1).replace('\\', ''))
            return self.url_result(rutube_url)

        dailymotion_url = next(DailymotionIE._extract_embed_urls(url, info_page), None)
        if dailymotion_url:
            return self.url_result(dailymotion_url, DailymotionIE.ie_key())

        odnoklassniki_url = OdnoklassnikiIE._extract_url(info_page)
        if odnoklassniki_url:
            return self.url_result(odnoklassniki_url, OdnoklassnikiIE.ie_key())

        sibnet_url = next(self._extract_embed_urls(url, info_page), None)
        if sibnet_url:
            return self.url_result(sibnet_url)

        m_opts = re.search(r'(?s)var\s+opts\s*=\s*({.+?});', info_page)
        if m_opts:
            m_opts_url = re.search(r"url\s*:\s*'((?!/\b)[^']+)", m_opts.group(1))
            if m_opts_url:
                opts_url = m_opts_url.group(1)
                if opts_url.startswith('//'):
                    opts_url = 'http:' + opts_url
                return self.url_result(opts_url)

        data = player['params'][0]
        title = unescapeHTML(data['md_title'])

        # 2 = live
        # 3 = post live (finished live)
        is_live = data.get('live') == 2

        timestamp = unified_timestamp(self._html_search_regex(
            r'class=["\']mv_info_date[^>]+>([^<]+)(?:<|from)', info_page,
            'upload date', default=None)) or int_or_none(data.get('date'))

        view_count = str_to_int(self._search_regex(
            r'class=["\']mv_views_count[^>]+>\s*([\d,.]+)',
            info_page, 'view count', default=None))

        formats = []
        for format_id, format_url in data.items():
            format_url = url_or_none(format_url)
            if not format_url or not format_url.startswith(('http', '//', 'rtmp')):
                continue
            if (format_id.startswith(('url', 'cache'))
                    or format_id in ('extra_data', 'live_mp4', 'postlive_mp4')):
                height = int_or_none(self._search_regex(
                    r'^(?:url|cache)(\d+)', format_id, 'height', default=None))
                formats.append({
                    'format_id': format_id,
                    'url': format_url,
                    'height': height,
                })
            elif format_id == 'hls':
                formats.extend(self._extract_m3u8_formats(
                    format_url, video_id, 'mp4', 'm3u8_native',
                    m3u8_id=format_id, fatal=False, live=is_live))
            elif format_id == 'rtmp':
                formats.append({
                    'format_id': format_id,
                    'url': format_url,
                    'ext': 'flv',
                })
        self._sort_formats(formats)

        subtitles = {}
        for sub in data.get('subs') or {}:
            subtitles.setdefault(sub.get('lang', 'en'), []).append({
                'ext': sub.get('title', '.srt').split('.')[-1],
                'url': url_or_none(sub.get('url')),
            })

        return {
            'id': video_id,
            'formats': formats,
            'title': title,
            'thumbnail': data.get('jpg'),
            'uploader': data.get('md_author'),
            'uploader_id': str_or_none(data.get('author_id') or mv_data.get('authorId')),
            'duration': int_or_none(data.get('duration') or mv_data.get('duration')),
            'timestamp': timestamp,
            'view_count': view_count,
            'like_count': int_or_none(mv_data.get('likes')),
            'comment_count': int_or_none(mv_data.get('commcount')),
            'is_live': is_live,
            'subtitles': subtitles,
        }

class VKUserVideosIE(VKBaseIE):
    IE_NAME = 'vk:uservideos'
    IE_DESC = "VK - User's Videos"
    _VALID_URL = r'https?://(?:(?:m|new)\.)?vk\.com/video/@(?P<id>[^?$#/&]+)(?!\?.*\bz=video)(?:[/?#&](?:.*?\bsection=(?P
\w+))?|$)'
    _TEMPLATE_URL = 'https://vk.com/videos'
    _TESTS = [{
        'url': 'https://vk.com/video/@mobidevices',
        'info_dict': {
            'id': '-17892518_all',
        },
        'playlist_mincount': 1355,
    }, {
        'url': 'https://vk.com/video/@mobidevices?section=uploaded',
        'info_dict': {
            'id': '-17892518_uploaded',
        },
        'playlist_mincount': 182,
    }]
    _VIDEO = collections.namedtuple('Video', ['owner_id', 'id'])

    def _entries(self, page_id, section):
        video_list_json = self._download_payload('al_video', page_id, {
            'act': 'load_videos_silent',
            'offset': 0,
            'oid': page_id,
            'section': section,
        })[0][section]
        count = video_list_json['count']
        total = video_list_json['total']
        video_list = video_list_json['list']

        while True:
            for video in video_list:
                v = self._VIDEO._make(video[:2])
                video_id = '%d_%d' % (v.owner_id, v.id)
                yield self.url_result(
                    'http://vk.com/video' + video_id, VKIE.ie_key(), video_id)
            if count >= total:
                break
            video_list_json = self._download_payload('al_video', page_id, {
                'act': 'load_videos_silent',
                'offset': count,
                'oid': page_id,
                'section': section,
            })[0][section]
            count += video_list_json['count']
            video_list = video_list_json['list']

    def _real_extract(self, url):
        u_id, section = self._match_valid_url(url).groups()
        webpage = self._download_webpage(url, u_id)
        page_id = self._search_regex(r'data-owner-id\s?=\s?"([^"]+)"', webpage, 'page_id')
        if not section:
            section = 'all'

        return self.playlist_result(self._entries(page_id, section), '%s_%s' % (page_id, section))

class VKWallPostIE(VKBaseIE):
    IE_NAME = 'vk:wallpost'
    _VALID_URL = r'https?://(?:(?:(?:(?:m|new)\.)?vk\.com/(?:[^?]+\?.*\bw=)?wall(?P<id>-?\d+_\d+)))'
    _TESTS = [{
        # public page URL, audio playlist
        'url': 'https://vk.com/bs.official?w=wall-23538238_35',
        'info_dict': {
            'id': '-23538238_35',
            'title': 'Black Shadow - Wall post -23538238_35',
            'description': 'md5:3f84b9c4f9ef499731cf1ced9998cc0c',
        },
        'playlist': [{
            'md5': '5ba93864ec5b85f7ce19a9af4af080f6',
            'info_dict': {
                'id': '135220665_111806521',
                'ext': 'mp4',
                'title': 'Black Shadow - Слепое Верование',
                'duration': 370,
                'uploader': 'Black Shadow',
                'artist': 'Black Shadow',
                'track': 'Слепое Верование',
            },
        }, {
            'md5': '4cc7e804579122b17ea95af7834c9233',
            'info_dict': {
                'id': '135220665_111802303',
                'ext': 'mp4',
                'title': 'Black Shadow - Война - Негасимое Бездны Пламя!',
                'duration': 423,
                'uploader': 'Black Shadow',
                'artist': 'Black Shadow',
                'track': 'Война - Негасимое Бездны Пламя!',
            },
        }],
        'params': {
            'skip_download': True,
        },
        'skip': 'Requires vk account credentials',
    }, {
        # single YouTube embed, no leading -
        'url': 'https://vk.com/wall85155021_6319',
        'info_dict': {
            'id': '85155021_6319',
            'title': 'Сергей Горбунов - Wall post 85155021_6319',
        },
        'playlist_count': 1,
        'skip': 'Requires vk account credentials',
    }, {
        # wall page URL
        'url': 'https://vk.com/wall-23538238_35',
        'only_matching': True,
    }, {
        # mobile wall page URL
        'url': 'https://m.vk.com/wall-23538238_35',
        'only_matching': True,
    }]
    _BASE64_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/='
    _AUDIO = collections.namedtuple('Audio', ['id', 'owner_id', 'url', 'title', 'performer', 'duration', 'album_id', 'unk', 'author_link', 'lyrics', 'flags', 'context', 'extra', 'hashes', 'cover_url', 'ads'])

    def _decode(self, enc):
        dec = ''
        e = n = 0
        for c in enc:
            r = self._BASE64_CHARS.index(c)
            cond = n % 4
            e = 64 * e + r if cond else r
            n += 1
            if cond:
                dec += chr(255 & e >> (-2 * n & 6))
        return dec

    def _unmask_url(self, mask_url, vk_id):
        if 'audio_api_unavailable' in mask_url:
            extra = mask_url.split('?extra=')[1].split('#')
            func, base = self._decode(extra[1]).split(chr(11))
            mask_url = list(self._decode(extra[0]))
            url_len = len(mask_url)
            indexes = [None] * url_len
            index = int(base) ^ vk_id
            for n in range(url_len - 1, -1, -1):
                index = (url_len * (n + 1) ^ index + n) % url_len
                indexes[n] = index
            for n in range(1, url_len):
                c = mask_url[n]
                index = indexes[url_len - 1 - n]
                mask_url[n] = mask_url[index]
                mask_url[index] = c
            mask_url = ''.join(mask_url)
        return mask_url

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

        webpage = self._download_payload('wkview', post_id, {
            'act': 'show',
            'w': 'wall' + post_id,
        })[1]

        description = clean_html(get_element_by_class('wall_post_text', webpage))
        uploader = clean_html(get_element_by_class('author', webpage))

        entries = []

        for audio in re.findall(r'data-audio="([^"]+)', webpage):
            audio = self._parse_json(unescapeHTML(audio), post_id)
            a = self._AUDIO._make(audio[:16])
            if not a.url:
                continue
            title = unescapeHTML(a.title)
            performer = unescapeHTML(a.performer)
            entries.append({
                'id': '%s_%s' % (a.owner_id, a.id),
                'url': self._unmask_url(a.url, a.ads['vk_id']),
                'title': '%s - %s' % (performer, title) if performer else title,
                'thumbnails': [{'url': c_url} for c_url in a.cover_url.split(',')] if a.cover_url else None,
                'duration': int_or_none(a.duration),
                'uploader': uploader,
                'artist': performer,
                'track': title,
                'ext': 'mp4',
                'protocol': 'm3u8_native',
            })

        for video in re.finditer(
                r']+href=(["\'])(?P<url>/video(?:-?[\d_]+).*?)\1', webpage):
            entries.append(self.url_result(
                compat_urlparse.urljoin(url, video.group('url')), VKIE.ie_key()))

        title = 'Wall post %s' % post_id

        return self.playlist_result(
            orderedSet(entries), post_id,
            '%s - %s' % (uploader, title) if uploader else title,
            description)

repo_name: Lesmiscore/ytdl


code:
import itertools
import json

from .naver import NaverBaseIE
from ..compat import (
    compat_HTTPError,
    compat_str,
)
from ..utils import (
    ExtractorError,
    int_or_none,
    LazyList,
    merge_dicts,
    str_or_none,
    strip_or_none,
    try_get,
    urlencode_postdata,
    url_or_none,
)

class VLiveBaseIE(NaverBaseIE):
    _NETRC_MACHINE = 'vlive'
    _logged_in = False

    def _perform_login(self, username, password):
        if self._logged_in:
            return
        LOGIN_URL = 'https://www.vlive.tv/auth/email/login'
        self._request_webpage(
            LOGIN_URL, None, note='Downloading login cookies')

        self._download_webpage(
            LOGIN_URL, None, note='Logging in',
            data=urlencode_postdata({'email': username, 'pwd': password}),
            headers={
                'Referer': LOGIN_URL,
                'Content-Type': 'application/x-www-form-urlencoded'
            })

        login_info = self._download_json(
            'https://www.vlive.tv/auth/loginInfo', None,
            note='Checking login status',
            headers={'Referer': 'https://www.vlive.tv/home'})

        if not try_get(login_info, lambda x: x['message']['login'], bool):
            raise ExtractorError('Unable to log in', expected=True)
        VLiveBaseIE._logged_in = True

    def _call_api(self, path_template, video_id, fields=None, query_add={}, note=None):
        if note is None:
            note = 'Downloading %s JSON metadata' % path_template.split('/')[-1].split('-')[0]
        query = {'appId': '8c6cc7b45d2568fb668be6e05b6e5a3b', 'gcc': 'KR', 'platformType': 'PC'}
        if fields:
            query['fields'] = fields
        if query_add:
            query.update(query_add)
        try:
            return self._download_json(
                'https://www.vlive.tv/globalv-web/vam-web/' + path_template % video_id, video_id,
                note, headers={'Referer': 'https://www.vlive.tv/'}, query=query)
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
                self.raise_login_required(json.loads(e.cause.read().decode('utf-8'))['message'])
            raise

class VLiveIE(VLiveBaseIE):
    IE_NAME = 'vlive'
    _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/(?:video|embed)/(?P<id>[0-9]+)'
    _TESTS = [{
        'url': 'http://www.vlive.tv/video/1326',
        'md5': 'cc7314812855ce56de70a06a27314983',
        'info_dict': {
            'id': '1326',
            'ext': 'mp4',
            'title': "Girl's Day's Broadcast",
            'creator': "Girl's Day",
            'view_count': int,
            'uploader_id': 'muploader_a',
            'upload_date': '20150817',
            'thumbnail': r're:^https?://.*\.(?:jpg|png)$',
            'timestamp': 1439816449,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.vlive.tv/video/16937',
        'info_dict': {
            'id': '16937',
            'ext': 'mp4',
            'title': '첸백시 걍방',
            'creator': 'EXO',
            'view_count': int,
            'subtitles': 'mincount:12',
            'uploader_id': 'muploader_j',
            'upload_date': '20161112',
            'thumbnail': r're:^https?://.*\.(?:jpg|png)$',
            'timestamp': 1478923074,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://www.vlive.tv/video/129100',
        'md5': 'ca2569453b79d66e5b919e5d308bff6b',
        'info_dict': {
            'id': '129100',
            'ext': 'mp4',
            'title': '[V LIVE] [BTS+] Run BTS! 2019 - EP.71 :: Behind the scene',
            'creator': 'BTS+',
            'view_count': int,
            'subtitles': 'mincount:10',
        },
        'skip': 'This video is only available for CH+ subscribers',
    }, {
        'url': 'https://www.vlive.tv/embed/1326',
        'only_matching': True,
    }, {
        # works only with gcc=KR
        'url': 'https://www.vlive.tv/video/225019',
        'only_matching': True,
    }, {
        'url': 'https://www.vlive.tv/video/223906',
        'info_dict': {
            'id': '58',
            'title': 'RUN BTS!'
        },
        'playlist_mincount': 120
    }]

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

        post = self._call_api(
            'post/v1.0/officialVideoPost-%s', video_id,
            'author{nickname},channel{channelCode,channelName},officialVideo{commentCount,exposeStatus,likeCount,playCount,playTime,status,title,type,vodId},playlist{playlistSeq,totalCount,name}')

        playlist_id = str_or_none(try_get(post, lambda x: x['playlist']['playlistSeq']))
        if not self._yes_playlist(playlist_id, video_id):
            video = post['officialVideo']
            return self._get_vlive_info(post, video, video_id)

        playlist_name = str_or_none(try_get(post, lambda x: x['playlist']['name']))
        playlist_count = str_or_none(try_get(post, lambda x: x['playlist']['totalCount']))

        playlist = self._call_api(
            'playlist/v1.0/playlist-%s/posts', playlist_id, 'data', {'limit': playlist_count})

        entries = []
        for video_data in playlist['data']:
            video = video_data.get('officialVideo')
            video_id = str_or_none(video.get('videoSeq'))
            entries.append(self._get_vlive_info(video_data, video, video_id))

        return self.playlist_result(entries, playlist_id, playlist_name)

    def _get_vlive_info(self, post, video, video_id):
        def get_common_fields():
            channel = post.get('channel') or {}
            return {
                'title': video.get('title'),
                'creator': post.get('author', {}).get('nickname'),
                'channel': channel.get('channelName'),
                'channel_id': channel.get('channelCode'),
                'duration': int_or_none(video.get('playTime')),
                'view_count': int_or_none(video.get('playCount')),
                'like_count': int_or_none(video.get('likeCount')),
                'comment_count': int_or_none(video.get('commentCount')),
                'timestamp': int_or_none(video.get('createdAt'), scale=1000),
                'thumbnail': video.get('thumb'),
            }

        video_type = video.get('type')
        if video_type == 'VOD':
            inkey = self._call_api('video/v1.0/vod/%s/inkey', video_id)['inkey']
            vod_id = video['vodId']
            info_dict = merge_dicts(
                get_common_fields(),
                self._extract_video_info(video_id, vod_id, inkey))
            thumbnail = video.get('thumb')
            if thumbnail:
                if not info_dict.get('thumbnails') and info_dict.get('thumbnail'):
                    info_dict['thumbnails'] = [{'url': info_dict.pop('thumbnail')}]
                info_dict.setdefault('thumbnails', []).append({'url': thumbnail, 'preference': 1})
            return info_dict
        elif video_type == 'LIVE':
            status = video.get('status')
            if status == 'ON_AIR':
                stream_url = self._call_api(
                    'old/v3/live/%s/playInfo',
                    video_id)['result']['adaptiveStreamUrl']
                formats = self._extract_m3u8_formats(stream_url, video_id, 'mp4')
                self._sort_formats(formats)
                info = get_common_fields()
                info.update({
                    'title': video['title'],
                    'id': video_id,
                    'formats': formats,
                    'is_live': True,
                })
                return info
            elif status == 'ENDED':
                raise ExtractorError(
                    'Uploading for replay. Please wait...', expected=True)
            elif status == 'RESERVED':
                raise ExtractorError('Coming soon!', expected=True)
            elif video.get('exposeStatus') == 'CANCEL':
                raise ExtractorError(
                    'We are sorry, but the live broadcast has been canceled.',
                    expected=True)
            else:
                raise ExtractorError('Unknown status ' + status)

class VLivePostIE(VLiveBaseIE):
    IE_NAME = 'vlive:post'
    _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/post/(?P<id>\d-\d+)'
    _TESTS = [{
        # uploadType = SOS
        'url': 'https://www.vlive.tv/post/1-20088044',
        'info_dict': {
            'id': '1-20088044',
            'title': 'Hola estrellitas la tierra les dice hola (si era así no?) Ha...',
            'description': 'md5:fab8a1e50e6e51608907f46c7fa4b407',
        },
        'playlist_count': 3,
    }, {
        # uploadType = V
        'url': 'https://www.vlive.tv/post/1-20087926',
        'info_dict': {
            'id': '1-20087926',
            'title': 'James Corden: And so, the baby becamos the Papa💜😭💪😭',
        },
        'playlist_count': 1,
    }]
    _FVIDEO_TMPL = 'fvideo/v1.0/fvideo-%%s/%s'

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

        post = self._call_api(
            'post/v1.0/post-%s', post_id,
            'attachments{video},officialVideo{videoSeq},plainBody,title')

        video_seq = str_or_none(try_get(
            post, lambda x: x['officialVideo']['videoSeq']))
        if video_seq:
            return self.url_result(
                'http://www.vlive.tv/video/' + video_seq,
                VLiveIE.ie_key(), video_seq)

        title = post['title']
        entries = []
        for idx, video in enumerate(post['attachments']['video'].values()):
            video_id = video.get('videoId')
            if not video_id:
                continue
            upload_type = video.get('uploadType')
            upload_info = video.get('uploadInfo') or {}
            entry = None
            if upload_type == 'SOS':
                download = self._call_api(
                    self._FVIDEO_TMPL % 'sosPlayInfo', video_id)['videoUrl']['download']
                formats = []
                for f_id, f_url in download.items():
                    formats.append({
                        'format_id': f_id,
                        'url': f_url,
                        'height': int_or_none(f_id[:-1]),
                    })
                self._sort_formats(formats)
                entry = {
                    'formats': formats,
                    'id': video_id,
                    'thumbnail': upload_info.get('imageUrl'),
                }
            elif upload_type == 'V':
                vod_id = upload_info.get('videoId')
                if not vod_id:
                    continue
                inkey = self._call_api(self._FVIDEO_TMPL % 'inKey', video_id)['inKey']
                entry = self._extract_video_info(video_id, vod_id, inkey)
            if entry:
                entry['title'] = '%s_part%s' % (title, idx)
                entries.append(entry)
        return self.playlist_result(
            entries, post_id, title, strip_or_none(post.get('plainBody')))

class VLiveChannelIE(VLiveBaseIE):
    IE_NAME = 'vlive:channel'
    _VALID_URL = r'https?://(?:channels\.vlive\.tv|(?:(?:www|m)\.)?vlive\.tv/channel)/(?P<channel_id>[0-9A-Z]+)(?:/board/(?P<posts_id>\d+))?'
    _TESTS = [{
        'url': 'http://channels.vlive.tv/FCD4B',
        'info_dict': {
            'id': 'FCD4B',
            'title': 'MAMAMOO',
        },
        'playlist_mincount': 110
    }, {
        'url': 'https://www.vlive.tv/channel/FCD4B',
        'only_matching': True,
    }, {
        'url': 'https://www.vlive.tv/channel/FCD4B/board/3546',
        'info_dict': {
            'id': 'FCD4B-3546',
            'title': 'MAMAMOO - Star Board',
        },
        'playlist_mincount': 880
    }]

    def _entries(self, posts_id, board_name):
        if board_name:
            posts_path = 'post/v1.0/board-%s/posts'
            query_add = {'limit': 100, 'sortType': 'LATEST'}
        else:
            posts_path = 'post/v1.0/channel-%s/starPosts'
            query_add = {'limit': 100}

        for page_num in itertools.count(1):
            video_list = self._call_api(
                posts_path, posts_id, 'channel{channelName},contentType,postId,title,url', query_add,
                note=f'Downloading playlist page {page_num}')

            for video in try_get(video_list, lambda x: x['data'], list) or []:
                video_id = str(video.get('postId'))
                video_title = str_or_none(video.get('title'))
                video_url = url_or_none(video.get('url'))
                if not all((video_id, video_title, video_url)) or video.get('contentType') != 'VIDEO':
                    continue
                channel_name = try_get(video, lambda x: x['channel']['channelName'], compat_str)
                yield self.url_result(video_url, VLivePostIE.ie_key(), video_id, video_title, channel=channel_name)

            after = try_get(video_list, lambda x: x['paging']['nextParams']['after'], compat_str)
            if not after:
                break
            query_add['after'] = after

    def _real_extract(self, url):
        channel_id, posts_id = self._match_valid_url(url).groups()

        board_name = None
        if posts_id:
            board = self._call_api(
                'board/v1.0/board-%s', posts_id, 'title,boardType')
            board_name = board.get('title') or 'Unknown'
            if board.get('boardType') not in ('STAR', 'VLIVE_PLUS'):
                raise ExtractorError(f'Board {board_name!r} is not supported', expected=True)

        entries = LazyList(self._entries(posts_id or channel_id, board_name))
        channel_name = entries[0]['channel']

        return self.playlist_result(
            entries,
            f'{channel_id}-{posts_id}' if posts_id else channel_id,
            f'{channel_name} - {board_name}' if channel_name and board_name else channel_name)


code:
from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    NO_DEFAULT,
    sanitized_Request,
    urlencode_postdata,
)

class VodlockerIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?vodlocker\.(?:com|city)/(?:embed-)?(?P<id>[0-9a-zA-Z]+)(?:\..*?)?'

    _TESTS = [{
        'url': 'http://vodlocker.com/e8wvyzz4sl42',
        'md5': 'ce0c2d18fa0735f1bd91b69b0e54aacf',
        'info_dict': {
            'id': 'e8wvyzz4sl42',
            'ext': 'mp4',
            'title': 'Germany vs Brazil',
            'thumbnail': r're:http://.*\.jpg',
        },
    }]

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

        if any(p in webpage for p in (
                '>THIS FILE WAS DELETED<',
                '>File Not Found<',
                'The file you were looking for could not be found, sorry for any inconvenience.<',
                '>The file was removed')):
            raise ExtractorError('Video %s does not exist' % video_id, expected=True)

        fields = self._hidden_inputs(webpage)

        if fields['op'] == 'download1':
            self._sleep(3, video_id)  # they do detect when requests happen too fast!
            post = urlencode_postdata(fields)
            req = sanitized_Request(url, post)
            req.add_header('Content-type', 'application/x-www-form-urlencoded')
            webpage = self._download_webpage(
                req, video_id, 'Downloading video page')

        def extract_file_url(html, default=NO_DEFAULT):
            return self._search_regex(
                r'file:\s*"(http[^\"]+)",', html, 'file url', default=default)

        video_url = extract_file_url(webpage, default=None)

        if not video_url:
            embed_url = self._search_regex(
                r'<iframe[^>]+src=(["\'])(?P<url>(?:https?://)?vodlocker\.(?:com|city)/embed-.+?)\1',
                webpage, 'embed url', group='url')
            embed_webpage = self._download_webpage(
                embed_url, video_id, 'Downloading embed webpage')
            video_url = extract_file_url(embed_webpage)
            thumbnail_webpage = embed_webpage
        else:
            thumbnail_webpage = webpage

        title = self._search_regex(
            r'id="file_title".*?>\s*(.*?)\s*<(?:br|span)', webpage, 'title')
        thumbnail = self._search_regex(
            r'image:\s*"(http[^\"]+)",', thumbnail_webpage, 'thumbnail', fatal=False)

        formats = [{
            'format_id': 'sd',
            'url': video_url,
        }]

        return {
            'id': video_id,
            'title': title,
            'thumbnail': thumbnail,
            'formats': formats,
        }


code:
from .onet import OnetBaseIE

class VODPlIE(OnetBaseIE):
    _VALID_URL = r'https?://vod\.pl/(?:[^/]+/)+(?P<id>[0-9a-zA-Z]+)'

    _TESTS = [{
        'url': 'https://vod.pl/filmy/chlopaki-nie-placza/3ep3jns',
        'md5': 'a7dc3b2f7faa2421aefb0ecaabf7ec74',
        'info_dict': {
            'id': '3ep3jns',
            'ext': 'mp4',
            'title': 'Chłopaki nie płaczą',
            'description': 'md5:f5f03b84712e55f5ac9f0a3f94445224',
            'timestamp': 1463415154,
            'duration': 5765,
            'upload_date': '20160516',
        },
    }, {
        'url': 'https://vod.pl/seriale/belfer-na-planie-praca-kamery-online/2c10heh',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(url, video_id)
        info_dict = self._extract_from_id(self._search_mvp_id(webpage), webpage)
        info_dict['id'] = video_id
        return info_dict


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

class VODPlatformIE(InfoExtractor):
    _VALID_URL = r'https?://(?:(?:www\.)?vod-platform\.net|embed\.kwikmotion\.com)/[eE]mbed/(?P<id>[^/?#]+)'
    _EMBED_REGEX = [r'<iframe[^>]+src=(["\'])(?P<url>(?:https?:)?//(?:(?:www\.)?vod-platform\.net|embed\.kwikmotion\.com)/[eE]mbed/.+?)\1']
    _TESTS = [{
        # from http://www.lbcgroup.tv/watch/chapter/29143/52844/%D8%A7%D9%84%D9%86%D8%B5%D8%B1%D8%A9-%D9%81%D9%8A-%D8%B6%D9%8A%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D9%80-cnn/ar
        'url': 'http://vod-platform.net/embed/RufMcytHDolTH1MuKHY9Fw',
        'md5': '1db2b7249ce383d6be96499006e951fc',
        'info_dict': {
            'id': 'RufMcytHDolTH1MuKHY9Fw',
            'ext': 'mp4',
            'title': 'LBCi News_ النصرة في ضيافة الـ "سي.أن.أن"',
        }
    }, {
        'url': 'http://embed.kwikmotion.com/embed/RufMcytHDolTH1MuKHY9Fw',
        'only_matching': True,
    }]

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

        title = unescapeHTML(self._og_search_title(webpage))
        hidden_inputs = self._hidden_inputs(webpage)

        formats = self._extract_wowza_formats(
            hidden_inputs.get('HiddenmyhHlsLink') or hidden_inputs['HiddenmyDashLink'], video_id, skip_protocols=['f4m', 'smil'])
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'thumbnail': hidden_inputs.get('HiddenThumbnail') or self._og_search_thumbnail(webpage),
            'formats': formats,
        }


code:
from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    ExtractorError,
    determine_ext,
    int_or_none,
    urljoin,
)

class VoiceRepublicIE(InfoExtractor):
    _VALID_URL = r'https?://voicerepublic\.com/(?:talks|embed)/(?P<id>[0-9a-z-]+)'
    _TESTS = [{
        'url': 'http://voicerepublic.com/talks/watching-the-watchers-building-a-sousveillance-state',
        'md5': 'b9174d651323f17783000876347116e3',
        'info_dict': {
            'id': '2296',
            'display_id': 'watching-the-watchers-building-a-sousveillance-state',
            'ext': 'm4a',
            'title': 'Watching the Watchers: Building a Sousveillance State',
            'description': 'Secret surveillance programs have metadata too. The people and companies that operate secret surveillance programs can be surveilled.',
            'duration': 1556,
            'view_count': int,
        }
    }, {
        'url': 'http://voicerepublic.com/embed/watching-the-watchers-building-a-sousveillance-state',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        if '>Queued for processing, please stand by...<' in webpage:
            raise ExtractorError(
                'Audio is still queued for processing', expected=True)

        talk = self._parse_json(self._search_regex(
            r'initialSnapshot\s*=\s*({.+?});',
            webpage, 'talk'), display_id)['talk']
        title = talk['title']
        formats = [{
            'url': urljoin(url, talk_url),
            'format_id': format_id,
            'ext': determine_ext(talk_url) or format_id,
            'vcodec': 'none',
        } for format_id, talk_url in talk['media_links'].items()]
        self._sort_formats(formats)

        return {
            'id': compat_str(talk.get('id') or display_id),
            'display_id': display_id,
            'title': title,
            'description': talk.get('teaser'),
            'thumbnail': talk.get('image_url'),
            'duration': int_or_none(talk.get('archived_duration')),
            'view_count': int_or_none(talk.get('play_count')),
            'formats': formats,
        }


code:
import itertools

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    smuggle_url,
    str_or_none,
    traverse_obj,
    unified_strdate,
    unsmuggle_url,
)

class VoicyBaseIE(InfoExtractor):
    def _extract_from_playlist_data(self, value):
        voice_id = str(value.get('PlaylistId'))
        upload_date = unified_strdate(value.get('Published'), False)
        items = [self._extract_single_article(voice_data) for voice_data in value['VoiceData']]
        return {
            '_type': 'multi_video',
            'entries': items,
            'id': voice_id,
            'title': str(value.get('PlaylistName')),
            'uploader': value.get('SpeakerName'),
            'uploader_id': str_or_none(value.get('SpeakerId')),
            'channel': value.get('ChannelName'),
            'channel_id': str_or_none(value.get('ChannelId')),
            'upload_date': upload_date,
        }

    def _extract_single_article(self, entry):
        formats = [{
            'url': entry.get('VoiceHlsFile'),
            'format_id': 'hls',
            'ext': 'm4a',
            'acodec': 'aac',
            'vcodec': 'none',
            'protocol': 'm3u8_native',
        }, {
            'url': entry.get('VoiceFile'),
            'format_id': 'mp3',
            'ext': 'mp3',
            'acodec': 'mp3',
            'vcodec': 'none',
        }]
        self._sort_formats(formats)
        return {
            'id': str(entry.get('ArticleId')),
            'title': entry.get('ArticleTitle'),
            'description': entry.get('MediaName'),
            'voice_id': str(entry.get('VoiceId')),
            'chapter_id': str(entry.get('ChapterId')),
            'formats': formats,
        }

    def _call_api(self, url, video_id, **kwargs):
        response = self._download_json(url, video_id, **kwargs)
        if response.get('Status') != 0:
            message = traverse_obj(response, ('Value', 'Error', 'Message'), expected_type=str)
            if not message:
                message = 'There was an error in the response: %d' % response.get('Status')
            raise ExtractorError(message, expected=False)
        return response.get('Value')

class VoicyIE(VoicyBaseIE):
    IE_NAME = 'voicy'
    _VALID_URL = r'https?://voicy\.jp/channel/(?P<channel_id>\d+)/(?P<id>\d+)'
    ARTICLE_LIST_API_URL = 'https://vmw.api.voicy.jp/articles_list?channel_id=%s&pid=%s'
    _TESTS = [{
        'note': 'chomado wa iizo (iitowaittenai)',
        'url': 'https://voicy.jp/channel/1253/122754',
        'info_dict': {
            'id': '122754',
            'title': '1/21(木)声日記：ついに原稿終わった！！',
            'uploader': 'ちょまど@ ITエンジニアなオタク',
            'uploader_id': '7339',
        },
        'playlist_mincount': 9,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        assert mobj
        voice_id = mobj.group('id')
        channel_id = mobj.group('channel_id')
        url, article_list = unsmuggle_url(url)
        if not article_list:
            article_list = self._call_api(self.ARTICLE_LIST_API_URL % (channel_id, voice_id), voice_id)
        return self._extract_from_playlist_data(article_list)

class VoicyChannelIE(VoicyBaseIE):
    IE_NAME = 'voicy:channel'
    _VALID_URL = r'https?://voicy\.jp/channel/(?P<id>\d+)'
    _TESTS = [{
        'note': 'chomado wa iizo (iitowaittenai)',
        'url': 'https://voicy.jp/channel/1253/',
        'info_dict': {
            'id': '7339',
            'title': 'ゆるふわ日常ラジオ #ちょまラジ',
            'uploader': 'ちょまど@ ITエンジニアなオタク',
            'uploader_id': '7339',
        },
        'playlist_mincount': 54,
    }]

    @classmethod
    def suitable(cls, url):
        return not VoicyIE.suitable(url) and super().suitable(url)

    def _entries(self, channel_id):
        pager = {}
        for count in itertools.count(1):
            article_list = self._call_api(
                'https://vmw.api.voicy.jp/program_list/all',
                channel_id, note=f'Paging #{count}',
                query={
                    'channel_id': channel_id,
                    'limit': '20',
                    'public_type': '3',
                    **pager,
                })
            playlist_data = article_list.get('PlaylistData')
            if not playlist_data:
                break
            yield from playlist_data
            last = playlist_data[-1]
            pager = {'pid': last['PlaylistId'], 'p_date': last['Published'], 'play_count': last['PlayCount']}

    def _real_extract(self, url):
        channel_id = self._match_id(url)
        articles = self._entries(channel_id)

        first_article = next(articles, None)
        title = traverse_obj(first_article, 'ChannelName', expected_type=str)
        speaker_name = traverse_obj(first_article, 'SpeakerName', expected_type=str)
        if not title and speaker_name:
            title = 'Uploads from %s' % speaker_name
        if not title:
            title = 'Uploads from channel ID %s' % channel_id

        articles = itertools.chain([first_article], articles) if first_article else articles

        playlist = (
            self.url_result(smuggle_url('https://voicy.jp/channel/%s/%d' % (channel_id, value['PlaylistId']), value), VoicyIE.ie_key())
            for value in articles)
        return {
            '_type': 'playlist',
            'entries': playlist,
            'id': channel_id,
            'title': title,
            'channel': speaker_name,
            'channel_id': channel_id,
        }


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

class VootIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    (?:
                        voot:|
                        https?://(?:www\.)?voot\.com/?
                        (?:
                            movies/[^/]+/|
                            (?:shows|kids)/(?:[^/]+/){4}
                        )
                     )
                    (?P<id>\d{3,})
                    '''
    _GEO_COUNTRIES = ['IN']
    _TESTS = [{
        'url': 'https://www.voot.com/shows/ishq-ka-rang-safed/1/360558/is-this-the-end-of-kamini-/441353',
        'info_dict': {
            'id': '0_8ledb18o',
            'ext': 'mp4',
            'title': 'Ishq Ka Rang Safed - Season 01 - Episode 340',
            'description': 'md5:06291fbbbc4dcbe21235c40c262507c1',
            'timestamp': 1472162937,
            'upload_date': '20160825',
            'series': 'Ishq Ka Rang Safed',
            'season_number': 1,
            'episode': 'Is this the end of Kamini?',
            'episode_number': 340,
            'view_count': int,
            'like_count': int,
        },
        'params': {
            'skip_download': True,
        },
        'expected_warnings': ['Failed to download m3u8 information'],
    }, {
        'url': 'https://www.voot.com/kids/characters/mighty-cat-masked-niyander-e-/400478/school-bag-disappears/440925',
        'only_matching': True,
    }, {
        'url': 'https://www.voot.com/movies/pandavas-5/424627',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        media_info = self._download_json(
            'https://wapi.voot.com/ws/ott/getMediaInfo.json', video_id,
            query={
                'platform': 'Web',
                'pId': 2,
                'mediaId': video_id,
            })

        status_code = try_get(media_info, lambda x: x['status']['code'], int)
        if status_code != 0:
            raise ExtractorError(media_info['status']['message'], expected=True)

        media = media_info['assets']

        entry_id = media['EntryId']
        title = media['MediaName']
        formats = self._extract_m3u8_formats(
            'https://cdnapisec.kaltura.com/p/1982551/playManifest/pt/https/f/applehttp/t/web/e/' + entry_id,
            video_id, 'mp4', m3u8_id='hls')
        self._sort_formats(formats)

        description, series, season_number, episode, episode_number = [None] * 5

        for meta in try_get(media, lambda x: x['Metas'], list) or []:
            key, value = meta.get('Key'), meta.get('Value')
            if not key or not value:
                continue
            if key == 'ContentSynopsis':
                description = value
            elif key == 'RefSeriesTitle':
                series = value
            elif key == 'RefSeriesSeason':
                season_number = int_or_none(value)
            elif key == 'EpisodeMainTitle':
                episode = value
            elif key == 'EpisodeNo':
                episode_number = int_or_none(value)
        return {
            'extractor_key': 'Kaltura',
            'id': entry_id,
            'title': title,
            'description': description,
            'series': series,
            'season_number': season_number,
            'episode': episode,
            'episode_number': episode_number,
            'timestamp': unified_timestamp(media.get('CreationDate')),
            'duration': int_or_none(media.get('Duration')),
            'view_count': int_or_none(media.get('ViewCounter')),
            'like_count': int_or_none(media.get('like_counter')),
            'formats': formats,
        }

class VootSeriesIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?voot\.com/shows/[^/]+/(?P<id>\d{3,})'
    _TESTS = [{
        'url': 'https://www.voot.com/shows/chakravartin-ashoka-samrat/100002',
        'playlist_mincount': 442,
        'info_dict': {
            'id': '100002',
        },
    }, {
        'url': 'https://www.voot.com/shows/ishq-ka-rang-safed/100003',
        'playlist_mincount': 341,
        'info_dict': {
            'id': '100003',
        },
    }]
    _SHOW_API = 'https://psapi.voot.com/media/voot/v1/voot-web/content/generic/season-by-show?sort=season%3Aasc&id={}&responseType=common'
    _SEASON_API = 'https://psapi.voot.com/media/voot/v1/voot-web/content/generic/series-wise-episode?sort=episode%3Aasc&id={}&responseType=common&page={:d}'

    def _entries(self, show_id):
        show_json = self._download_json(self._SHOW_API.format(show_id), video_id=show_id)
        for season in show_json.get('result', []):
            page_num = 1
            season_id = try_get(season, lambda x: x['id'], compat_str)
            season_json = self._download_json(self._SEASON_API.format(season_id, page_num),
                                              video_id=season_id,
                                              note='Downloading JSON metadata page %d' % page_num)
            episodes_json = season_json.get('result', [])
            while episodes_json:
                page_num += 1
                for episode in episodes_json:
                    video_id = episode.get('id')
                    yield self.url_result(
                        'voot:%s' % video_id, ie=VootIE.ie_key(), video_id=video_id)
                episodes_json = self._download_json(self._SEASON_API.format(season_id, page_num),
                                                    video_id=season_id,
                                                    note='Downloading JSON metadata page %d' % page_num)['result']

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


code:
from .common import InfoExtractor
from .once import OnceIE
from ..compat import compat_urllib_parse_unquote
from ..utils import (
    ExtractorError,
    int_or_none,
    try_get,
    unified_timestamp,
)

class VoxMediaVolumeIE(OnceIE):
    _VALID_URL = r'https?://volume\.vox-cdn\.com/embed/(?P<id>[0-9a-f]{9})'

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

        setup = self._parse_json(self._search_regex(
            r'setup\s*=\s*({.+});', webpage, 'setup'), video_id)
        player_setup = setup.get('player_setup') or setup
        video_data = player_setup.get('video') or {}
        formatted_metadata = video_data.get('formatted_metadata') or {}
        info = {
            'id': video_id,
            'title': player_setup.get('title') or video_data.get('title_short'),
            'description': video_data.get('description_long') or video_data.get('description_short'),
            'thumbnail': formatted_metadata.get('thumbnail') or video_data.get('brightcove_thumbnail'),
            'timestamp': unified_timestamp(formatted_metadata.get('video_publish_date')),
        }
        asset = try_get(setup, lambda x: x['embed_assets']['chorus'], dict) or {}

        formats = []
        hls_url = asset.get('hls_url')
        if hls_url:
            formats.extend(self._extract_m3u8_formats(
                hls_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False))
        mp4_url = asset.get('mp4_url')
        if mp4_url:
            tbr = self._search_regex(r'-(\d+)k\.', mp4_url, 'bitrate', default=None)
            format_id = 'http'
            if tbr:
                format_id += '-' + tbr
            formats.append({
                'format_id': format_id,
                'url': mp4_url,
                'tbr': int_or_none(tbr),
            })
        if formats:
            self._sort_formats(formats)
            info['formats'] = formats
            info['duration'] = int_or_none(asset.get('duration'))
            return info

        for provider_video_type in ('ooyala', 'youtube', 'brightcove'):
            provider_video_id = video_data.get('%s_id' % provider_video_type)
            if not provider_video_id:
                continue
            if provider_video_type == 'brightcove':
                info['formats'] = self._extract_once_formats(provider_video_id)
                self._sort_formats(info['formats'])
            else:
                info.update({
                    '_type': 'url_transparent',
                    'url': provider_video_id if provider_video_type == 'youtube' else '%s:%s' % (provider_video_type, provider_video_id),
                    'ie_key': provider_video_type.capitalize(),
                })
            return info
        raise ExtractorError('Unable to find provider video id')

class VoxMediaIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?(?:(?:theverge|vox|sbnation|eater|polygon|curbed|racked|funnyordie)\.com|recode\.net)/(?:[^/]+/)*(?P<id>[^/?]+)'
    _EMBED_REGEX = [r'<iframe[^>]+?src="(?P<url>https?://(?:www\.)?funnyordie\.com/embed/[^"]+)"']
    _TESTS = [{
        # Volume embed, Youtube
        'url': 'http://www.theverge.com/2014/6/27/5849272/material-world-how-google-discovered-what-software-is-made-of',
        'info_dict': {
            'id': 'j4mLW6x17VM',
            'ext': 'mp4',
            'title': 'Material world: how Google discovered what software is made of',
            'description': 'md5:dfc17e7715e3b542d66e33a109861382',
            'upload_date': '20190710',
            'uploader_id': 'TheVerge',
            'uploader': 'The Verge',
        },
        'add_ie': ['Youtube'],
    }, {
        # Volume embed, Youtube
        'url': 'http://www.theverge.com/2014/10/21/7025853/google-nexus-6-hands-on-photos-video-android-phablet',
        'md5': 'fd19aa0cf3a0eea515d4fd5c8c0e9d68',
        'info_dict': {
            'id': 'Gy8Md3Eky38',
            'ext': 'mp4',
            'title': 'The Nexus 6: hands-on with Google\'s phablet',
            'description': 'md5:d9f0216e5fb932dd2033d6db37ac3f1d',
            'uploader_id': 'TheVerge',
            'upload_date': '20141021',
            'uploader': 'The Verge',
            'timestamp': 1413907200,
        },
        'add_ie': ['Youtube'],
        'skip': 'similar to the previous test',
    }, {
        # Volume embed, Youtube
        'url': 'http://www.vox.com/2016/3/31/11336640/mississippi-lgbt-religious-freedom-bill',
        'info_dict': {
            'id': '22986359b',
            'ext': 'mp4',
            'title': "Mississippi's laws are so bad that its anti-LGBTQ law isn't needed to allow discrimination",
            'description': 'md5:fc1317922057de31cd74bce91eb1c66c',
            'upload_date': '20150915',
            'timestamp': 1442332800,
            'duration': 285,
        },
        'add_ie': ['Youtube'],
        'skip': 'similar to the previous test',
    }, {
        # youtube embed
        'url': 'http://www.vox.com/2016/3/24/11291692/robot-dance',
        'md5': '83b3080489fb103941e549352d3e0977',
        'info_dict': {
            'id': 'FcNHTJU1ufM',
            'ext': 'mp4',
            'title': 'How "the robot" became the greatest novelty dance of all time',
            'description': 'md5:b081c0d588b8b2085870cda55e6da176',
            'upload_date': '20160324',
            'uploader_id': 'voxdotcom',
            'uploader': 'Vox',
        },
        'add_ie': ['Youtube'],
        'skip': 'Page no longer contain videos',
    }, {
        # SBN.VideoLinkset.entryGroup multiple ooyala embeds
        'url': 'http://www.sbnation.com/college-football-recruiting/2015/2/3/7970291/national-signing-day-rationalizations-itll-be-ok-itll-be-ok',
        'info_dict': {
            'id': 'national-signing-day-rationalizations-itll-be-ok-itll-be-ok',
            'title': '25 lies you will tell yourself on National Signing Day',
            'description': 'It\'s the most self-delusional time of the year, and everyone\'s gonna tell the same lies together!',
        },
        'playlist': [{
            'md5': '721fededf2ab74ae4176c8c8cbfe092e',
            'info_dict': {
                'id': 'p3cThlMjE61VDi_SD9JlIteSNPWVDBB9',
                'ext': 'mp4',
                'title': 'Buddy Hield vs Steph Curry (and the world)',
                'description': 'Let’s dissect only the most important Final Four storylines.',
            },
        }, {
            'md5': 'bf0c5cc115636af028be1bab79217ea9',
            'info_dict': {
                'id': 'BmbmVjMjE6esPHxdALGubTrouQ0jYLHj',
                'ext': 'mp4',
                'title': 'Chasing Cinderella 2016: Syracuse basketball',
                'description': 'md5:e02d56b026d51aa32c010676765a690d',
            },
        }],
        'skip': 'Page no longer contain videos',
    }, {
        # volume embed, Brightcove Once
        'url': 'https://www.recode.net/2014/6/17/11628066/post-post-pc-ceo-the-full-code-conference-video-of-microsofts-satya',
        'md5': '2dbc77b8b0bff1894c2fce16eded637d',
        'info_dict': {
            'id': '1231c973d',
            'ext': 'mp4',
            'title': 'Post-Post-PC CEO: The Full Code Conference Video of Microsoft\'s Satya Nadella',
            'description': 'The longtime veteran was chosen earlier this year as the software giant\'s third leader in its history.',
            'timestamp': 1402938000,
            'upload_date': '20140616',
            'duration': 4114,
        },
        'add_ie': ['VoxMediaVolume'],
    }]

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

        def create_entry(provider_video_id, provider_video_type, title=None, description=None):
            video_url = {
                'youtube': '%s',
                'ooyala': 'ooyala:%s',
                'volume': 'http://volume.vox-cdn.com/embed/%s',
            }[provider_video_type] % provider_video_id
            return {
                '_type': 'url_transparent',
                'url': video_url,
                'title': title or self._og_search_title(webpage),
                'description': description or self._og_search_description(webpage),
            }

        entries = []
        entries_data = self._search_regex([
            r'Chorus\.VideoContext\.addVideo\((\[{.+}\])\);',
            r'var\s+entry\s*=\s*({.+});',
            r'SBN\.VideoLinkset\.entryGroup\(\s*(\[.+\])',
        ], webpage, 'video data', default=None)
        if entries_data:
            entries_data = self._parse_json(entries_data, display_id)
            if isinstance(entries_data, dict):
                entries_data = [entries_data]
            for video_data in entries_data:
                provider_video_id = video_data.get('provider_video_id')
                provider_video_type = video_data.get('provider_video_type')
                if provider_video_id and provider_video_type:
                    entries.append(create_entry(
                        provider_video_id, provider_video_type,
                        video_data.get('title'), video_data.get('description')))

        provider_video_id = self._search_regex(
            r'data-ooyala-id="([^"]+)"', webpage, 'ooyala id', default=None)
        if provider_video_id:
            entries.append(create_entry(provider_video_id, 'ooyala'))

        volume_uuid = self._search_regex(
            r'data-volume-uuid="([^"]+)"', webpage, 'volume uuid', default=None)
        if volume_uuid:
            entries.append(create_entry(volume_uuid, 'volume'))

        if len(entries) == 1:
            return entries[0]
        else:
            return self.playlist_result(entries, display_id, self._og_search_title(webpage), self._og_search_description(webpage))


code:
import re

from .common import InfoExtractor
from .brightcove import BrightcoveNewIE
from ..utils import (
    int_or_none,
    parse_age_limit,
    smuggle_url,
    unescapeHTML,
)

class VrakIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?vrak\.tv/videos\?.*?\btarget=(?P<id>[\d.]+)'
    _TEST = {
        'url': 'http://www.vrak.tv/videos?target=1.2306782&filtre=emission&id=1.1806721',
        'info_dict': {
            'id': '5345661243001',
            'ext': 'mp4',
            'title': 'Obésité, film de hockey et Roseline Filion',
            'timestamp': 1488492126,
            'upload_date': '20170302',
            'uploader_id': '2890187628001',
            'creator': 'VRAK.TV',
            'age_limit': 8,
            'series': 'ALT (Actualité Légèrement Tordue)',
            'episode': 'Obésité, film de hockey et Roseline Filion',
            'tags': list,
        },
        'params': {
            'skip_download': True,
        },
    }
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/2890187628001/default_default/index.html?videoId=%s'

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

        webpage = self._download_webpage(url, video_id)

        title = self._html_search_regex(
            r'<h\d\b[^>]+\bclass=["\']videoTitle["\'][^>]*>([^<]+)',
            webpage, 'title', default=None) or self._og_search_title(webpage)

        content = self._parse_json(
            self._search_regex(
                r'data-player-options-content=(["\'])(?P<content>{.+?})\1',
                webpage, 'content', default='{}', group='content'),
            video_id, transform_source=unescapeHTML)

        ref_id = content.get('refId') or self._search_regex(
            r'refId":"([^&]+)"', webpage, 'ref id')

        brightcove_id = self._search_regex(
            r'''(?x)
                java\.lang\.String\s+value\s*=\s*["']brightcove\.article\.\d+\.%s
                [^>]*
                java\.lang\.String\s+value\s*=\s*["'](\d+)
            ''' % re.escape(ref_id), webpage, 'brightcove id')

        return {
            '_type': 'url_transparent',
            'ie_key': BrightcoveNewIE.ie_key(),
            'url': smuggle_url(
                self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id,
                {'geo_countries': ['CA']}),
            'id': brightcove_id,
            'description': content.get('description'),
            'creator': content.get('brand'),
            'age_limit': parse_age_limit(content.get('rating')),
            'series': content.get('showName') or content.get(
                'episodeName'),  # this is intentional
            'season_number': int_or_none(content.get('seasonNumber')),
            'episode': title,
            'episode_number': int_or_none(content.get('episodeNumber')),
            'tags': content.get('tags', []),
        }


code:
from .common import InfoExtractor
from ..utils import (
    extract_attributes,
    float_or_none,
    get_element_by_class,
    strip_or_none,
    unified_timestamp,
)

class VRTIE(InfoExtractor):
    IE_DESC = 'VRT NWS, Flanders News, Flandern Info and Sporza'
    _VALID_URL = r'https?://(?:www\.)?(?P<site>vrt\.be/vrtnws|sporza\.be)/[a-z]{2}/\d{4}/\d{2}/\d{2}/(?P<id>[^/?&#]+)'
    _TESTS = [{
        'url': 'https://www.vrt.be/vrtnws/nl/2019/05/15/beelden-van-binnenkant-notre-dame-een-maand-na-de-brand/',
        'md5': 'e1663accf5cf13f375f3cd0d10476669',
        'info_dict': {
            'id': 'pbs-pub-7855fc7b-1448-49bc-b073-316cb60caa71$vid-2ca50305-c38a-4762-9890-65cbd098b7bd',
            'ext': 'mp4',
            'title': 'Beelden van binnenkant Notre-Dame, één maand na de brand',
            'description': 'Op maandagavond 15 april ging een deel van het dakgebinte van de Parijse kathedraal in vlammen op.',
            'timestamp': 1557924660,
            'upload_date': '20190515',
            'duration': 31.2,
        },
    }, {
        'url': 'https://sporza.be/nl/2019/05/15/de-belgian-cats-zijn-klaar-voor-het-ek/',
        'md5': '910bba927566e9ab992278f647eb4b75',
        'info_dict': {
            'id': 'pbs-pub-f2c86a46-8138-413a-a4b9-a0015a16ce2c$vid-1f112b31-e58e-4379-908d-aca6d80f8818',
            'ext': 'mp4',
            'title': 'De Belgian Cats zijn klaar voor het EK mét Ann Wauters',
            'timestamp': 1557923760,
            'upload_date': '20190515',
            'duration': 115.17,
        },
    }, {
        'url': 'https://www.vrt.be/vrtnws/en/2019/05/15/belgium_s-eurovision-entry-falls-at-the-first-hurdle/',
        'only_matching': True,
    }, {
        'url': 'https://www.vrt.be/vrtnws/de/2019/05/15/aus-fuer-eliott-im-halbfinale-des-eurosongfestivals/',
        'only_matching': True,
    }]
    _CLIENT_MAP = {
        'vrt.be/vrtnws': 'vrtnieuws',
        'sporza.be': 'sporza',
    }

    def _real_extract(self, url):
        site, display_id = self._match_valid_url(url).groups()
        webpage = self._download_webpage(url, display_id)
        attrs = extract_attributes(self._search_regex(
            r'(<[^>]+class="vrtvideo( [^"]*)?"[^>]*>)', webpage, 'vrt video'))

        asset_id = attrs['data-video-id']
        publication_id = attrs.get('data-publication-id')
        if publication_id:
            asset_id = publication_id + '$' + asset_id
        client = attrs.get('data-client-code') or self._CLIENT_MAP[site]

        title = strip_or_none(get_element_by_class(
            'vrt-title', webpage) or self._html_search_meta(
            ['og:title', 'twitter:title', 'name'], webpage))
        description = self._html_search_meta(
            ['og:description', 'twitter:description', 'description'], webpage)
        if description == '…':
            description = None
        timestamp = unified_timestamp(self._html_search_meta(
            'article:published_time', webpage))

        return {
            '_type': 'url_transparent',
            'id': asset_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'thumbnail': attrs.get('data-posterimage'),
            'timestamp': timestamp,
            'duration': float_or_none(attrs.get('data-duration'), 1000),
            'url': 'https://mediazone.vrt.be/api/v1/%s/assets/%s' % (client, asset_id),
            'ie_key': 'Canvas',
        }