code:
# coding: utf-8
# NOTE: no longer used after 2021/05/02, but keep remained
#   for future use and reference
from __future__ import unicode_literals

from ..compat import compat_str
from ..utils import str_or_none

# keep it sync with extractors.py
from .youtube import (
    YoutubeFavouritesIE,
    YoutubeHistoryIE,
    YoutubeTabIE,
    YoutubePlaylistIE,
    YoutubeRecommendedIE,
    YoutubeSearchDateIE,
    YoutubeSearchIE,
    # YoutubeSearchURLIE,
    YoutubeSubscriptionsIE,
    YoutubeTruncatedIDIE,
    YoutubeTruncatedURLIE,
    YoutubeYtBeIE,
    YoutubeYtUserIE,
    YoutubeWatchLaterIE,
)

ytie = (
    YoutubeFavouritesIE,
    YoutubeHistoryIE,
    YoutubeTabIE,
    YoutubePlaylistIE,
    YoutubeRecommendedIE,
    YoutubeSearchDateIE,
    YoutubeSearchIE,
    YoutubeSubscriptionsIE,
    YoutubeTruncatedIDIE,
    YoutubeTruncatedURLIE,
    YoutubeYtBeIE,
    YoutubeYtUserIE,
    YoutubeWatchLaterIE,
)

def _convert_result(ret, prefix):
    if not isinstance(ret, dict):
        return ret

    type = str_or_none(ret.get('_type')) or ''
    if type == 'playlist':
        ret['entries'] = [_convert_result(x, prefix) for x in ret['entries']]
    elif type.startswith('url'):
        ret['url'] = prefix + ret['url']

    if 'ie_key' in ret:
        del ret['ie_key']

    return ret

def _convert_test_only_matching(test, prefix):
    return {
        'url': prefix + test['url'],
        'only_matching': True,
    }

def ___real_extract(self, url):
    url = self.remove_prefix(url)
    try:  # Python 2.x
        real_extract_func = self.BASE_IE._real_extract.__func__
    except (TypeError, AttributeError):
        real_extract_func = self.BASE_IE._real_extract
    ret = real_extract_func(self, url)
    return _convert_result(ret, self.PREFIXES[0])

for base_ie in tuple():
    for value in ytie:
        key = value.__name__
        obj = value()
        clazz_name = str(base_ie.__name__[:-2] + key[7:])
        clazz_dict = {
            'BASE_IE': value,
            '_real_extract': ___real_extract,
        }

        if hasattr(value, '_TEST') and isinstance(getattr(value, '_TEST'), dict):
            clazz_dict['_TEST'] = _convert_test_only_matching(obj._TEST, base_ie.PREFIXES[0])
        if hasattr(value, '_TESTS') and isinstance(getattr(value, '_TESTS'), list):
            clazz_dict['_TESTS'] = [_convert_test_only_matching(x, base_ie.PREFIXES[0]) for x in obj._TESTS]

        if hasattr(value, 'IE_NAME'):
            ie_name = obj.IE_NAME
            if not isinstance(value.IE_NAME, compat_str):
                ie_name = '%s' % ie_name
            if ie_name.startswith('youtube:'):
                ie_name = base_ie.IE_NAME + ie_name[7:]
            elif ie_name == key[:-2]:
                ie_name = clazz_name[:-2]
            else:
                ie_name = base_ie.IE_NAME + ':' + ie_name
            clazz_dict['IE_NAME'] = ie_name

        if hasattr(value, '_VALID_URL'):
            clazz_dict['_VALID_URL'] = value._VALID_URL

        globals()[clazz_name] = type(clazz_name, (base_ie, value), clazz_dict)

repo_name: Lesmiscore/ytdl


code:
import re

from .common import InfoExtractor
from ..utils import (
    parse_duration,
    parse_iso8601,
    xpath_with_ns,
    xpath_text,
    int_or_none,
)

class ZapiksIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?zapiks\.(?:fr|com)/(?:(?:[a-z]{2}/)?(?P<display_id>.+?)\.html|index\.php\?.*\bmedia_id=(?P<id>\d+))'
    _EMBED_REGEX = [r'<iframe[^>]+src="(?P<url>https?://(?:www\.)?zapiks\.fr/index\.php\?.+?)"']
    _TESTS = [
        {
            'url': 'http://www.zapiks.fr/ep2s3-bon-appetit-eh-be-viva.html',
            'md5': 'aeb3c473b2d564b2d46d664d28d5f050',
            'info_dict': {
                'id': '80798',
                'ext': 'mp4',
                'title': 'EP2S3 - Bon Appétit - Eh bé viva les pyrénées con!',
                'description': 'md5:7054d6f6f620c6519be1fe710d4da847',
                'thumbnail': r're:^https?://.*\.jpg$',
                'duration': 528,
                'timestamp': 1359044972,
                'upload_date': '20130124',
                'view_count': int,
            },
        },
        {
            'url': 'http://www.zapiks.com/ep3s5-bon-appetit-baqueira-m-1.html',
            'only_matching': True,
        },
        {
            'url': 'http://www.zapiks.com/nl/ep3s5-bon-appetit-baqueira-m-1.html',
            'only_matching': True,
        },
        {
            'url': 'http://www.zapiks.fr/index.php?action=playerIframe&media_id=118046&width=640&height=360&autoStart=false&language=fr',
            'only_matching': True,
        },
    ]

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

        webpage = self._download_webpage(url, display_id)

        if not video_id:
            video_id = self._search_regex(
                r'data-media-id="(\d+)"', webpage, 'video id')

        playlist = self._download_xml(
            'http://www.zapiks.fr/view/index.php?action=playlist&media_id=%s&lang=en' % video_id,
            display_id)

        NS_MAP = {
            'jwplayer': 'http://rss.jwpcdn.com/'
        }

        def ns(path):
            return xpath_with_ns(path, NS_MAP)

        item = playlist.find('./channel/item')

        title = xpath_text(item, 'title', 'title') or self._og_search_title(webpage)
        description = self._og_search_description(webpage, default=None)
        thumbnail = xpath_text(
            item, ns('./jwplayer:image'), 'thumbnail') or self._og_search_thumbnail(webpage, default=None)
        duration = parse_duration(self._html_search_meta(
            'duration', webpage, 'duration', default=None))
        timestamp = parse_iso8601(self._html_search_meta(
            'uploadDate', webpage, 'upload date', default=None), ' ')

        view_count = int_or_none(self._search_regex(
            r'UserPlays:(\d+)', webpage, 'view count', default=None))
        comment_count = int_or_none(self._search_regex(
            r'UserComments:(\d+)', webpage, 'comment count', default=None))

        formats = []
        for source in item.findall(ns('./jwplayer:source')):
            format_id = source.attrib['label']
            f = {
                'url': source.attrib['file'],
                'format_id': format_id,
            }
            m = re.search(r'^(?P<height>\d+)[pP]', format_id)
            if m:
                f['height'] = int(m.group('height'))
            formats.append(f)
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'duration': duration,
            'timestamp': timestamp,
            'view_count': view_count,
            'comment_count': comment_count,
            'formats': formats,
        }


code:
import re
from uuid import uuid4

from .common import InfoExtractor
from ..compat import compat_HTTPError, compat_str
from ..utils import (
    ExtractorError,
    int_or_none,
    join_nonempty,
    try_get,
    url_or_none,
    urlencode_postdata,
)

