code:
import itertools
import random
import re

from .common import InfoExtractor
from ..compat import compat_HTTPError, compat_str
from ..utils import (
    ExtractorError,
    determine_ext,
    int_or_none,
    parse_duration,
    parse_iso8601,
    str_or_none,
    try_get,
    url_or_none,
    urljoin,
)

class NRKBaseIE(InfoExtractor):
    _GEO_COUNTRIES = ['NO']
    _CDN_REPL_REGEX = r'''(?x)://
        (?:
            nrkod\d{1,2}-httpcache0-47115-cacheod0\.dna\.ip-only\.net/47115-cacheod0|
            nrk-od-no\.telenorcdn\.net|
            minicdn-od\.nrk\.no/od/nrkhd-osl-rr\.netwerk\.no/no
        )/'''

    def _extract_nrk_formats(self, asset_url, video_id):
        if re.match(r'https?://[^/]+\.akamaihd\.net/i/', asset_url):
            return self._extract_akamai_formats(asset_url, video_id)
        asset_url = re.sub(r'(?:bw_(?:low|high)=\d+|no_audio_only)&?', '', asset_url)
        formats = self._extract_m3u8_formats(
            asset_url, video_id, 'mp4', 'm3u8_native', fatal=False)
        if not formats and re.search(self._CDN_REPL_REGEX, asset_url):
            formats = self._extract_m3u8_formats(
                re.sub(self._CDN_REPL_REGEX, '://nrk-od-%02d.akamaized.net/no/' % random.randint(0, 99), asset_url),
                video_id, 'mp4', 'm3u8_native', fatal=False)
        return formats

    def _raise_error(self, data):
        MESSAGES = {
            'ProgramRightsAreNotReady': 'Du kan dessverre ikke se eller høre programmet',
            'ProgramRightsHasExpired': 'Programmet har gått ut',
            'NoProgramRights': 'Ikke tilgjengelig',
            'ProgramIsGeoBlocked': 'NRK har ikke rettigheter til å vise dette programmet utenfor Norge',
        }
        message_type = data.get('messageType', '')
        # Can be ProgramIsGeoBlocked or ChannelIsGeoBlocked*
        if 'IsGeoBlocked' in message_type or try_get(data, lambda x: x['usageRights']['isGeoBlocked']) is True:
            self.raise_geo_restricted(
                msg=MESSAGES.get('ProgramIsGeoBlocked'),
                countries=self._GEO_COUNTRIES)
        message = data.get('endUserMessage') or MESSAGES.get(message_type, message_type)
        raise ExtractorError('%s said: %s' % (self.IE_NAME, message), expected=True)

    def _call_api(self, path, video_id, item=None, note=None, fatal=True, query=None):
        return self._download_json(
            urljoin('https://psapi.nrk.no/', path),
            video_id, note or 'Downloading %s JSON' % item,
            fatal=fatal, query=query,
            headers={'Accept-Encoding': 'gzip, deflate, br'})

