code:
import itertools
import re
from urllib.parse import urlparse, parse_qs

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    clean_html,
    dict_get,
    int_or_none,
    merge_dicts,
    parse_duration,
    traverse_obj,
    try_get,
    unified_timestamp,
    update_url_query,
)

class NaverBaseIE(InfoExtractor):
    _CAPTION_EXT_RE = r'\.(?:ttml|vtt)'

    def _extract_video_info(self, video_id, vid, key):
        video_data = self._download_json(
            'http://play.rmcnmv.naver.com/vod/play/v2.0/' + vid,
            video_id, query={
                'key': key,
            })
        meta = video_data['meta']
        title = meta['subject']
        formats = []
        get_list = lambda x: try_get(video_data, lambda y: y[x + 's']['list'], list) or []

        def extract_formats(streams, stream_type, query={}):
            for stream in streams:
                stream_url = stream.get('source')
                if not stream_url:
                    continue
                stream_url = update_url_query(stream_url, query)
                encoding_option = stream.get('encodingOption', {})
                bitrate = stream.get('bitrate', {})
                formats.append({
                    'format_id': '%s_%s' % (stream.get('type') or stream_type, dict_get(encoding_option, ('name', 'id'))),
                    'url': stream_url,
                    'ext': 'mp4',
                    'width': int_or_none(encoding_option.get('width')),
                    'height': int_or_none(encoding_option.get('height')),
                    'vbr': int_or_none(bitrate.get('video')),
                    'abr': int_or_none(bitrate.get('audio')),
                    'filesize': int_or_none(stream.get('size')),
                    'protocol': 'm3u8_native' if stream_type == 'HLS' else None,
                })

        extract_formats(get_list('video'), 'H264')
        for stream_set in video_data.get('streams', []):
            query = {}
            for param in stream_set.get('keys', []):
                query[param['name']] = param['value']
            stream_type = stream_set.get('type')
            videos = stream_set.get('videos')
            if videos:
                extract_formats(videos, stream_type, query)
            elif stream_type == 'HLS':
                stream_url = stream_set.get('source')
                if not stream_url:
                    continue
                formats.extend(self._extract_m3u8_formats(
                    update_url_query(stream_url, query), video_id,
                    'mp4', 'm3u8_native', m3u8_id=stream_type, fatal=False))
        self._sort_formats(formats)

        replace_ext = lambda x, y: re.sub(self._CAPTION_EXT_RE, '.' + y, x)

        def get_subs(caption_url):
            if re.search(self._CAPTION_EXT_RE, caption_url):
                return [{
                    'url': replace_ext(caption_url, 'ttml'),
                }, {
                    'url': replace_ext(caption_url, 'vtt'),
                }]
            else:
                return [{'url': caption_url}]

        automatic_captions = {}
        subtitles = {}
        for caption in get_list('caption'):
            caption_url = caption.get('source')
            if not caption_url:
                continue
            sub_dict = automatic_captions if caption.get('type') == 'auto' else subtitles
            sub_dict.setdefault(dict_get(caption, ('locale', 'language')), []).extend(get_subs(caption_url))

        user = meta.get('user', {})

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'subtitles': subtitles,
            'automatic_captions': automatic_captions,
            'thumbnail': try_get(meta, lambda x: x['cover']['source']),
            'view_count': int_or_none(meta.get('count')),
            'uploader_id': user.get('id'),
            'uploader': user.get('name'),
            'uploader_url': user.get('url'),
        }