class ZattooPlatformBaseIE(InfoExtractor):
    _power_guide_hash = None

    def _host_url(self):
        return 'https://%s' % (self._API_HOST if hasattr(self, '_API_HOST') else self._HOST)

    def _real_initialize(self):
        if not self._power_guide_hash:
            self.raise_login_required('An account is needed to access this media', method='password')

    def _perform_login(self, username, password):
        try:
            data = self._download_json(
                '%s/zapi/v2/account/login' % self._host_url(), None, 'Logging in',
                data=urlencode_postdata({
                    'login': username,
                    'password': password,
                    'remember': 'true',
                }), headers={
                    'Referer': '%s/login' % self._host_url(),
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                })
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
                raise ExtractorError(
                    'Unable to login: incorrect username and/or password',
                    expected=True)
            raise

        self._power_guide_hash = data['session']['power_guide_hash']

    def _initialize_pre_login(self):
        session_token = self._download_json(
            f'{self._host_url()}/token.json', None, 'Downloading session token')['session_token']

        # Will setup appropriate cookies
        self._request_webpage(
            '%s/zapi/v3/session/hello' % self._host_url(), None,
            'Opening session', data=urlencode_postdata({
                'uuid': compat_str(uuid4()),
                'lang': 'en',
                'app_version': '1.8.2',
                'format': 'json',
                'client_app_token': session_token,
            }))

    def _extract_video_id_from_recording(self, recid):
        playlist = self._download_json(
            f'{self._host_url()}/zapi/v2/playlist', recid, 'Downloading playlist')
        try:
            return next(
                str(item['program_id']) for item in playlist['recordings']
                if item.get('program_id') and str(item.get('id')) == recid)
        except (StopIteration, KeyError):
            raise ExtractorError('Could not extract video id from recording')

    def _extract_cid(self, video_id, channel_name):
        channel_groups = self._download_json(
            '%s/zapi/v2/cached/channels/%s' % (self._host_url(),
                                               self._power_guide_hash),
            video_id, 'Downloading channel list',
            query={'details': False})['channel_groups']
        channel_list = []
        for chgrp in channel_groups:
            channel_list.extend(chgrp['channels'])
        try:
            return next(
                chan['cid'] for chan in channel_list
                if chan.get('cid') and (
                    chan.get('display_alias') == channel_name
                    or chan.get('cid') == channel_name))
        except StopIteration:
            raise ExtractorError('Could not extract channel id')

    def _extract_cid_and_video_info(self, video_id):
        data = self._download_json(
            '%s/zapi/v2/cached/program/power_details/%s' % (
                self._host_url(), self._power_guide_hash),
            video_id,
            'Downloading video information',
            query={
                'program_ids': video_id,
                'complete': True,
            })

        p = data['programs'][0]
        cid = p['cid']

        info_dict = {
            'id': video_id,
            'title': p.get('t') or p['et'],
            'description': p.get('d'),
            'thumbnail': p.get('i_url'),
            'creator': p.get('channel_name'),
            'episode': p.get('et'),
            'episode_number': int_or_none(p.get('e_no')),
            'season_number': int_or_none(p.get('s_no')),
            'release_year': int_or_none(p.get('year')),
            'categories': try_get(p, lambda x: x['c'], list),
            'tags': try_get(p, lambda x: x['g'], list)
        }

        return cid, info_dict

    def _extract_ondemand_info(self, ondemand_id):
        """
        @returns    (ondemand_token, ondemand_type, info_dict)
        """
        data = self._download_json(
            '%s/zapi/vod/movies/%s' % (self._host_url(), ondemand_id),
            ondemand_id, 'Downloading ondemand information')
        info_dict = {
            'id': ondemand_id,
            'title': data.get('title'),
            'description': data.get('description'),
            'duration': int_or_none(data.get('duration')),
            'release_year': int_or_none(data.get('year')),
            'episode_number': int_or_none(data.get('episode_number')),
            'season_number': int_or_none(data.get('season_number')),
            'categories': try_get(data, lambda x: x['categories'], list),
        }
        return data['terms_catalog'][0]['terms'][0]['token'], data['type'], info_dict

    def _extract_formats(self, cid, video_id, record_id=None, ondemand_id=None, ondemand_termtoken=None, ondemand_type=None, is_live=False):
        postdata_common = {
            'https_watch_urls': True,
        }

        if is_live:
            postdata_common.update({'timeshift': 10800})
            url = '%s/zapi/watch/live/%s' % (self._host_url(), cid)
        elif record_id:
            url = '%s/zapi/watch/recording/%s' % (self._host_url(), record_id)
        elif ondemand_id:
            postdata_common.update({
                'teasable_id': ondemand_id,
                'term_token': ondemand_termtoken,
                'teasable_type': ondemand_type
            })
            url = '%s/zapi/watch/vod/video' % self._host_url()
        else:
            url = '%s/zapi/v3/watch/replay/%s/%s' % (self._host_url(), cid, video_id)
        formats = []
        subtitles = {}
        for stream_type in ('dash', 'hls7'):
            postdata = postdata_common.copy()
            postdata['stream_type'] = stream_type

            data = self._download_json(
                url, video_id, 'Downloading %s formats' % stream_type.upper(),
                data=urlencode_postdata(postdata), fatal=False)
            if not data:
                continue

            watch_urls = try_get(
                data, lambda x: x['stream']['watch_urls'], list)
            if not watch_urls:
                continue

            for watch in watch_urls:
                if not isinstance(watch, dict):
                    continue
                watch_url = url_or_none(watch.get('url'))
                if not watch_url:
                    continue
                audio_channel = watch.get('audio_channel')
                preference = 1 if audio_channel == 'A' else None
                format_id = join_nonempty(stream_type, watch.get('maxrate'), audio_channel)
                if stream_type.startswith('dash'):
                    this_formats, subs = self._extract_mpd_formats_and_subtitles(
                        watch_url, video_id, mpd_id=format_id, fatal=False)
                    self._merge_subtitles(subs, target=subtitles)
                elif stream_type.startswith('hls'):
                    this_formats, subs = self._extract_m3u8_formats_and_subtitles(
                        watch_url, video_id, 'mp4',
                        entry_protocol='m3u8_native', m3u8_id=format_id,
                        fatal=False)
                    self._merge_subtitles(subs, target=subtitles)
                elif stream_type == 'hds':
                    this_formats = self._extract_f4m_formats(
                        watch_url, video_id, f4m_id=format_id, fatal=False)
                elif stream_type == 'smooth_playready':
                    this_formats = self._extract_ism_formats(
                        watch_url, video_id, ism_id=format_id, fatal=False)
                else:
                    assert False
                for this_format in this_formats:
                    this_format['quality'] = preference
                formats.extend(this_formats)
        self._sort_formats(formats)
        return formats, subtitles

    def _extract_video(self, video_id, record_id=None):
        cid, info_dict = self._extract_cid_and_video_info(video_id)
        info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
        return info_dict

    def _extract_live(self, channel_name):
        cid = self._extract_cid(channel_name, channel_name)
        formats, subtitles = self._extract_formats(cid, cid, is_live=True)
        return {
            'id': channel_name,
            'title': channel_name,
            'is_live': True,
            'formats': formats,
            'subtitles': subtitles
        }

    def _extract_record(self, record_id):
        video_id = self._extract_video_id_from_recording(record_id)
        cid, info_dict = self._extract_cid_and_video_info(video_id)
        info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
        return info_dict

    def _extract_ondemand(self, ondemand_id):
        ondemand_termtoken, ondemand_type, info_dict = self._extract_ondemand_info(ondemand_id)
        info_dict['formats'], info_dict['subtitles'] = self._extract_formats(
            None, ondemand_id, ondemand_id=ondemand_id,
            ondemand_termtoken=ondemand_termtoken, ondemand_type=ondemand_type)
        return info_dict

    def _real_extract(self, url):
        video_id, record_id = self._match_valid_url(url).groups()
        return getattr(self, f'_extract_{self._TYPE}')(video_id or record_id)

def _create_valid_url(host, match, qs, base_re=None):
    match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
    return rf'''(?x)https?://(?:www\.)?{re.escape(host)}/(?:
        [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
        {match_base}
    )'''

class ZattooBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'zattoo'
    _HOST = 'zattoo.com'

class ZattooIE(ZattooBaseIE):
    _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://zattoo.com/program/zdf/250170418',
        'info_dict': {
            'id': '250170418',
            'ext': 'mp4',
            'title': 'Markus Lanz',
            'description': 'md5:e41cb1257de008ca62a73bb876ffa7fc',
            'thumbnail': 're:http://images.zattic.com/cms/.+/format_480x360.jpg',
            'creator': 'ZDF HD',
            'release_year': 2022,
            'episode': 'Folge 1655',
            'categories': 'count:1',
            'tags': 'count:2'
        },
        'params': {'skip_download': 'm3u8'}
    }, {
        'url': 'https://zattoo.com/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://zattoo.com/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class ZattooLiveIE(ZattooBaseIE):
    _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://zattoo.com/live/srf1',
        'only_matching': True,
    }]

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

class ZattooMoviesIE(ZattooBaseIE):
    _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\w+', 'movie_id', 'vod/movies')
    _TYPE = 'ondemand'
    _TESTS = [{
        'url': 'https://zattoo.com/vod/movies/7521',
        'only_matching': True,
    }, {
        'url': 'https://zattoo.com/ondemand?movie_id=7521&term_token=9f00f43183269484edde',
        'only_matching': True,
    }]

class ZattooRecordingsIE(ZattooBaseIE):
    _VALID_URL = _create_valid_url('zattoo.com', r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://zattoo.com/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://zattoo.com/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class NetPlusTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'netplus'
    _HOST = 'netplus.tv'
    _API_HOST = 'www.%s' % _HOST

class NetPlusTVIE(NetPlusTVBaseIE):
    _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://netplus.tv/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://netplus.tv/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class NetPlusTVLiveIE(NetPlusTVBaseIE):
    _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://netplus.tv/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://netplus.tv/live/srf1',
        'only_matching': True,
    }]

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

class NetPlusTVRecordingsIE(NetPlusTVBaseIE):
    _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://netplus.tv/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://netplus.tv/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class MNetTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'mnettv'
    _HOST = 'tvplus.m-net.de'

class MNetTVIE(MNetTVBaseIE):
    _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://tvplus.m-net.de/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://tvplus.m-net.de/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class MNetTVLiveIE(MNetTVBaseIE):
    _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://tvplus.m-net.de/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://tvplus.m-net.de/live/srf1',
        'only_matching': True,
    }]

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

class MNetTVRecordingsIE(MNetTVBaseIE):
    _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://tvplus.m-net.de/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://tvplus.m-net.de/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class WalyTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'walytv'
    _HOST = 'player.waly.tv'

class WalyTVIE(WalyTVBaseIE):
    _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://player.waly.tv/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://player.waly.tv/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class WalyTVLiveIE(WalyTVBaseIE):
    _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://player.waly.tv/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://player.waly.tv/live/srf1',
        'only_matching': True,
    }]

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

class WalyTVRecordingsIE(WalyTVBaseIE):
    _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://player.waly.tv/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://player.waly.tv/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class BBVTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'bbvtv'
    _HOST = 'bbv-tv.net'
    _API_HOST = 'www.%s' % _HOST

class BBVTVIE(BBVTVBaseIE):
    _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://bbv-tv.net/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://bbv-tv.net/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class BBVTVLiveIE(BBVTVBaseIE):
    _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://bbv-tv.net/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://bbv-tv.net/live/srf1',
        'only_matching': True,
    }]

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

class BBVTVRecordingsIE(BBVTVBaseIE):
    _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://bbv-tv.net/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://bbv-tv.net/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class VTXTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'vtxtv'
    _HOST = 'vtxtv.ch'
    _API_HOST = 'www.%s' % _HOST

class VTXTVIE(VTXTVBaseIE):
    _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://vtxtv.ch/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://vtxtv.ch/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class VTXTVLiveIE(VTXTVBaseIE):
    _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://vtxtv.ch/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://vtxtv.ch/live/srf1',
        'only_matching': True,
    }]

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