class NRKIE(NRKBaseIE):
    _VALID_URL = r'''(?x)
                        (?:
                            nrk:|
                            https?://
                                (?:
                                    (?:www\.)?nrk\.no/video/(?:PS\*|[^_]+_)|
                                    v8[-.]psapi\.nrk\.no/mediaelement/
                                )
                            )
                            (?P<id>[^?\#&]+)
                        '''

    _TESTS = [{
        # video
        'url': 'http://www.nrk.no/video/PS*150533',
        'md5': 'f46be075326e23ad0e524edfcb06aeb6',
        'info_dict': {
            'id': '150533',
            'ext': 'mp4',
            'title': 'Dompap og andre fugler i Piip-Show',
            'description': 'md5:d9261ba34c43b61c812cb6b0269a5c8f',
            'duration': 262,
        }
    }, {
        # audio
        'url': 'http://www.nrk.no/video/PS*154915',
        # MD5 is unstable
        'info_dict': {
            'id': '154915',
            'ext': 'mp4',
            'title': 'Slik høres internett ut når du er blind',
            'description': 'md5:a621f5cc1bd75c8d5104cb048c6b8568',
            'duration': 20,
        }
    }, {
        'url': 'nrk:ecc1b952-96dc-4a98-81b9-5296dc7a98d9',
        'only_matching': True,
    }, {
        'url': 'nrk:clip/7707d5a3-ebe7-434a-87d5-a3ebe7a34a70',
        'only_matching': True,
    }, {
        'url': 'https://v8-psapi.nrk.no/mediaelement/ecc1b952-96dc-4a98-81b9-5296dc7a98d9',
        'only_matching': True,
    }, {
        'url': 'https://www.nrk.no/video/dompap-og-andre-fugler-i-piip-show_150533',
        'only_matching': True,
    }, {
        'url': 'https://www.nrk.no/video/humor/kommentatorboksen-reiser-til-sjos_d1fda11f-a4ad-437a-a374-0398bc84e999',
        'only_matching': True,
    }, {
        # podcast
        'url': 'nrk:l_96f4f1b0-de54-4e6a-b4f1-b0de54fe6af8',
        'only_matching': True,
    }, {
        'url': 'nrk:podcast/l_96f4f1b0-de54-4e6a-b4f1-b0de54fe6af8',
        'only_matching': True,
    }, {
        # clip
        'url': 'nrk:150533',
        'only_matching': True,
    }, {
        'url': 'nrk:clip/150533',
        'only_matching': True,
    }, {
        # program
        'url': 'nrk:MDDP12000117',
        'only_matching': True,
    }, {
        'url': 'nrk:program/ENRK10100318',
        'only_matching': True,
    }, {
        # direkte
        'url': 'nrk:nrk1',
        'only_matching': True,
    }, {
        'url': 'nrk:channel/nrk1',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url).split('/')[-1]

        def call_playback_api(item, query=None):
            try:
                return self._call_api(f'playback/{item}/program/{video_id}', video_id, item, query=query)
            except ExtractorError as e:
                if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
                    return self._call_api(f'playback/{item}/{video_id}', video_id, item, query=query)
                raise

        # known values for preferredCdn: akamai, iponly, minicdn and telenor
        manifest = call_playback_api('manifest', {'preferredCdn': 'akamai'})

        video_id = try_get(manifest, lambda x: x['id'], compat_str) or video_id

        if manifest.get('playability') == 'nonPlayable':
            self._raise_error(manifest['nonPlayable'])

        playable = manifest['playable']

        formats = []
        for asset in playable['assets']:
            if not isinstance(asset, dict):
                continue
            if asset.get('encrypted'):
                continue
            format_url = url_or_none(asset.get('url'))
            if not format_url:
                continue
            asset_format = (asset.get('format') or '').lower()
            if asset_format == 'hls' or determine_ext(format_url) == 'm3u8':
                formats.extend(self._extract_nrk_formats(format_url, video_id))
            elif asset_format == 'mp3':
                formats.append({
                    'url': format_url,
                    'format_id': asset_format,
                    'vcodec': 'none',
                })
        self._sort_formats(formats)

        data = call_playback_api('metadata')

        preplay = data['preplay']
        titles = preplay['titles']
        title = titles['title']
        alt_title = titles.get('subtitle')

        description = try_get(preplay, lambda x: x['description'].replace('\r', '\n'))
        duration = parse_duration(playable.get('duration')) or parse_duration(data.get('duration'))

        thumbnails = []
        for image in try_get(
                preplay, lambda x: x['poster']['images'], list) or []:
            if not isinstance(image, dict):
                continue
            image_url = url_or_none(image.get('url'))
            if not image_url:
                continue
            thumbnails.append({
                'url': image_url,
                'width': int_or_none(image.get('pixelWidth')),
                'height': int_or_none(image.get('pixelHeight')),
            })

        subtitles = {}
        for sub in try_get(playable, lambda x: x['subtitles'], list) or []:
            if not isinstance(sub, dict):
                continue
            sub_url = url_or_none(sub.get('webVtt'))
            if not sub_url:
                continue
            sub_key = str_or_none(sub.get('language')) or 'nb'
            sub_type = str_or_none(sub.get('type'))
            if sub_type:
                sub_key += '-%s' % sub_type
            subtitles.setdefault(sub_key, []).append({
                'url': sub_url,
            })

        legal_age = try_get(
            data, lambda x: x['legalAge']['body']['rating']['code'], compat_str)
        # https://en.wikipedia.org/wiki/Norwegian_Media_Authority
        age_limit = None
        if legal_age:
            if legal_age == 'A':
                age_limit = 0
            elif legal_age.isdigit():
                age_limit = int_or_none(legal_age)

        is_series = try_get(data, lambda x: x['_links']['series']['name']) == 'series'

        info = {
            'id': video_id,
            'title': title,
            'alt_title': alt_title,
            'description': description,
            'duration': duration,
            'thumbnails': thumbnails,
            'age_limit': age_limit,
            'formats': formats,
            'subtitles': subtitles,
            'timestamp': parse_iso8601(try_get(manifest, lambda x: x['availability']['onDemand']['from'], str))
        }

        if is_series:
            series = season_id = season_number = episode = episode_number = None
            programs = self._call_api(
                'programs/%s' % video_id, video_id, 'programs', fatal=False)
            if programs and isinstance(programs, dict):
                series = str_or_none(programs.get('seriesTitle'))
                season_id = str_or_none(programs.get('seasonId'))
                season_number = int_or_none(programs.get('seasonNumber'))
                episode = str_or_none(programs.get('episodeTitle'))
                episode_number = int_or_none(programs.get('episodeNumber'))
            if not series:
                series = title
            if alt_title:
                title += ' - %s' % alt_title
            if not season_number:
                season_number = int_or_none(self._search_regex(
                    r'Sesong\s+(\d+)', description or '', 'season number',
                    default=None))
            if not episode:
                episode = alt_title if is_series else None
            if not episode_number:
                episode_number = int_or_none(self._search_regex(
                    r'^(\d+)\.', episode or '', 'episode number',
                    default=None))
            if not episode_number:
                episode_number = int_or_none(self._search_regex(
                    r'\((\d+)\s*:\s*\d+\)', description or '',
                    'episode number', default=None))
            info.update({
                'title': title,
                'series': series,
                'season_id': season_id,
                'season_number': season_number,
                'episode': episode,
                'episode_number': episode_number,
            })

        return info

class NRKTVIE(InfoExtractor):
    IE_DESC = 'NRK TV and NRK Radio'
    _EPISODE_RE = r'(?P<id>[a-zA-Z]{4}\d{8})'
    _VALID_URL = r'https?://(?:tv|radio)\.nrk(?:super)?\.no/(?:[^/]+/)*%s' % _EPISODE_RE
    _TESTS = [{
        'url': 'https://tv.nrk.no/program/MDDP12000117',
        'md5': 'c4a5960f1b00b40d47db65c1064e0ab1',
        'info_dict': {
            'id': 'MDDP12000117',
            'ext': 'mp4',
            'title': 'Alarm Trolltunga',
            'description': 'md5:46923a6e6510eefcce23d5ef2a58f2ce',
            'duration': 2223.44,
            'age_limit': 6,
            'subtitles': {
                'nb-nor': [{
                    'ext': 'vtt',
                }],
                'nb-ttv': [{
                    'ext': 'vtt',
                }]
            },
        },
    }, {
        'url': 'https://tv.nrk.no/serie/20-spoersmaal-tv/MUHH48000314/23-05-2014',
        'md5': '8d40dab61cea8ab0114e090b029a0565',
        'info_dict': {
            'id': 'MUHH48000314',
            'ext': 'mp4',
            'title': '20 spørsmål - 23. mai 2014',
            'alt_title': '23. mai 2014',
            'description': 'md5:bdea103bc35494c143c6a9acdd84887a',
            'duration': 1741,
            'series': '20 spørsmål',
            'episode': '23. mai 2014',
            'age_limit': 0,
        },
    }, {
        'url': 'https://tv.nrk.no/program/mdfp15000514',
        'info_dict': {
            'id': 'MDFP15000514',
            'ext': 'mp4',
            'title': 'Kunnskapskanalen - Grunnlovsjubiléet - Stor ståhei for ingenting',
            'description': 'md5:89290c5ccde1b3a24bb8050ab67fe1db',
            'duration': 4605.08,
            'series': 'Kunnskapskanalen',
            'episode': 'Grunnlovsjubiléet - Stor ståhei for ingenting',
            'age_limit': 0,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # single playlist video
        'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015#del=2',
        'info_dict': {
            'id': 'MSPO40010515',
            'ext': 'mp4',
            'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015',
            'description': 'md5:c03aba1e917561eface5214020551b7a',
            'age_limit': 0,
        },
        'params': {
            'skip_download': True,
        },
        'expected_warnings': ['Failed to download m3u8 information'],
        'skip': 'particular part is not supported currently',
    }, {
        'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015',
        'info_dict': {
            'id': 'MSPO40010515',
            'ext': 'mp4',
            'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015',
            'description': 'md5:c03aba1e917561eface5214020551b7a',
            'age_limit': 0,
        },
        'expected_warnings': ['Failed to download m3u8 information'],
        'skip': 'Ikke tilgjengelig utenfor Norge',
    }, {
        'url': 'https://tv.nrk.no/serie/anno/KMTE50001317/sesong-3/episode-13',
        'info_dict': {
            'id': 'KMTE50001317',
            'ext': 'mp4',
            'title': 'Anno - 13. episode',
            'description': 'md5:11d9613661a8dbe6f9bef54e3a4cbbfa',
            'duration': 2340,
            'series': 'Anno',
            'episode': '13. episode',
            'season_number': 3,
            'episode_number': 13,
            'age_limit': 0,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://tv.nrk.no/serie/nytt-paa-nytt/MUHH46000317/27-01-2017',
        'info_dict': {
            'id': 'MUHH46000317',
            'ext': 'mp4',
            'title': 'Nytt på Nytt 27.01.2017',
            'description': 'md5:5358d6388fba0ea6f0b6d11c48b9eb4b',
            'duration': 1796,
            'series': 'Nytt på nytt',
            'episode': '27.01.2017',
            'age_limit': 0,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'ProgramRightsHasExpired',
    }, {
        'url': 'https://radio.nrk.no/serie/dagsnytt/NPUB21019315/12-07-2015#',
        'only_matching': True,
    }, {
        'url': 'https://tv.nrk.no/serie/lindmo/2018/MUHU11006318/avspiller',
        'only_matching': True,
    }, {
        'url': 'https://radio.nrk.no/serie/dagsnytt/sesong/201507/NPUB21019315',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        return self.url_result(
            'nrk:%s' % video_id, ie=NRKIE.ie_key(), video_id=video_id)

class NRKTVEpisodeIE(InfoExtractor):
    _VALID_URL = r'https?://tv\.nrk\.no/serie/(?P<id>[^/]+/sesong/(?P<season_number>\d+)/episode/(?P<episode_number>\d+))'
    _TESTS = [{
        'url': 'https://tv.nrk.no/serie/hellums-kro/sesong/1/episode/2',
        'info_dict': {
            'id': 'MUHH36005220',
            'ext': 'mp4',
            'title': 'Hellums kro - 2. Kro, krig og kjærlighet',
            'description': 'md5:ad92ddffc04cea8ce14b415deef81787',
            'duration': 1563.92,
            'series': 'Hellums kro',
            'season_number': 1,
            'episode_number': 2,
            'episode': '2. Kro, krig og kjærlighet',
            'age_limit': 6,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://tv.nrk.no/serie/backstage/sesong/1/episode/8',
        'info_dict': {
            'id': 'MSUI14000816',
            'ext': 'mp4',
            'title': 'Backstage - 8. episode',
            'description': 'md5:de6ca5d5a2d56849e4021f2bf2850df4',
            'duration': 1320,
            'series': 'Backstage',
            'season_number': 1,
            'episode_number': 8,
            'episode': '8. episode',
            'age_limit': 0,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'ProgramRightsHasExpired',
    }]

    def _real_extract(self, url):
        display_id, season_number, episode_number = self._match_valid_url(url).groups()

        webpage = self._download_webpage(url, display_id)

        info = self._search_json_ld(webpage, display_id, default={})
        nrk_id = info.get('@id') or self._html_search_meta(
            'nrk:program-id', webpage, default=None) or self._search_regex(
            r'data-program-id=["\'](%s)' % NRKTVIE._EPISODE_RE, webpage,
            'nrk id')
        assert re.match(NRKTVIE._EPISODE_RE, nrk_id)

        info.update({
            '_type': 'url',
            'id': nrk_id,
            'url': 'nrk:%s' % nrk_id,
            'ie_key': NRKIE.ie_key(),
            'season_number': int(season_number),
            'episode_number': int(episode_number),
        })
        return info

class NRKTVSerieBaseIE(NRKBaseIE):
    def _extract_entries(self, entry_list):
        if not isinstance(entry_list, list):
            return []
        entries = []
        for episode in entry_list:
            nrk_id = episode.get('prfId') or episode.get('episodeId')
            if not nrk_id or not isinstance(nrk_id, compat_str):
                continue
            entries.append(self.url_result(
                'nrk:%s' % nrk_id, ie=NRKIE.ie_key(), video_id=nrk_id))
        return entries

    _ASSETS_KEYS = ('episodes', 'instalments',)

    def _extract_assets_key(self, embedded):
        for asset_key in self._ASSETS_KEYS:
            if embedded.get(asset_key):
                return asset_key

    @staticmethod
    def _catalog_name(serie_kind):
        return 'podcast' if serie_kind in ('podcast', 'podkast') else 'series'

    def _entries(self, data, display_id):
        for page_num in itertools.count(1):
            embedded = data.get('_embedded') or data
            if not isinstance(embedded, dict):
                break
            assets_key = self._extract_assets_key(embedded)
            if not assets_key:
                break
            # Extract entries
            entries = try_get(
                embedded,
                (lambda x: x[assets_key]['_embedded'][assets_key],
                 lambda x: x[assets_key]),
                list)
            for e in self._extract_entries(entries):
                yield e
            # Find next URL
            next_url_path = try_get(
                data,
                (lambda x: x['_links']['next']['href'],
                 lambda x: x['_embedded'][assets_key]['_links']['next']['href']),
                compat_str)
            if not next_url_path:
                break
            data = self._call_api(
                next_url_path, display_id,
                note='Downloading %s JSON page %d' % (assets_key, page_num),
                fatal=False)
            if not data:
                break

class NRKTVSeasonIE(NRKTVSerieBaseIE):
    _VALID_URL = r'''(?x)
                    https?://
                        (?P<domain>tv|radio)\.nrk\.no/
                        (?P<serie_kind>serie|pod[ck]ast)/
                        (?P<serie>[^/]+)/
                        (?:
                            (?:sesong/)?(?P<id>\d+)|
                            sesong/(?P<id_2>[^/?#&]+)
                        )
                    '''
    _TESTS = [{
        'url': 'https://tv.nrk.no/serie/backstage/sesong/1',
        'info_dict': {
            'id': 'backstage/1',
            'title': 'Sesong 1',
        },
        'playlist_mincount': 30,
    }, {
        # no /sesong/ in path
        'url': 'https://tv.nrk.no/serie/lindmo/2016',
        'info_dict': {
            'id': 'lindmo/2016',
            'title': '2016',
        },
        'playlist_mincount': 29,
    }, {
        # weird nested _embedded in catalog JSON response
        'url': 'https://radio.nrk.no/serie/dickie-dick-dickens/sesong/1',
        'info_dict': {
            'id': 'dickie-dick-dickens/1',
            'title': 'Sesong 1',
        },
        'playlist_mincount': 11,
    }, {
        # 841 entries, multi page
        'url': 'https://radio.nrk.no/serie/dagsnytt/sesong/201509',
        'info_dict': {
            'id': 'dagsnytt/201509',
            'title': 'September 2015',
        },
        'playlist_mincount': 841,
    }, {
        # 180 entries, single page
        'url': 'https://tv.nrk.no/serie/spangas/sesong/1',
        'only_matching': True,
    }, {
        'url': 'https://radio.nrk.no/podkast/hele_historien/sesong/diagnose-kverulant',
        'info_dict': {
            'id': 'hele_historien/diagnose-kverulant',
            'title': 'Diagnose kverulant',
        },
        'playlist_mincount': 3,
    }, {
        'url': 'https://radio.nrk.no/podkast/loerdagsraadet/sesong/202101',
        'only_matching': True,
    }]

    @classmethod
    def suitable(cls, url):
        return (False if NRKTVIE.suitable(url) or NRKTVEpisodeIE.suitable(url) or NRKRadioPodkastIE.suitable(url)
                else super(NRKTVSeasonIE, cls).suitable(url))

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        domain = mobj.group('domain')
        serie_kind = mobj.group('serie_kind')
        serie = mobj.group('serie')
        season_id = mobj.group('id') or mobj.group('id_2')
        display_id = '%s/%s' % (serie, season_id)

        data = self._call_api(
            '%s/catalog/%s/%s/seasons/%s'
            % (domain, self._catalog_name(serie_kind), serie, season_id),
            display_id, 'season', query={'pageSize': 50})

        title = try_get(data, lambda x: x['titles']['title'], compat_str) or display_id
        return self.playlist_result(
            self._entries(data, display_id),
            display_id, title)

class NRKTVSeriesIE(NRKTVSerieBaseIE):
    _VALID_URL = r'https?://(?P<domain>(?:tv|radio)\.nrk|(?:tv\.)?nrksuper)\.no/(?P<serie_kind>serie|pod[ck]ast)/(?P<id>[^/]+)'
    _TESTS = [{
        # new layout, instalments
        'url': 'https://tv.nrk.no/serie/groenn-glede',
        'info_dict': {
            'id': 'groenn-glede',
            'title': 'Grønn glede',
            'description': 'md5:7576e92ae7f65da6993cf90ee29e4608',
        },
        'playlist_mincount': 90,
    }, {
        # new layout, instalments, more entries
        'url': 'https://tv.nrk.no/serie/lindmo',
        'only_matching': True,
    }, {
        'url': 'https://tv.nrk.no/serie/blank',
        'info_dict': {
            'id': 'blank',
            'title': 'Blank',
            'description': 'md5:7664b4e7e77dc6810cd3bca367c25b6e',
        },
        'playlist_mincount': 30,
    }, {
        # new layout, seasons
        'url': 'https://tv.nrk.no/serie/backstage',
        'info_dict': {
            'id': 'backstage',
            'title': 'Backstage',
            'description': 'md5:63692ceb96813d9a207e9910483d948b',
        },
        'playlist_mincount': 60,
    }, {
        # old layout
        'url': 'https://tv.nrksuper.no/serie/labyrint',
        'info_dict': {
            'id': 'labyrint',
            'title': 'Labyrint',
            'description': 'I Daidalos sin undersjøiske Labyrint venter spennende oppgaver, skumle robotskapninger og slim.',
        },
        'playlist_mincount': 3,
    }, {
        'url': 'https://tv.nrk.no/serie/broedrene-dal-og-spektralsteinene',
        'only_matching': True,
    }, {
        'url': 'https://tv.nrk.no/serie/saving-the-human-race',
        'only_matching': True,
    }, {
        'url': 'https://tv.nrk.no/serie/postmann-pat',
        'only_matching': True,
    }, {
        'url': 'https://radio.nrk.no/serie/dickie-dick-dickens',
        'info_dict': {
            'id': 'dickie-dick-dickens',
            'title': 'Dickie Dick Dickens',
            'description': 'md5:19e67411ffe57f7dce08a943d7a0b91f',
        },
        'playlist_mincount': 8,
    }, {
        'url': 'https://nrksuper.no/serie/labyrint',
        'only_matching': True,
    }, {
        'url': 'https://radio.nrk.no/podkast/ulrikkes_univers',
        'info_dict': {
            'id': 'ulrikkes_univers',
        },
        'playlist_mincount': 10,
    }, {
        'url': 'https://radio.nrk.no/podkast/ulrikkes_univers/nrkno-poddkast-26588-134079-05042018030000',
        'only_matching': True,
    }]

    @classmethod
    def suitable(cls, url):
        return (
            False if any(ie.suitable(url)
                         for ie in (NRKTVIE, NRKTVEpisodeIE, NRKRadioPodkastIE, NRKTVSeasonIE))
            else super(NRKTVSeriesIE, cls).suitable(url))

    def _real_extract(self, url):
        site, serie_kind, series_id = self._match_valid_url(url).groups()
        is_radio = site == 'radio.nrk'
        domain = 'radio' if is_radio else 'tv'

        size_prefix = 'p' if is_radio else 'embeddedInstalmentsP'
        series = self._call_api(
            '%s/catalog/%s/%s'
            % (domain, self._catalog_name(serie_kind), series_id),
            series_id, 'serie', query={size_prefix + 'ageSize': 50})
        titles = try_get(series, [
            lambda x: x['titles'],
            lambda x: x[x['type']]['titles'],
            lambda x: x[x['seriesType']]['titles'],
        ]) or {}

        entries = []
        entries.extend(self._entries(series, series_id))
        embedded = series.get('_embedded') or {}
        linked_seasons = try_get(series, lambda x: x['_links']['seasons']) or []
        embedded_seasons = embedded.get('seasons') or []
        if len(linked_seasons) > len(embedded_seasons):
            for season in linked_seasons:
                season_url = urljoin(url, season.get('href'))
                if not season_url:
                    season_name = season.get('name')
                    if season_name and isinstance(season_name, compat_str):
                        season_url = 'https://%s.nrk.no/serie/%s/sesong/%s' % (domain, series_id, season_name)
                if season_url:
                    entries.append(self.url_result(
                        season_url, ie=NRKTVSeasonIE.ie_key(),
                        video_title=season.get('title')))
        else:
            for season in embedded_seasons:
                entries.extend(self._entries(season, series_id))
        entries.extend(self._entries(
            embedded.get('extraMaterial') or {}, series_id))

        return self.playlist_result(
            entries, series_id, titles.get('title'), titles.get('subtitle'))

class NRKTVDirekteIE(NRKTVIE):
    IE_DESC = 'NRK TV Direkte and NRK Radio Direkte'
    _VALID_URL = r'https?://(?:tv|radio)\.nrk\.no/direkte/(?P<id>[^/?#&]+)'

    _TESTS = [{
        'url': 'https://tv.nrk.no/direkte/nrk1',
        'only_matching': True,
    }, {
        'url': 'https://radio.nrk.no/direkte/p1_oslo_akershus',
        'only_matching': True,
    }]

class NRKRadioPodkastIE(InfoExtractor):
    _VALID_URL = r'https?://radio\.nrk\.no/pod[ck]ast/(?:[^/]+/)+(?P<id>l_[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'

    _TESTS = [{
        'url': 'https://radio.nrk.no/podkast/ulrikkes_univers/l_96f4f1b0-de54-4e6a-b4f1-b0de54fe6af8',
        'md5': '8d40dab61cea8ab0114e090b029a0565',
        'info_dict': {
            'id': 'MUHH48000314AA',
            'ext': 'mp4',
            'title': '20 spørsmål 23.05.2014',
            'description': 'md5:bdea103bc35494c143c6a9acdd84887a',
            'duration': 1741,
            'series': '20 spørsmål',
            'episode': '23.05.2014',
        },
    }, {
        'url': 'https://radio.nrk.no/podcast/ulrikkes_univers/l_96f4f1b0-de54-4e6a-b4f1-b0de54fe6af8',
        'only_matching': True,
    }, {
        'url': 'https://radio.nrk.no/podkast/ulrikkes_univers/sesong/1/l_96f4f1b0-de54-4e6a-b4f1-b0de54fe6af8',
        'only_matching': True,
    }, {
        'url': 'https://radio.nrk.no/podkast/hele_historien/sesong/bortfoert-i-bergen/l_774d1a2c-7aa7-4965-8d1a-2c7aa7d9652c',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        return self.url_result(
            'nrk:%s' % video_id, ie=NRKIE.ie_key(), video_id=video_id)

class NRKPlaylistBaseIE(InfoExtractor):
    def _extract_description(self, webpage):
        pass

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

        webpage = self._download_webpage(url, playlist_id)

        entries = [
            self.url_result('nrk:%s' % video_id, NRKIE.ie_key())
            for video_id in re.findall(self._ITEM_RE, webpage)
        ]

        playlist_title = self._extract_title(webpage)
        playlist_description = self._extract_description(webpage)

        return self.playlist_result(
            entries, playlist_id, playlist_title, playlist_description)

class NRKPlaylistIE(NRKPlaylistBaseIE):
    _VALID_URL = r'https?://(?:www\.)?nrk\.no/(?!video|skole)(?:[^/]+/)+(?P<id>[^/]+)'
    _ITEM_RE = r'class="[^"]*\brich\b[^"]*"[^>]+data-video-id="([^"]+)"'
    _TESTS = [{
        'url': 'http://www.nrk.no/troms/gjenopplev-den-historiske-solformorkelsen-1.12270763',
        'info_dict': {
            'id': 'gjenopplev-den-historiske-solformorkelsen-1.12270763',
            'title': 'Gjenopplev den historiske solformørkelsen',
            'description': 'md5:c2df8ea3bac5654a26fc2834a542feed',
        },
        'playlist_count': 2,
    }, {
        'url': 'http://www.nrk.no/kultur/bok/rivertonprisen-til-karin-fossum-1.12266449',
        'info_dict': {
            'id': 'rivertonprisen-til-karin-fossum-1.12266449',
            'title': 'Rivertonprisen til Karin Fossum',
            'description': 'Første kvinne på 15 år til å vinne krimlitteraturprisen.',
        },
        'playlist_count': 2,
    }]

    def _extract_title(self, webpage):
        return self._og_search_title(webpage, fatal=False)

    def _extract_description(self, webpage):
        return self._og_search_description(webpage)

class NRKTVEpisodesIE(NRKPlaylistBaseIE):
    _VALID_URL = r'https?://tv\.nrk\.no/program/[Ee]pisodes/[^/]+/(?P<id>\d+)'
    _ITEM_RE = r'data-episode=["\']%s' % NRKTVIE._EPISODE_RE
    _TESTS = [{
        'url': 'https://tv.nrk.no/program/episodes/nytt-paa-nytt/69031',
        'info_dict': {
            'id': '69031',
            'title': 'Nytt på nytt, sesong: 201210',
        },
        'playlist_count': 4,
    }]

    def _extract_title(self, webpage):
        return self._html_search_regex(
            r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)

class NRKSkoleIE(InfoExtractor):
    IE_DESC = 'NRK Skole'
    _VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/?\?.*\bmediaId=(?P<id>\d+)'

    _TESTS = [{
        'url': 'https://www.nrk.no/skole/?page=search&q=&mediaId=14099',
        'md5': '18c12c3d071953c3bf8d54ef6b2587b7',
        'info_dict': {
            'id': '6021',
            'ext': 'mp4',
            'title': 'Genetikk og eneggede tvillinger',
            'description': 'md5:3aca25dcf38ec30f0363428d2b265f8d',
            'duration': 399,
        },
    }, {
        'url': 'https://www.nrk.no/skole/?page=objectives&subject=naturfag&objective=K15114&mediaId=19355',
        'only_matching': True,
    }]

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

        nrk_id = self._download_json(
            'https://nrkno-skole-prod.kube.nrk.no/skole/api/media/%s' % video_id,
            video_id)['psId']

        return self.url_result('nrk:%s' % nrk_id)


code:
from .common import InfoExtractor

class NRLTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?nrl\.com/tv(/[^/]+)*/(?P<id>[^/?&#]+)'
    _TEST = {
        'url': 'https://www.nrl.com/tv/news/match-highlights-titans-v-knights-862805/',
        'info_dict': {
            'id': 'YyNnFuaDE6kPJqlDhG4CGQ_w89mKTau4',
            'ext': 'mp4',
            'title': 'Match Highlights: Titans v Knights',
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
    }

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        q_data = self._parse_json(self._html_search_regex(
            r'(?s)q-data="({.+?})"', webpage, 'player data'), display_id)
        ooyala_id = q_data['videoId']
        return self.url_result(
            'ooyala:' + ooyala_id, 'Ooyala', ooyala_id, q_data.get('title'))

repo_name: Lesmiscore/ytdl


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

class NTVCoJpCUIE(InfoExtractor):
    IE_NAME = 'cu.ntv.co.jp'
    IE_DESC = 'Nippon Television Network'
    _VALID_URL = r'https?://cu\.ntv\.co\.jp/(?!program)(?P<id>[^/?&#]+)'
    _TEST = {
        'url': 'https://cu.ntv.co.jp/televiva-chill-gohan_181031/',
        'info_dict': {
            'id': '5978891207001',
            'ext': 'mp4',
            'title': '桜エビと炒り卵がポイント！ 「中華風 エビチリおにぎり」──『美虎』五十嵐美幸',
            'upload_date': '20181213',
            'description': 'md5:1985b51a9abc285df0104d982a325f2a',
            'uploader_id': '3855502814001',
            'timestamp': 1544669941,
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
    }

    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        player_config = self._search_nuxt_data(webpage, display_id)
        video_id = traverse_obj(player_config, ('movie', 'video_id'))
        if not video_id:
            raise ExtractorError('Failed to extract video ID for Brightcove')
        account_id = traverse_obj(player_config, ('player', 'account')) or '3855502814001'
        title = traverse_obj(player_config, ('movie', 'name'))
        if not title:
            og_title = self._og_search_title(webpage, fatal=False) or traverse_obj(player_config, ('player', 'title'))
            if og_title:
                title = og_title.split('(', 1)[0].strip()
        description = (traverse_obj(player_config, ('movie', 'description'))
                       or self._html_search_meta(['description', 'og:description'], webpage))
        return {
            '_type': 'url_transparent',
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'url': smuggle_url(self.BRIGHTCOVE_URL_TEMPLATE % (account_id, video_id), {'geo_countries': ['JP']}),
            'ie_key': 'BrightcoveNew',
        }


code:
import re

from .common import InfoExtractor
from ..compat import compat_urlparse
from ..utils import (
    int_or_none,
    js_to_json,
    parse_duration,
)

class NTVDeIE(InfoExtractor):
    IE_NAME = 'n-tv.de'
    _VALID_URL = r'https?://(?:www\.)?n-tv\.de/mediathek/videos/[^/?#]+/[^/?#]+-article(?P<id>.+)\.html'

    _TESTS = [{
        'url': 'http://www.n-tv.de/mediathek/videos/panorama/Schnee-und-Glaette-fuehren-zu-zahlreichen-Unfaellen-und-Staus-article14438086.html',
        'md5': '6ef2514d4b1e8e03ca24b49e2f167153',
        'info_dict': {
            'id': '14438086',
            'ext': 'mp4',
            'thumbnail': r're:^https?://.*\.jpg$',
            'title': 'Schnee und Glätte führen zu zahlreichen Unfällen und Staus',
            'alt_title': 'Winterchaos auf deutschen Straßen',
            'description': 'Schnee und Glätte sorgen deutschlandweit für einen chaotischen Start in die Woche: Auf den Straßen kommt es zu kilometerlangen Staus und Dutzenden Glätteunfällen. In Düsseldorf und München wirbelt der Schnee zudem den Flugplan durcheinander. Dutzende Flüge landen zu spät, einige fallen ganz aus.',
            'duration': 4020,
            'timestamp': 1422892797,
            'upload_date': '20150202',
        },
    }]

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

        info = self._parse_json(self._search_regex(
            r'(?s)ntv\.pageInfo\.article\s*=\s*(\{.*?\});', webpage, 'info'),
            video_id, transform_source=js_to_json)
        timestamp = int_or_none(info.get('publishedDateAsUnixTimeStamp'))
        vdata = self._parse_json(self._search_regex(
            r'(?s)\$\(\s*"\#player"\s*\)\s*\.data\(\s*"player",\s*(\{.*?\})\);',
            webpage, 'player data'), video_id,
            transform_source=lambda s: js_to_json(re.sub(r'advertising:\s*{[^}]+},', '', s)))
        duration = parse_duration(vdata.get('duration'))

        formats = []
        if vdata.get('video'):
            formats.append({
                'format_id': 'flash',
                'url': 'rtmp://fms.n-tv.de/%s' % vdata['video'],
            })
        if vdata.get('videoMp4'):
            formats.append({
                'format_id': 'mobile',
                'url': compat_urlparse.urljoin('http://video.n-tv.de', vdata['videoMp4']),
                'tbr': 400,  # estimation
            })
        if vdata.get('videoM3u8'):
            m3u8_url = compat_urlparse.urljoin('http://video.n-tv.de', vdata['videoM3u8'])
            formats.extend(self._extract_m3u8_formats(
                m3u8_url, video_id, ext='mp4', entry_protocol='m3u8_native',
                quality=1, m3u8_id='hls', fatal=False))
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': info['headline'],
            'description': info.get('intro'),
            'alt_title': info.get('kicker'),
            'timestamp': timestamp,
            'thumbnail': vdata.get('html5VideoPoster'),
            'duration': duration,
            'formats': formats,
        }


code:
from .common import InfoExtractor
from ..utils import (
    int_or_none,
    strip_or_none,
    unescapeHTML,
    xpath_text,
)

class NTVRuIE(InfoExtractor):
    IE_NAME = 'ntv.ru'
    _VALID_URL = r'https?://(?:www\.)?ntv\.ru/(?:[^/]+/)*(?P<id>[^/?#&]+)'

    _TESTS = [{
        'url': 'http://www.ntv.ru/novosti/863142/',
        'md5': 'ba7ea172a91cb83eb734cad18c10e723',
        'info_dict': {
            'id': '746000',
            'ext': 'mp4',
            'title': 'Командующий Черноморским флотом провел переговоры в штабе ВМС Украины',
            'description': 'Командующий Черноморским флотом провел переговоры в штабе ВМС Украины',
            'thumbnail': r're:^http://.*\.jpg',
            'duration': 136,
        },
    }, {
        'url': 'http://www.ntv.ru/video/novosti/750370/',
        'md5': 'adecff79691b4d71e25220a191477124',
        'info_dict': {
            'id': '750370',
            'ext': 'mp4',
            'title': 'Родные пассажиров пропавшего Boeing не верят в трагический исход',
            'description': 'Родные пассажиров пропавшего Boeing не верят в трагический исход',
            'thumbnail': r're:^http://.*\.jpg',
            'duration': 172,
        },
    }, {
        'url': 'http://www.ntv.ru/peredacha/segodnya/m23700/o232416',
        'md5': '82dbd49b38e3af1d00df16acbeab260c',
        'info_dict': {
            'id': '747480',
            'ext': 'mp4',
            'title': '«Сегодня». 21 марта 2014 года. 16:00',
            'description': '«Сегодня». 21 марта 2014 года. 16:00',
            'thumbnail': r're:^http://.*\.jpg',
            'duration': 1496,
        },
    }, {
        'url': 'https://www.ntv.ru/kino/Koma_film/m70281/o336036/video/',
        'md5': 'e9c7cde24d9d3eaed545911a04e6d4f4',
        'info_dict': {
            'id': '1126480',
            'ext': 'mp4',
            'title': 'Остросюжетный фильм «Кома»',
            'description': 'Остросюжетный фильм «Кома»',
            'thumbnail': r're:^http://.*\.jpg',
            'duration': 5592,
        },
    }, {
        'url': 'http://www.ntv.ru/serial/Delo_vrachey/m31760/o233916/',
        'md5': '9320cd0e23f3ea59c330dc744e06ff3b',
        'info_dict': {
            'id': '751482',
            'ext': 'mp4',
            'title': '«Дело врачей»: «Деревце жизни»',
            'description': '«Дело врачей»: «Деревце жизни»',
            'thumbnail': r're:^http://.*\.jpg',
            'duration': 2590,
        },
    }, {
        # Schemeless file URL
        'url': 'https://www.ntv.ru/video/1797442',
        'only_matching': True,
    }]

    _VIDEO_ID_REGEXES = [
        r'<meta property="og:url" content="http://www\.ntv\.ru/video/(\d+)',
        r'<video embed=[^>]+><id>(\d+)</id>',
        r'<video restriction[^>]+><key>(\d+)</key>',
    ]

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

        webpage = self._download_webpage(url, video_id)

        video_url = self._og_search_property(
            ('video', 'video:iframe'), webpage, default=None)
        if video_url:
            video_id = self._search_regex(
                r'https?://(?:www\.)?ntv\.ru/video/(?:embed/)?(\d+)',
                video_url, 'video id', default=None)

        if not video_id:
            video_id = self._html_search_regex(
                self._VIDEO_ID_REGEXES, webpage, 'video id')

        player = self._download_xml(
            'http://www.ntv.ru/vi%s/' % video_id,
            video_id, 'Downloading video XML')

        title = strip_or_none(unescapeHTML(xpath_text(player, './data/title', 'title', fatal=True)))

        video = player.find('./data/video')

        formats = []
        for format_id in ['', 'hi', 'webm']:
            file_ = xpath_text(video, './%sfile' % format_id)
            if not file_:
                continue
            if file_.startswith('//'):
                file_ = self._proto_relative_url(file_)
            elif not file_.startswith('http'):
                file_ = 'http://media.ntv.ru/vod/' + file_
            formats.append({
                'url': file_,
                'filesize': int_or_none(xpath_text(video, './%ssize' % format_id)),
            })
        self._sort_formats(formats)

        return {
            'id': xpath_text(video, './id'),
            'title': title,
            'description': strip_or_none(unescapeHTML(xpath_text(player, './data/description'))),
            'thumbnail': xpath_text(video, './splash'),
            'duration': int_or_none(xpath_text(video, './totaltime')),
            'view_count': int_or_none(xpath_text(video, './views')),
            'formats': formats,
        }


code:
from .common import InfoExtractor

from ..utils import (
    float_or_none,
    xpath_text
)

class NuevoBaseIE(InfoExtractor):
    def _extract_nuevo(self, config_url, video_id, headers={}):
        config = self._download_xml(
            config_url, video_id, transform_source=lambda s: s.strip(),
            headers=headers)

        title = xpath_text(config, './title', 'title', fatal=True).strip()
        video_id = xpath_text(config, './mediaid', default=video_id)
        thumbnail = xpath_text(config, ['./image', './thumb'])
        duration = float_or_none(xpath_text(config, './duration'))

        formats = []
        for element_name, format_id in (('file', 'sd'), ('filehd', 'hd')):
            video_url = xpath_text(config, element_name)
            if video_url:
                formats.append({
                    'url': video_url,
                    'format_id': format_id,
                })
        self._check_formats(formats, video_id)

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


code:
import re

from .common import InfoExtractor
from ..utils import (
    parse_duration,
    int_or_none,
    strip_or_none,
    traverse_obj,
    url_or_none,
)

class NuvidIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www|m)\.nuvid\.com/video/(?P<id>[0-9]+)'
    _TESTS = [{
        'url': 'https://www.nuvid.com/video/6513023/italian-babe',
        'md5': '772d2f8288f3d3c5c45f7a41761c7844',
        'info_dict': {
            'id': '6513023',
            'ext': 'mp4',
            'title': 'italian babe',
            'duration': 321.0,
            'age_limit': 18,
            'thumbnail': r're:https?://.+\.jpg',
        }
    }, {
        'url': 'https://m.nuvid.com/video/6523263',
        'md5': 'ebd22ce8e47e1d9a4d0756a15c67da52',
        'info_dict': {
            'id': '6523263',
            'ext': 'mp4',
            'title': 'Slut brunette college student anal dorm',
            'duration': 421.0,
            'age_limit': 18,
            'thumbnail': r're:https?://.+\.jpg',
            'thumbnails': list,
        }
    }, {
        'url': 'http://m.nuvid.com/video/6415801/',
        'md5': '638d5ececb138d5753593f751ae3f697',
        'info_dict': {
            'id': '6415801',
            'ext': 'mp4',
            'title': 'My best friend wanted to fuck my wife for a long time',
            'duration': 1882,
            'age_limit': 18,
            'thumbnail': r're:https?://.+\.jpg',
        }
    }]

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

        qualities = {
            'lq': '360p',
            'hq': '720p',
        }

        json_url = f'https://www.nuvid.com/player_config_json/?vid={video_id}&aid=0&domain_id=0&embed=0&check_speed=0'
        video_data = self._download_json(
            json_url, video_id, headers={
                'Accept': 'application/json, text/javascript, */*; q = 0.01',
                'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
            })

        webpage = self._download_webpage(
            'http://m.nuvid.com/video/%s' % (video_id, ),
            video_id, 'Downloading video page', fatal=False) or ''

        title = strip_or_none(video_data.get('title') or self._html_search_regex(
            (r'''<span\s[^>]*?\btitle\s*=\s*(?P<q>"|'|\b)(?P<title>[^"]+)(?P=q)\s*>''',
                r'''<div\s[^>]*?\bclass\s*=\s*(?P<q>"|'|\b)thumb-holder video(?P=q)>\s*<h5\b[^>]*>(?P<title>[^<]+)</h5''',
                r'''<span\s[^>]*?\bclass\s*=\s*(?P<q>"|'|\b)title_thumb(?P=q)>(?P<title>[^<]+)</span'''),
            webpage, 'title', group='title'))

        formats = [{
            'url': source,
            'format_id': qualities.get(quality),
            'height': int_or_none(qualities.get(quality)[:-1]),
        } for quality, source in video_data.get('files').items() if source]

        self._check_formats(formats, video_id)
        self._sort_formats(formats)

        duration = parse_duration(traverse_obj(video_data, 'duration', 'duration_format'))
        thumbnails = [
            {'url': thumb_url} for thumb_url in re.findall(
                r'<div\s+class\s*=\s*"video-tmb-wrap"\s*>\s*<img\s+src\s*=\s*"([^"]+)"\s*/>', webpage)
            if url_or_none(thumb_url)]
        if url_or_none(video_data.get('poster')):
            thumbnails.append({'url': video_data['poster'], 'preference': 1})

        return {
            'id': video_id,
            'formats': formats,
            'title': title,
            'thumbnails': thumbnails,
            'duration': duration,
            'age_limit': 18,
        }


code:
import hmac
import hashlib
import base64

from .common import InfoExtractor
from ..utils import (
    determine_ext,
    float_or_none,
    int_or_none,
    js_to_json,
    mimetype2ext,
    parse_iso8601,
    remove_start,
)

class NYTimesBaseIE(InfoExtractor):
    _SECRET = b'pX(2MbU2);4N{7J8)>YwKRJ+/pQ3JkiU2Q^V>mFYv6g6gYvt6v'

    def _extract_video_from_id(self, video_id):
        # Authorization generation algorithm is reverse engineered from `signer` in
        # http://graphics8.nytimes.com/video/vhs/vhs-2.x.min.js
        path = '/svc/video/api/v3/video/' + video_id
        hm = hmac.new(self._SECRET, (path + ':vhs').encode(), hashlib.sha512).hexdigest()
        video_data = self._download_json('http://www.nytimes.com' + path, video_id, 'Downloading video JSON', headers={
            'Authorization': 'NYTV ' + base64.b64encode(hm.encode()).decode(),
            'X-NYTV': 'vhs',
        }, fatal=False)
        if not video_data:
            video_data = self._download_json(
                'http://www.nytimes.com/svc/video/api/v2/video/' + video_id,
                video_id, 'Downloading video JSON')

        title = video_data['headline']

        def get_file_size(file_size):
            if isinstance(file_size, int):
                return file_size
            elif isinstance(file_size, dict):
                return int(file_size.get('value', 0))
            else:
                return None

        urls = []
        formats = []
        subtitles = {}
        for video in video_data.get('renditions', []):
            video_url = video.get('url')
            format_id = video.get('type')
            if not video_url or format_id == 'thumbs' or video_url in urls:
                continue
            urls.append(video_url)
            ext = mimetype2ext(video.get('mimetype')) or determine_ext(video_url)
            if ext == 'm3u8':
                m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
                    video_url, video_id, 'mp4', 'm3u8_native',
                    m3u8_id=format_id or 'hls', fatal=False)
                formats.extend(m3u8_fmts)
                subtitles = self._merge_subtitles(subtitles, m3u8_subs)
            elif ext == 'mpd':
                continue
            #     formats.extend(self._extract_mpd_formats(
            #         video_url, video_id, format_id or 'dash', fatal=False))
            else:
                formats.append({
                    'url': video_url,
                    'format_id': format_id,
                    'vcodec': video.get('videoencoding') or video.get('video_codec'),
                    'width': int_or_none(video.get('width')),
                    'height': int_or_none(video.get('height')),
                    'filesize': get_file_size(video.get('file_size') or video.get('fileSize')),
                    'tbr': int_or_none(video.get('bitrate'), 1000) or None,
                    'ext': ext,
                })
        self._sort_formats(formats)

        thumbnails = []
        for image in video_data.get('images', []):
            image_url = image.get('url')
            if not image_url:
                continue
            thumbnails.append({
                'url': 'http://www.nytimes.com/' + image_url,
                'width': int_or_none(image.get('width')),
                'height': int_or_none(image.get('height')),
            })

        publication_date = video_data.get('publication_date')
        timestamp = parse_iso8601(publication_date[:-8]) if publication_date else None

        return {
            'id': video_id,
            'title': title,
            'description': video_data.get('summary'),
            'timestamp': timestamp,
            'uploader': video_data.get('byline'),
            'duration': float_or_none(video_data.get('duration'), 1000),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnails': thumbnails,
        }

class NYTimesIE(NYTimesBaseIE):
    _VALID_URL = r'https?://(?:(?:www\.)?nytimes\.com/video/(?:[^/]+/)+?|graphics8\.nytimes\.com/bcvideo/\d+(?:\.\d+)?/iframe/embed\.html\?videoId=)(?P<id>\d+)'
    _EMBED_REGEX = [r'<iframe[^>]+src=(["\'])(?P<url>(?:https?:)?//graphics8\.nytimes\.com/bcvideo/[^/]+/iframe/embed\.html.+?)\1>']

    _TESTS = [{
        'url': 'http://www.nytimes.com/video/opinion/100000002847155/verbatim-what-is-a-photocopier.html?playlistId=100000001150263',
        'md5': 'd665342765db043f7e225cff19df0f2d',
        'info_dict': {
            'id': '100000002847155',
            'ext': 'mov',
            'title': 'Verbatim: What Is a Photocopier?',
            'description': 'md5:93603dada88ddbda9395632fdc5da260',
            'timestamp': 1398631707,
            'upload_date': '20140427',
            'uploader': 'Brett Weiner',
            'duration': 419,
        }
    }, {
        'url': 'http://www.nytimes.com/video/travel/100000003550828/36-hours-in-dubai.html',
        'only_matching': True,
    }]

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

        return self._extract_video_from_id(video_id)

class NYTimesArticleIE(NYTimesBaseIE):
    _VALID_URL = r'https?://(?:www\.)?nytimes\.com/(.(?<!video))*?/(?:[^/]+/)*(?P<id>[^.]+)(?:\.html)?'
    _TESTS = [{
        'url': 'http://www.nytimes.com/2015/04/14/business/owner-of-gravity-payments-a-credit-card-processor-is-setting-a-new-minimum-wage-70000-a-year.html?_r=0',
        'md5': 'e2076d58b4da18e6a001d53fd56db3c9',
        'info_dict': {
            'id': '100000003628438',
            'ext': 'mov',
            'title': 'New Minimum Wage: $70,000 a Year',
            'description': 'Dan Price, C.E.O. of Gravity Payments, surprised his 120-person staff by announcing that he planned over the next three years to raise the salary of every employee to $70,000 a year.',
            'timestamp': 1429033037,
            'upload_date': '20150414',
            'uploader': 'Matthew Williams',
        }
    }, {
        'url': 'http://www.nytimes.com/2016/10/14/podcasts/revelations-from-the-final-weeks.html',
        'md5': 'e0d52040cafb07662acf3c9132db3575',
        'info_dict': {
            'id': '100000004709062',
            'title': 'The Run-Up: ‘He Was Like an Octopus’',
            'ext': 'mp3',
            'description': 'md5:fb5c6b93b12efc51649b4847fe066ee4',
            'series': 'The Run-Up',
            'episode': '‘He Was Like an Octopus’',
            'episode_number': 20,
            'duration': 2130,
        }
    }, {
        'url': 'http://www.nytimes.com/2016/10/16/books/review/inside-the-new-york-times-book-review-the-rise-of-hitler.html',
        'info_dict': {
            'id': '100000004709479',
            'title': 'The Rise of Hitler',
            'ext': 'mp3',
            'description': 'md5:bce877fd9e3444990cb141875fab0028',
            'creator': 'Pamela Paul',
            'duration': 3475,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.nytimes.com/news/minute/2014/03/17/times-minute-whats-next-in-crimea/?_php=true&_type=blogs&_php=true&_type=blogs&_r=1',
        'only_matching': True,
    }]

    def _extract_podcast_from_json(self, json, page_id, webpage):
        podcast_audio = self._parse_json(
            json, page_id, transform_source=js_to_json)

        audio_data = podcast_audio['data']
        track = audio_data['track']

        episode_title = track['title']
        video_url = track['source']

        description = track.get('description') or self._html_search_meta(
            ['og:description', 'twitter:description'], webpage)

        podcast_title = audio_data.get('podcast', {}).get('title')
        title = ('%s: %s' % (podcast_title, episode_title)
                 if podcast_title else episode_title)

        episode = audio_data.get('podcast', {}).get('episode') or ''
        episode_number = int_or_none(self._search_regex(
            r'[Ee]pisode\s+(\d+)', episode, 'episode number', default=None))

        return {
            'id': remove_start(podcast_audio.get('target'), 'FT') or page_id,
            'url': video_url,
            'title': title,
            'description': description,
            'creator': track.get('credit'),
            'series': podcast_title,
            'episode': episode_title,
            'episode_number': episode_number,
            'duration': int_or_none(track.get('duration')),
        }

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

        webpage = self._download_webpage(url, page_id)

        video_id = self._search_regex(
            r'data-videoid=["\'](\d+)', webpage, 'video id',
            default=None, fatal=False)
        if video_id is not None:
            return self._extract_video_from_id(video_id)

        podcast_data = self._search_regex(
            (r'NYTD\.FlexTypes\.push\s*\(\s*({.+?})\s*\)\s*;\s*</script',
             r'NYTD\.FlexTypes\.push\s*\(\s*({.+})\s*\)\s*;'),
            webpage, 'podcast data')
        return self._extract_podcast_from_json(podcast_data, page_id, webpage)

class NYTimesCookingIE(NYTimesBaseIE):
    _VALID_URL = r'https?://cooking\.nytimes\.com/(?:guid|recip)es/(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://cooking.nytimes.com/recipes/1017817-cranberry-curd-tart',
        'md5': 'dab81fa2eaeb3f9ed47498bdcfcdc1d3',
        'info_dict': {
            'id': '100000004756089',
            'ext': 'mov',
            'timestamp': 1479383008,
            'uploader': 'By SHAW LASH, ADAM SAEWITZ and JAMES HERRON',
            'title': 'Cranberry Tart',
            'upload_date': '20161117',
            'description': 'If you are a fan of lemon curd or the classic French tarte au citron, you will love this cranberry version.',
        },
    }, {
        'url': 'https://cooking.nytimes.com/guides/13-how-to-cook-a-turkey',
        'md5': '4b2e8c70530a89b8d905a2b572316eb8',
        'info_dict': {
            'id': '100000003951728',
            'ext': 'mov',
            'timestamp': 1445509539,
            'description': 'Turkey guide',
            'upload_date': '20151022',
            'title': 'Turkey',
        }
    }]

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

        webpage = self._download_webpage(url, page_id)

        video_id = self._search_regex(
            r'data-video-id=["\'](\d+)', webpage, 'video id')

        return self._extract_video_from_id(video_id)


code:
from .brightcove import BrightcoveNewIE
from .common import InfoExtractor

from ..compat import compat_str
from ..utils import (
    ExtractorError,
    traverse_obj
)

class NZHeraldIE(InfoExtractor):
    IE_NAME = 'nzherald'
    _VALID_URL = r'https?://(?:www\.)?nzherald\.co\.nz/[\w\/-]+\/(?P<id>[A-Z0-9]+)'
    _TESTS = [
        {
            'url': 'https://www.nzherald.co.nz/nz/weather-heavy-rain-gales-across-nz-most-days-this-week/PTG7QWY4E2225YHZ5NAIRBTYTQ/',
            'info_dict': {
                'id': '6271084466001',
                'ext': 'mp4',
                'title': 'MetService severe weather warning: September 6th - 7th',
                'timestamp': 1630891576,
                'upload_date': '20210906',
                'uploader_id': '1308227299001',
                'description': 'md5:db6ca335a22e2cdf37ab9d2bcda52902'
            }

        }, {
            # Webpage has brightcove embed player url
            'url': 'https://www.nzherald.co.nz/travel/pencarrow-coastal-trail/HDVTPJEPP46HJ2UEMK4EGD2DFI/',
            'info_dict': {
                'id': '6261791733001',
                'ext': 'mp4',
                'title': 'Pencarrow Coastal Trail',
                'timestamp': 1625102897,
                'upload_date': '20210701',
                'uploader_id': '1308227299001',
                'description': 'md5:d361aaa0c6498f7ac1bc4fc0a0aec1e4'
            }

        }, {
            # two video embeds of the same video
            'url': 'https://www.nzherald.co.nz/nz/truck-driver-captured-cutting-off-motorist-on-state-highway-1-in-canterbury/FIHNJB7PLLPHWQPK4S7ZBDUC4I/',
            'info_dict': {
                'id': '6251114530001',
                'ext': 'mp4',
                'title': 'Truck travelling north from Rakaia runs car off road',
                'timestamp': 1619730509,
                'upload_date': '20210429',
                'uploader_id': '1308227299001',
                'description': 'md5:4cae7dfb7613ac4c73b9e73a75c6b5d7'
            }
        }, {
            'url': 'https://www.nzherald.co.nz/kahu/kaupapa-companies-my-taiao-supporting-maori-in-study-and-business/PQBO2J25WCG77VGRX7W7BVYEAI/',
            'only_matching': True
        }, {
            'url': 'https://nzherald.co.nz/the-country/video/focus-nzs-first-mass-covid-19-vaccination-event/N5I7IL3BRFLZSD33TLDLYJDGK4/',
            'only_matching': True
        }, {
            'url': 'https://www.nzherald.co.nz/the-vision-is-clear/news/tvic-damian-roper-planting-trees-an-addiction/AN2AAEPNRK5VLISDWQAJZB6ATQ',
            'only_matching': True
        }
    ]

    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/1308227299001/S1BXZn8t_default/index.html?videoId=%s'

    def _extract_bc_embed_url(self, webpage):
        """The initial webpage may include the brightcove player embed url"""
        bc_url = BrightcoveNewIE._extract_url(self, webpage)
        return bc_url or self._search_regex(
            r'(?:embedUrl)\"\s*:\s*\"(?P<embed_url>%s)' % BrightcoveNewIE._VALID_URL,
            webpage, 'embed url', default=None, group='embed_url')

    def _real_extract(self, url):
        article_id = self._match_id(url)
        webpage = self._download_webpage(url, article_id)
        bc_url = self._extract_bc_embed_url(webpage)

        if not bc_url:
            fusion_metadata = self._parse_json(
                self._search_regex(r'Fusion\.globalContent\s*=\s*({.+?})\s*;', webpage, 'fusion metadata'), article_id)

            video_metadata = fusion_metadata.get('video')
            bc_video_id = traverse_obj(
                video_metadata or fusion_metadata,  # fusion metadata is the video metadata for video-only pages
                'brightcoveId', ('content_elements', ..., 'referent', 'id'),
                get_all=False, expected_type=compat_str)

            if not bc_video_id:
                if isinstance(video_metadata, dict) and len(video_metadata) == 0:
                    raise ExtractorError('This article does not have a video.', expected=True)
                else:
                    raise ExtractorError('Failed to extract brightcove video id')
            bc_url = self.BRIGHTCOVE_URL_TEMPLATE % bc_video_id

        return self.url_result(bc_url, 'BrightcoveNew')


code:
import re

from .common import InfoExtractor
from ..utils import (
    extract_attributes,
)

class NZZIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?nzz\.ch/(?:[^/]+/)*[^/?#]+-ld\.(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://www.nzz.ch/zuerich/gymizyte/gymizyte-schreiben-schueler-heute-noch-diktate-ld.9153',
        'info_dict': {
            'id': '9153',
        },
        'playlist_mincount': 6,
    }, {
        'url': 'https://www.nzz.ch/video/nzz-standpunkte/cvp-auf-der-suche-nach-dem-mass-der-mitte-ld.1368112',
        'info_dict': {
            'id': '1368112',
        },
        'playlist_count': 1,
    }]

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

        entries = []
        for player_element in re.findall(
                r'(<[^>]+class="kalturaPlayer[^"]*"[^>]*>)', webpage):
            player_params = extract_attributes(player_element)
            if player_params.get('data-type') not in ('kaltura_singleArticle',):
                self.report_warning('Unsupported player type')
                continue
            entry_id = player_params['data-id']
            entries.append(self.url_result(
                'kaltura:1750922:' + entry_id, 'Kaltura', entry_id))

        return self.playlist_result(entries, page_id)


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

class OdaTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?odatv\.com/(?:mob|vid)_video\.php\?.*\bid=(?P<id>[^&]+)'
    _TESTS = [{
        'url': 'http://odatv.com/vid_video.php?id=8E388',
        'md5': 'dc61d052f205c9bf2da3545691485154',
        'info_dict': {
            'id': '8E388',
            'ext': 'mp4',
            'title': 'Artık Davutoğlu ile devam edemeyiz'
        }
    }, {
        # mobile URL
        'url': 'http://odatv.com/mob_video.php?id=8E388',
        'only_matching': True,
    }, {
        # no video
        'url': 'http://odatv.com/mob_video.php?id=8E900',
        'only_matching': True,
    }]

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

        no_video = 'NO VIDEO!' in webpage

        video_url = self._search_regex(
            r'mp4\s*:\s*(["\'])(?P<url>http.+?)\1', webpage, 'video url',
            default=None if no_video else NO_DEFAULT, group='url')

        if no_video:
            raise ExtractorError('Video %s does not exist' % video_id, expected=True)

        return {
            'id': video_id,
            'url': video_url,
            'title': remove_start(self._og_search_title(webpage), 'Video: '),
            'thumbnail': self._og_search_thumbnail(webpage),
        }


code:
from .common import InfoExtractor
from ..compat import (
    compat_etree_fromstring,
    compat_parse_qs,
    compat_urllib_parse_unquote,
    compat_urllib_parse_urlparse,
)
from ..utils import (
    ExtractorError,
    float_or_none,
    unified_strdate,
    int_or_none,
    qualities,
    unescapeHTML,
    urlencode_postdata,
)

class OdnoklassnikiIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                https?://
                    (?:(?:www|m|mobile)\.)?
                    (?:odnoklassniki|ok)\.ru/
                    (?:
                        video(?:embed)?/|
                        web-api/video/moviePlayer/|
                        live/|
                        dk\?.*?st\.mvId=
                    )
                    (?P<id>[\d-]+)
                '''
    _EMBED_REGEX = [r']+src=(["\'])(?P<url>(?:https?:)?//(?:odnoklassniki|ok)\.ru/videoembed/.+?)\1']
    _TESTS = [{
        'note': 'Coub embedded',
        'url': 'http://ok.ru/video/1484130554189',
        'info_dict': {
            'id': '1keok9',
            'ext': 'mp4',
            'timestamp': 1545580896,
            'view_count': int,
            'thumbnail': 'https://coub-anubis-a.akamaized.net/coub_storage/coub/simple/cw_image/c5ac87553bd/608e806a1239c210ab692/1545580913_00026.jpg',
            'title': 'Народная забава',
            'uploader': 'Nevata',
            'upload_date': '20181223',
            'age_limit': 0,
            'uploader_id': 'nevata.s',
            'like_count': int,
            'duration': 8.08,
            'repost_count': int,
        },
    }, {
        'note': 'vk.com embedded',
        'url': 'https://ok.ru/video/3568183087575',
        'info_dict': {
            'id': '-165101755_456243749',
            'ext': 'mp4',
            'uploader_id': '-165101755',
            'duration': 132,
            'timestamp': 1642869935,
            'upload_date': '20220122',
            'thumbnail': str,
            'title': str,
            'uploader': str,
        },
    }, {
        # metadata in JSON
        'url': 'http://ok.ru/video/20079905452',
        'md5': '0b62089b479e06681abaaca9d204f152',
        'info_dict': {
            'id': '20079905452',
            'ext': 'mp4',
            'title': 'Культура меняет нас (прекрасный ролик!))',
            'duration': 100,
            'upload_date': '20141207',
            'uploader_id': '330537914540',
            'uploader': 'Виталий Добровольский',
            'like_count': int,
            'age_limit': 0,
        },
    }, {
        # metadataUrl
        'url': 'http://ok.ru/video/63567059965189-0?fromTime=5',
        'md5': '6ff470ea2dd51d5d18c295a355b0b6bc',
        'info_dict': {
            'id': '63567059965189-0',
            'ext': 'mp4',
            'title': 'Девушка без комплексов ...',
            'duration': 191,
            'upload_date': '20150518',
            'uploader_id': '534380003155',
            'uploader': '☭ Андрей Мещанинов ☭',
            'like_count': int,
            'age_limit': 0,
            'start_time': 5,
        },
    }, {
        # YouTube embed (metadataUrl, provider == USER_YOUTUBE)
        'url': 'http://ok.ru/video/64211978996595-1',
        'md5': '2f206894ffb5dbfcce2c5a14b909eea5',
        'info_dict': {
            'id': 'V_VztHT5BzY',
            'ext': 'mp4',
            'title': 'Космическая среда от 26 августа 2015',
            'description': 'md5:848eb8b85e5e3471a3a803dae1343ed0',
            'duration': 440,
            'upload_date': '20150826',
            'uploader_id': 'tvroscosmos',
            'uploader': 'Телестудия Роскосмоса',
            'age_limit': 0,
        },
    }, {
        # YouTube embed (metadata, provider == USER_YOUTUBE, no metadata.movie.title field)
        'url': 'http://ok.ru/video/62036049272859-0',
        'info_dict': {
            'id': '62036049272859-0',
            'ext': 'mp4',
            'title': 'МУЗЫКА     ДОЖДЯ .',
            'description': 'md5:6f1867132bd96e33bf53eda1091e8ed0',
            'upload_date': '20120106',
            'uploader_id': '473534735899',
            'uploader': 'МARINA D',
            'age_limit': 0,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'Video has not been found',
    }, {
        'note': 'Only available in mobile webpage',
        'url': 'https://m.ok.ru/video/2361249957145',
        'info_dict': {
            'id': '2361249957145',
            'title': 'Быковское крещение',
            'duration': 3038.181,
        },
    }, {
        'url': 'http://ok.ru/web-api/video/moviePlayer/20079905452',
        'only_matching': True,
    }, {
        'url': 'http://www.ok.ru/video/20648036891',
        'only_matching': True,
    }, {
        'url': 'http://www.ok.ru/videoembed/20648036891',
        'only_matching': True,
    }, {
        'url': 'http://m.ok.ru/video/20079905452',
        'only_matching': True,
    }, {
        'url': 'http://mobile.ok.ru/video/20079905452',
        'only_matching': True,
    }, {
        'url': 'https://www.ok.ru/live/484531969818',
        'only_matching': True,
    }, {
        'url': 'https://m.ok.ru/dk?st.cmd=movieLayer&st.discId=863789452017&st.retLoc=friend&st.rtu=%2Fdk%3Fst.cmd%3DfriendMovies%26st.mode%3Down%26st.mrkId%3D%257B%2522uploadedMovieMarker%2522%253A%257B%2522marker%2522%253A%25221519410114503%2522%252C%2522hasMore%2522%253Atrue%257D%252C%2522sharedMovieMarker%2522%253A%257B%2522marker%2522%253Anull%252C%2522hasMore%2522%253Afalse%257D%257D%26st.friendId%3D561722190321%26st.frwd%3Don%26_prevCmd%3DfriendMovies%26tkn%3D7257&st.discType=MOVIE&st.mvId=863789452017&_prevCmd=friendMovies&tkn=3648#lst#',
        'only_matching': True,
    }, {
        # Paid video
        'url': 'https://ok.ru/video/954886983203',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        try:
            return self._extract_desktop(url)
        except ExtractorError as e:
            try:
                return self._extract_mobile(url)
            except ExtractorError:
                # error message of desktop webpage is in English
                raise e

    def _extract_desktop(self, url):
        start_time = int_or_none(compat_parse_qs(
            compat_urllib_parse_urlparse(url).query).get('fromTime', [None])[0])

        video_id = self._match_id(url)

        webpage = self._download_webpage(
            'http://ok.ru/video/%s' % video_id, video_id,
            note='Downloading desktop webpage')

        error = self._search_regex(
            r'[^>]+class="vp_video_stub_txt"[^>]*>([^<]+)<',
            webpage, 'error', default=None)
        if error:
            raise ExtractorError(error, expected=True)

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

        # embedded external player
        if player.get('isExternalPlayer') and player.get('url'):
            return self.url_result(player['url'])

        flashvars = player['flashvars']

        metadata = flashvars.get('metadata')
        if metadata:
            metadata = self._parse_json(metadata, video_id)
        else:
            data = {}
            st_location = flashvars.get('location')
            if st_location:
                data['st.location'] = st_location
            metadata = self._download_json(
                compat_urllib_parse_unquote(flashvars['metadataUrl']),
                video_id, 'Downloading metadata JSON',
                data=urlencode_postdata(data))

        movie = metadata['movie']

        # Some embedded videos may not contain title in movie dict (e.g.
        # http://ok.ru/video/62036049272859-0) thus we allow missing title
        # here and it's going to be extracted later by an extractor that
        # will process the actual embed.
        provider = metadata.get('provider')
        title = movie['title'] if provider == 'UPLOADED_ODKL' else movie.get('title')

        thumbnail = movie.get('poster')
        duration = int_or_none(movie.get('duration'))

        author = metadata.get('author', {})
        uploader_id = author.get('id')
        uploader = author.get('name')

        upload_date = unified_strdate(self._html_search_meta(
            'ya:ovs:upload_date', webpage, 'upload date', default=None))

        age_limit = None
        adult = self._html_search_meta(
            'ya:ovs:adult', webpage, 'age limit', default=None)
        if adult:
            age_limit = 18 if adult == 'true' else 0

        like_count = int_or_none(metadata.get('likeCount'))

        info = {
            'id': video_id,
            'title': title,
            'thumbnail': thumbnail,
            'duration': duration,
            'upload_date': upload_date,
            'uploader': uploader,
            'uploader_id': uploader_id,
            'like_count': like_count,
            'age_limit': age_limit,
            'start_time': start_time,
        }

        # pladform
        if provider == 'OPEN_GRAPH':
            info.update({
                '_type': 'url_transparent',
                'url': movie['contentId'],
            })
            return info

        if provider == 'USER_YOUTUBE':
            info.update({
                '_type': 'url_transparent',
                'url': movie['contentId'],
            })
            return info

        assert title
        if provider == 'LIVE_TV_APP':
            info['title'] = title

        quality = qualities(('4', '0', '1', '2', '3', '5'))

        formats = [{
            'url': f['url'],
            'ext': 'mp4',
            'format_id': f['name'],
        } for f in metadata['videos']]

        m3u8_url = metadata.get('hlsManifestUrl')
        if m3u8_url:
            formats.extend(self._extract_m3u8_formats(
                m3u8_url, video_id, 'mp4', 'm3u8_native',
                m3u8_id='hls', fatal=False))

        dash_manifest = metadata.get('metadataEmbedded')
        if dash_manifest:
            formats.extend(self._parse_mpd_formats(
                compat_etree_fromstring(dash_manifest), 'mpd'))

        for fmt in formats:
            fmt_type = self._search_regex(
                r'\btype[/=](\d)', fmt['url'],
                'format type', default=None)
            if fmt_type:
                fmt['quality'] = quality(fmt_type)

        # Live formats
        m3u8_url = metadata.get('hlsMasterPlaylistUrl')
        if m3u8_url:
            formats.extend(self._extract_m3u8_formats(
                m3u8_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
        rtmp_url = metadata.get('rtmpUrl')
        if rtmp_url:
            formats.append({
                'url': rtmp_url,
                'format_id': 'rtmp',
                'ext': 'flv',
            })

        if not formats:
            payment_info = metadata.get('paymentInfo')
            if payment_info:
                self.raise_no_formats('This video is paid, subscribe to download it', expected=True)

        self._sort_formats(formats)

        info['formats'] = formats
        return info

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

        webpage = self._download_webpage(
            'http://m.ok.ru/video/%s' % video_id, video_id,
            note='Downloading mobile webpage')

        error = self._search_regex(
            r'видео\s*
(.+?)
',
            webpage, 'error', default=None)
        if error:
            raise ExtractorError(error, expected=True)

        json_data = self._search_regex(
            r'data-video="(.+?)"', webpage, 'json data')
        json_data = self._parse_json(unescapeHTML(json_data), video_id) or {}

        return {
            'id': video_id,
            'title': json_data.get('videoName'),
            'duration': float_or_none(json_data.get('videoDuration'), scale=1000),
            'thumbnail': json_data.get('videoPosterSrc'),
            'formats': [{
                'format_id': 'mobile',
                'url': json_data.get('videoSrc'),
                'ext': 'mp4',
            }]
        }


code:
from .common import InfoExtractor

class OktoberfestTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?oktoberfest-tv\.de/[^/]+/[^/]+/video/(?P<id>[^/?#]+)'

    _TEST = {
        'url': 'http://www.oktoberfest-tv.de/de/kameras/video/hb-zelt',
        'info_dict': {
            'id': 'hb-zelt',
            'ext': 'mp4',
            'title': 're:^Live-Kamera: Hofbräuzelt [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'thumbnail': r're:^https?://.*\.jpg$',
            'is_live': True,
        },
        'params': {
            'skip_download': True,
        }
    }

    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')

        clip = self._search_regex(
            r"clip:\s*\{\s*url:\s*'([^']+)'", webpage, 'clip')
        ncurl = self._search_regex(
            r"netConnectionUrl:\s*'([^']+)'", webpage, 'rtmp base')
        video_url = ncurl + clip
        thumbnail = self._search_regex(
            r"canvas:\s*\{\s*backgroundImage:\s*'url\(([^)]+)\)'", webpage,
            'thumbnail', fatal=False)

        return {
            'id': video_id,
            'title': title,
            'url': video_url,
            'ext': 'mp4',
            'is_live': True,
            'thumbnail': thumbnail,
        }


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

class OlympicsReplayIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?olympics\.com(?:/tokyo-2020)?/[a-z]{2}/(?:replay|video)/(?P<id>[^/#&?]+)'
    _TESTS = [{
        'url': 'https://olympics.com/fr/video/men-s-109kg-group-a-weightlifting-tokyo-2020-replays',
        'info_dict': {
            'id': 'f6a0753c-8e6f-4b7d-a435-027054a4f8e9',
            'ext': 'mp4',
            'title': '+109kg (H) Groupe A - Haltérophilie | Replay de Tokyo 2020',
            'upload_date': '20210801',
            'timestamp': 1627783200,
            'description': 'md5:c66af4a5bc7429dbcc43d15845ff03b3',
            'uploader': 'International Olympic Committee',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://olympics.com/tokyo-2020/en/replay/bd242924-4b22-49a5-a846-f1d4c809250d/mens-bronze-medal-match-hun-esp',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, id)
        title = self._html_search_meta(('title', 'og:title', 'twitter:title'), webpage)
        uuid = self._html_search_meta('episode_uid', webpage)
        m3u8_url = self._html_search_meta('video_url', webpage)
        json_ld = self._search_json_ld(webpage, uuid)
        thumbnails_list = json_ld.get('image')
        if not thumbnails_list:
            thumbnails_list = self._html_search_regex(
                r'["\']image["\']:\s*["\']([^"\']+)["\']', webpage, 'images', default='')
            thumbnails_list = thumbnails_list.replace('[', '').replace(']', '').split(',')
            thumbnails_list = [thumbnail.strip() for thumbnail in thumbnails_list]
        thumbnails = []
        for thumbnail in thumbnails_list:
            width_a, height_a, width = self._search_regex(
                r'/images/image/private/t_(?P<width_a>\d+)-(?P<height_a>\d+)_(?P<width>\d+)/primary/[\W\w\d]+',
                thumbnail, 'thumb', group=(1, 2, 3), default=(None, None, None))
            width_a, height_a, width = int_or_none(width_a), int_or_none(height_a), int_or_none(width)
            thumbnails.append({
                'url': thumbnail,
                'width': width,
                'height': int_or_none(try_get(width, lambda x: x * height_a / width_a))
            })
        m3u8_url = self._download_json(
            f'https://olympics.com/tokenGenerator?url={m3u8_url}', uuid, note='Downloading m3u8 url')
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, uuid, 'mp4', m3u8_id='hls')
        self._sort_formats(formats)

        return {
            'id': uuid,
            'title': title,
            'thumbnails': thumbnails,
            'formats': formats,
            'subtitles': subtitles,
            **json_ld
        }


code:
from .common import InfoExtractor
from ..utils import (
    int_or_none,
    strip_or_none,
    try_get,
    urljoin,
)

class On24IE(InfoExtractor):
    IE_NAME = 'on24'
    IE_DESC = 'ON24'

    _VALID_URL = r'''(?x)
                    https?://event\.on24\.com/(?:
                        wcc/r/(?P<id_1>\d{7})/(?P<key_1>[0-9A-F]{32})|
                        eventRegistration/(?:console/EventConsoleApollo|EventLobbyServlet\?target=lobby30)
                            \.jsp\?(?:[^/#?]*&)?eventid=(?P<id_2>\d{7})[^/#?]*&key=(?P<key_2>[0-9A-F]{32})
                    )'''

    _TESTS = [{
        'url': 'https://event.on24.com/eventRegistration/console/EventConsoleApollo.jsp?uimode=nextgeneration&eventid=2197467&sessionid=1&key=5DF57BE53237F36A43B478DD36277A84&contenttype=A&eventuserid=305999&playerwidth=1000&playerheight=650&caller=previewLobby&text_language_id=en&format=fhaudio&newConsole=false',
        'info_dict': {
            'id': '2197467',
            'ext': 'wav',
            'title': 'Pearson Test of English General/Pearson English International Certificate Teacher Training Guide',
            'upload_date': '20200219',
            'timestamp': 1582149600.0,
            'view_count': int,
        }
    }, {
        'url': 'https://event.on24.com/wcc/r/2639291/82829018E813065A122363877975752E?mode=login&email=johnsmith@gmail.com',
        'only_matching': True,
    }, {
        'url': 'https://event.on24.com/eventRegistration/console/EventConsoleApollo.jsp?&eventid=2639291&sessionid=1&username=&partnerref=&format=fhvideo1&mobile=&flashsupportedmobiledevice=&helpcenter=&key=82829018E813065A122363877975752E&newConsole=true&nxChe=true&newTabCon=true&text_language_id=en&playerwidth=748&playerheight=526&eventuserid=338788762&contenttype=A&mediametricsessionid=384764716&mediametricid=3558192&usercd=369267058&mode=launch',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        event_id = mobj.group('id_1') or mobj.group('id_2')
        event_key = mobj.group('key_1') or mobj.group('key_2')

        event_data = self._download_json(
            'https://event.on24.com/apic/utilApp/EventConsoleCachedServlet',
            event_id, query={
                'eventId': event_id,
                'displayProfile': 'player',
                'key': event_key,
                'contentType': 'A'
            })
        event_id = str(try_get(event_data, lambda x: x['presentationLogInfo']['eventid'])) or event_id
        language = event_data.get('localelanguagecode')

        formats = []
        for media in event_data.get('mediaUrlInfo', []):
            media_url = urljoin('https://event.on24.com/media/news/corporatevideo/events/', str(media.get('url')))
            if not media_url:
                continue
            media_type = media.get('code')
            if media_type == 'fhvideo1':
                formats.append({
                    'format_id': 'video',
                    'url': media_url,
                    'language': language,
                    'ext': 'mp4',
                    'vcodec': 'avc1.640020',
                    'acodec': 'mp4a.40.2',
                })
            elif media_type == 'audio':
                formats.append({
                    'format_id': 'audio',
                    'url': media_url,
                    'language': language,
                    'ext': 'wav',
                    'vcodec': 'none',
                    'acodec': 'wav'
                })
        self._sort_formats(formats)

        return {
            'id': event_id,
            'title': strip_or_none(event_data.get('description')),
            'timestamp': int_or_none(try_get(event_data, lambda x: x['session']['startdate']), 1000),
            'webpage_url': f'https://event.on24.com/wcc/r/{event_id}/{event_key}',
            'view_count': event_data.get('registrantcount'),
            'formats': formats,
        }


code:
import re

from .common import InfoExtractor

class OnceIE(InfoExtractor):
    _VALID_URL = r'https?://.+?\.unicornmedia\.com/now/(?:ads/vmap/)?[^/]+/[^/]+/(?P<domain_id>[^/]+)/(?P<application_id>[^/]+)/(?:[^/]+/)?(?P<media_item_id>[^/]+)/content\.(?:once|m3u8|mp4)'
    ADAPTIVE_URL_TEMPLATE = 'http://once.unicornmedia.com/now/master/playlist/%s/%s/%s/content.m3u8'
    PROGRESSIVE_URL_TEMPLATE = 'http://once.unicornmedia.com/now/media/progressive/%s/%s/%s/%s/content.mp4'

    def _extract_once_formats(self, url, http_formats_preference=None):
        domain_id, application_id, media_item_id = re.match(
            OnceIE._VALID_URL, url).groups()
        formats = self._extract_m3u8_formats(
            self.ADAPTIVE_URL_TEMPLATE % (
                domain_id, application_id, media_item_id),
            media_item_id, 'mp4', m3u8_id='hls', fatal=False)
        progressive_formats = []
        for adaptive_format in formats:
            # Prevent advertisement from embedding into m3u8 playlist (see
            # https://github.com/ytdl-org/youtube-dl/issues/8893#issuecomment-199912684)
            adaptive_format['url'] = re.sub(
                r'\badsegmentlength=\d+', r'adsegmentlength=0', adaptive_format['url'])
            rendition_id = self._search_regex(
                r'/now/media/playlist/[^/]+/[^/]+/([^/]+)',
                adaptive_format['url'], 'redition id', default=None)
            if rendition_id:
                progressive_format = adaptive_format.copy()
                progressive_format.update({
                    'url': self.PROGRESSIVE_URL_TEMPLATE % (
                        domain_id, application_id, rendition_id, media_item_id),
                    'format_id': adaptive_format['format_id'].replace(
                        'hls', 'http'),
                    'protocol': 'http',
                    'preference': http_formats_preference,
                })
                progressive_formats.append(progressive_format)
        self._check_formats(progressive_formats, media_item_id)
        formats.extend(progressive_formats)
        return formats


code:
import re

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

class OnDemandKoreaIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?ondemandkorea\.com/(?P<id>[^/]+)\.html'
    _GEO_COUNTRIES = ['US', 'CA']
    _TESTS = [{
        'url': 'https://www.ondemandkorea.com/ask-us-anything-e43.html',
        'info_dict': {
            'id': 'ask-us-anything-e43',
            'ext': 'mp4',
            'title': 'Ask Us Anything : Gain, Ji Soo - 09/24/2016',
            'description': 'A talk show/game show with a school theme where celebrity guests appear as “transfer students.”',
            'thumbnail': r're:^https?://.*\.jpg$',
        },
        'params': {
            'skip_download': 'm3u8 download'
        }
    }, {
        'url': 'https://www.ondemandkorea.com/confession-e01-1.html',
        'info_dict': {
            'id': 'confession-e01-1',
            'ext': 'mp4',
            'title': 'Confession : E01',
            'description': 'Choi Do-hyun, a criminal attorney, is the son of a death row convict. Ever since Choi Pil-su got arrested for murder, Do-hyun has wanted to solve his ',
            'thumbnail': r're:^https?://.*\.jpg$',
            'subtitles': {
                'English': 'mincount:1',
            },
        },
        'params': {
            'skip_download': 'm3u8 download'
        }
    }]

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

        if not webpage:
            # Page sometimes returns captcha page with HTTP 403
            raise ExtractorError(
                'Unable to access page. You may have been blocked.',
                expected=True)

        if 'msg_block_01.png' in webpage:
            self.raise_geo_restricted(
                msg='This content is not available in your region',
                countries=self._GEO_COUNTRIES)

        if 'This video is only available to ODK PLUS members.' in webpage:
            raise ExtractorError(
                'This video is only available to ODK PLUS members.',
                expected=True)

        if 'ODK PREMIUM Members Only' in webpage:
            raise ExtractorError(
                'This video is only available to ODK PREMIUM members.',
                expected=True)

        title = self._search_regex(
            r'class=["\']episode_title["\'][^>]*>([^<]+)',
            webpage, 'episode_title', fatal=False) or self._og_search_title(webpage)

        jw_config = self._parse_json(
            self._search_regex(
                r'playlist\s*=\s*\[(?P<options>.+)];?$',
                webpage, 'jw config', flags=re.MULTILINE, group='options'),
            video_id, transform_source=js_to_json)
        info = self._parse_jwplayer_data(
            jw_config, video_id, require_title=False, m3u8_id='hls',
            base_url=url)

        info.update({
            'title': title,
            'description': self._og_search_description(webpage),
            'thumbnail': self._og_search_thumbnail(webpage)
        })
        return info


code:
from .common import InfoExtractor

class OneFootballIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?onefootball\.com/[a-z]{2}/video/[^/&?#]+-(?P<id>\d+)'

    _TESTS = [{
        'url': 'https://onefootball.com/en/video/highlights-fc-zuerich-3-3-fc-basel-34012334',
        'info_dict': {
            'id': '34012334',
            'ext': 'mp4',
            'title': 'Highlights: FC Zürich 3-3 FC Basel',
            'description': 'md5:33d9855cb790702c4fe42a513700aba8',
            'thumbnail': 'https://photobooth-api.onefootball.com/api/screenshot/https:%2F%2Fperegrine-api.onefootball.com%2Fv2%2Fphotobooth%2Fcms%2Fen%2F34012334',
            'timestamp': 1635874604,
            'upload_date': '20211102'
        },
        'params': {'skip_download': True}
    }, {
        'url': 'https://onefootball.com/en/video/klopp-fumes-at-var-decisions-in-west-ham-defeat-34041020',
        'info_dict': {
            'id': '34041020',
            'ext': 'mp4',
            'title': 'Klopp fumes at VAR decisions in West Ham defeat',
            'description': 'md5:9c50371095a01ad3f63311c73d8f51a5',
            'thumbnail': 'https://photobooth-api.onefootball.com/api/screenshot/https:%2F%2Fperegrine-api.onefootball.com%2Fv2%2Fphotobooth%2Fcms%2Fen%2F34041020',
            'timestamp': 1636314103,
            'upload_date': '20211107'
        },
        'params': {'skip_download': True}
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        data_json = self._search_json_ld(webpage, id)
        m3u8_url = self._html_search_regex(r'(https://cdn\.jwplayer\.com/manifests/.+\.m3u8)', webpage, 'm3u8_url')
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, id)
        self._sort_formats(formats)
        return {
            'id': id,
            'title': data_json.get('title'),
            'description': data_json.get('description'),
            'thumbnail': data_json.get('thumbnail'),
            'timestamp': data_json.get('timestamp'),
            'formats': formats,
            'subtitles': subtitles,
        }