class NaverIE(NaverBaseIE):
    _VALID_URL = r'https?://(?:m\.)?tv(?:cast)?\.naver\.com/(?:v|embed)/(?P<id>\d+)'
    _GEO_BYPASS = False
    _TESTS = [{
        'url': 'http://tv.naver.com/v/81652',
        'info_dict': {
            'id': '81652',
            'ext': 'mp4',
            'title': '[9월 모의고사 해설강의][수학_김상희] 수학 A형 16~20번',
            'description': '메가스터디 수학 김상희 선생님이 9월 모의고사 수학A형 16번에서 20번까지 해설강의를 공개합니다.',
            'timestamp': 1378200754,
            'upload_date': '20130903',
            'uploader': '메가스터디, 합격불변의 법칙',
            'uploader_id': 'megastudy',
        },
    }, {
        'url': 'http://tv.naver.com/v/395837',
        'md5': '8a38e35354d26a17f73f4e90094febd3',
        'info_dict': {
            'id': '395837',
            'ext': 'mp4',
            'title': '9년이 지나도 아픈 기억, 전효성의 아버지',
            'description': 'md5:eb6aca9d457b922e43860a2a2b1984d3',
            'timestamp': 1432030253,
            'upload_date': '20150519',
            'uploader': '4가지쇼 시즌2',
            'uploader_id': 'wrappinguser29',
        },
        'skip': 'Georestricted',
    }, {
        'url': 'http://tvcast.naver.com/v/81652',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        content = self._download_json(
            'https://tv.naver.com/api/json/v/' + video_id,
            video_id, headers=self.geo_verification_headers())
        player_info_json = content.get('playerInfoJson') or {}
        current_clip = player_info_json.get('currentClip') or {}

        vid = current_clip.get('videoId')
        in_key = current_clip.get('inKey')

        if not vid or not in_key:
            player_auth = try_get(player_info_json, lambda x: x['playerOption']['auth'])
            if player_auth == 'notCountry':
                self.raise_geo_restricted(countries=['KR'])
            elif player_auth == 'notLogin':
                self.raise_login_required()
            raise ExtractorError('couldn\'t extract vid and key')
        info = self._extract_video_info(video_id, vid, in_key)
        info.update({
            'description': clean_html(current_clip.get('description')),
            'timestamp': int_or_none(current_clip.get('firstExposureTime'), 1000),
            'duration': parse_duration(current_clip.get('displayPlayTime')),
            'like_count': int_or_none(current_clip.get('recommendPoint')),
            'age_limit': 19 if current_clip.get('adult') else None,
        })
        return info

class NaverLiveIE(InfoExtractor):
    IE_NAME = 'Naver:live'
    _VALID_URL = r'https?://(?:m\.)?tv(?:cast)?\.naver\.com/l/(?P<id>\d+)'
    _GEO_BYPASS = False
    _TESTS = [{
        'url': 'https://tv.naver.com/l/52010',
        'info_dict': {
            'id': '52010',
            'ext': 'mp4',
            'title': '[LIVE] 뉴스특보 : "수도권 거리두기, 2주간 2단계로 조정"',
            'description': 'md5:df7f0c237a5ed5e786ce5c91efbeaab3',
            'channel_id': 'NTV-ytnnews24-0',
            'start_time': 1597026780000,
        },
    }, {
        'url': 'https://tv.naver.com/l/51549',
        'info_dict': {
            'id': '51549',
            'ext': 'mp4',
            'title': '연합뉴스TV - 코로나19 뉴스특보',
            'description': 'md5:c655e82091bc21e413f549c0eaccc481',
            'channel_id': 'NTV-yonhapnewstv-0',
            'start_time': 1596406380000,
        },
    }, {
        'url': 'https://tv.naver.com/l/54887',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        page = self._download_webpage(url, video_id, 'Downloading Page', 'Unable to download Page')
        secure_url = self._search_regex(r'sApiF:\s+(?:"|\')([^"\']+)', page, 'secureurl')

        info = self._extract_video_info(video_id, secure_url)
        info.update({
            'description': self._og_search_description(page)
        })

        return info

    def _extract_video_info(self, video_id, url):
        video_data = self._download_json(url, video_id, headers=self.geo_verification_headers())
        meta = video_data.get('meta')
        status = meta.get('status')

        if status == 'CLOSED':
            raise ExtractorError('Stream is offline.', expected=True)
        elif status != 'OPENED':
            raise ExtractorError('Unknown status %s' % status)

        title = meta.get('title')
        stream_list = video_data.get('streams')

        if stream_list is None:
            raise ExtractorError('Could not get stream data.', expected=True)

        formats = []
        for quality in stream_list:
            if not quality.get('url'):
                continue

            prop = quality.get('property')
            if prop.get('abr'):  # This abr doesn't mean Average audio bitrate.
                continue

            formats.extend(self._extract_m3u8_formats(
                quality.get('url'), video_id, 'mp4',
                m3u8_id=quality.get('qualityId'), live=True
            ))
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'channel_id': meta.get('channelId'),
            'channel_url': meta.get('channelUrl'),
            'thumbnail': meta.get('imgUrl'),
            'start_time': meta.get('startTime'),
            'categories': [meta.get('categoryId')],
            'is_live': True
        }

class NaverNowIE(NaverBaseIE):
    IE_NAME = 'navernow'
    _VALID_URL = r'https?://now\.naver\.com/s/now\.(?P<id>[0-9]+)'
    _API_URL = 'https://apis.naver.com/now_web/oldnow_web/v4'
    _TESTS = [{
        'url': 'https://now.naver.com/s/now.4759?shareReplayId=26331132#replay=',
        'md5': 'e05854162c21c221481de16b2944a0bc',
        'info_dict': {
            'id': '4759-26331132',
            'title': '아이키X노제\r\n💖꽁냥꽁냥💖(1)',
            'ext': 'mp4',
            'thumbnail': r're:^https?://.*\.jpg',
            'timestamp': 1650369600,
            'upload_date': '20220419',
            'uploader_id': 'now',
            'view_count': int,
            'uploader_url': 'https://now.naver.com/show/4759',
            'uploader': '아이키의 떰즈업',
        },
        'params': {
            'noplaylist': True,
        }
    }, {
        'url': 'https://now.naver.com/s/now.4759?shareHightlight=26601461#highlight=',
        'md5': '9f6118e398aa0f22b2152f554ea7851b',
        'info_dict': {
            'id': '4759-26601461',
            'title': '아이키: 나 리정한테 흔들렸어,,, 질투 폭발하는 노제 여보😾 [아이키의 떰즈업]ㅣ네이버 NOW.',
            'ext': 'mp4',
            'thumbnail': r're:^https?://.*\.jpg',
            'upload_date': '20220504',
            'timestamp': 1651648311,
            'uploader_id': 'now',
            'view_count': int,
            'uploader_url': 'https://now.naver.com/show/4759',
            'uploader': '아이키의 떰즈업',
        },
        'params': {
            'noplaylist': True,
        },
    }, {
        'url': 'https://now.naver.com/s/now.4759',
        'info_dict': {
            'id': '4759',
            'title': '아이키의 떰즈업',
        },
        'playlist_mincount': 101
    }, {
        'url': 'https://now.naver.com/s/now.4759?shareReplayId=26331132#replay',
        'info_dict': {
            'id': '4759',
            'title': '아이키의 떰즈업',
        },
        'playlist_mincount': 101,
    }, {
        'url': 'https://now.naver.com/s/now.4759?shareHightlight=26601461#highlight=',
        'info_dict': {
            'id': '4759',
            'title': '아이키의 떰즈업',
        },
        'playlist_mincount': 101,
    }]

    def _extract_replay(self, show_id, replay_id):
        vod_info = self._download_json(f'{self._API_URL}/shows/now.{show_id}/vod/{replay_id}', replay_id)
        in_key = self._download_json(f'{self._API_URL}/shows/now.{show_id}/vod/{replay_id}/inkey', replay_id)['inKey']
        return merge_dicts({
            'id': f'{show_id}-{replay_id}',
            'title': traverse_obj(vod_info, ('episode', 'title')),
            'timestamp': unified_timestamp(traverse_obj(vod_info, ('episode', 'start_time'))),
            'thumbnail': vod_info.get('thumbnail_image_url'),
        }, self._extract_video_info(replay_id, vod_info['video_id'], in_key))

    def _extract_show_replays(self, show_id):
        page_size = 15
        page = 1
        while True:
            show_vod_info = self._download_json(
                f'{self._API_URL}/vod-shows/now.{show_id}', show_id,
                query={'page': page, 'page_size': page_size},
                note=f'Downloading JSON vod list for show {show_id} - page {page}'
            )['response']['result']
            for v in show_vod_info.get('vod_list') or []:
                yield self._extract_replay(show_id, v['id'])

            if len(show_vod_info.get('vod_list') or []) < page_size:
                break
            page += 1

    def _extract_show_highlights(self, show_id, highlight_id=None):
        page_size = 10
        page = 1
        while True:
            highlights_videos = self._download_json(
                f'{self._API_URL}/shows/now.{show_id}/highlights/videos/', show_id,
                query={'page': page, 'page_size': page_size},
                note=f'Downloading JSON highlights for show {show_id} - page {page}')

            for highlight in highlights_videos.get('results') or []:
                if highlight_id and highlight.get('clip_no') != int(highlight_id):
                    continue
                yield merge_dicts({
                    'id': f'{show_id}-{highlight["clip_no"]}',
                    'title': highlight.get('title'),
                    'timestamp': unified_timestamp(highlight.get('regdate')),
                    'thumbnail': highlight.get('thumbnail_url'),
                }, self._extract_video_info(highlight['clip_no'], highlight['video_id'], highlight['video_inkey']))

            if len(highlights_videos.get('results') or []) < page_size:
                break
            page += 1

    def _extract_highlight(self, show_id, highlight_id):
        try:
            return next(self._extract_show_highlights(show_id, highlight_id))
        except StopIteration:
            raise ExtractorError(f'Unable to find highlight {highlight_id} for show {show_id}')

    def _real_extract(self, url):
        show_id = self._match_id(url)
        qs = parse_qs(urlparse(url).query)

        if not self._yes_playlist(show_id, qs.get('shareHightlight')):
            return self._extract_highlight(show_id, qs['shareHightlight'][0])
        elif not self._yes_playlist(show_id, qs.get('shareReplayId')):
            return self._extract_replay(show_id, qs['shareReplayId'][0])

        show_info = self._download_json(
            f'{self._API_URL}/shows/now.{show_id}/', show_id,
            note=f'Downloading JSON vod list for show {show_id}')

        return self.playlist_result(
            itertools.chain(self._extract_show_replays(show_id), self._extract_show_highlights(show_id)),
            show_id, show_info.get('title'))


code:
import functools
import re

from .turner import TurnerBaseIE
from ..compat import (
    compat_str,
    compat_urllib_parse_unquote,
)
from ..utils import (
    int_or_none,
    merge_dicts,
    OnDemandPagedList,
    parse_duration,
    parse_iso8601,
    parse_qs,
    try_get,
    update_url_query,
    urljoin,
)

class NBACVPBaseIE(TurnerBaseIE):
    def _extract_nba_cvp_info(self, path, video_id, fatal=False):
        return self._extract_cvp_info(
            'http://secure.nba.com/%s' % path, video_id, {
                'default': {
                    'media_src': 'http://nba.cdn.turner.com/nba/big',
                },
                'm3u8': {
                    'media_src': 'http://nbavod-f.akamaihd.net',
                },
            }, fatal=fatal)

class NBAWatchBaseIE(NBACVPBaseIE):
    _VALID_URL_BASE = r'https?://(?:(?:www\.)?nba\.com(?:/watch)?|watch\.nba\.com)/'

    def _extract_video(self, filter_key, filter_value):
        video = self._download_json(
            'https://neulionscnbav2-a.akamaihd.net/solr/nbad_program/usersearch',
            filter_value, query={
                'fl': 'description,image,name,pid,releaseDate,runtime,tags,seoName',
                'q': filter_key + ':' + filter_value,
                'wt': 'json',
            })['response']['docs'][0]

        video_id = str(video['pid'])
        title = video['name']

        formats = []
        m3u8_url = (self._download_json(
            'https://watch.nba.com/service/publishpoint', video_id, query={
                'type': 'video',
                'format': 'json',
                'id': video_id,
            }, headers={
                'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0_1 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A402 Safari/604.1',
            }, fatal=False) or {}).get('path')
        if m3u8_url:
            m3u8_formats = self._extract_m3u8_formats(
                re.sub(r'_(?:pc|iphone)\.', '.', m3u8_url), video_id, 'mp4',
                'm3u8_native', m3u8_id='hls', fatal=False)
            formats.extend(m3u8_formats)
            for f in m3u8_formats:
                http_f = f.copy()
                http_f.update({
                    'format_id': http_f['format_id'].replace('hls-', 'http-'),
                    'protocol': 'http',
                    'url': http_f['url'].replace('.m3u8', ''),
                })
                formats.append(http_f)

        info = {
            'id': video_id,
            'title': title,
            'thumbnail': urljoin('https://nbadsdmt.akamaized.net/media/nba/nba/thumbs/', video.get('image')),
            'description': video.get('description'),
            'duration': int_or_none(video.get('runtime')),
            'timestamp': parse_iso8601(video.get('releaseDate')),
            'tags': video.get('tags'),
        }

        seo_name = video.get('seoName')
        if seo_name and re.search(r'\d{4}/\d{2}/\d{2}/', seo_name):
            base_path = ''
            if seo_name.startswith('teams/'):
                base_path += seo_name.split('/')[1] + '/'
            base_path += 'video/'
            cvp_info = self._extract_nba_cvp_info(
                base_path + seo_name + '.xml', video_id, False)
            if cvp_info:
                formats.extend(cvp_info['formats'])
                info = merge_dicts(info, cvp_info)

        self._sort_formats(formats)
        info['formats'] = formats
        return info

class NBAWatchEmbedIE(NBAWatchBaseIE):
    IENAME = 'nba:watch:embed'
    _VALID_URL = NBAWatchBaseIE._VALID_URL_BASE + r'embed\?.*?\bid=(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://watch.nba.com/embed?id=659395',
        'md5': 'b7e3f9946595f4ca0a13903ce5edd120',
        'info_dict': {
            'id': '659395',
            'ext': 'mp4',
            'title': 'Mix clip: More than 7 points of  Joe Ingles, Luc Mbah a Moute, Blake Griffin and 6 more in Utah Jazz vs. the Clippers, 4/15/2017',
            'description': 'Mix clip: More than 7 points of  Joe Ingles, Luc Mbah a Moute, Blake Griffin and 6 more in Utah Jazz vs. the Clippers, 4/15/2017',
            'timestamp': 1492228800,
            'upload_date': '20170415',
        },
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        return self._extract_video('pid', video_id)

class NBAWatchIE(NBAWatchBaseIE):
    IE_NAME = 'nba:watch'
    _VALID_URL = NBAWatchBaseIE._VALID_URL_BASE + r'(?:nba/)?video/(?P<id>.+?(?=/index\.html)|(?:[^/]+/)*[^/?#&]+)'
    _TESTS = [{
        'url': 'http://www.nba.com/video/games/nets/2012/12/04/0021200253-okc-bkn-recap.nba/index.html',
        'md5': '9d902940d2a127af3f7f9d2f3dc79c96',
        'info_dict': {
            'id': '70946',
            'ext': 'mp4',
            'title': 'Thunder vs. Nets',
            'description': 'Kevin Durant scores 32 points and dishes out six assists as the Thunder beat the Nets in Brooklyn.',
            'duration': 181,
            'timestamp': 1354597200,
            'upload_date': '20121204',
        },
    }, {
        'url': 'http://www.nba.com/video/games/hornets/2014/12/05/0021400276-nyk-cha-play5.nba/',
        'only_matching': True,
    }, {
        'url': 'http://watch.nba.com/video/channels/playoffs/2015/05/20/0041400301-cle-atl-recap.nba',
        'md5': 'b2b39b81cf28615ae0c3360a3f9668c4',
        'info_dict': {
            'id': '330865',
            'ext': 'mp4',
            'title': 'Hawks vs. Cavaliers Game 1',
            'description': 'md5:8094c3498d35a9bd6b1a8c396a071b4d',
            'duration': 228,
            'timestamp': 1432094400,
            'upload_date': '20150521',
        },
    }, {
        'url': 'http://watch.nba.com/nba/video/channels/nba_tv/2015/06/11/YT_go_big_go_home_Game4_061115',
        'only_matching': True,
    }, {
        # only CVP mp4 format available
        'url': 'https://watch.nba.com/video/teams/cavaliers/2012/10/15/sloan121015mov-2249106',
        'only_matching': True,
    }, {
        'url': 'https://watch.nba.com/video/top-100-dunks-from-the-2019-20-season?plsrc=nba&collection=2019-20-season-highlights',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        display_id = self._match_id(url)
        collection_id = parse_qs(url).get('collection', [None])[0]
        if self._yes_playlist(collection_id, display_id):
            return self.url_result(
                'https://www.nba.com/watch/list/collection/' + collection_id,
                NBAWatchCollectionIE.ie_key(), collection_id)
        return self._extract_video('seoName', display_id)

class NBAWatchCollectionIE(NBAWatchBaseIE):
    IE_NAME = 'nba:watch:collection'
    _VALID_URL = NBAWatchBaseIE._VALID_URL_BASE + r'list/collection/(?P<id>[^/?#&]+)'
    _TESTS = [{
        'url': 'https://watch.nba.com/list/collection/season-preview-2020',
        'info_dict': {
            'id': 'season-preview-2020',
        },
        'playlist_mincount': 43,
    }]
    _PAGE_SIZE = 100

    def _fetch_page(self, collection_id, page):
        page += 1
        videos = self._download_json(
            'https://content-api-prod.nba.com/public/1/endeavor/video-list/collection/' + collection_id,
            collection_id, 'Downloading page %d JSON metadata' % page, query={
                'count': self._PAGE_SIZE,
                'page': page,
            })['results']['videos']
        for video in videos:
            program = video.get('program') or {}
            seo_name = program.get('seoName') or program.get('slug')
            if not seo_name:
                continue
            yield {
                '_type': 'url',
                'id': program.get('id'),
                'title': program.get('title') or video.get('title'),
                'url': 'https://www.nba.com/watch/video/' + seo_name,
                'thumbnail': video.get('image'),
                'description': program.get('description') or video.get('description'),
                'duration': parse_duration(program.get('runtimeHours')),
                'timestamp': parse_iso8601(video.get('releaseDate')),
            }

    def _real_extract(self, url):
        collection_id = self._match_id(url)
        entries = OnDemandPagedList(
            functools.partial(self._fetch_page, collection_id),
            self._PAGE_SIZE)
        return self.playlist_result(entries, collection_id)

class NBABaseIE(NBACVPBaseIE):
    _VALID_URL_BASE = r'''(?x)
        https?://(?:www\.)?nba\.com/
            (?P<team>
                blazers|
                bucks|
                bulls|
                cavaliers|
                celtics|
                clippers|
                grizzlies|
                hawks|
                heat|
                hornets|
                jazz|
                kings|
                knicks|
                lakers|
                magic|
                mavericks|
                nets|
                nuggets|
                pacers|
                pelicans|
                pistons|
                raptors|
                rockets|
                sixers|
                spurs|
                suns|
                thunder|
                timberwolves|
                warriors|
                wizards
            )
        (?:/play\#)?/'''
    _CHANNEL_PATH_REGEX = r'video/channel|series'

    def _embed_url_result(self, team, content_id):
        return self.url_result(update_url_query(
            'https://secure.nba.com/assets/amp/include/video/iframe.html', {
                'contentId': content_id,
                'team': team,
            }), NBAEmbedIE.ie_key())

    def _call_api(self, team, content_id, query, resource):
        return self._download_json(
            'https://api.nba.net/2/%s/video,imported_video,wsc/' % team,
            content_id, 'Download %s JSON metadata' % resource,
            query=query, headers={
                'accessToken': 'internal|bb88df6b4c2244e78822812cecf1ee1b',
            })['response']['result']

    def _extract_video(self, video, team, extract_all=True):
        video_id = compat_str(video['nid'])
        team = video['brand']

        info = {
            'id': video_id,
            'title': video.get('title') or video.get('headline') or video['shortHeadline'],
            'description': video.get('description'),
            'timestamp': parse_iso8601(video.get('published')),
        }

        subtitles = {}
        captions = try_get(video, lambda x: x['videoCaptions']['sidecars'], dict) or {}
        for caption_url in captions.values():
            subtitles.setdefault('en', []).append({'url': caption_url})

        formats = []
        mp4_url = video.get('mp4')
        if mp4_url:
            formats.append({
                'url': mp4_url,
            })

        if extract_all:
            source_url = video.get('videoSource')
            if source_url and not source_url.startswith('s3://') and self._is_valid_url(source_url, video_id, 'source'):
                formats.append({
                    'format_id': 'source',
                    'url': source_url,
                    'quality': 1,
                })

            m3u8_url = video.get('m3u8')
            if m3u8_url:
                if '.akamaihd.net/i/' in m3u8_url:
                    formats.extend(self._extract_akamai_formats(
                        m3u8_url, video_id, {'http': 'pmd.cdn.turner.com'}))
                else:
                    formats.extend(self._extract_m3u8_formats(
                        m3u8_url, video_id, 'mp4',
                        'm3u8_native', m3u8_id='hls', fatal=False))

            content_xml = video.get('contentXml')
            if team and content_xml:
                cvp_info = self._extract_nba_cvp_info(
                    team + content_xml, video_id, fatal=False)
                if cvp_info:
                    formats.extend(cvp_info['formats'])
                    subtitles = self._merge_subtitles(subtitles, cvp_info['subtitles'])
                    info = merge_dicts(info, cvp_info)

            self._sort_formats(formats)
        else:
            info.update(self._embed_url_result(team, video['videoId']))

        info.update({
            'formats': formats,
            'subtitles': subtitles,
        })

        return info

    def _real_extract(self, url):
        team, display_id = self._match_valid_url(url).groups()
        if '/play#/' in url:
            display_id = compat_urllib_parse_unquote(display_id)
        else:
            webpage = self._download_webpage(url, display_id)
            display_id = self._search_regex(
                self._CONTENT_ID_REGEX + r'\s*:\s*"([^"]+)"', webpage, 'video id')
        return self._extract_url_results(team, display_id)

class NBAEmbedIE(NBABaseIE):
    IENAME = 'nba:embed'
    _VALID_URL = r'https?://secure\.nba\.com/assets/amp/include/video/(?:topI|i)frame\.html\?.*?\bcontentId=(?P<id>[^?#&]+)'
    _TESTS = [{
        'url': 'https://secure.nba.com/assets/amp/include/video/topIframe.html?contentId=teams/bulls/2020/12/04/3478774/1607105587854-20201204_SCHEDULE_RELEASE_FINAL_DRUPAL-3478774&team=bulls&adFree=false&profile=71&videoPlayerName=TAMPCVP&baseUrl=&videoAdsection=nba.com_mobile_web_teamsites_chicagobulls&ampEnv=',
        'only_matching': True,
    }, {
        'url': 'https://secure.nba.com/assets/amp/include/video/iframe.html?contentId=2016/10/29/0021600027boschaplay7&adFree=false&profile=71&team=&videoPlayerName=LAMPCVP',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        qs = parse_qs(url)
        content_id = qs['contentId'][0]
        team = qs.get('team', [None])[0]
        if not team:
            return self.url_result(
                'https://watch.nba.com/video/' + content_id, NBAWatchIE.ie_key())
        video = self._call_api(team, content_id, {'videoid': content_id}, 'video')[0]
        return self._extract_video(video, team)

class NBAIE(NBABaseIE):
    IENAME = 'nba'
    _VALID_URL = NBABaseIE._VALID_URL_BASE + '(?!%s)video/(?P<id>(?:[^/]+/)*[^/?#&]+)' % NBABaseIE._CHANNEL_PATH_REGEX
    _TESTS = [{
        'url': 'https://www.nba.com/bulls/video/teams/bulls/2020/12/04/3478774/1607105587854-20201204schedulereleasefinaldrupal-3478774',
        'info_dict': {
            'id': '45039',
            'ext': 'mp4',
            'title': 'AND WE BACK.',
            'description': 'Part 1 of our 2020-21 schedule is here! Watch our games on NBC Sports Chicago.',
            'duration': 94,
            'timestamp': 1607112000,
            'upload_date': '20201218',
        },
    }, {
        'url': 'https://www.nba.com/bucks/play#/video/teams%2Fbucks%2F2020%2F12%2F17%2F64860%2F1608252863446-Op_Dream_16x9-64860',
        'only_matching': True,
    }, {
        'url': 'https://www.nba.com/bucks/play#/video/wsc%2Fteams%2F2787C911AA1ACD154B5377F7577CCC7134B2A4B0',
        'only_matching': True,
    }]
    _CONTENT_ID_REGEX = r'videoID'

    def _extract_url_results(self, team, content_id):
        return self._embed_url_result(team, content_id)

class NBAChannelIE(NBABaseIE):
    IENAME = 'nba:channel'
    _VALID_URL = NBABaseIE._VALID_URL_BASE + '(?:%s)/(?P<id>[^/?#&]+)' % NBABaseIE._CHANNEL_PATH_REGEX
    _TESTS = [{
        'url': 'https://www.nba.com/blazers/video/channel/summer_league',
        'info_dict': {
            'title': 'Summer League',
        },
        'playlist_mincount': 138,
    }, {
        'url': 'https://www.nba.com/bucks/play#/series/On%20This%20Date',
        'only_matching': True,
    }]
    _CONTENT_ID_REGEX = r'videoSubCategory'
    _PAGE_SIZE = 100

    def _fetch_page(self, team, channel, page):
        results = self._call_api(team, channel, {
            'channels': channel,
            'count': self._PAGE_SIZE,
            'offset': page * self._PAGE_SIZE,
        }, 'page %d' % (page + 1))
        for video in results:
            yield self._extract_video(video, team, False)

    def _extract_url_results(self, team, content_id):
        entries = OnDemandPagedList(
            functools.partial(self._fetch_page, team, content_id),
            self._PAGE_SIZE)
        return self.playlist_result(entries, playlist_title=content_id)

repo_name: Lesmiscore/ytdl


code:
import base64
import json
import re

from .common import InfoExtractor
from .theplatform import ThePlatformIE
from .adobepass import AdobePassIE
from ..compat import compat_urllib_parse_unquote
from ..utils import (
    int_or_none,
    parse_age_limit,
    parse_duration,
    RegexNotFoundError,
    smuggle_url,
    try_get,
    unified_timestamp,
    update_url_query,
)

class NBCIE(ThePlatformIE):
    _VALID_URL = r'https?(?P<permalink>://(?:www\.)?nbc\.com/(?:classic-tv/)?[^/]+/video/[^/]+/(?P<id>n?\d+))'

    _TESTS = [
        {
            'url': 'http://www.nbc.com/the-tonight-show/video/jimmy-fallon-surprises-fans-at-ben-jerrys/2848237',
            'info_dict': {
                'id': '2848237',
                'ext': 'mp4',
                'title': 'Jimmy Fallon Surprises Fans at Ben & Jerry\'s',
                'description': 'Jimmy gives out free scoops of his new "Tonight Dough" ice cream flavor by surprising customers at the Ben & Jerry\'s scoop shop.',
                'timestamp': 1424246400,
                'upload_date': '20150218',
                'uploader': 'NBCU-COM',
            },
            'params': {
                # m3u8 download
                'skip_download': True,
            },
        },
        {
            'url': 'http://www.nbc.com/saturday-night-live/video/star-wars-teaser/2832821',
            'info_dict': {
                'id': '2832821',
                'ext': 'mp4',
                'title': 'Star Wars Teaser',
                'description': 'md5:0b40f9cbde5b671a7ff62fceccc4f442',
                'timestamp': 1417852800,
                'upload_date': '20141206',
                'uploader': 'NBCU-COM',
            },
            'params': {
                # m3u8 download
                'skip_download': True,
            },
            'skip': 'Only works from US',
        },
        {
            # HLS streams requires the 'hdnea3' cookie
            'url': 'http://www.nbc.com/Kings/video/goliath/n1806',
            'info_dict': {
                'id': '101528f5a9e8127b107e98c5e6ce4638',
                'ext': 'mp4',
                'title': 'Goliath',
                'description': 'When an unknown soldier saves the life of the King\'s son in battle, he\'s thrust into the limelight and politics of the kingdom.',
                'timestamp': 1237100400,
                'upload_date': '20090315',
                'uploader': 'NBCU-COM',
            },
            'params': {
                'skip_download': True,
            },
            'skip': 'Only works from US',
        },
        {
            'url': 'https://www.nbc.com/classic-tv/charles-in-charge/video/charles-in-charge-pilot/n3310',
            'only_matching': True,
        },
        {
            # Percent escaped url
            'url': 'https://www.nbc.com/up-all-night/video/day-after-valentine%27s-day/n2189',
            'only_matching': True,
        }
    ]

    def _real_extract(self, url):
        permalink, video_id = self._match_valid_url(url).groups()
        permalink = 'http' + compat_urllib_parse_unquote(permalink)
        video_data = self._download_json(
            'https://friendship.nbc.co/v2/graphql', video_id, query={
                'query': '''query bonanzaPage(
  $app: NBCUBrands! = nbc
  $name: String!
  $oneApp: Boolean
  $platform: SupportedPlatforms! = web
  $type: EntityPageType! = VIDEO
  $userId: String!
) {
  bonanzaPage(
    app: $app
    name: $name
    oneApp: $oneApp
    platform: $platform
    type: $type
    userId: $userId
  ) {
    metadata {
      ... on VideoPageData {
        description
        episodeNumber
        keywords
        locked
        mpxAccountId
        mpxGuid
        rating
        resourceId
        seasonNumber
        secondaryTitle
        seriesShortTitle
      }
    }
  }
}''',
                'variables': json.dumps({
                    'name': permalink,
                    'oneApp': True,
                    'userId': '0',
                }),
            })['data']['bonanzaPage']['metadata']
        query = {
            'mbr': 'true',
            'manifest': 'm3u',
        }
        video_id = video_data['mpxGuid']
        tp_path = 'NnzsPC/media/guid/%s/%s' % (video_data.get('mpxAccountId') or '2410887629', video_id)
        tpm = self._download_theplatform_metadata(tp_path, video_id)
        title = tpm.get('title') or video_data.get('secondaryTitle')
        if video_data.get('locked'):
            resource = self._get_mvpd_resource(
                video_data.get('resourceId') or 'nbcentertainment',
                title, video_id, video_data.get('rating'))
            query['auth'] = self._extract_mvpd_auth(
                url, video_id, 'nbcentertainment', resource)
        theplatform_url = smuggle_url(update_url_query(
            'http://link.theplatform.com/s/NnzsPC/media/guid/%s/%s' % (video_data.get('mpxAccountId') or '2410887629', video_id),
            query), {'force_smil_url': True})

        # Empty string or 0 can be valid values for these. So the check must be `is None`
        description = video_data.get('description')
        if description is None:
            description = tpm.get('description')
        episode_number = int_or_none(video_data.get('episodeNumber'))
        if episode_number is None:
            episode_number = int_or_none(tpm.get('nbcu$airOrder'))
        rating = video_data.get('rating')
        if rating is None:
            try_get(tpm, lambda x: x['ratings'][0]['rating'])
        season_number = int_or_none(video_data.get('seasonNumber'))
        if season_number is None:
            season_number = int_or_none(tpm.get('nbcu$seasonNumber'))
        series = video_data.get('seriesShortTitle')
        if series is None:
            series = tpm.get('nbcu$seriesShortTitle')
        tags = video_data.get('keywords')
        if tags is None or len(tags) == 0:
            tags = tpm.get('keywords')

        return {
            '_type': 'url_transparent',
            'age_limit': parse_age_limit(rating),
            'description': description,
            'episode': title,
            'episode_number': episode_number,
            'id': video_id,
            'ie_key': 'ThePlatform',
            'season_number': season_number,
            'series': series,
            'tags': tags,
            'title': title,
            'url': theplatform_url,
        }

class NBCSportsVPlayerIE(InfoExtractor):
    _VALID_URL_BASE = r'https?://(?:vplayer\.nbcsports\.com|(?:www\.)?nbcsports\.com/vplayer)/'
    _VALID_URL = _VALID_URL_BASE + r'(?:[^/]+/)+(?P<id>[0-9a-zA-Z_]+)'
    _EMBED_REGEX = [r'(?:iframe[^>]+|var video|div[^>]+data-(?:mpx-)?)[sS]rc\s?=\s?"(?P<url>%s[^\"]+)' % _VALID_URL_BASE]

    _TESTS = [{
        'url': 'https://vplayer.nbcsports.com/p/BxmELC/nbcsports_embed/select/9CsDKds0kvHI',
        'info_dict': {
            'id': '9CsDKds0kvHI',
            'ext': 'mp4',
            'description': 'md5:df390f70a9ba7c95ff1daace988f0d8d',
            'title': 'Tyler Kalinoski hits buzzer-beater to lift Davidson',
            'timestamp': 1426270238,
            'upload_date': '20150313',
            'uploader': 'NBCU-SPORTS',
            'duration': 72.818,
            'chapters': [],
            'thumbnail': r're:^https?://.*\.jpg$'
        }
    }, {
        'url': 'https://vplayer.nbcsports.com/p/BxmELC/nbcsports_embed/select/media/PEgOtlNcC_y2',
        'only_matching': True,
    }, {
        'url': 'https://www.nbcsports.com/vplayer/p/BxmELC/nbcsports/select/PHJSaFWbrTY9?form=html&autoPlay=true',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(url, video_id)
        theplatform_url = self._html_search_regex(r'tp:releaseUrl="(.+?)"', webpage, 'url')
        return self.url_result(theplatform_url, 'ThePlatform')

class NBCSportsIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?nbcsports\.com//?(?!vplayer/)(?:[^/]+/)+(?P<id>[0-9a-z-]+)'

    _TESTS = [{
        # iframe src
        'url': 'http://www.nbcsports.com//college-basketball/ncaab/tom-izzo-michigan-st-has-so-much-respect-duke',
        'info_dict': {
            'id': 'PHJSaFWbrTY9',
            'ext': 'mp4',
            'title': 'Tom Izzo, Michigan St. has \'so much respect\' for Duke',
            'description': 'md5:ecb459c9d59e0766ac9c7d5d0eda8113',
            'uploader': 'NBCU-SPORTS',
            'upload_date': '20150330',
            'timestamp': 1427726529,
            'chapters': [],
            'thumbnail': 'https://hdliveextra-a.akamaihd.net/HD/image_sports/NBCU_Sports_Group_-_nbcsports/253/303/izzodps.jpg',
            'duration': 528.395,
        }
    }, {
        # data-mpx-src
        'url': 'https://www.nbcsports.com/philadelphia/philadelphia-phillies/bruce-bochy-hector-neris-hes-idiot',
        'only_matching': True,
    }, {
        # data-src
        'url': 'https://www.nbcsports.com/boston/video/report-card-pats-secondary-no-match-josh-allen',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(url, video_id)
        return self.url_result(
            NBCSportsVPlayerIE._extract_url(webpage), 'NBCSportsVPlayer')

class NBCSportsStreamIE(AdobePassIE):
    _VALID_URL = r'https?://stream\.nbcsports\.com/.+?\bpid=(?P<id>\d+)'
    _TEST = {
        'url': 'http://stream.nbcsports.com/nbcsn/generic?pid=206559',
        'info_dict': {
            'id': '206559',
            'ext': 'mp4',
            'title': 'Amgen Tour of California Women\'s Recap',
            'description': 'md5:66520066b3b5281ada7698d0ea2aa894',
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
        'skip': 'Requires Adobe Pass Authentication',
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        live_source = self._download_json(
            'http://stream.nbcsports.com/data/live_sources_%s.json' % video_id,
            video_id)
        video_source = live_source['videoSources'][0]
        title = video_source['title']
        source_url = None
        for k in ('source', 'msl4source', 'iossource', 'hlsv4'):
            sk = k + 'Url'
            source_url = video_source.get(sk) or video_source.get(sk + 'Alt')
            if source_url:
                break
        else:
            source_url = video_source['ottStreamUrl']
        is_live = video_source.get('type') == 'live' or video_source.get('status') == 'Live'
        resource = self._get_mvpd_resource('nbcsports', title, video_id, '')
        token = self._extract_mvpd_auth(url, video_id, 'nbcsports', resource)
        tokenized_url = self._download_json(
            'https://token.playmakerservices.com/cdn',
            video_id, data=json.dumps({
                'requestorId': 'nbcsports',
                'pid': video_id,
                'application': 'NBCSports',
                'version': 'v1',
                'platform': 'desktop',
                'cdn': 'akamai',
                'url': video_source['sourceUrl'],
                'token': base64.b64encode(token.encode()).decode(),
                'resourceId': base64.b64encode(resource.encode()).decode(),
            }).encode())['tokenizedUrl']
        formats = self._extract_m3u8_formats(tokenized_url, video_id, 'mp4')
        self._sort_formats(formats)
        return {
            'id': video_id,
            'title': title,
            'description': live_source.get('description'),
            'formats': formats,
            'is_live': is_live,
        }

class NBCNewsIE(ThePlatformIE):
    _VALID_URL = r'(?x)https?://(?:www\.)?(?:nbcnews|today|msnbc)\.com/([^/]+/)*(?:.*-)?(?P<id>[^/?]+)'
    _EMBED_REGEX = [r'<iframe[^>]+src=(["\'])(?P<url>(?:https?:)?//www\.nbcnews\.com/widget/video-embed/[^"\']+)\1']

    _TESTS = [
        {
            'url': 'http://www.nbcnews.com/watch/nbcnews-com/how-twitter-reacted-to-the-snowden-interview-269389891880',
            'md5': 'cf4bc9e6ce0130f00f545d80ecedd4bf',
            'info_dict': {
                'id': '269389891880',
                'ext': 'mp4',
                'title': 'How Twitter Reacted To The Snowden Interview',
                'description': 'md5:65a0bd5d76fe114f3c2727aa3a81fe64',
                'timestamp': 1401363060,
                'upload_date': '20140529',
            },
        },
        {
            'url': 'http://www.nbcnews.com/feature/dateline-full-episodes/full-episode-family-business-n285156',
            'md5': 'fdbf39ab73a72df5896b6234ff98518a',
            'info_dict': {
                'id': '529953347624',
                'ext': 'mp4',
                'title': 'FULL EPISODE: Family Business',
                'description': 'md5:757988edbaae9d7be1d585eb5d55cc04',
            },
            'skip': 'This page is unavailable.',
        },
        {
            'url': 'http://www.nbcnews.com/nightly-news/video/nightly-news-with-brian-williams-full-broadcast-february-4-394064451844',
            'md5': '8eb831eca25bfa7d25ddd83e85946548',
            'info_dict': {
                'id': '394064451844',
                'ext': 'mp4',
                'title': 'Nightly News with Brian Williams Full Broadcast (February 4)',
                'description': 'md5:1c10c1eccbe84a26e5debb4381e2d3c5',
                'timestamp': 1423104900,
                'upload_date': '20150205',
            },
        },
        {
            'url': 'http://www.nbcnews.com/business/autos/volkswagen-11-million-vehicles-could-have-suspect-software-emissions-scandal-n431456',
            'md5': '4a8c4cec9e1ded51060bdda36ff0a5c0',
            'info_dict': {
                'id': 'n431456',
                'ext': 'mp4',
                'title': "Volkswagen U.S. Chief:  We 'Totally Screwed Up'",
                'description': 'md5:d22d1281a24f22ea0880741bb4dd6301',
                'upload_date': '20150922',
                'timestamp': 1442917800,
            },
        },
        {
            'url': 'http://www.today.com/video/see-the-aurora-borealis-from-space-in-stunning-new-nasa-video-669831235788',
            'md5': '118d7ca3f0bea6534f119c68ef539f71',
            'info_dict': {
                'id': '669831235788',
                'ext': 'mp4',
                'title': 'See the aurora borealis from space in stunning new NASA video',
                'description': 'md5:74752b7358afb99939c5f8bb2d1d04b1',
                'upload_date': '20160420',
                'timestamp': 1461152093,
            },
        },
        {
            'url': 'http://www.msnbc.com/all-in-with-chris-hayes/watch/the-chaotic-gop-immigration-vote-314487875924',
            'md5': '6d236bf4f3dddc226633ce6e2c3f814d',
            'info_dict': {
                'id': '314487875924',
                'ext': 'mp4',
                'title': 'The chaotic GOP immigration vote',
                'description': 'The Republican House votes on a border bill that has no chance of getting through the Senate or signed by the President and is drawing criticism from all sides.',
                'thumbnail': r're:^https?://.*\.jpg$',
                'timestamp': 1406937606,
                'upload_date': '20140802',
            },
        },
        {
            'url': 'http://www.nbcnews.com/watch/dateline/full-episode--deadly-betrayal-386250819952',
            'only_matching': True,
        },
        {
            # From http://www.vulture.com/2016/06/letterman-couldnt-care-less-about-late-night.html
            'url': 'http://www.nbcnews.com/widget/video-embed/701714499682',
            'only_matching': True,
        },
    ]

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

        data = self._search_nextjs_data(webpage, video_id)['props']['initialState']
        video_data = try_get(data, lambda x: x['video']['current'], dict)
        if not video_data:
            video_data = data['article']['content'][0]['primaryMedia']['video']
        title = video_data['headline']['primary']

        formats = []
        for va in video_data.get('videoAssets', []):
            public_url = va.get('publicUrl')
            if not public_url:
                continue
            if '://link.theplatform.com/' in public_url:
                public_url = update_url_query(public_url, {'format': 'redirect'})
            format_id = va.get('format')
            if format_id == 'M3U':
                formats.extend(self._extract_m3u8_formats(
                    public_url, video_id, 'mp4', 'm3u8_native',
                    m3u8_id=format_id, fatal=False))
                continue
            tbr = int_or_none(va.get('bitrate'), 1000)
            if tbr:
                format_id += '-%d' % tbr
            formats.append({
                'format_id': format_id,
                'url': public_url,
                'width': int_or_none(va.get('width')),
                'height': int_or_none(va.get('height')),
                'tbr': tbr,
                'ext': 'mp4',
            })
        self._sort_formats(formats)

        subtitles = {}
        closed_captioning = video_data.get('closedCaptioning')
        if closed_captioning:
            for cc_url in closed_captioning.values():
                if not cc_url:
                    continue
                subtitles.setdefault('en', []).append({
                    'url': cc_url,
                })

        return {
            'id': video_id,
            'title': title,
            'description': try_get(video_data, lambda x: x['description']['primary']),
            'thumbnail': try_get(video_data, lambda x: x['primaryImage']['url']['primary']),
            'duration': parse_duration(video_data.get('duration')),
            'timestamp': unified_timestamp(video_data.get('datePublished')),
            'formats': formats,
            'subtitles': subtitles,
        }

class NBCOlympicsIE(InfoExtractor):
    IE_NAME = 'nbcolympics'
    _VALID_URL = r'https?://www\.nbcolympics\.com/videos?/(?P<id>[0-9a-z-]+)'

    _TEST = {
        # Geo-restricted to US
        'url': 'http://www.nbcolympics.com/video/justin-roses-son-leo-was-tears-after-his-dad-won-gold',
        'md5': '54fecf846d05429fbaa18af557ee523a',
        'info_dict': {
            'id': 'WjTBzDXx5AUq',
            'display_id': 'justin-roses-son-leo-was-tears-after-his-dad-won-gold',
            'ext': 'mp4',
            'title': 'Rose\'s son Leo was in tears after his dad won gold',
            'description': 'Olympic gold medalist Justin Rose gets emotional talking to the impact his win in men\'s golf has already had on his children.',
            'timestamp': 1471274964,
            'upload_date': '20160815',
            'uploader': 'NBCU-SPORTS',
        },
    }

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

        webpage = self._download_webpage(url, display_id)

        try:
            drupal_settings = self._parse_json(self._search_regex(
                r'jQuery\.extend\(Drupal\.settings\s*,\s*({.+?})\);',
                webpage, 'drupal settings'), display_id)

            iframe_url = drupal_settings['vod']['iframe_url']
            theplatform_url = iframe_url.replace(
                'vplayer.nbcolympics.com', 'player.theplatform.com')
        except RegexNotFoundError:
            theplatform_url = self._search_regex(
                r"([\"'])embedUrl\1: *([\"'])(?P<embedUrl>.+)\2",
                webpage, 'embedding URL', group="embedUrl")

        return {
            '_type': 'url_transparent',
            'url': theplatform_url,
            'ie_key': ThePlatformIE.ie_key(),
            'display_id': display_id,
        }

class NBCOlympicsStreamIE(AdobePassIE):
    IE_NAME = 'nbcolympics:stream'
    _VALID_URL = r'https?://stream\.nbcolympics\.com/(?P<id>[0-9a-z-]+)'
    _TESTS = [
        {
            'note': 'Tokenized m3u8 source URL',
            'url': 'https://stream.nbcolympics.com/womens-soccer-group-round-11',
            'info_dict': {
                'id': '2019740',
                'ext': 'mp4',
                'title': r"re:Women's Group Stage - Netherlands vs\. Brazil [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$",
            },
            'params': {
                'skip_download': 'm3u8',
            },
        }, {
            'note': 'Plain m3u8 source URL',
            'url': 'https://stream.nbcolympics.com/gymnastics-event-finals-mens-floor-pommel-horse-womens-vault-bars',
            'info_dict': {
                'id': '2021729',
                'ext': 'mp4',
                'title': r're:Event Finals: M Floor, W Vault, M Pommel, W Uneven Bars [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            },
            'params': {
                'skip_download': 'm3u8',
            },
        },
    ]

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        pid = self._search_regex(r'pid\s*=\s*(\d+);', webpage, 'pid')

        event_config = self._download_json(
            f'http://stream.nbcolympics.com/data/event_config_{pid}.json',
            pid, 'Downloading event config')['eventConfig']

        title = event_config['eventTitle']
        is_live = {'live': True, 'replay': False}.get(event_config.get('eventStatus'))

        source_url = self._download_json(
            f'https://api-leap.nbcsports.com/feeds/assets/{pid}?application=NBCOlympics&platform=desktop&format=nbc-player&env=staging',
            pid, 'Downloading leap config'
        )['videoSources'][0]['cdnSources']['primary'][0]['sourceUrl']

        if event_config.get('cdnToken'):
            ap_resource = self._get_mvpd_resource(
                event_config.get('resourceId', 'NBCOlympics'),
                re.sub(r'[^\w\d ]+', '', event_config['eventTitle']), pid,
                event_config.get('ratingId', 'NO VALUE'))
            media_token = self._extract_mvpd_auth(url, pid, event_config.get('requestorId', 'NBCOlympics'), ap_resource)

            source_url = self._download_json(
                'https://tokens.playmakerservices.com/', pid, 'Retrieving tokenized URL',
                data=json.dumps({
                    'application': 'NBCSports',
                    'authentication-type': 'adobe-pass',
                    'cdn': 'akamai',
                    'pid': pid,
                    'platform': 'desktop',
                    'requestorId': 'NBCOlympics',
                    'resourceId': base64.b64encode(ap_resource.encode()).decode(),
                    'token': base64.b64encode(media_token.encode()).decode(),
                    'url': source_url,
                    'version': 'v1',
                }).encode(),
            )['akamai'][0]['tokenizedUrl']

        formats = self._extract_m3u8_formats(source_url, pid, 'mp4', live=is_live)
        for f in formats:
            # -http_seekable requires ffmpeg 4.3+ but it doesnt seem possible to
            # download with ffmpeg without this option
            f['downloader_options'] = {'ffmpeg_args': ['-seekable', '0', '-http_seekable', '0', '-icy', '0']}
        self._sort_formats(formats)

        return {
            'id': pid,
            'display_id': display_id,
            'title': title,
            'formats': formats,
            'is_live': is_live,
        }


code:
import re

from .common import InfoExtractor
from ..compat import compat_urllib_parse_urlparse
from ..utils import (
    determine_ext,
    ExtractorError,
    int_or_none,
    merge_dicts,
    parse_iso8601,
    qualities,
    try_get,
    urljoin,
)

class NDRBaseIE(InfoExtractor):
    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        display_id = next(group for group in mobj.groups() if group)
        webpage = self._download_webpage(url, display_id)
        return self._extract_embed(webpage, display_id, url)

class NDRIE(NDRBaseIE):
    IE_NAME = 'ndr'
    IE_DESC = 'NDR.de - Norddeutscher Rundfunk'
    _VALID_URL = r'https?://(?:\w+\.)*ndr\.de/(?:[^/]+/)*(?P<id>[^/?#]+),[\da-z]+\.html'
    _TESTS = [{
        # httpVideo, same content id
        'url': 'http://www.ndr.de/fernsehen/Party-Poette-und-Parade,hafengeburtstag988.html',
        'md5': '6515bc255dc5c5f8c85bbc38e035a659',
        'info_dict': {
            'id': 'hafengeburtstag988',
            'display_id': 'Party-Poette-und-Parade',
            'ext': 'mp4',
            'title': 'Party, Pötte und Parade',
            'description': 'md5:ad14f9d2f91d3040b6930c697e5f6b4c',
            'uploader': 'ndrtv',
            'timestamp': 1431255671,
            'upload_date': '20150510',
            'duration': 3498,
        },
        'params': {
            'skip_download': True,
        },
        'expected_warnings': ['Unable to download f4m manifest'],
    }, {
        # httpVideo, different content id
        'url': 'http://www.ndr.de/sport/fussball/40-Osnabrueck-spielt-sich-in-einen-Rausch,osna270.html',
        'md5': '1043ff203eab307f0c51702ec49e9a71',
        'info_dict': {
            'id': 'osna272',
            'display_id': '40-Osnabrueck-spielt-sich-in-einen-Rausch',
            'ext': 'mp4',
            'title': 'Osnabrück - Wehen Wiesbaden: Die Highlights',
            'description': 'md5:32e9b800b3d2d4008103752682d5dc01',
            'uploader': 'ndrtv',
            'timestamp': 1442059200,
            'upload_date': '20150912',
            'duration': 510,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'No longer available',
    }, {
        # httpAudio, same content id
        'url': 'http://www.ndr.de/info/La-Valette-entgeht-der-Hinrichtung,audio51535.html',
        'md5': 'bb3cd38e24fbcc866d13b50ca59307b8',
        'info_dict': {
            'id': 'audio51535',
            'display_id': 'La-Valette-entgeht-der-Hinrichtung',
            'ext': 'mp3',
            'title': 'La Valette entgeht der Hinrichtung',
            'description': 'md5:22f9541913a40fe50091d5cdd7c9f536',
            'uploader': 'ndrinfo',
            'timestamp': 1631711863,
            'upload_date': '20210915',
            'duration': 884,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # with subtitles
        'url': 'https://www.ndr.de/fernsehen/sendungen/extra_3/extra-3-Satiremagazin-mit-Christian-Ehring,sendung1091858.html',
        'info_dict': {
            'id': 'extra18674',
            'display_id': 'extra-3-Satiremagazin-mit-Christian-Ehring',
            'ext': 'mp4',
            'title': 'Extra 3 vom 11.11.2020 mit Christian Ehring',
            'description': 'md5:700f6de264010585012a72f97b0ac0c9',
            'uploader': 'ndrtv',
            'upload_date': '20201207',
            'timestamp': 1614349457,
            'duration': 1749,
            'subtitles': {
                'de': [{
                    'ext': 'ttml',
                    'url': r're:^https://www\.ndr\.de.+',
                }],
            },
        },
        'params': {
            'skip_download': True,
        },
        'expected_warnings': ['Unable to download f4m manifest'],
    }, {
        'url': 'https://www.ndr.de/Fettes-Brot-Ferris-MC-und-Thees-Uhlmann-live-on-stage,festivalsommer116.html',
        'only_matching': True,
    }]

    def _extract_embed(self, webpage, display_id, url):
        embed_url = (
            self._html_search_meta(
                'embedURL', webpage, 'embed URL',
                default=None)
            or self._search_regex(
                r'\bembedUrl["\']\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
                'embed URL', group='url', default=None)
            or self._search_regex(
                r'\bvar\s*sophoraID\s*=\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
                'embed URL', group='url', default=''))
        # some more work needed if we only found sophoraID
        if re.match(r'^[a-z]+\d+$', embed_url):
            # get the initial part of the url path,. eg /panorama/archiv/2022/
            parsed_url = compat_urllib_parse_urlparse(url)
            path = self._search_regex(r'(.+/)%s' % display_id, parsed_url.path or '', 'embed URL', default='')
            # find tell-tale image with the actual ID
            ndr_id = self._search_regex(r'%s([a-z]+\d+)(?!\.)\b' % (path, ), webpage, 'embed URL', default=None)
            # or try to use special knowledge!
            NDR_INFO_URL_TPL = 'https://www.ndr.de/info/%s-player.html'
            embed_url = 'ndr:%s' % (ndr_id, ) if ndr_id else NDR_INFO_URL_TPL % (embed_url, )
        if not embed_url:
            raise ExtractorError('Unable to extract embedUrl')

        description = self._search_regex(
            r'
]+itemprop="description">([^<]+)
',
            webpage, 'description', default=None) or self._og_search_description(webpage)
        timestamp = parse_iso8601(
            self._search_regex(
                (r']+itemprop="(?:datePublished|uploadDate)"[^>]+content="(?P<cont>[^"]+)"',
                 r'\bvar\s*pdt\s*=\s*(?P<q>["\'])(?P<cont>(?:(?!(?P=q)).)+)(?P=q)', ),
                webpage, 'upload date', group='cont', default=None))
        info = self._search_json_ld(webpage, display_id, default={})
        return merge_dicts({
            '_type': 'url_transparent',
            'url': embed_url,
            'display_id': display_id,
            'description': description,
            'timestamp': timestamp,
        }, info)

class NJoyIE(NDRBaseIE):
    IE_NAME = 'njoy'
    IE_DESC = 'N-JOY'
    _VALID_URL = r'https?://(?:www\.)?n-joy\.de/(?:[^/]+/)*(?:(?P<display_id>[^/?#]+),)?(?P<id>[\da-z]+)\.html'
    _TESTS = [{
        # httpVideo, same content id
        'url': 'http://www.n-joy.de/entertainment/comedy/comedy_contest/Benaissa-beim-NDR-Comedy-Contest,comedycontest2480.html',
        'md5': 'cb63be60cd6f9dd75218803146d8dc67',
        'info_dict': {
            'id': 'comedycontest2480',
            'display_id': 'Benaissa-beim-NDR-Comedy-Contest',
            'ext': 'mp4',
            'title': 'Benaissa beim NDR Comedy Contest',
            'description': 'md5:f057a6c4e1c728b10d33b5ffd36ddc39',
            'uploader': 'ndrtv',
            'upload_date': '20141129',
            'duration': 654,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'No longer available',
    }, {
        # httpVideo, different content id
        'url': 'http://www.n-joy.de/musik/Das-frueheste-DJ-Set-des-Nordens-live-mit-Felix-Jaehn-,felixjaehn168.html',
        'md5': '417660fffa90e6df2fda19f1b40a64d8',
        'info_dict': {
            'id': 'livestream283',
            'display_id': 'Das-frueheste-DJ-Set-des-Nordens-live-mit-Felix-Jaehn-',
            'ext': 'mp3',
            'title': 'Das frueheste DJ Set des Nordens live mit Felix Jaehn',
            'description': 'md5:681698f527b8601e511e7b79edde7d2c',
            'uploader': 'njoy',
            'upload_date': '20210830',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.n-joy.de/radio/webradio/morningshow209.html',
        'only_matching': True,
    }]

    def _extract_embed(self, webpage, display_id, url=None):
        # find tell-tale URL with the actual ID, or ...
        video_id = self._search_regex(
            (r'''\bsrc\s*=\s*["']?(?:/\w+)+/([a-z]+\d+)(?!\.)\b''',
             r']+id="pp_([\da-z]+)"', ),
            webpage, 'NDR id', default=None)

        description = (
            self._html_search_meta('description', webpage)
            or self._search_regex(
                r'
]+class="subline"[^>]*>[^<]+
\s*
([^<]+)
',
                webpage, 'description', fatal=False))
        return {
            '_type': 'url_transparent',
            'ie_key': 'NDREmbedBase',
            'url': 'ndr:%s' % video_id,
            'display_id': display_id,
            'description': description,
            'title': display_id.replace('-', ' ').strip(),
        }

class NDREmbedBaseIE(InfoExtractor):
    IE_NAME = 'ndr:embed:base'
    _VALID_URL = r'(?:ndr:(?P<id_s>[\da-z]+)|https?://www\.ndr\.de/(?P<id>[\da-z]+)-ppjson\.json)'
    _TESTS = [{
        'url': 'ndr:soundcheck3366',
        'only_matching': True,
    }, {
        'url': 'http://www.ndr.de/soundcheck3366-ppjson.json',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        video_id = mobj.group('id') or mobj.group('id_s')

        ppjson = self._download_json(
            'http://www.ndr.de/%s-ppjson.json' % video_id, video_id)

        playlist = ppjson['playlist']

        formats = []
        quality_key = qualities(('xs', 's', 'm', 'l', 'xl'))

        for format_id, f in playlist.items():
            src = f.get('src')
            if not src:
                continue
            ext = determine_ext(src, None)
            if ext == 'f4m':
                formats.extend(self._extract_f4m_formats(
                    src + '?hdcore=3.7.0&plugin=aasp-3.7.0.39.44', video_id,
                    f4m_id='hds', fatal=False))
            elif ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    src, video_id, 'mp4', m3u8_id='hls',
                    entry_protocol='m3u8_native', fatal=False))
            else:
                quality = f.get('quality')
                ff = {
                    'url': src,
                    'format_id': quality or format_id,
                    'quality': quality_key(quality),
                }
                type_ = f.get('type')
                if type_ and type_.split('/')[0] == 'audio':
                    ff['vcodec'] = 'none'
                    ff['ext'] = ext or 'mp3'
                formats.append(ff)
        self._sort_formats(formats)

        config = playlist['config']

        live = playlist.get('config', {}).get('streamType') in ['httpVideoLive', 'httpAudioLive']
        title = config['title']
        uploader = ppjson.get('config', {}).get('branding')
        upload_date = ppjson.get('config', {}).get('publicationDate')
        duration = int_or_none(config.get('duration'))

        thumbnails = []
        poster = try_get(config, lambda x: x['poster'], dict) or {}
        for thumbnail_id, thumbnail in poster.items():
            thumbnail_url = urljoin(url, thumbnail.get('src'))
            if not thumbnail_url:
                continue
            thumbnails.append({
                'id': thumbnail.get('quality') or thumbnail_id,
                'url': thumbnail_url,
                'preference': quality_key(thumbnail.get('quality')),
            })

        subtitles = {}
        tracks = config.get('tracks')
        if tracks and isinstance(tracks, list):
            for track in tracks:
                if not isinstance(track, dict):
                    continue
                track_url = urljoin(url, track.get('src'))
                if not track_url:
                    continue
                subtitles.setdefault(track.get('srclang') or 'de', []).append({
                    'url': track_url,
                    'ext': 'ttml',
                })

        return {
            'id': video_id,
            'title': title,
            'is_live': live,
            'uploader': uploader if uploader != '-' else None,
            'upload_date': upload_date[0:8] if upload_date else None,
            'duration': duration,
            'thumbnails': thumbnails,
            'formats': formats,
            'subtitles': subtitles,
        }

class NDREmbedIE(NDREmbedBaseIE):
    IE_NAME = 'ndr:embed'
    _VALID_URL = r'https?://(?:\w+\.)*ndr\.de/(?:[^/]+/)*(?P<id>[\da-z]+)-(?:(?:ard)?player|externalPlayer)\.html'
    _TESTS = [{
        'url': 'http://www.ndr.de/fernsehen/sendungen/ndr_aktuell/ndraktuell28488-player.html',
        'md5': '8b9306142fe65bbdefb5ce24edb6b0a9',
        'info_dict': {
            'id': 'ndraktuell28488',
            'ext': 'mp4',
            'title': 'Norddeutschland begrüßt Flüchtlinge',
            'is_live': False,
            'uploader': 'ndrtv',
            'upload_date': '20150907',
            'duration': 132,
        },
        'skip': 'No longer available',
    }, {
        'url': 'http://www.ndr.de/ndr2/events/soundcheck/soundcheck3366-player.html',
        'md5': '002085c44bae38802d94ae5802a36e78',
        'info_dict': {
            'id': 'soundcheck3366',
            'ext': 'mp4',
            'title': 'Ella Henderson braucht Vergleiche nicht zu scheuen',
            'is_live': False,
            'uploader': 'ndr2',
            'upload_date': '20150912',
            'duration': 3554,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'No longer available',
    }, {
        'url': 'http://www.ndr.de/info/audio51535-player.html',
        'md5': 'bb3cd38e24fbcc866d13b50ca59307b8',
        'info_dict': {
            'id': 'audio51535',
            'ext': 'mp3',
            'title': 'La Valette entgeht der Hinrichtung',
            'is_live': False,
            'uploader': 'ndrinfo',
            'upload_date': '20210915',
            'duration': 884,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.ndr.de/fernsehen/sendungen/visite/visite11010-externalPlayer.html',
        'md5': 'ae57f80511c1e1f2fd0d0d3d31aeae7c',
        'info_dict': {
            'id': 'visite11010',
            'ext': 'mp4',
            'title': 'Visite - die ganze Sendung',
            'is_live': False,
            'uploader': 'ndrtv',
            'upload_date': '20150902',
            'duration': 3525,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'No longer available',
    }, {
        # httpVideoLive
        'url': 'http://www.ndr.de/fernsehen/livestream/livestream217-externalPlayer.html',
        'info_dict': {
            'id': 'livestream217',
            'ext': 'mp4',
            'title': r're:^NDR Fernsehen Niedersachsen \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
            'is_live': True,
            'upload_date': '20210409',
            'uploader': 'ndrtv',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.ndr.de/ndrkultur/audio255020-player.html',
        'only_matching': True,
    }, {
        'url': 'http://www.ndr.de/fernsehen/sendungen/nordtour/nordtour7124-player.html',
        'only_matching': True,
    }, {
        'url': 'http://www.ndr.de/kultur/film/videos/videoimport10424-player.html',
        'only_matching': True,
    }, {
        'url': 'http://www.ndr.de/fernsehen/sendungen/hamburg_journal/hamj43006-player.html',
        'only_matching': True,
    }, {
        'url': 'http://www.ndr.de/fernsehen/sendungen/weltbilder/weltbilder4518-player.html',
        'only_matching': True,
    }, {
        'url': 'http://www.ndr.de/fernsehen/doku952-player.html',
        'only_matching': True,
    }]

class NJoyEmbedIE(NDREmbedBaseIE):
    IE_NAME = 'njoy:embed'
    _VALID_URL = r'https?://(?:www\.)?n-joy\.de/(?:[^/]+/)*(?P<id>[\da-z]+)-(?:player|externalPlayer)_[^/]+\.html'
    _TESTS = [{
        # httpVideo
        'url': 'http://www.n-joy.de/events/reeperbahnfestival/doku948-player_image-bc168e87-5263-4d6d-bd27-bb643005a6de_theme-n-joy.html',
        'md5': '8483cbfe2320bd4d28a349d62d88bd74',
        'info_dict': {
            'id': 'doku948',
            'ext': 'mp4',
            'title': 'Zehn Jahre Reeperbahn Festival - die Doku',
            'is_live': False,
            'upload_date': '20200826',
            'duration': 1011,
        },
        'expected_warnings': ['Unable to download f4m manifest'],
    }, {
        # httpAudio
        'url': 'http://www.n-joy.de/news_wissen/stefanrichter100-player_image-d5e938b1-f21a-4b9a-86b8-aaba8bca3a13_theme-n-joy.html',
        'md5': 'd989f80f28ac954430f7b8a48197188a',
        'info_dict': {
            'id': 'stefanrichter100',
            'ext': 'mp3',
            'title': 'Interview mit einem Augenzeugen',
            'is_live': False,
            'uploader': 'njoy',
            'upload_date': '20150909',
            'duration': 140,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'No longer available',
    }, {
        # httpAudioLive, no explicit ext
        'url': 'http://www.n-joy.de/news_wissen/webradioweltweit100-player_image-3fec0484-2244-4565-8fb8-ed25fd28b173_theme-n-joy.html',
        'info_dict': {
            'id': 'webradioweltweit100',
            'ext': 'mp3',
            'title': r're:^N-JOY Weltweit \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
            'is_live': True,
            'uploader': 'njoy',
            'upload_date': '20210830',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.n-joy.de/musik/dockville882-player_image-3905259e-0803-4764-ac72-8b7de077d80a_theme-n-joy.html',
        'only_matching': True,
    }, {
        'url': 'http://www.n-joy.de/radio/sendungen/morningshow/urlaubsfotos190-player_image-066a5df1-5c95-49ec-a323-941d848718db_theme-n-joy.html',
        'only_matching': True,
    }, {
        'url': 'http://www.n-joy.de/entertainment/comedy/krudetv290-player_image-ab261bfe-51bf-4bf3-87ba-c5122ee35b3d_theme-n-joy.html',
        'only_matching': True,
    }]


code:
import urllib.parse

from .common import InfoExtractor
from ..utils import parse_duration, remove_end, unified_strdate, urljoin

class NDTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:[^/]+\.)?ndtv\.com/(?:[^/]+/)*videos?/?(?:[^/]+/)*[^/?^&]+-(?P<id>\d+)'

    _TESTS = [
        {
            'url': 'https://khabar.ndtv.com/video/show/prime-time/prime-time-ill-system-and-poor-education-468818',
            'md5': '78efcf3880ef3fd9b83d405ca94a38eb',
            'info_dict': {
                'id': '468818',
                'ext': 'mp4',
                'title': "प्राइम टाइम: सिस्टम बीमार, स्कूल बदहाल",
                'description': 'md5:f410512f1b49672e5695dea16ef2731d',
                'upload_date': '20170928',
                'duration': 2218,
                'thumbnail': r're:https?://.*\.jpg',
            }
        },
        {
            # __filename is url
            'url': 'http://movies.ndtv.com/videos/cracker-free-diwali-wishes-from-karan-johar-kriti-sanon-other-stars-470304',
            'md5': 'f1d709352305b44443515ac56b45aa46',
            'info_dict': {
                'id': '470304',
                'ext': 'mp4',
                'title': "Cracker-Free Diwali Wishes From Karan Johar, Kriti Sanon & Other Stars",
                'description': 'md5:f115bba1adf2f6433fa7c1ade5feb465',
                'upload_date': '20171019',
                'duration': 137,
                'thumbnail': r're:https?://.*\.jpg',
            }
        },
        {
            'url': 'https://www.ndtv.com/video/news/news/delhi-s-air-quality-status-report-after-diwali-is-very-poor-470372',
            'only_matching': True
        },
        {
            'url': 'https://auto.ndtv.com/videos/the-cnb-daily-october-13-2017-469935',
            'only_matching': True
        },
        {
            'url': 'https://sports.ndtv.com/cricket/videos/2nd-t20i-rock-thrown-at-australia-cricket-team-bus-after-win-over-india-469764',
            'only_matching': True
        },
        {
            'url': 'http://gadgets.ndtv.com/videos/uncharted-the-lost-legacy-review-465568',
            'only_matching': True
        },
        {
            'url': 'http://profit.ndtv.com/videos/news/video-indian-economy-on-very-solid-track-international-monetary-fund-chief-470040',
            'only_matching': True
        },
        {
            'url': 'http://food.ndtv.com/video-basil-seeds-coconut-porridge-419083',
            'only_matching': True
        },
        {
            'url': 'https://doctor.ndtv.com/videos/top-health-stories-of-the-week-467396',
            'only_matching': True
        },
        {
            'url': 'https://swirlster.ndtv.com/video/how-to-make-friends-at-work-469324',
            'only_matching': True
        }
    ]

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

        # '__title' does not contain extra words such as sub-site name, "Video" etc.
        title = urllib.parse.unquote_plus(
            self._search_regex(r"__title\s*=\s*'([^']+)'", webpage, 'title', default=None)
            or self._og_search_title(webpage))

        filename = self._search_regex(
            r"(?:__)?filename\s*[:=]\s*'([^']+)'", webpage, 'video filename')
        # in "movies" sub-site pages, filename is URL
        video_url = urljoin('https://ndtvod.bc-ssl.cdn.bitgravity.com/23372/ndtv/', filename.lstrip('/'))

        # "doctor" sub-site has MM:SS format
        duration = parse_duration(self._search_regex(
            r"(?:__)?duration\s*[:=]\s*'([^']+)'", webpage, 'duration', fatal=False))

        # "sports", "doctor", "swirlster" sub-sites don't have 'publish-date'
        upload_date = unified_strdate(self._html_search_meta(
            'publish-date', webpage, 'upload date', default=None) or self._html_search_meta(
            'uploadDate', webpage, 'upload date', default=None) or self._search_regex(
            r'datePublished"\s*:\s*"([^"]+)"', webpage, 'upload date', fatal=False))

        description = remove_end(self._og_search_description(webpage), ' (Read more)')

        return {
            'id': video_id,
            'url': video_url,
            'title': title,
            'description': description,
            'thumbnail': self._og_search_thumbnail(webpage),
            'duration': duration,
            'upload_date': upload_date,
        }


code:
import itertools
import json
import time
import urllib.error
import urllib.parse

from .common import InfoExtractor
from ..utils import ExtractorError, parse_iso8601, try_get

_BASE_URL_RE = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app|nebula\.tv)'

class NebulaBaseIE(InfoExtractor):
    _NETRC_MACHINE = 'watchnebula'

    _nebula_api_token = None
    _nebula_bearer_token = None
    _zype_access_token = None

    def _perform_nebula_auth(self, username, password):
        if not username or not password:
            self.raise_login_required()

        data = json.dumps({'email': username, 'password': password}).encode('utf8')
        response = self._download_json(
            'https://api.watchnebula.com/api/v1/auth/login/',
            data=data, fatal=False, video_id=None,
            headers={
                'content-type': 'application/json',
                # Submitting the 'sessionid' cookie always causes a 403 on auth endpoint
                'cookie': ''
            },
            note='Logging in to Nebula with supplied credentials',
            errnote='Authentication failed or rejected')
        if not response or not response.get('key'):
            self.raise_login_required()

        # save nebula token as cookie
        self._set_cookie(
            'nebula.app', 'nebula-auth',
            urllib.parse.quote(
                json.dumps({
                    "apiToken": response["key"],
                    "isLoggingIn": False,
                    "isLoggingOut": False,
                }, separators=(",", ":"))),
            expire_time=int(time.time()) + 86400 * 365,
        )

        return response['key']

    def _retrieve_nebula_api_token(self, username=None, password=None):
        """
        Check cookie jar for valid token. Try to authenticate using credentials if no valid token
        can be found in the cookie jar.
        """
        nebula_cookies = self._get_cookies('https://nebula.app')
        nebula_cookie = nebula_cookies.get('nebula-auth')
        if nebula_cookie:
            self.to_screen('Authenticating to Nebula with token from cookie jar')
            nebula_cookie_value = urllib.parse.unquote(nebula_cookie.value)
            nebula_api_token = self._parse_json(nebula_cookie_value, None).get('apiToken')
            if nebula_api_token:
                return nebula_api_token

        return self._perform_nebula_auth(username, password)

    def _call_nebula_api(self, url, video_id=None, method='GET', auth_type='api', note=''):
        assert method in ('GET', 'POST',)
        assert auth_type in ('api', 'bearer',)

        def inner_call():
            authorization = f'Token {self._nebula_api_token}' if auth_type == 'api' else f'Bearer {self._nebula_bearer_token}'
            return self._download_json(
                url, video_id, note=note, headers={'Authorization': authorization},
                data=b'' if method == 'POST' else None)

        try:
            return inner_call()
        except ExtractorError as exc:
            # if 401 or 403, attempt credential re-auth and retry
            if exc.cause and isinstance(exc.cause, urllib.error.HTTPError) and exc.cause.code in (401, 403):
                self.to_screen(f'Reauthenticating to Nebula and retrying, because last {auth_type} call resulted in error {exc.cause.code}')
                self._perform_login()
                return inner_call()
            else:
                raise

    def _fetch_nebula_bearer_token(self):
        """
        Get a Bearer token for the Nebula API. This will be required to fetch video meta data.
        """
        response = self._call_nebula_api('https://api.watchnebula.com/api/v1/authorization/',
                                         method='POST',
                                         note='Authorizing to Nebula')
        return response['token']

    def _fetch_zype_access_token(self):
        """
        Get a Zype access token, which is required to access video streams -- in our case: to
        generate video URLs.
        """
        user_object = self._call_nebula_api('https://api.watchnebula.com/api/v1/auth/user/', note='Retrieving Zype access token')

        access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], str)
        if not access_token:
            if try_get(user_object, lambda x: x['is_subscribed'], bool):
                # TODO: Reimplement the same Zype token polling the Nebula frontend implements
                # see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532
                raise ExtractorError(
                    'Unable to extract Zype access token from Nebula API authentication endpoint. '
                    'Open an arbitrary video in a browser with this account to generate a token',
                    expected=True)
            raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')
        return access_token

    def _build_video_info(self, episode):
        zype_id = episode['zype_id']
        zype_video_url = f'https://player.zype.com/embed/{zype_id}.html?access_token={self._zype_access_token}'
        channel_slug = episode['channel_slug']
        return {
            'id': episode['zype_id'],
            'display_id': episode['slug'],
            '_type': 'url_transparent',
            'ie_key': 'Zype',
            'url': zype_video_url,
            'title': episode['title'],
            'description': episode['description'],
            'timestamp': parse_iso8601(episode['published_at']),
            'thumbnails': [{
                # 'id': tn.get('name'),  # this appears to be null
                'url': tn['original'],
                'height': key,
            } for key, tn in episode['assets']['thumbnail'].items()],
            'duration': episode['duration'],
            'channel': episode['channel_title'],
            'channel_id': channel_slug,
            'channel_url': f'https://nebula.app/{channel_slug}',
            'uploader': episode['channel_title'],
            'uploader_id': channel_slug,
            'uploader_url': f'https://nebula.app/{channel_slug}',
            'series': episode['channel_title'],
            'creator': episode['channel_title'],
        }

    def _perform_login(self, username=None, password=None):
        self._nebula_api_token = self._retrieve_nebula_api_token(username, password)
        self._nebula_bearer_token = self._fetch_nebula_bearer_token()
        self._zype_access_token = self._fetch_zype_access_token()

class NebulaIE(NebulaBaseIE):
    _VALID_URL = rf'{_BASE_URL_RE}/videos/(?P<id>[-\w]+)'
    _TESTS = [
        {
            'url': 'https://nebula.app/videos/that-time-disney-remade-beauty-and-the-beast',
            'md5': '14944cfee8c7beeea106320c47560efc',
            'info_dict': {
                'id': '5c271b40b13fd613090034fd',
                'ext': 'mp4',
                'title': 'That Time Disney Remade Beauty and the Beast',
                'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',
                'upload_date': '20180731',
                'timestamp': 1533009600,
                'channel': 'Lindsay Ellis',
                'channel_id': 'lindsayellis',
                'uploader': 'Lindsay Ellis',
                'uploader_id': 'lindsayellis',
                'timestamp': 1533009600,
                'uploader_url': 'https://nebula.app/lindsayellis',
                'series': 'Lindsay Ellis',
                'average_rating': int,
                'display_id': 'that-time-disney-remade-beauty-and-the-beast',
                'channel_url': 'https://nebula.app/lindsayellis',
                'creator': 'Lindsay Ellis',
                'duration': 2212,
                'view_count': int,
                'thumbnail': r're:https://\w+\.cloudfront\.net/[\w-]+\.jpeg?.*',
            },
        },
        {
            'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
            'md5': 'd05739cf6c38c09322422f696b569c23',
            'info_dict': {
                'id': '5e7e78171aaf320001fbd6be',
                'ext': 'mp4',
                'title': 'Landing Craft - How The Allies Got Ashore',
                'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
                'upload_date': '20200327',
                'timestamp': 1585348140,
                'channel': 'Real Engineering',
                'channel_id': 'realengineering',
                'uploader': 'Real Engineering',
                'uploader_id': 'realengineering',
                'view_count': int,
                'series': 'Real Engineering',
                'average_rating': int,
                'display_id': 'the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
                'creator': 'Real Engineering',
                'duration': 841,
                'channel_url': 'https://nebula.app/realengineering',
                'uploader_url': 'https://nebula.app/realengineering',
                'thumbnail': r're:https://\w+\.cloudfront\.net/[\w-]+\.jpeg?.*',
            },
        },
        {
            'url': 'https://nebula.app/videos/money-episode-1-the-draw',
            'md5': 'ebe28a7ad822b9ee172387d860487868',
            'info_dict': {
                'id': '5e779ebdd157bc0001d1c75a',
                'ext': 'mp4',
                'title': 'Episode 1: The Draw',
                'description': r'contains:There’s free money on offer… if the players can all work together.',
                'upload_date': '20200323',
                'timestamp': 1584980400,
                'channel': 'Tom Scott Presents: Money',
                'channel_id': 'tom-scott-presents-money',
                'uploader': 'Tom Scott Presents: Money',
                'uploader_id': 'tom-scott-presents-money',
                'uploader_url': 'https://nebula.app/tom-scott-presents-money',
                'duration': 825,
                'channel_url': 'https://nebula.app/tom-scott-presents-money',
                'view_count': int,
                'series': 'Tom Scott Presents: Money',
                'display_id': 'money-episode-1-the-draw',
                'thumbnail': r're:https://\w+\.cloudfront\.net/[\w-]+\.jpeg?.*',
                'average_rating': int,
                'creator': 'Tom Scott Presents: Money',
            },
        },
        {
            'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
            'only_matching': True,
        },
    ]

    def _fetch_video_metadata(self, slug):
        return self._call_nebula_api(f'https://content.watchnebula.com/video/{slug}/',
                                     video_id=slug,
                                     auth_type='bearer',
                                     note='Fetching video meta data')

    def _real_extract(self, url):
        slug = self._match_id(url)
        video = self._fetch_video_metadata(slug)
        return self._build_video_info(video)

class NebulaSubscriptionsIE(NebulaBaseIE):
    IE_NAME = 'nebula:subscriptions'
    _VALID_URL = rf'{_BASE_URL_RE}/myshows'
    _TESTS = [
        {
            'url': 'https://nebula.app/myshows',
            'playlist_mincount': 1,
            'info_dict': {
                'id': 'myshows',
            },
        },
    ]

    def _generate_playlist_entries(self):
        next_url = 'https://content.watchnebula.com/library/video/?page_size=100'
        page_num = 1
        while next_url:
            channel = self._call_nebula_api(next_url, 'myshows', auth_type='bearer',
                                            note=f'Retrieving subscriptions page {page_num}')
            for episode in channel['results']:
                yield self._build_video_info(episode)
            next_url = channel['next']
            page_num += 1

    def _real_extract(self, url):
        return self.playlist_result(self._generate_playlist_entries(), 'myshows')

class NebulaChannelIE(NebulaBaseIE):
    IE_NAME = 'nebula:channel'
    _VALID_URL = rf'{_BASE_URL_RE}/(?!myshows|videos/)(?P<id>[-\w]+)'
    _TESTS = [
        {
            'url': 'https://nebula.app/tom-scott-presents-money',
            'info_dict': {
                'id': 'tom-scott-presents-money',
                'title': 'Tom Scott Presents: Money',
                'description': 'Tom Scott hosts a series all about trust, negotiation and money.',
            },
            'playlist_count': 5,
        }, {
            'url': 'https://nebula.app/lindsayellis',
            'info_dict': {
                'id': 'lindsayellis',
                'title': 'Lindsay Ellis',
                'description': 'Enjoy these hottest of takes on Disney, Transformers, and Musicals.',
            },
            'playlist_mincount': 100,
        },
    ]

    def _generate_playlist_entries(self, collection_id, channel):
        episodes = channel['episodes']['results']
        for page_num in itertools.count(2):
            for episode in episodes:
                yield self._build_video_info(episode)
            next_url = channel['episodes']['next']
            if not next_url:
                break
            channel = self._call_nebula_api(next_url, collection_id, auth_type='bearer',
                                            note=f'Retrieving channel page {page_num}')
            episodes = channel['episodes']['results']

    def _real_extract(self, url):
        collection_id = self._match_id(url)
        channel_url = f'https://content.watchnebula.com/video/channels/{collection_id}/'
        channel = self._call_nebula_api(channel_url, collection_id, auth_type='bearer', note='Retrieving channel')
        channel_details = channel['details']

        return self.playlist_result(
            entries=self._generate_playlist_entries(collection_id, channel),
            playlist_id=collection_id,
            playlist_title=channel_details['title'],
            playlist_description=channel_details['description']
        )


code:
import datetime

from .common import InfoExtractor

class NerdCubedFeedIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?nerdcubed\.co\.uk/feed\.json'
    _TEST = {
        'url': 'http://www.nerdcubed.co.uk/feed.json',
        'info_dict': {
            'id': 'nerdcubed-feed',
            'title': 'nerdcubed.co.uk feed',
        },
        'playlist_mincount': 1300,
    }

    def _real_extract(self, url):
        feed = self._download_json(url, url, 'Downloading NerdCubed JSON feed')

        entries = [{
            '_type': 'url',
            'title': feed_entry['title'],
            'uploader': feed_entry['source']['name'] if feed_entry['source'] else None,
            'upload_date': datetime.datetime.strptime(feed_entry['date'], '%Y-%m-%d').strftime('%Y%m%d'),
            'url': 'http://www.youtube.com/watch?v=' + feed_entry['youtube_id'],
        } for feed_entry in feed]

        return {
            '_type': 'playlist',
            'title': 'nerdcubed.co.uk feed',
            'id': 'nerdcubed-feed',
            'entries': entries,
        }