class VTXTVRecordingsIE(VTXTVBaseIE):
    _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://vtxtv.ch/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://vtxtv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class GlattvisionTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'glattvisiontv'
    _HOST = 'iptv.glattvision.ch'

class GlattvisionTVIE(GlattvisionTVBaseIE):
    _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://iptv.glattvision.ch/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://iptv.glattvision.ch/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class GlattvisionTVLiveIE(GlattvisionTVBaseIE):
    _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://iptv.glattvision.ch/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://iptv.glattvision.ch/live/srf1',
        'only_matching': True,
    }]

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

class GlattvisionTVRecordingsIE(GlattvisionTVBaseIE):
    _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://iptv.glattvision.ch/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://iptv.glattvision.ch/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class SAKTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'saktv'
    _HOST = 'saktv.ch'
    _API_HOST = 'www.%s' % _HOST

class SAKTVIE(SAKTVBaseIE):
    _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://saktv.ch/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://saktv.ch/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class SAKTVLiveIE(SAKTVBaseIE):
    _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://saktv.ch/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://saktv.ch/live/srf1',
        'only_matching': True,
    }]

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

class SAKTVRecordingsIE(SAKTVBaseIE):
    _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://saktv.ch/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://saktv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class EWETVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'ewetv'
    _HOST = 'tvonline.ewe.de'

class EWETVIE(EWETVBaseIE):
    _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://tvonline.ewe.de/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://tvonline.ewe.de/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class EWETVLiveIE(EWETVBaseIE):
    _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://tvonline.ewe.de/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://tvonline.ewe.de/live/srf1',
        'only_matching': True,
    }]

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

class EWETVRecordingsIE(EWETVBaseIE):
    _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://tvonline.ewe.de/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://tvonline.ewe.de/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class QuantumTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'quantumtv'
    _HOST = 'quantum-tv.com'
    _API_HOST = 'www.%s' % _HOST

class QuantumTVIE(QuantumTVBaseIE):
    _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://quantum-tv.com/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://quantum-tv.com/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class QuantumTVLiveIE(QuantumTVBaseIE):
    _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://quantum-tv.com/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://quantum-tv.com/live/srf1',
        'only_matching': True,
    }]

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

class QuantumTVRecordingsIE(QuantumTVBaseIE):
    _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://quantum-tv.com/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://quantum-tv.com/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class OsnatelTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'osnateltv'
    _HOST = 'tvonline.osnatel.de'

class OsnatelTVIE(OsnatelTVBaseIE):
    _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://tvonline.osnatel.de/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://tvonline.osnatel.de/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class OsnatelTVLiveIE(OsnatelTVBaseIE):
    _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://tvonline.osnatel.de/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://tvonline.osnatel.de/live/srf1',
        'only_matching': True,
    }]

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

class OsnatelTVRecordingsIE(OsnatelTVBaseIE):
    _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://tvonline.osnatel.de/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://tvonline.osnatel.de/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class EinsUndEinsTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = '1und1tv'
    _HOST = '1und1.tv'
    _API_HOST = 'www.%s' % _HOST

class EinsUndEinsTVIE(EinsUndEinsTVBaseIE):
    _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://1und1.tv/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://1und1.tv/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class EinsUndEinsTVLiveIE(EinsUndEinsTVBaseIE):
    _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://1und1.tv/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://1und1.tv/live/srf1',
        'only_matching': True,
    }]

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

class EinsUndEinsTVRecordingsIE(EinsUndEinsTVBaseIE):
    _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://1und1.tv/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://1und1.tv/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]

class SaltTVBaseIE(ZattooPlatformBaseIE):
    _NETRC_MACHINE = 'salttv'
    _HOST = 'tv.salt.ch'

class SaltTVIE(SaltTVBaseIE):
    _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
    _TYPE = 'video'
    _TESTS = [{
        'url': 'https://tv.salt.ch/program/daserste/210177916',
        'only_matching': True,
    }, {
        'url': 'https://tv.salt.ch/guide/german?channel=srf1&program=169860555',
        'only_matching': True,
    }]

class SaltTVLiveIE(SaltTVBaseIE):
    _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
    _TYPE = 'live'
    _TESTS = [{
        'url': 'https://tv.salt.ch/channels/german?channel=srf_zwei',
        'only_matching': True,
    }, {
        'url': 'https://tv.salt.ch/live/srf1',
        'only_matching': True,
    }]

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

class SaltTVRecordingsIE(SaltTVBaseIE):
    _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'recording')
    _TYPE = 'record'
    _TESTS = [{
        'url': 'https://tv.salt.ch/recordings?recording=193615508',
        'only_matching': True,
    }, {
        'url': 'https://tv.salt.ch/tc/ptc_recordings_all_recordings?recording=193615420',
        'only_matching': True,
    }]


code:
import re

from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    determine_ext,
    float_or_none,
    int_or_none,
    join_nonempty,
    merge_dicts,
    NO_DEFAULT,
    orderedSet,
    parse_codecs,
    qualities,
    traverse_obj,
    try_get,
    unified_timestamp,
    update_url_query,
    url_or_none,
    urljoin,
)

class ZDFBaseIE(InfoExtractor):
    _GEO_COUNTRIES = ['DE']
    _QUALITIES = ('auto', 'low', 'med', 'high', 'veryhigh', 'hd')

    def _call_api(self, url, video_id, item, api_token=None, referrer=None):
        headers = {}
        if api_token:
            headers['Api-Auth'] = 'Bearer %s' % api_token
        if referrer:
            headers['Referer'] = referrer
        return self._download_json(
            url, video_id, 'Downloading JSON %s' % item, headers=headers)

    @staticmethod
    def _extract_subtitles(src):
        subtitles = {}
        for caption in try_get(src, lambda x: x['captions'], list) or []:
            subtitle_url = url_or_none(caption.get('uri'))
            if subtitle_url:
                lang = caption.get('language', 'deu')
                subtitles.setdefault(lang, []).append({
                    'url': subtitle_url,
                })
        return subtitles

    def _extract_format(self, video_id, formats, format_urls, meta):
        format_url = url_or_none(meta.get('url'))
        if not format_url or format_url in format_urls:
            return
        format_urls.add(format_url)

        mime_type, ext = meta.get('mimeType'), determine_ext(format_url)
        if mime_type == 'application/x-mpegURL' or ext == 'm3u8':
            new_formats = self._extract_m3u8_formats(
                format_url, video_id, 'mp4', m3u8_id='hls',
                entry_protocol='m3u8_native', fatal=False)
        elif mime_type == 'application/f4m+xml' or ext == 'f4m':
            new_formats = self._extract_f4m_formats(
                update_url_query(format_url, {'hdcore': '3.7.0'}), video_id, f4m_id='hds', fatal=False)
        else:
            f = parse_codecs(meta.get('mimeCodec'))
            if not f and meta.get('type'):
                data = meta['type'].split('_')
                if try_get(data, lambda x: x[2]) == ext:
                    f = {'vcodec': data[0], 'acodec': data[1]}
            f.update({
                'url': format_url,
                'format_id': join_nonempty('http', meta.get('type'), meta.get('quality')),
                'tbr': int_or_none(self._search_regex(r'_(\d+)k_', format_url, 'tbr', default=None))
            })
            new_formats = [f]
        formats.extend(merge_dicts(f, {
            'format_note': join_nonempty('quality', 'class', from_dict=meta, delim=', '),
            'language': meta.get('language'),
            'language_preference': 10 if meta.get('class') == 'main' else -10 if meta.get('class') == 'ad' else -1,
            'quality': qualities(self._QUALITIES)(meta.get('quality')),
        }) for f in new_formats)

    def _extract_ptmd(self, ptmd_url, video_id, api_token, referrer):
        ptmd = self._call_api(
            ptmd_url, video_id, 'metadata', api_token, referrer)

        content_id = ptmd.get('basename') or ptmd_url.split('/')[-1]

        formats = []
        track_uris = set()
        for p in ptmd['priorityList']:
            formitaeten = p.get('formitaeten')
            if not isinstance(formitaeten, list):
                continue
            for f in formitaeten:
                f_qualities = f.get('qualities')
                if not isinstance(f_qualities, list):
                    continue
                for quality in f_qualities:
                    tracks = try_get(quality, lambda x: x['audio']['tracks'], list)
                    if not tracks:
                        continue
                    for track in tracks:
                        self._extract_format(
                            content_id, formats, track_uris, {
                                'url': track.get('uri'),
                                'type': f.get('type'),
                                'mimeType': f.get('mimeType'),
                                'quality': quality.get('quality'),
                                'class': track.get('class'),
                                'language': track.get('language'),
                            })
        self._sort_formats(formats, ('tbr', 'res', 'quality', 'language_preference'))

        duration = float_or_none(try_get(
            ptmd, lambda x: x['attributes']['duration']['value']), scale=1000)

        return {
            'extractor_key': ZDFIE.ie_key(),
            'id': content_id,
            'duration': duration,
            'formats': formats,
            'subtitles': self._extract_subtitles(ptmd),
        }

    def _extract_player(self, webpage, video_id, fatal=True):
        return self._parse_json(
            self._search_regex(
                r'(?s)data-zdfplayer-jsb=(["\'])(?P<json>{.+?})\1', webpage,
                'player JSON', default='{}' if not fatal else NO_DEFAULT,
                group='json'),
            video_id)

class ZDFIE(ZDFBaseIE):
    _VALID_URL = r'https?://www\.zdf\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
    _TESTS = [{
        # Same as https://www.phoenix.de/sendungen/ereignisse/corona-nachgehakt/wohin-fuehrt-der-protest-in-der-pandemie-a-2050630.html
        'url': 'https://www.zdf.de/politik/phoenix-sendungen/wohin-fuehrt-der-protest-in-der-pandemie-100.html',
        'md5': '34ec321e7eb34231fd88616c65c92db0',
        'info_dict': {
            'id': '210222_phx_nachgehakt_corona_protest',
            'ext': 'mp4',
            'title': 'Wohin führt der Protest in der Pandemie?',
            'description': 'md5:7d643fe7f565e53a24aac036b2122fbd',
            'duration': 1691,
            'timestamp': 1613948400,
            'upload_date': '20210221',
        },
        'skip': 'No longer available: "Diese Seite wurde leider nicht gefunden"',
    }, {
        # Same as https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html
        'url': 'https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html',
        'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
        'info_dict': {
            'id': '141007_ab18_10wochensommer_film',
            'ext': 'mp4',
            'title': 'Ab 18! - 10 Wochen Sommer',
            'description': 'md5:8253f41dc99ce2c3ff892dac2d65fe26',
            'duration': 2660,
            'timestamp': 1608604200,
            'upload_date': '20201222',
        },
        'skip': 'No longer available: "Diese Seite wurde leider nicht gefunden"',
    }, {
        'url': 'https://www.zdf.de/nachrichten/heute-journal/heute-journal-vom-30-12-2021-100.html',
        'info_dict': {
            'id': '211230_sendung_hjo',
            'ext': 'mp4',
            'description': 'md5:47dff85977bde9fb8cba9e9c9b929839',
            'duration': 1890.0,
            'upload_date': '20211230',
            'chapters': list,
            'thumbnail': 'md5:e65f459f741be5455c952cd820eb188e',
            'title': 'heute journal vom 30.12.2021',
            'timestamp': 1640897100,
        }
    }, {
        'url': 'https://www.zdf.de/dokumentation/terra-x/die-magie-der-farben-von-koenigspurpur-und-jeansblau-100.html',
        'info_dict': {
            'id': '151025_magie_farben2_tex',
            'ext': 'mp4',
            'title': 'Die Magie der Farben (2/2)',
            'description': 'md5:a89da10c928c6235401066b60a6d5c1a',
            'duration': 2615,
            'timestamp': 1465021200,
            'upload_date': '20160604',
            'thumbnail': 'https://www.zdf.de/assets/mauve-im-labor-100~768x432?cb=1464909117806',
        },
    }, {
        'url': 'https://www.zdf.de/funk/druck-11790/funk-alles-ist-verzaubert-102.html',
        'md5': '57af4423db0455a3975d2dc4578536bc',
        'info_dict': {
            'ext': 'mp4',
            'id': 'video_funk_1770473',
            'duration': 1278,
            'description': 'Die Neue an der Schule verdreht Ismail den Kopf.',
            'title': 'Alles ist verzaubert',
            'timestamp': 1635520560,
            'upload_date': '20211029',
            'thumbnail': 'https://www.zdf.de/assets/teaser-funk-alles-ist-verzaubert-100~1920x1080?cb=1636466431799',
        },
    }, {
        # Same as https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche
        'url': 'https://www.zdf.de/politik/phoenix-sendungen/die-gesten-der-maechtigen-100.html',
        'only_matching': True,
    }, {
        # Same as https://www.3sat.de/film/spielfilm/der-hauptmann-100.html
        'url': 'https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html',
        'only_matching': True,
    }, {
        # Same as https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html, equal media ids
        'url': 'https://www.zdf.de/wissen/nano/nano-21-mai-2019-102.html',
        'only_matching': True,
    }, {
        'url': 'https://www.zdf.de/service-und-hilfe/die-neue-zdf-mediathek/zdfmediathek-trailer-100.html',
        'only_matching': True,
    }, {
        'url': 'https://www.zdf.de/filme/taunuskrimi/die-lebenden-und-die-toten-1---ein-taunuskrimi-100.html',
        'only_matching': True,
    }, {
        'url': 'https://www.zdf.de/dokumentation/planet-e/planet-e-uebersichtsseite-weitere-dokumentationen-von-planet-e-100.html',
        'only_matching': True,
    }, {
        'url': 'https://www.zdf.de/arte/todliche-flucht/page-video-artede-toedliche-flucht-16-100.html',
        'info_dict': {
            'id': 'video_artede_083871-001-A',
            'ext': 'mp4',
            'title': 'Tödliche Flucht (1/6)',
            'description': 'md5:e34f96a9a5f8abd839ccfcebad3d5315',
            'duration': 3193.0,
            'timestamp': 1641355200,
            'upload_date': '20220105',
        },
        'skip': 'No longer available "Diese Seite wurde leider nicht gefunden"'
    }, {
        'url': 'https://www.zdf.de/serien/soko-stuttgart/das-geld-anderer-leute-100.html',
        'info_dict': {
            'id': '191205_1800_sendung_sok8',
            'ext': 'mp4',
            'title': 'Das Geld anderer Leute',
            'description': 'md5:cb6f660850dc5eb7d1ab776ea094959d',
            'duration': 2581.0,
            'timestamp': 1654790700,
            'upload_date': '20220609',
            'thumbnail': 'https://epg-image.zdf.de/fotobase-webdelivery/images/e2d7e55a-09f0-424e-ac73-6cac4dd65f35?layout=2400x1350',
        },
    }]

    def _extract_entry(self, url, player, content, video_id):
        title = content.get('title') or content['teaserHeadline']

        t = content['mainVideoContent']['http://zdf.de/rels/target']

        ptmd_path = t.get('http://zdf.de/rels/streams/ptmd')

        if not ptmd_path:
            ptmd_path = traverse_obj(
                t, ('streams', 'default', 'http://zdf.de/rels/streams/ptmd-template'),
                'http://zdf.de/rels/streams/ptmd-template').replace(
                '{playerId}', 'ngplayer_2_4')

        info = self._extract_ptmd(
            urljoin(url, ptmd_path), video_id, player['apiToken'], url)

        thumbnails = []
        layouts = try_get(
            content, lambda x: x['teaserImageRef']['layouts'], dict)
        if layouts:
            for layout_key, layout_url in layouts.items():
                layout_url = url_or_none(layout_url)
                if not layout_url:
                    continue
                thumbnail = {
                    'url': layout_url,
                    'format_id': layout_key,
                }
                mobj = re.search(r'(?P<width>\d+)x(?P<height>\d+)', layout_key)
                if mobj:
                    thumbnail.update({
                        'width': int(mobj.group('width')),
                        'height': int(mobj.group('height')),
                    })
                thumbnails.append(thumbnail)

        chapter_marks = t.get('streamAnchorTag') or []
        chapter_marks.append({'anchorOffset': int_or_none(t.get('duration'))})
        chapters = [{
            'start_time': chap.get('anchorOffset'),
            'end_time': next_chap.get('anchorOffset'),
            'title': chap.get('anchorLabel')
        } for chap, next_chap in zip(chapter_marks, chapter_marks[1:])]

        return merge_dicts(info, {
            'title': title,
            'description': content.get('leadParagraph') or content.get('teasertext'),
            'duration': int_or_none(t.get('duration')),
            'timestamp': unified_timestamp(content.get('editorialDate')),
            'thumbnails': thumbnails,
            'chapters': chapters or None
        })

    def _extract_regular(self, url, player, video_id):
        content = self._call_api(
            player['content'], video_id, 'content', player['apiToken'], url)
        return self._extract_entry(player['content'], player, content, video_id)

    def _extract_mobile(self, video_id):
        video = self._download_json(
            'https://zdf-cdn.live.cellular.de/mediathekV2/document/%s' % video_id,
            video_id)

        document = video['document']

        title = document['titel']
        content_id = document['basename']

        formats = []
        format_urls = set()
        for f in document['formitaeten']:
            self._extract_format(content_id, formats, format_urls, f)
        self._sort_formats(formats)

        thumbnails = []
        teaser_bild = document.get('teaserBild')
        if isinstance(teaser_bild, dict):
            for thumbnail_key, thumbnail in teaser_bild.items():
                thumbnail_url = try_get(
                    thumbnail, lambda x: x['url'], compat_str)
                if thumbnail_url:
                    thumbnails.append({
                        'url': thumbnail_url,
                        'id': thumbnail_key,
                        'width': int_or_none(thumbnail.get('width')),
                        'height': int_or_none(thumbnail.get('height')),
                    })

        return {
            'id': content_id,
            'title': title,
            'description': document.get('beschreibung'),
            'duration': int_or_none(document.get('length')),
            'timestamp': unified_timestamp(document.get('date')) or unified_timestamp(
                try_get(video, lambda x: x['meta']['editorialDate'], compat_str)),
            'thumbnails': thumbnails,
            'subtitles': self._extract_subtitles(document),
            'formats': formats,
        }

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

        webpage = self._download_webpage(url, video_id, fatal=False)
        if webpage:
            player = self._extract_player(webpage, url, fatal=False)
            if player:
                return self._extract_regular(url, player, video_id)

        return self._extract_mobile(video_id)

class ZDFChannelIE(ZDFBaseIE):
    _VALID_URL = r'https?://www\.zdf\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)'
    _TESTS = [{
        'url': 'https://www.zdf.de/sport/das-aktuelle-sportstudio',
        'info_dict': {
            'id': 'das-aktuelle-sportstudio',
            'title': 'das aktuelle sportstudio | ZDF',
        },
        'playlist_mincount': 23,
    }, {
        'url': 'https://www.zdf.de/dokumentation/planet-e',
        'info_dict': {
            'id': 'planet-e',
            'title': 'planet e.',
        },
        'playlist_mincount': 50,
    }, {
        'url': 'https://www.zdf.de/filme/taunuskrimi/',
        'only_matching': True,
    }]

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

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

        webpage = self._download_webpage(url, channel_id)

        entries = [
            self.url_result(item_url, ie=ZDFIE.ie_key())
            for item_url in orderedSet(re.findall(
                r'data-plusbar-url=["\'](http.+?\.html)', webpage))]

        return self.playlist_result(
            entries, channel_id, self._og_search_title(webpage, fatal=False))

        r"""
        player = self._extract_player(webpage, channel_id)

        channel_id = self._search_regex(
            r'docId\s*:\s*(["\'])(?P<id>(?!\1).+?)\1', webpage,
            'channel id', group='id')

        channel = self._call_api(
            'https://api.zdf.de/content/documents/%s.json' % channel_id,
            player, url, channel_id)

        items = []
        for module in channel['module']:
            for teaser in try_get(module, lambda x: x['teaser'], list) or []:
                t = try_get(
                    teaser, lambda x: x['http://zdf.de/rels/target'], dict)
                if not t:
                    continue
                items.extend(try_get(
                    t,
                    lambda x: x['resultsWithVideo']['http://zdf.de/rels/search/results'],
                    list) or [])
            items.extend(try_get(
                module,
                lambda x: x['filterRef']['resultsWithVideo']['http://zdf.de/rels/search/results'],
                list) or [])

        entries = []
        entry_urls = set()
        for item in items:
            t = try_get(item, lambda x: x['http://zdf.de/rels/target'], dict)
            if not t:
                continue
            sharing_url = t.get('http://zdf.de/rels/sharing-url')
            if not sharing_url or not isinstance(sharing_url, compat_str):
                continue
            if sharing_url in entry_urls:
                continue
            entry_urls.add(sharing_url)
            entries.append(self.url_result(
                sharing_url, ie=ZDFIE.ie_key(), video_id=t.get('id')))

        return self.playlist_result(entries, channel_id, channel.get('title'))
        """


code:
import json

from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    ExtractorError,
    int_or_none,
    parse_age_limit,
    str_or_none,
    try_get,
    unified_strdate,
    unified_timestamp,
    url_or_none,
)

class Zee5IE(InfoExtractor):
    _VALID_URL = r'''(?x)
                     (?:
                        zee5:|
                        https?://(?:www\.)?zee5\.com/(?:[^#?]+/)?
                        (?:
                            (?:tv-shows|kids|web-series|zee5originals)(?:/[^#/?]+){3}
                            |movies/[^#/?]+
                        )/(?P<display_id>[^#/?]+)/
                     )
                     (?P<id>[^#/?]+)/?(?:$|[?#])
                     '''
    _TESTS = [{
        'url': 'https://www.zee5.com/movies/details/adavari-matalaku-ardhale-verule/0-0-movie_1143162669',
        'info_dict': {
            'id': '0-0-movie_1143162669',
            'ext': 'mp4',
            'display_id': 'adavari-matalaku-ardhale-verule',
            'title': 'Adavari Matalaku Ardhale Verule',
            'duration': 9360,
            'description': compat_str,
            'alt_title': 'Adavari Matalaku Ardhale Verule',
            'uploader': 'Zee Entertainment Enterprises Ltd',
            'release_date': '20070427',
            'upload_date': '20070427',
            'timestamp': 1177632000,
            'thumbnail': r're:^https?://.*\.jpg$',
            'episode_number': 0,
            'episode': 'Episode 0',
            'tags': list
        },
        'params': {
            'format': 'bv',
        },
    }, {
        'url': 'https://www.zee5.com/kids/kids-shows/bandbudh-aur-budbak/0-6-1899/yoga-se-hoga-bandbudh-aur-budbak/0-1-239839',
        'info_dict': {
            'id': '0-1-239839',
            'ext': 'mp4',
            'display_id': 'yoga-se-hoga-bandbudh-aur-budbak',
            'title': 'Yoga Se Hoga-Bandbudh aur Budbak',
            'duration': 659,
            'description': compat_str,
            'alt_title': 'Yoga Se Hoga-Bandbudh aur Budbak',
            'uploader': 'Zee Entertainment Enterprises Ltd',
            'release_date': '20150101',
            'upload_date': '20150101',
            'timestamp': 1420070400,
            'thumbnail': r're:^https?://.*\.jpg$',
            'series': 'Bandbudh Aur Budbak',
            'season_number': 1,
            'episode_number': 1,
            'episode': 'Episode 1',
            'season': 'Season 1',
            'tags': list,
        },
        'params': {
            'format': 'bv',
        },
    }, {
        'url': 'https://www.zee5.com/hi/tv-shows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730?country=IN',
        'only_matching': True
    }, {
        'url': 'https://www.zee5.com/global/hi/tv-shows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730',
        'only_matching': True
    }, {
        'url': 'https://www.zee5.com/web-series/details/mithya/0-6-4z587408/maine-dekhi-hai-uski-mrityu/0-1-6z587412',
        'only_matching': True
    }]
    _DETAIL_API_URL = 'https://spapi.zee5.com/singlePlayback/getDetails/secure?content_id={}&device_id={}&platform_name=desktop_web&country=IN&check_parental_control=false'
    _DEVICE_ID = '1q70TH8Wz0wTyw4buVgg000000000000'
    _USER_TOKEN = None
    _LOGIN_HINT = 'Use "--username <mobile_number>" to login using otp or "--username token" and "--password <user_token>" to login using user token.'
    _NETRC_MACHINE = 'zee5'
    _GEO_COUNTRIES = ['IN']

    def _perform_login(self, username, password):
        if len(username) == 10 and username.isdigit() and self._USER_TOKEN is None:
            self.report_login()
            otp_request_json = self._download_json(f'https://b2bapi.zee5.com/device/sendotp_v1.php?phoneno=91{username}',
                                                   None, note='Sending OTP')
            if otp_request_json['code'] == 0:
                self.to_screen(otp_request_json['message'])
            else:
                raise ExtractorError(otp_request_json['message'], expected=True)
            otp_code = self._get_tfa_info('OTP')
            otp_verify_json = self._download_json(f'https://b2bapi.zee5.com/device/verifyotp_v1.php?phoneno=91{username}&otp={otp_code}&guest_token={self._DEVICE_ID}&platform=web',
                                                  None, note='Verifying OTP', fatal=False)
            if not otp_verify_json:
                raise ExtractorError('Unable to verify OTP.', expected=True)
            self._USER_TOKEN = otp_verify_json.get('token')
            if not self._USER_TOKEN:
                raise ExtractorError(otp_request_json['message'], expected=True)
        elif username.lower() == 'token' and len(password) > 1198:
            self._USER_TOKEN = password
        else:
            raise ExtractorError(self._LOGIN_HINT, expected=True)

    def _real_extract(self, url):
        video_id, display_id = self._match_valid_url(url).group('id', 'display_id')
        access_token_request = self._download_json(
            'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app',
            video_id, note='Downloading access token')
        data = {
            'x-access-token': access_token_request['token']
        }
        if self._USER_TOKEN:
            data['Authorization'] = 'bearer %s' % self._USER_TOKEN
        else:
            data['X-Z5-Guest-Token'] = self._DEVICE_ID

        json_data = self._download_json(
            self._DETAIL_API_URL.format(video_id, self._DEVICE_ID),
            video_id, headers={'content-type': 'application/json'}, data=json.dumps(data).encode('utf-8'))
        asset_data = json_data['assetDetails']
        show_data = json_data.get('showDetails', {})
        if 'premium' in asset_data['business_type']:
            raise ExtractorError('Premium content is DRM protected.', expected=True)
        if not asset_data.get('hls_url'):
            self.raise_login_required(self._LOGIN_HINT, metadata_available=True, method=None)
        formats, m3u8_subs = self._extract_m3u8_formats_and_subtitles(asset_data['hls_url'], video_id, 'mp4', fatal=False)
        self._sort_formats(formats)

        subtitles = {}
        for sub in asset_data.get('subtitle_url', []):
            sub_url = sub.get('url')
            if not sub_url:
                continue
            subtitles.setdefault(sub.get('language', 'en'), []).append({
                'url': self._proto_relative_url(sub_url),
            })
        subtitles = self._merge_subtitles(subtitles, m3u8_subs)
        return {
            'id': video_id,
            'display_id': display_id,
            'title': asset_data['title'],
            'formats': formats,
            'subtitles': subtitles,
            'duration': int_or_none(asset_data.get('duration')),
            'description': str_or_none(asset_data.get('description')),
            'alt_title': str_or_none(asset_data.get('original_title')),
            'uploader': str_or_none(asset_data.get('content_owner')),
            'age_limit': parse_age_limit(asset_data.get('age_rating')),
            'release_date': unified_strdate(asset_data.get('release_date')),
            'timestamp': unified_timestamp(asset_data.get('release_date')),
            'thumbnail': url_or_none(asset_data.get('image_url')),
            'series': str_or_none(asset_data.get('tvshow_name')),
            'season': try_get(show_data, lambda x: x['seasons']['title'], str),
            'season_number': int_or_none(try_get(show_data, lambda x: x['seasons'][0]['orderid'])),
            'episode_number': int_or_none(try_get(asset_data, lambda x: x['orderid'])),
            'tags': try_get(asset_data, lambda x: x['tags'], list)
        }

class Zee5SeriesIE(InfoExtractor):
    IE_NAME = 'zee5:series'
    _VALID_URL = r'''(?x)
                     (?:
                        zee5:series:|
                        https?://(?:www\.)?zee5\.com/(?:[^#?]+/)?
                        (?:tv-shows|web-series|kids|zee5originals)(?:/[^#/?]+){2}/
                     )
                     (?P<id>[^#/?]+)(?:/episodes)?/?(?:$|[?#])
                     '''
    _TESTS = [{
        'url': 'https://www.zee5.com/kids/kids-shows/bandbudh-aur-budbak/0-6-1899',
        'playlist_mincount': 156,
        'info_dict': {
            'id': '0-6-1899',
        },
    }, {
        'url': 'https://www.zee5.com/tv-shows/details/bhabi-ji-ghar-par-hai/0-6-199',
        'playlist_mincount': 1500,
        'info_dict': {
            'id': '0-6-199',
        },
    }, {
        'url': 'https://www.zee5.com/tv-shows/details/agent-raghav-crime-branch/0-6-965',
        'playlist_mincount': 24,
        'info_dict': {
            'id': '0-6-965',
        },
    }, {
        'url': 'https://www.zee5.com/ta/tv-shows/details/nagabhairavi/0-6-3201',
        'playlist_mincount': 3,
        'info_dict': {
            'id': '0-6-3201',
        },
    }, {
        'url': 'https://www.zee5.com/global/hi/tv-shows/details/khwaabon-ki-zamin-par/0-6-270',
        'playlist_mincount': 150,
        'info_dict': {
            'id': '0-6-270',
        },
    }, {
        'url': 'https://www.zee5.com/tv-shows/details/chala-hawa-yeu-dya-ladies-zindabaad/0-6-2943/episodes',
        'only_matching': True,
    }, {
        'url': 'https://www.zee5.com/web-series/details/mithya/0-6-4z587408',
        'only_matching': True,
    }]

    def _entries(self, show_id):
        access_token_request = self._download_json(
            'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app',
            show_id, note='Downloading access token')
        headers = {
            'X-Access-Token': access_token_request['token'],
            'Referer': 'https://www.zee5.com/',
        }
        show_url = f'https://gwapi.zee5.com/content/tvshow/{show_id}?translation=en&country=IN'

        page_num = 0
        show_json = self._download_json(show_url, video_id=show_id, headers=headers)
        for season in show_json.get('seasons') or []:
            season_id = try_get(season, lambda x: x['id'], compat_str)
            next_url = f'https://gwapi.zee5.com/content/tvshow/?season_id={season_id}&type=episode&translation=en&country=IN&on_air=false&asset_subtype=tvshow&page=1&limit=100'
            while next_url:
                page_num += 1
                episodes_json = self._download_json(
                    next_url, video_id=show_id, headers=headers,
                    note='Downloading JSON metadata page %d' % page_num)
                for episode in try_get(episodes_json, lambda x: x['episode'], list) or []:
                    video_id = episode.get('id')
                    yield self.url_result(
                        'zee5:%s' % video_id,
                        ie=Zee5IE.ie_key(), video_id=video_id)
                next_url = url_or_none(episodes_json.get('next_episode_api'))

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


code:
from .common import InfoExtractor
from ..utils import format_field, float_or_none, int_or_none

class ZhihuIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?zhihu\.com/zvideo/(?P<id>[0-9]+)'
    _TEST = {
        'url': 'https://www.zhihu.com/zvideo/1342930761977176064',
        'md5': 'c8d4c9cd72dd58e6f9bc9c2c84266464',
        'info_dict': {
            'id': '1342930761977176064',
            'ext': 'mp4',
            'title': '写春联也太难了吧！',
            'thumbnail': r're:^https?://.*\.jpg',
            'uploader': '桥半舫',
            'timestamp': 1612959715,
            'upload_date': '20210210',
            'uploader_id': '244ecb13b0fd7daf92235288c8ca3365',
            'duration': 146.333,
            'view_count': int,
            'like_count': int,
            'comment_count': int,
        }
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        zvideo = self._download_json(
            'https://www.zhihu.com/api/v4/zvideos/' + video_id, video_id)
        title = zvideo['title']
        video = zvideo.get('video') or {}

        formats = []
        for format_id, q in (video.get('playlist') or {}).items():
            play_url = q.get('url') or q.get('play_url')
            if not play_url:
                continue
            formats.append({
                'asr': int_or_none(q.get('sample_rate')),
                'filesize': int_or_none(q.get('size')),
                'format_id': format_id,
                'fps': int_or_none(q.get('fps')),
                'height': int_or_none(q.get('height')),
                'tbr': float_or_none(q.get('bitrate')),
                'url': play_url,
                'width': int_or_none(q.get('width')),
            })
        self._sort_formats(formats)

        author = zvideo.get('author') or {}
        url_token = author.get('url_token')

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'thumbnail': video.get('thumbnail') or zvideo.get('image_url'),
            'uploader': author.get('name'),
            'timestamp': int_or_none(zvideo.get('published_at')),
            'uploader_id': author.get('id'),
            'uploader_url': format_field(url_token, None, 'https://www.zhihu.com/people/%s'),
            'duration': float_or_none(video.get('duration')),
            'view_count': int_or_none(zvideo.get('play_count')),
            'like_count': int_or_none(zvideo.get('liked_count')),
            'comment_count': int_or_none(zvideo.get('comment_count')),
        }


code:
import functools
import hashlib
import hmac
import json
import urllib.parse

from .common import InfoExtractor
from ..utils import (
    OnDemandPagedList,
    int_or_none,
    traverse_obj,
    urljoin,
)

class ZingMp3BaseIE(InfoExtractor):
    _VALID_URL_TMPL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<type>(?:%s))/[^/?#]+/(?P<id>\w+)(?:\.html|\?)'
    _GEO_COUNTRIES = ['VN']
    _DOMAIN = 'https://zingmp3.vn'
    _PER_PAGE = 50
    _API_SLUGS = {
        # Audio/video
        'bai-hat': '/api/v2/page/get/song',
        'embed': '/api/v2/page/get/song',
        'video-clip': '/api/v2/page/get/video',
        'lyric': '/api/v2/lyric/get/lyric',
        'song-streaming': '/api/v2/song/get/streaming',
        # Playlist
        'playlist': '/api/v2/page/get/playlist',
        'album': '/api/v2/page/get/playlist',
        # Chart
        'zing-chart': '/api/v2/page/get/chart-home',
        'zing-chart-tuan': '/api/v2/page/get/week-chart',
        'moi-phat-hanh': '/api/v2/page/get/newrelease-chart',
        'the-loai-video': '/api/v2/video/get/list',
        # User
        'info-artist': '/api/v2/page/get/artist',
        'user-list-song': '/api/v2/song/get/list',
        'user-list-video': '/api/v2/video/get/list',
    }

    def _api_url(self, url_type, params):
        api_slug = self._API_SLUGS[url_type]
        params.update({'ctime': '1'})
        sha256 = hashlib.sha256(
            ''.join(f'{k}={v}' for k, v in sorted(params.items())).encode()).hexdigest()
        data = {
            **params,
            'apiKey': '88265e23d4284f25963e6eedac8fbfa3',
            'sig': hmac.new(
                b'2aa2d1c561e809b267f3638c4a307aab', f'{api_slug}{sha256}'.encode(), hashlib.sha512).hexdigest(),
        }
        return f'{self._DOMAIN}{api_slug}?{urllib.parse.urlencode(data)}'

    def _call_api(self, url_type, params, display_id=None, **kwargs):
        resp = self._download_json(
            self._api_url(url_type, params), display_id or params.get('id'),
            note=f'Downloading {url_type} JSON metadata', **kwargs)
        return (resp or {}).get('data') or {}

    def _real_initialize(self):
        if not self._cookies_passed:
            self._request_webpage(
                self._api_url('bai-hat', {'id': ''}), None, note='Updating cookies')

    def _parse_items(self, items):
        for url in traverse_obj(items, (..., 'link')) or []:
            yield self.url_result(urljoin(self._DOMAIN, url))

class ZingMp3IE(ZingMp3BaseIE):
    _VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'bai-hat|video-clip|embed'
    IE_NAME = 'zingmp3'
    IE_DESC = 'zingmp3.vn'
    _TESTS = [{
        'url': 'https://mp3.zing.vn/bai-hat/Xa-Mai-Xa-Bao-Thy/ZWZB9WAB.html',
        'md5': 'ead7ae13693b3205cbc89536a077daed',
        'info_dict': {
            'id': 'ZWZB9WAB',
            'title': 'Xa Mãi Xa',
            'ext': 'mp3',
            'thumbnail': r're:^https?://.+\.jpg',
            'subtitles': {
                'origin': [{
                    'ext': 'lrc',
                }]
            },
            'duration': 255,
            'track': 'Xa Mãi Xa',
            'artist': 'Bảo Thy',
            'album': 'Special Album',
            'album_artist': 'Bảo Thy',
        },
    }, {
        'url': 'https://zingmp3.vn/video-clip/Suong-Hoa-Dua-Loi-K-ICM-RYO/ZO8ZF7C7.html',
        'md5': '3c2081e79471a2f4a3edd90b70b185ea',
        'info_dict': {
            'id': 'ZO8ZF7C7',
            'title': 'Sương Hoa Đưa Lối',
            'ext': 'mp4',
            'thumbnail': r're:^https?://.+\.jpg',
            'duration': 207,
            'track': 'Sương Hoa Đưa Lối',
            'artist': 'K-ICM, RYO',
            'album': 'Sương Hoa Đưa Lối (Single)',
            'album_artist': 'K-ICM, RYO',
        },
    }, {
        'url': 'https://zingmp3.vn/bai-hat/Nguoi-Yeu-Toi-Lanh-Lung-Sat-Da-Mr-Siro/ZZ6IW7OU.html',
        'md5': '3e9f7a9bd0d965573dbff8d7c68b629d',
        'info_dict': {
            'id': 'ZZ6IW7OU',
            'title': 'Người Yêu Tôi Lạnh Lùng Sắt Đá',
            'ext': 'mp3',
            'thumbnail': r're:^https?://.+\.jpg',
            'duration': 303,
            'track': 'Người Yêu Tôi Lạnh Lùng Sắt Đá',
            'artist': 'Mr. Siro',
            'album': 'Người Yêu Tôi Lạnh Lùng Sắt Đá (Single)',
            'album_artist': 'Mr. Siro',
        },
    }, {
        'url': 'https://zingmp3.vn/embed/song/ZWZEI76B?start=false',
        'only_matching': True,
    }, {
        'url': 'https://zingmp3.vn/bai-hat/Xa-Mai-Xa-Bao-Thy/ZWZB9WAB.html',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        song_id, url_type = self._match_valid_url(url).group('id', 'type')
        item = self._call_api(url_type, {'id': song_id})

        item_id = item.get('encodeId') or song_id
        if url_type == 'video-clip':
            source = item.get('streaming')
            source['mp4'] = self._download_json(
                'http://api.mp3.zing.vn/api/mobile/video/getvideoinfo', item_id,
                query={'requestdata': json.dumps({'id': item_id})},
                note='Downloading mp4 JSON metadata').get('source')
        else:
            source = self._call_api('song-streaming', {'id': item_id})

        formats = []
        for k, v in (source or {}).items():
            if not v or v == 'VIP':
                continue
            if k not in ('mp4', 'hls'):
                formats.append({
                    'ext': 'mp3',
                    'format_id': k,
                    'tbr': int_or_none(k),
                    'url': self._proto_relative_url(v),
                    'vcodec': 'none',
                })
                continue
            for res, video_url in v.items():
                if not video_url:
                    continue
                if k == 'hls':
                    formats.extend(self._extract_m3u8_formats(video_url, item_id, 'mp4', m3u8_id=k, fatal=False))
                    continue
                formats.append({
                    'format_id': f'mp4-{res}',
                    'url': video_url,
                    'height': int_or_none(res),
                })

        if not formats and item.get('msg') == 'Sorry, this content is not available in your country.':
            self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
        self._sort_formats(formats)

        lyric = item.get('lyric') or self._call_api('lyric', {'id': item_id}, fatal=False).get('file')

        return {
            'id': item_id,
            'title': traverse_obj(item, 'title', 'alias'),
            'thumbnail': traverse_obj(item, 'thumbnail', 'thumbnailM'),
            'duration': int_or_none(item.get('duration')),
            'track': traverse_obj(item, 'title', 'alias'),
            'artist': traverse_obj(item, 'artistsNames', 'artists_names'),
            'album': traverse_obj(item, ('album', ('name', 'title')), get_all=False),
            'album_artist': traverse_obj(item, ('album', ('artistsNames', 'artists_names')), get_all=False),
            'formats': formats,
            'subtitles': {'origin': [{'url': lyric}]} if lyric else None,
        }

class ZingMp3AlbumIE(ZingMp3BaseIE):
    _VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'album|playlist'
    _TESTS = [{
        'url': 'http://mp3.zing.vn/album/Lau-Dai-Tinh-Ai-Bang-Kieu-Minh-Tuyet/ZWZBWDAF.html',
        'info_dict': {
            'id': 'ZWZBWDAF',
            'title': 'Lâu Đài Tình Ái',
        },
        'playlist_mincount': 9,
    }, {
        'url': 'https://zingmp3.vn/album/Nhung-Bai-Hat-Hay-Nhat-Cua-Mr-Siro-Mr-Siro/ZWZAEZZD.html',
        'info_dict': {
            'id': 'ZWZAEZZD',
            'title': 'Những Bài Hát Hay Nhất Của Mr. Siro',
        },
        'playlist_mincount': 49,
    }, {
        'url': 'http://mp3.zing.vn/playlist/Duong-Hong-Loan-apollobee/IWCAACCB.html',
        'only_matching': True,
    }, {
        'url': 'https://zingmp3.vn/album/Lau-Dai-Tinh-Ai-Bang-Kieu-Minh-Tuyet/ZWZBWDAF.html',
        'only_matching': True,
    }]
    IE_NAME = 'zingmp3:album'

    def _real_extract(self, url):
        song_id, url_type = self._match_valid_url(url).group('id', 'type')
        data = self._call_api(url_type, {'id': song_id})
        return self.playlist_result(
            self._parse_items(traverse_obj(data, ('song', 'items'))),
            traverse_obj(data, 'id', 'encodeId'), traverse_obj(data, 'name', 'title'))

class ZingMp3ChartHomeIE(ZingMp3BaseIE):
    _VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<id>(?:zing-chart|moi-phat-hanh))/?(?:[#?]|$)'
    _TESTS = [{
        'url': 'https://zingmp3.vn/zing-chart',
        'info_dict': {
            'id': 'zing-chart',
        },
        'playlist_mincount': 100,
    }, {
        'url': 'https://zingmp3.vn/moi-phat-hanh',
        'info_dict': {
            'id': 'moi-phat-hanh',
        },
        'playlist_mincount': 100,
    }]
    IE_NAME = 'zingmp3:chart-home'

    def _real_extract(self, url):
        url_type = self._match_id(url)
        data = self._call_api(url_type, {'id': url_type})
        items = traverse_obj(data, ('RTChart', 'items') if url_type == 'zing-chart' else 'items')
        return self.playlist_result(self._parse_items(items), url_type)

class ZingMp3WeekChartIE(ZingMp3BaseIE):
    _VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'zing-chart-tuan'
    IE_NAME = 'zingmp3:week-chart'
    _TESTS = [{
        'url': 'https://zingmp3.vn/zing-chart-tuan/Bai-hat-Viet-Nam/IWZ9Z08I.html',
        'info_dict': {
            'id': 'IWZ9Z08I',
            'title': 'zing-chart-vn',
        },
        'playlist_mincount': 10,
    }, {
        'url': 'https://zingmp3.vn/zing-chart-tuan/Bai-hat-US-UK/IWZ9Z0BW.html',
        'info_dict': {
            'id': 'IWZ9Z0BW',
            'title': 'zing-chart-us',
        },
        'playlist_mincount': 10,
    }, {
        'url': 'https://zingmp3.vn/zing-chart-tuan/Bai-hat-KPop/IWZ9Z0BO.html',
        'info_dict': {
            'id': 'IWZ9Z0BO',
            'title': 'zing-chart-korea',
        },
        'playlist_mincount': 10,
    }]

    def _real_extract(self, url):
        song_id, url_type = self._match_valid_url(url).group('id', 'type')
        data = self._call_api(url_type, {'id': song_id})
        return self.playlist_result(
            self._parse_items(data['items']), song_id, f'zing-chart-{data.get("country", "")}')

class ZingMp3ChartMusicVideoIE(ZingMp3BaseIE):
    _VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<type>the-loai-video)/(?P<regions>[^/]+)/(?P<id>[^\.]+)'
    IE_NAME = 'zingmp3:chart-music-video'
    _TESTS = [{
        'url': 'https://zingmp3.vn/the-loai-video/Viet-Nam/IWZ9Z08I.html',
        'info_dict': {
            'id': 'IWZ9Z08I',
            'title': 'the-loai-video_Viet-Nam',
        },
        'playlist_mincount': 400,
    }, {
        'url': 'https://zingmp3.vn/the-loai-video/Au-My/IWZ9Z08O.html',
        'info_dict': {
            'id': 'IWZ9Z08O',
            'title': 'the-loai-video_Au-My',
        },
        'playlist_mincount': 40,
    }, {
        'url': 'https://zingmp3.vn/the-loai-video/Han-Quoc/IWZ9Z08W.html',
        'info_dict': {
            'id': 'IWZ9Z08W',
            'title': 'the-loai-video_Han-Quoc',
        },
        'playlist_mincount': 30,
    }, {
        'url': 'https://zingmp3.vn/the-loai-video/Khong-Loi/IWZ9Z086.html',
        'info_dict': {
            'id': 'IWZ9Z086',
            'title': 'the-loai-video_Khong-Loi',
        },
        'playlist_mincount': 10,
    }]

    def _fetch_page(self, song_id, url_type, page):
        return self._parse_items(self._call_api(url_type, {
            'id': song_id,
            'type': 'genre',
            'page': page + 1,
            'count': self._PER_PAGE
        }).get('items'))

    def _real_extract(self, url):
        song_id, regions, url_type = self._match_valid_url(url).group('id', 'regions', 'type')
        return self.playlist_result(
            OnDemandPagedList(functools.partial(self._fetch_page, song_id, url_type), self._PER_PAGE),
            song_id, f'{url_type}_{regions}')

class ZingMp3UserIE(ZingMp3BaseIE):
    _VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<user>[^/]+)/(?P<type>bai-hat|single|album|video)/?(?:[?#]|$)'
    IE_NAME = 'zingmp3:user'
    _TESTS = [{
        'url': 'https://zingmp3.vn/Mr-Siro/bai-hat',
        'info_dict': {
            'id': 'IWZ98609',
            'title': 'Mr. Siro - bai-hat',
            'description': 'md5:85ab29bd7b21725c12bf76fd1d6922e5',
        },
        'playlist_mincount': 91,
    }, {
        'url': 'https://zingmp3.vn/Mr-Siro/album',
        'info_dict': {
            'id': 'IWZ98609',
            'title': 'Mr. Siro - album',
            'description': 'md5:85ab29bd7b21725c12bf76fd1d6922e5',
        },
        'playlist_mincount': 3,
    }, {
        'url': 'https://zingmp3.vn/Mr-Siro/single',
        'info_dict': {
            'id': 'IWZ98609',
            'title': 'Mr. Siro - single',
            'description': 'md5:85ab29bd7b21725c12bf76fd1d6922e5',
        },
        'playlist_mincount': 20,
    }, {
        'url': 'https://zingmp3.vn/Mr-Siro/video',
        'info_dict': {
            'id': 'IWZ98609',
            'title': 'Mr. Siro - video',
            'description': 'md5:85ab29bd7b21725c12bf76fd1d6922e5',
        },
        'playlist_mincount': 15,
    }]

    def _fetch_page(self, user_id, url_type, page):
        url_type = 'user-list-song' if url_type == 'bai-hat' else 'user-list-video'
        return self._parse_items(self._call_api(url_type, {
            'id': user_id,
            'type': 'artist',
            'page': page + 1,
            'count': self._PER_PAGE
        }, query={'sort': 'new', 'sectionId': 'aSong'}).get('items'))

    def _real_extract(self, url):
        user_alias, url_type = self._match_valid_url(url).group('user', 'type')
        if not url_type:
            url_type = 'bai-hat'

        user_info = self._call_api('info-artist', {}, user_alias, query={'alias': user_alias})
        if url_type in ('bai-hat', 'video'):
            entries = OnDemandPagedList(
                functools.partial(self._fetch_page, user_info['id'], url_type), self._PER_PAGE)
        else:
            entries = self._parse_items(traverse_obj(user_info, (
                'sections', lambda _, v: v['link'] == f'/{user_alias}/{url_type}', 'items', ...)))
        return self.playlist_result(
            entries, user_info['id'], f'{user_info.get("name")} - {url_type}', user_info.get('biography'))


code:
from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    int_or_none,
    str_or_none,
    js_to_json,
    parse_filesize,
    urlencode_postdata,
    urljoin,
)

class ZoomIE(InfoExtractor):
    IE_NAME = 'zoom'
    _VALID_URL = r'(?P<base_url>https?://(?:[^.]+\.)?zoom.us/)rec(?:ording)?/(?:play|share)/(?P<id>[A-Za-z0-9_.-]+)'
    _TEST = {
        'url': 'https://economist.zoom.us/rec/play/dUk_CNBETmZ5VA2BwEl-jjakPpJ3M1pcfVYAPRsoIbEByGsLjUZtaa4yCATQuOL3der8BlTwxQePl_j0.EImBkXzTIaPvdZO5',
        'md5': 'ab445e8c911fddc4f9adc842c2c5d434',
        'info_dict': {
            'id': 'dUk_CNBETmZ5VA2BwEl-jjakPpJ3M1pcfVYAPRsoIbEByGsLjUZtaa4yCATQuOL3der8BlTwxQePl_j0.EImBkXzTIaPvdZO5',
            'ext': 'mp4',
            'title': 'China\'s "two sessions" and the new five-year plan',
        },
        'skip': 'Recording requires email authentication to access',
    }

    def _real_extract(self, url):
        base_url, play_id = self._match_valid_url(url).groups()
        webpage = self._download_webpage(url, play_id)

        try:
            form = self._form_hidden_inputs('password_form', webpage)
        except ExtractorError:
            form = None
        if form:
            password = self.get_param('videopassword')
            if not password:
                raise ExtractorError(
                    'This video is protected by a passcode, use the --video-password option', expected=True)
            is_meeting = form.get('useWhichPasswd') == 'meeting'
            validation = self._download_json(
                base_url + 'rec/validate%s_passwd' % ('_meet' if is_meeting else ''),
                play_id, 'Validating passcode', 'Wrong passcode', data=urlencode_postdata({
                    'id': form[('meet' if is_meeting else 'file') + 'Id'],
                    'passwd': password,
                    'action': form.get('action'),
                }))
            if not validation.get('status'):
                raise ExtractorError(validation['errorMessage'], expected=True)
            webpage = self._download_webpage(url, play_id)

        data = self._parse_json(self._search_regex(
            r'(?s)window\.__data__\s*=\s*({.+?});',
            webpage, 'data'), play_id, js_to_json)

        subtitles = {}
        for _type in ('transcript', 'cc', 'chapter'):
            if data.get('%sUrl' % _type):
                subtitles[_type] = [{
                    'url': urljoin(base_url, data['%sUrl' % _type]),
                    'ext': 'vtt',
                }]

        formats = []

        if data.get('viewMp4Url'):
            formats.append({
                'format_note': 'Camera stream',
                'url': str_or_none(data.get('viewMp4Url')),
                'width': int_or_none(data.get('viewResolvtionsWidth')),
                'height': int_or_none(data.get('viewResolvtionsHeight')),
                'format_id': str_or_none(data.get('recordingId')),
                'ext': 'mp4',
                'filesize_approx': parse_filesize(data.get('fileSize')),
                'preference': 0
            })

        if data.get('shareMp4Url'):
            formats.append({
                'format_note': 'Screen share stream',
                'url': str_or_none(data.get('shareMp4Url')),
                'width': int_or_none(data.get('shareResolvtionsWidth')),
                'height': int_or_none(data.get('shareResolvtionsHeight')),
                'format_id': str_or_none(data.get('shareVideoId')),
                'ext': 'mp4',
                'preference': -1
            })

        self._sort_formats(formats)

        return {
            'id': play_id,
            'title': data.get('topic'),
            'subtitles': subtitles,
            'formats': formats,
            'http_headers': {
                'Referer': base_url,
            },
        }


code:
import re

from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    dict_get,
    ExtractorError,
    int_or_none,
    js_to_json,
    parse_iso8601,
)

class ZypeIE(InfoExtractor):
    _ID_RE = r'[\da-fA-F]+'
    _COMMON_RE = r'//player\.zype\.com/embed/%s\.(?:js|json|html)\?.*?(?:access_token|(?:ap[ip]|player)_key)='
    _VALID_URL = r'https?:%s[^&]+' % (_COMMON_RE % ('(?P<id>%s)' % _ID_RE))
    _EMBED_REGEX = [fr'<script[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?{_COMMON_RE % _ID_RE}.+?)\1']
    _TEST = {
        'url': 'https://player.zype.com/embed/5b400b834b32992a310622b9.js?api_key=jZ9GUhRmxcPvX7M3SlfejB6Hle9jyHTdk2jVxG7wOHPLODgncEKVdPYBhuz9iWXQ&autoplay=false&controls=true&da=false',
        'md5': 'eaee31d474c76a955bdaba02a505c595',
        'info_dict': {
            'id': '5b400b834b32992a310622b9',
            'ext': 'mp4',
            'title': 'Smoky Barbecue Favorites',
            'thumbnail': r're:^https?://.*\.jpe?g',
            'description': 'md5:5ff01e76316bd8d46508af26dc86023b',
            'timestamp': 1504915200,
            'upload_date': '20170909',
        },
    }

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

        try:
            response = self._download_json(re.sub(
                r'\.(?:js|html)\?', '.json?', url), video_id)['response']
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code in (400, 401, 403):
                raise ExtractorError(self._parse_json(
                    e.cause.read().decode(), video_id)['message'], expected=True)
            raise

        body = response['body']
        video = response['video']
        title = video['title']

        subtitles = {}

        if isinstance(body, dict):
            formats = []
            for output in body.get('outputs', []):
                output_url = output.get('url')
                if not output_url:
                    continue
                name = output.get('name')
                if name == 'm3u8':
                    formats, subtitles = self._extract_m3u8_formats_and_subtitles(
                        output_url, video_id, 'mp4',
                        'm3u8_native', m3u8_id='hls', fatal=False)
                else:
                    f = {
                        'format_id': name,
                        'tbr': int_or_none(output.get('bitrate')),
                        'url': output_url,
                    }
                    if name in ('m4a', 'mp3'):
                        f['vcodec'] = 'none'
                    else:
                        f.update({
                            'height': int_or_none(output.get('height')),
                            'width': int_or_none(output.get('width')),
                        })
                    formats.append(f)
            text_tracks = body.get('subtitles') or []
        else:
            m3u8_url = self._search_regex(
                r'(["\'])(?P<url>(?:(?!\1).)+\.m3u8(?:(?!\1).)*)\1',
                body, 'm3u8 url', group='url', default=None)
            if not m3u8_url:
                source = self._search_regex(
                    r'(?s)sources\s*:\s*\[\s*({.+?})\s*\]', body, 'source')

                def get_attr(key):
                    return self._search_regex(
                        r'\b%s\s*:\s*([\'"])(?P<val>(?:(?!\1).)+)\1' % key,
                        source, key, group='val')

                if get_attr('integration') == 'verizon-media':
                    m3u8_url = 'https://content.uplynk.com/%s.m3u8' % get_attr('id')
            formats, subtitles = self._extract_m3u8_formats_and_subtitles(
                m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls')
            text_tracks = self._search_regex(
                r'textTracks\s*:\s*(\[[^]]+\])',
                body, 'text tracks', default=None)
            if text_tracks:
                text_tracks = self._parse_json(
                    text_tracks, video_id, js_to_json, False)
        self._sort_formats(formats)

        if text_tracks:
            for text_track in text_tracks:
                tt_url = dict_get(text_track, ('file', 'src'))
                if not tt_url:
                    continue
                subtitles.setdefault(text_track.get('label') or 'English', []).append({
                    'url': tt_url,
                })

        thumbnails = []
        for thumbnail in video.get('thumbnails', []):
            thumbnail_url = thumbnail.get('url')
            if not thumbnail_url:
                continue
            thumbnails.append({
                'url': thumbnail_url,
                'width': int_or_none(thumbnail.get('width')),
                'height': int_or_none(thumbnail.get('height')),
            })

        return {
            'id': video_id,
            'display_id': video.get('friendly_title'),
            'title': title,
            'thumbnails': thumbnails,
            'description': dict_get(video, ('description', 'ott_description', 'short_description')),
            'timestamp': parse_iso8601(video.get('published_at')),
            'duration': int_or_none(video.get('duration')),
            'view_count': int_or_none(video.get('request_count')),
            'average_rating': int_or_none(video.get('rating')),
            'season_number': int_or_none(video.get('season')),
            'episode_number': int_or_none(video.get('episode')),
            'formats': formats,
            'subtitles': subtitles,
        }