code:
import re

from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    determine_ext,
    ExtractorError,
    int_or_none,
    float_or_none,
    js_to_json,
    parse_iso8601,
    remove_end,
    strip_or_none,
    try_get,
)

class TV2IE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tv2\.no/v\d*/(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://www.tv2.no/v/916509/',
        'info_dict': {
            'id': '916509',
            'ext': 'mp4',
            'title': 'Se Frode Gryttens hyllest av Steven Gerrard',
            'description': 'TV 2 Sportens huspoet tar avskjed med Liverpools kaptein Steven Gerrard.',
            'timestamp': 1431715610,
            'upload_date': '20150515',
            'duration': 157,
            'view_count': int,
            'categories': list,
        },
    }, {
        'url': 'http://www.tv2.no/v2/916509',
        'only_matching': True,
    }]
    _PROTOCOLS = ('HLS', 'DASH')
    _GEO_COUNTRIES = ['NO']

    def _real_extract(self, url):
        video_id = self._match_id(url)
        asset = self._download_json('https://sumo.tv2.no/rest/assets/' + video_id, video_id,
                                    'Downloading metadata JSON')
        title = asset['title']
        is_live = asset.get('live') is True

        formats = []
        format_urls = []
        for protocol in self._PROTOCOLS:
            try:
                data = self._download_json('https://api.sumo.tv2.no/play/%s?stream=%s' % (video_id, protocol),
                                           video_id, 'Downloading playabck JSON',
                                           headers={'content-type': 'application/json'},
                                           data='{"device":{"id":"1-1-1","name":"Nettleser (HTML)"}}'.encode())['playback']
            except ExtractorError as e:
                if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
                    error = self._parse_json(e.cause.read().decode(), video_id)['error']
                    error_code = error.get('code')
                    if error_code == 'ASSET_PLAYBACK_INVALID_GEO_LOCATION':
                        self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
                    elif error_code == 'SESSION_NOT_AUTHENTICATED':
                        self.raise_login_required()
                    raise ExtractorError(error['description'])
                raise
            items = data.get('streams', [])
            for item in items:
                video_url = item.get('url')
                if not video_url or video_url in format_urls:
                    continue
                format_id = '%s-%s' % (protocol.lower(), item.get('type'))
                if not self._is_valid_url(video_url, video_id, format_id):
                    continue
                format_urls.append(video_url)
                ext = determine_ext(video_url)
                if ext == 'f4m':
                    formats.extend(self._extract_f4m_formats(
                        video_url, video_id, f4m_id=format_id, fatal=False))
                elif ext == 'm3u8':
                    if not data.get('drmProtected'):
                        formats.extend(self._extract_m3u8_formats(
                            video_url, video_id, 'mp4', live=is_live, m3u8_id=format_id, fatal=False))
                elif ext == 'mpd':
                    formats.extend(self._extract_mpd_formats(
                        video_url, video_id, format_id, fatal=False))
                elif ext == 'ism' or video_url.endswith('.ism/Manifest'):
                    pass
                else:
                    formats.append({
                        'url': video_url,
                        'format_id': format_id,
                    })
        if not formats and data.get('drmProtected'):
            self.report_drm(video_id)
        self._sort_formats(formats)

        thumbnails = [{
            'id': type,
            'url': thumb_url,
        } for type, thumb_url in (asset.get('images') or {}).items()]

        return {
            'id': video_id,
            'url': video_url,
            'title': title,
            'description': strip_or_none(asset.get('description')),
            'thumbnails': thumbnails,
            'timestamp': parse_iso8601(asset.get('live_broadcast_time') or asset.get('update_time')),
            'duration': float_or_none(asset.get('accurateDuration') or asset.get('duration')),
            'view_count': int_or_none(asset.get('views')),
            'categories': asset.get('tags', '').split(','),
            'formats': formats,
            'is_live': is_live,
        }

class TV2ArticleIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tv2\.no/(?:a|\d{4}/\d{2}/\d{2}(/[^/]+)+)/(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://www.tv2.no/2015/05/16/nyheter/alesund/krim/pingvin/6930542',
        'info_dict': {
            'id': '6930542',
            'title': 'Russen hetses etter pingvintyveri - innrømmer å ha åpnet luken på buret',
            'description': 'De fire siktede nekter fortsatt for å ha stjålet pingvinbabyene, men innrømmer å ha åpnet luken til de små kyllingene.',
        },
        'playlist_count': 2,
    }, {
        'url': 'http://www.tv2.no/a/6930542',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, playlist_id)

        # Old embed pattern (looks unused nowadays)
        assets = re.findall(r'data-assetid=["\'](\d+)', webpage)

        if not assets:
            # New embed pattern
            for v in re.findall(r'(?s)TV2ContentboxVideo\(({.+?})\)', webpage):
                video = self._parse_json(
                    v, playlist_id, transform_source=js_to_json, fatal=False)
                if not video:
                    continue
                asset = video.get('assetId')
                if asset:
                    assets.append(asset)

        entries = [
            self.url_result('http://www.tv2.no/v/%s' % asset_id, 'TV2')
            for asset_id in assets]

        title = remove_end(self._og_search_title(webpage), ' - TV2.no')
        description = remove_end(self._og_search_description(webpage), ' - TV2.no')

        return self.playlist_result(entries, playlist_id, title, description)

class KatsomoIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?(?:katsomo|mtv(uutiset)?)\.fi/(?:sarja/[0-9a-z-]+-\d+/[0-9a-z-]+-|(?:#!/)?jakso/(?:\d+/[^/]+/)?|video/prog)(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://www.mtv.fi/sarja/mtv-uutiset-live-33001002003/lahden-pelicans-teki-kovan-ratkaisun-ville-nieminen-pihalle-1181321',
        'info_dict': {
            'id': '1181321',
            'ext': 'mp4',
            'title': 'Lahden Pelicans teki kovan ratkaisun – Ville Nieminen pihalle',
            'description': 'Päätöksen teki Pelicansin hallitus.',
            'timestamp': 1575116484,
            'upload_date': '20191130',
            'duration': 37.12,
            'view_count': int,
            'categories': list,
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
    }, {
        'url': 'http://www.katsomo.fi/#!/jakso/33001005/studio55-fi/658521/jukka-kuoppamaki-tekee-yha-lauluja-vaikka-lentokoneessa',
        'only_matching': True,
    }, {
        'url': 'https://www.mtvuutiset.fi/video/prog1311159',
        'only_matching': True,
    }, {
        'url': 'https://www.katsomo.fi/#!/jakso/1311159',
        'only_matching': True,
    }]
    _API_DOMAIN = 'api.katsomo.fi'
    _PROTOCOLS = ('HLS', 'MPD')
    _GEO_COUNTRIES = ['FI']

    def _real_extract(self, url):
        video_id = self._match_id(url)
        api_base = 'http://%s/api/web/asset/%s' % (self._API_DOMAIN, video_id)

        asset = self._download_json(
            api_base + '.json', video_id,
            'Downloading metadata JSON')['asset']
        title = asset.get('subtitle') or asset['title']
        is_live = asset.get('live') is True

        formats = []
        format_urls = []
        for protocol in self._PROTOCOLS:
            try:
                data = self._download_json(
                    api_base + '/play.json?protocol=%s&videoFormat=SMIL+ISMUSP' % protocol,
                    video_id, 'Downloading play JSON')['playback']
            except ExtractorError as e:
                if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
                    error = self._parse_json(e.cause.read().decode(), video_id)['error']
                    error_code = error.get('code')
                    if error_code == 'ASSET_PLAYBACK_INVALID_GEO_LOCATION':
                        self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
                    elif error_code == 'SESSION_NOT_AUTHENTICATED':
                        self.raise_login_required()
                    raise ExtractorError(error['description'])
                raise
            items = try_get(data, lambda x: x['items']['item'])
            if not items:
                continue
            if not isinstance(items, list):
                items = [items]
            for item in items:
                if not isinstance(item, dict):
                    continue
                video_url = item.get('url')
                if not video_url or video_url in format_urls:
                    continue
                format_id = '%s-%s' % (protocol.lower(), item.get('mediaFormat'))
                if not self._is_valid_url(video_url, video_id, format_id):
                    continue
                format_urls.append(video_url)
                ext = determine_ext(video_url)
                if ext == 'f4m':
                    formats.extend(self._extract_f4m_formats(
                        video_url, video_id, f4m_id=format_id, fatal=False))
                elif ext == 'm3u8':
                    if not data.get('drmProtected'):
                        formats.extend(self._extract_m3u8_formats(
                            video_url, video_id, 'mp4', live=is_live, m3u8_id=format_id, fatal=False))
                elif ext == 'mpd':
                    formats.extend(self._extract_mpd_formats(
                        video_url, video_id, format_id, fatal=False))
                elif ext == 'ism' or video_url.endswith('.ism/Manifest'):
                    pass
                else:
                    formats.append({
                        'url': video_url,
                        'format_id': format_id,
                        'tbr': int_or_none(item.get('bitrate')),
                        'filesize': int_or_none(item.get('fileSize')),
                    })
        if not formats and data.get('drmProtected'):
            self.report_drm(video_id)
        self._sort_formats(formats)

        thumbnails = [{
            'id': thumbnail.get('@type'),
            'url': thumbnail.get('url'),
        } for _, thumbnail in (asset.get('imageVersions') or {}).items()]

        return {
            'id': video_id,
            'url': video_url,
            'title': title,
            'description': strip_or_none(asset.get('description')),
            'thumbnails': thumbnails,
            'timestamp': parse_iso8601(asset.get('createTime')),
            'duration': float_or_none(asset.get('accurateDuration') or asset.get('duration')),
            'view_count': int_or_none(asset.get('views')),
            'categories': asset.get('keywords', '').split(','),
            'formats': formats,
            'is_live': is_live,
        }

class MTVUutisetArticleIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)mtvuutiset\.fi/artikkeli/[^/]+/(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://www.mtvuutiset.fi/artikkeli/tallaisia-vaurioita-viking-amorellassa-on-useamman-osaston-alla-vetta/7931384',
        'info_dict': {
            'id': '1311159',
            'ext': 'mp4',
            'title': 'Viking Amorellan matkustajien evakuointi on alkanut – tältä operaatio näyttää laivalla',
            'description': 'Viking Amorellan matkustajien evakuointi on alkanut – tältä operaatio näyttää laivalla',
            'timestamp': 1600608966,
            'upload_date': '20200920',
            'duration': 153.7886666,
            'view_count': int,
            'categories': list,
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
    }, {
        # multiple Youtube embeds
        'url': 'https://www.mtvuutiset.fi/artikkeli/50-vuotta-subarun-vastaiskua/6070962',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        article_id = self._match_id(url)
        article = self._download_json(
            'http://api.mtvuutiset.fi/mtvuutiset/api/json/' + article_id,
            article_id)

        def entries():
            for video in (article.get('videos') or []):
                video_type = video.get('videotype')
                video_url = video.get('url')
                if not (video_url and video_type in ('katsomo', 'youtube')):
                    continue
                yield self.url_result(
                    video_url, video_type.capitalize(), video.get('video_id'))

        return self.playlist_result(
            entries(), article_id, article.get('title'), article.get('description'))


code:
import json
import re

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

class TV2DKIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:www\.)?
                        (?:
                            tvsyd|
                            tv2ostjylland|
                            tvmidtvest|
                            tv2fyn|
                            tv2east|
                            tv2lorry|
                            tv2nord
                        )\.dk/
                        (:[^/]+/)*
                        (?P<id>[^/?\#&]+)
                    '''
    _TESTS = [{
        'url': 'https://www.tvsyd.dk/nyheder/28-10-2019/1930/1930-28-okt-2019?autoplay=1#player',
        'info_dict': {
            'id': '0_52jmwa0p',
            'ext': 'mp4',
            'title': '19:30 - 28. okt. 2019',
            'timestamp': 1572290248,
            'upload_date': '20191028',
            'uploader_id': 'tvsyd',
            'duration': 1347,
            'view_count': int,
        },
        'add_ie': ['Kaltura'],
    }, {
        'url': 'https://www.tv2lorry.dk/gadekamp/gadekamp-6-hoejhuse-i-koebenhavn',
        'info_dict': {
            'id': '1_7iwll9n0',
            'ext': 'mp4',
            'upload_date': '20211027',
            'title': 'Gadekamp #6 - Højhuse i København',
            'uploader_id': 'tv2lorry',
            'timestamp': 1635345229,
        },
        'add_ie': ['Kaltura'],
    }, {
        'url': 'https://www.tv2ostjylland.dk/artikel/minister-gaar-ind-i-sag-om-diabetes-teknologi',
        'only_matching': True,
    }, {
        'url': 'https://www.tv2ostjylland.dk/nyheder/28-10-2019/22/2200-nyhederne-mandag-d-28-oktober-2019?autoplay=1#player',
        'only_matching': True,
    }, {
        'url': 'https://www.tvmidtvest.dk/nyheder/27-10-2019/1930/1930-27-okt-2019',
        'only_matching': True,
    }, {
        'url': 'https://www.tv2fyn.dk/artikel/fyn-kan-faa-landets-foerste-fabrik-til-groent-jetbraendstof',
        'only_matching': True,
    }, {
        'url': 'https://www.tv2east.dk/artikel/gods-faar-indleveret-tonsvis-af-aebler-100-kilo-aebler-gaar-til-en-aeblebrandy',
        'only_matching': True,
    }, {
        'url': 'https://www.tv2lorry.dk/koebenhavn/rasmus-paludan-evakueret-til-egen-demonstration#player',
        'only_matching': True,
    }, {
        'url': 'https://www.tv2nord.dk/artikel/dybt-uacceptabelt',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, video_id)

        entries = []

        def add_entry(partner_id, kaltura_id):
            entries.append(self.url_result(
                'kaltura:%s:%s' % (partner_id, kaltura_id), 'Kaltura',
                video_id=kaltura_id))

        for video_el in re.findall(r'(?s)<[^>]+\bdata-entryid\s*=[^>]*>', webpage):
            video = extract_attributes(video_el)
            kaltura_id = video.get('data-entryid')
            if not kaltura_id:
                continue
            partner_id = video.get('data-partnerid')
            if not partner_id:
                continue
            add_entry(partner_id, kaltura_id)
        if not entries:
            kaltura_id = self._search_regex(
                (r'entry_id\s*:\s*["\']([0-9a-z_]+)',
                 r'\\u002FentryId\\u002F(\w+)\\u002F'), webpage, 'kaltura id')
            partner_id = self._search_regex(
                (r'\\u002Fp\\u002F(\d+)\\u002F', r'/p/(\d+)/'), webpage,
                'partner id')
            add_entry(partner_id, kaltura_id)
        if len(entries) == 1:
            return entries[0]
        return self.playlist_result(entries)

class TV2DKBornholmPlayIE(InfoExtractor):
    _VALID_URL = r'https?://play\.tv2bornholm\.dk/\?.*?\bid=(?P<id>\d+)'
    _TEST = {
        'url': 'http://play.tv2bornholm.dk/?area=specifikTV&id=781021',
        'info_dict': {
            'id': '781021',
            'ext': 'mp4',
            'title': '12Nyheder-27.11.19',
        },
        'params': {
            'skip_download': True,
        },
    }

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

        video = self._download_json(
            'https://play.tv2bornholm.dk/controls/AJAX.aspx/specifikVideo', video_id,
            data=json.dumps({
                'playlist_id': video_id,
                'serienavn': '',
            }).encode(), headers={
                'X-Requested-With': 'XMLHttpRequest',
                'Content-Type': 'application/json; charset=UTF-8',
            })['d']

        # TODO: generalize flowplayer
        title = self._search_regex(
            r'title\s*:\s*(["\'])(?P<value>(?:(?!\1).)+)\1', video, 'title',
            group='value')
        sources = self._parse_json(self._search_regex(
            r'(?s)sources:\s*(\[.+?\]),', video, 'sources'),
            video_id, js_to_json)

        formats = []
        srcs = set()
        for source in sources:
            src = url_or_none(source.get('src'))
            if not src:
                continue
            if src in srcs:
                continue
            srcs.add(src)
            ext = determine_ext(src)
            src_type = source.get('type')
            if src_type == 'application/x-mpegurl' or ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    src, video_id, ext='mp4', entry_protocol='m3u8_native',
                    m3u8_id='hls', fatal=False))
            elif src_type == 'application/dash+xml' or ext == 'mpd':
                formats.extend(self._extract_mpd_formats(
                    src, video_id, mpd_id='dash', fatal=False))
            else:
                formats.append({
                    'url': src,
                })
        self._sort_formats(formats)

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

repo_name: Lesmiscore/ytdl


code:
# encoding: utf-8
from .common import InfoExtractor
from ..utils import (
    traverse_obj,
    UnsupportedError,
)

class TV2HuIE(InfoExtractor):
    IE_NAME = 'tv2play.hu'
    _VALID_URL = r'https?://(?:www\.)?tv2play\.hu/(?!szalag/)(?P<id>[^#&?]+)'
    _TESTS = [{
        'url': 'https://tv2play.hu/mintaapak/mintaapak_213_epizod_resz',
        'info_dict': {
            'id': '249240',
            'ext': 'mp4',
            'title': 'Mintaapák - 213. epizód',
            'series': 'Mintaapák',
            'duration': 2164,
            'description': 'md5:7350147e75485a59598e806c47967b07',
            'thumbnail': r're:^https?://.*\.jpg$',
            'release_date': '20210825',
            'season_number': None,
            'episode_number': 213,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://tv2play.hu/taxi_2',
        'md5': '585e58e2e090f34603804bb2c48e98d8',
        'info_dict': {
            'id': '199363',
            'ext': 'mp4',
            'title': 'Taxi 2',
            'series': 'Taxi 2',
            'duration': 5087,
            'description': 'md5:47762155dc9a50241797ded101b1b08c',
            'thumbnail': r're:^https?://.*\.jpg$',
            'release_date': '20210118',
            'season_number': None,
            'episode_number': None,
        },
        'params': {
            'skip_download': True,
        },
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        json_data = self._download_json(f'https://tv2play.hu/api/search/{id}', id)

        if json_data['contentType'] == 'showpage':
            ribbon_ids = traverse_obj(json_data, ('pages', ..., 'tabs', ..., 'ribbonIds'), get_all=False, expected_type=list)
            entries = [self.url_result(f'https://tv2play.hu/szalag/{ribbon_id}',
                                       ie=TV2HuSeriesIE.ie_key(), video_id=ribbon_id) for ribbon_id in ribbon_ids]
            return self.playlist_result(entries, playlist_id=id)
        elif json_data['contentType'] != 'video':
            raise UnsupportedError(url)

        video_id = str(json_data['id'])
        player_id = json_data.get('playerId')
        series_json = json_data.get('seriesInfo', {})

        video_json_url = self._download_json(f'https://tv2play.hu/api/streaming-url?playerId={player_id}', video_id)['url']
        video_json = self._download_json(video_json_url, video_id)
        m3u8_url = self._proto_relative_url(traverse_obj(video_json, ('bitrates', 'hls')))
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id)
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': json_data['title'],
            'series': json_data.get('seriesTitle'),
            'duration': json_data.get('length'),
            'description': json_data.get('description'),
            'thumbnail': 'https://tv2play.hu' + json_data.get('thumbnailUrl'),
            'release_date': json_data.get('uploadedAt').replace('.', ''),
            'season_number': series_json.get('seasonNr'),
            'episode_number': series_json.get('episodeNr'),
            'formats': formats,
            'subtitles': subtitles,
        }

class TV2HuSeriesIE(InfoExtractor):
    IE_NAME = 'tv2playseries.hu'
    _VALID_URL = r'https?://(?:www\.)?tv2play\.hu/szalag/(?P<id>[^#&?]+)'

    _TESTS = [{
        'url': 'https://tv2play.hu/szalag/59?rendezes=nepszeruseg',
        'playlist_mincount': 284,
        'info_dict': {
            'id': '59',
        }
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        json_data = self._download_json(f'https://tv2play.hu/api/ribbons/{id}/0?size=100000', id)
        entries = []
        for card in json_data.get('cards', []):
            video_id = card.get('slug')
            if video_id:
                entries.append(self.url_result(f'https://tv2play.hu/{video_id}',
                                               ie=TV2HuIE.ie_key(), video_id=video_id))

        return self.playlist_result(entries, playlist_id=id)


code:
import re

from .common import InfoExtractor
from ..utils import (
    int_or_none,
    parse_iso8601,
)

class TV4IE(InfoExtractor):
    IE_DESC = 'tv4.se and tv4play.se'
    _VALID_URL = r'''(?x)https?://(?:www\.)?
        (?:
            tv4\.se/(?:[^/]+)/klipp/(?:.*)-|
            tv4play\.se/
            (?:
                (?:program|barn)/(?:(?:[^/]+/){1,2}|(?:[^\?]+)\?video_id=)|
                iframe/video/|
                film/|
                sport/|
            )
        )(?P<id>[0-9]+)'''
    _GEO_COUNTRIES = ['SE']
    _TESTS = [
        {
            'url': 'http://www.tv4.se/kalla-fakta/klipp/kalla-fakta-5-english-subtitles-2491650',
            'md5': 'cb837212f342d77cec06e6dad190e96d',
            'info_dict': {
                'id': '2491650',
                'ext': 'mp4',
                'title': 'Kalla Fakta 5 (english subtitles)',
                'thumbnail': r're:^https?://.*\.jpg$',
                'timestamp': int,
                'upload_date': '20131125',
            },
        },
        {
            'url': 'http://www.tv4play.se/iframe/video/3054113',
            'md5': 'cb837212f342d77cec06e6dad190e96d',
            'info_dict': {
                'id': '3054113',
                'ext': 'mp4',
                'title': 'Så här jobbar ficktjuvarna - se avslöjande bilder',
                'thumbnail': r're:^https?://.*\.jpg$',
                'description': 'Unika bilder avslöjar hur turisternas fickor vittjas mitt på Stockholms central. Två experter på ficktjuvarna avslöjar knepen du ska se upp för.',
                'timestamp': int,
                'upload_date': '20150130',
            },
        },
        {
            'url': 'http://www.tv4play.se/sport/3060959',
            'only_matching': True,
        },
        {
            'url': 'http://www.tv4play.se/film/2378136',
            'only_matching': True,
        },
        {
            'url': 'http://www.tv4play.se/barn/looney-tunes?video_id=3062412',
            'only_matching': True,
        },
        {
            'url': 'http://www.tv4play.se/program/farang/3922081',
            'only_matching': True,
        },
        {
            'url': 'https://www.tv4play.se/program/nyheterna/avsnitt/13315940',
            'only_matching': True,
        }
    ]

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

        info = self._download_json(
            'https://playback-api.b17g.net/asset/%s' % video_id,
            video_id, 'Downloading video info JSON', query={
                'service': 'tv4',
                'device': 'browser',
                'protocol': 'hls,dash',
                'drm': 'widevine',
            })['metadata']

        title = info['title']

        manifest_url = self._download_json(
            'https://playback-api.b17g.net/media/' + video_id,
            video_id, query={
                'service': 'tv4',
                'device': 'browser',
                'protocol': 'hls',
            })['playbackItem']['manifestUrl']
        formats = []
        subtitles = {}

        fmts, subs = self._extract_m3u8_formats_and_subtitles(
            manifest_url, video_id, 'mp4',
            'm3u8_native', m3u8_id='hls', fatal=False)
        formats.extend(fmts)
        subtitles = self._merge_subtitles(subtitles, subs)

        fmts, subs = self._extract_mpd_formats_and_subtitles(
            manifest_url.replace('.m3u8', '.mpd'),
            video_id, mpd_id='dash', fatal=False)
        formats.extend(fmts)
        subtitles = self._merge_subtitles(subtitles, subs)

        fmts = self._extract_f4m_formats(
            manifest_url.replace('.m3u8', '.f4m'),
            video_id, f4m_id='hds', fatal=False)
        formats.extend(fmts)

        fmts, subs = self._extract_ism_formats_and_subtitles(
            re.sub(r'\.ism/.*?\.m3u8', r'.ism/Manifest', manifest_url),
            video_id, ism_id='mss', fatal=False)
        formats.extend(fmts)
        subtitles = self._merge_subtitles(subtitles, subs)

        if not formats and info.get('is_geo_restricted'):
            self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)

        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'subtitles': subtitles,
            'description': info.get('description'),
            'timestamp': parse_iso8601(info.get('broadcast_date_time')),
            'duration': int_or_none(info.get('duration')),
            'thumbnail': info.get('image'),
            'is_live': info.get('isLive') is True,
            'series': info.get('seriesTitle'),
            'season_number': int_or_none(info.get('seasonNumber')),
            'episode': info.get('episodeTitle'),
            'episode_number': int_or_none(info.get('episodeNumber')),
        }


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

class TV5MondePlusIE(InfoExtractor):
    IE_DESC = 'TV5MONDE+'
    _VALID_URL = r'https?://(?:www\.)?(?:tv5mondeplus|revoir\.tv5monde)\.com/toutes-les-videos/[^/]+/(?P<id>[^/?#]+)'
    _TESTS = [{
        # movie
        'url': 'https://revoir.tv5monde.com/toutes-les-videos/cinema/ceux-qui-travaillent',
        'md5': '32fa0cde16a4480d1251502a66856d5f',
        'info_dict': {
            'id': 'dc57a011-ec4b-4648-2a9a-4f03f8352ed3',
            'display_id': 'ceux-qui-travaillent',
            'ext': 'mp4',
            'title': 'Ceux qui travaillent',
            'description': 'md5:570e8bb688036ace873b2d50d24c026d',
            'upload_date': '20210819',
        },
    }, {
        # series episode
        'url': 'https://revoir.tv5monde.com/toutes-les-videos/series-fictions/vestiaires-caro-actrice',
        'info_dict': {
            'id': '9e9d599e-23af-6915-843e-ecbf62e97925',
            'display_id': 'vestiaires-caro-actrice',
            'ext': 'mp4',
            'title': "Vestiaires - Caro actrice",
            'description': 'md5:db15d2e1976641e08377f942778058ea',
            'upload_date': '20210819',
            'series': "Vestiaires",
            'episode': 'Caro actrice',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://revoir.tv5monde.com/toutes-les-videos/series-fictions/neuf-jours-en-hiver-neuf-jours-en-hiver',
        'only_matching': True,
    }, {
        'url': 'https://revoir.tv5monde.com/toutes-les-videos/info-societe/le-journal-de-la-rts-edition-du-30-01-20-19h30',
        'only_matching': True,
    }]
    _GEO_BYPASS = False

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

        if ">Ce programme n'est malheureusement pas disponible pour votre zone géographique.<" in webpage:
            self.raise_geo_restricted(countries=['FR'])

        title = episode = self._html_search_regex(r'<h1>([^<]+)', webpage, 'title')
        vpl_data = extract_attributes(self._search_regex(
            r'(<[^>]+class="video_player_loader"[^>]+>)',
            webpage, 'video player loader'))

        video_files = self._parse_json(
            vpl_data['data-broadcast'], display_id)
        formats = []
        for video_file in video_files:
            v_url = video_file.get('url')
            if not v_url:
                continue
            video_format = video_file.get('format') or determine_ext(v_url)
            if video_format == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    v_url, display_id, 'mp4', 'm3u8_native',
                    m3u8_id='hls', fatal=False))
            else:
                formats.append({
                    'url': v_url,
                    'format_id': video_format,
                })
        self._sort_formats(formats)

        metadata = self._parse_json(
            vpl_data['data-metadata'], display_id)
        duration = (int_or_none(try_get(metadata, lambda x: x['content']['duration']))
                    or parse_duration(self._html_search_meta('duration', webpage)))

        description = self._html_search_regex(
            r'(?s)<div[^>]+class=["\']episode-texte[^>]+>(.+?)</div>', webpage,
            'description', fatal=False)

        series = self._html_search_regex(
            r'<p[^>]+class=["\']episode-emission[^>]+>([^<]+)', webpage,
            'series', default=None)

        if series and series != title:
            title = '%s - %s' % (series, title)

        upload_date = self._search_regex(
            r'(?:date_publication|publish_date)["\']\s*:\s*["\'](\d{4}_\d{2}_\d{2})',
            webpage, 'upload date', default=None)
        if upload_date:
            upload_date = upload_date.replace('_', '')

        video_id = self._search_regex(
            (r'data-guid=["\']([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})',
             r'id_contenu["\']\s:\s*(\d+)'), webpage, 'video id',
            default=display_id)

        return {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'thumbnail': vpl_data.get('data-image'),
            'duration': duration,
            'upload_date': upload_date,
            'formats': formats,
            'series': series,
            'episode': episode,
        }


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

class TV5UnisBaseIE(InfoExtractor):
    _GEO_COUNTRIES = ['CA']

    def _real_extract(self, url):
        groups = self._match_valid_url(url).groups()
        product = self._download_json(
            'https://api.tv5unis.ca/graphql', groups[0], query={
                'query': '''{
  %s(%s) {
    collection {
      title
    }
    episodeNumber
    rating {
      name
    }
    seasonNumber
    tags
    title
    videoElement {
      ... on Video {
        mediaId
      }
    }
  }
}''' % (self._GQL_QUERY_NAME, self._gql_args(groups)),
            })['data'][self._GQL_QUERY_NAME]
        media_id = product['videoElement']['mediaId']

        return {
            '_type': 'url_transparent',
            'id': media_id,
            'title': product.get('title'),
            'url': smuggle_url('limelight:media:' + media_id, {'geo_countries': self._GEO_COUNTRIES}),
            'age_limit': parse_age_limit(try_get(product, lambda x: x['rating']['name'])),
            'tags': product.get('tags'),
            'series': try_get(product, lambda x: x['collection']['title']),
            'season_number': int_or_none(product.get('seasonNumber')),
            'episode_number': int_or_none(product.get('episodeNumber')),
            'ie_key': 'LimelightMedia',
        }

class TV5UnisVideoIE(TV5UnisBaseIE):
    IE_NAME = 'tv5unis:video'
    _VALID_URL = r'https?://(?:www\.)?tv5unis\.ca/videos/[^/]+/(?P<id>\d+)'
    _TEST = {
        'url': 'https://www.tv5unis.ca/videos/bande-annonces/71843',
        'md5': '3d794164928bda97fb87a17e89923d9b',
        'info_dict': {
            'id': 'a883684aecb2486cad9bdc7bbe17f861',
            'ext': 'mp4',
            'title': 'Watatatow',
            'duration': 10.01,
        }
    }
    _GQL_QUERY_NAME = 'productById'

    @staticmethod
    def _gql_args(groups):
        return 'id: %s' % groups

class TV5UnisIE(TV5UnisBaseIE):
    IE_NAME = 'tv5unis'
    _VALID_URL = r'https?://(?:www\.)?tv5unis\.ca/videos/(?P<id>[^/]+)(?:/saisons/(?P<season_number>\d+)/episodes/(?P<episode_number>\d+))?/?(?:[?#&]|$)'
    _TESTS = [{
        'url': 'https://www.tv5unis.ca/videos/watatatow/saisons/6/episodes/1',
        'md5': 'a479907d2e531a73e1f8dc48d6388d02',
        'info_dict': {
            'id': 'e5ee23a586c44612a56aad61accf16ef',
            'ext': 'mp4',
            'title': 'Je ne peux pas lui résister',
            'description': "Atys, le nouveau concierge de l'école, a réussi à ébranler la confiance de Mado en affirmant qu\'une médaille, ce n'est que du métal. Comme Mado essaie de lui prouver que ses valeurs sont solides, il veut la mettre à l'épreuve...",
            'subtitles': {
                'fr': 'count:1',
            },
            'duration': 1370,
            'age_limit': 8,
            'tags': 'count:3',
            'series': 'Watatatow',
            'season_number': 6,
            'episode_number': 1,
        },
    }, {
        'url': 'https://www.tv5unis.ca/videos/le-voyage-de-fanny',
        'md5': '9ca80ebb575c681d10cae1adff3d4774',
        'info_dict': {
            'id': '726188eefe094d8faefb13381d42bc06',
            'ext': 'mp4',
            'title': 'Le voyage de Fanny',
            'description': "Fanny, 12 ans, cachée dans un foyer loin de ses parents, s'occupe de ses deux soeurs. Devant fuir, Fanny prend la tête d'un groupe de huit enfants et s'engage dans un dangereux périple à travers la France occupée pour rejoindre la frontière suisse.",
            'subtitles': {
                'fr': 'count:1',
            },
            'duration': 5587.034,
            'tags': 'count:4',
        },
    }]
    _GQL_QUERY_NAME = 'productByRootProductSlug'

    @staticmethod
    def _gql_args(groups):
        args = 'rootProductSlug: "%s"' % groups[0]
        if groups[1]:
            args += ', seasonNumber: %s, episodeNumber: %s' % groups[1:]
        return args


code:
from .common import InfoExtractor
from ..utils import (
    float_or_none,
    int_or_none,
    smuggle_url,
    strip_or_none,
)

class TVAIE(InfoExtractor):
    _VALID_URL = r'https?://videos?\.tva\.ca/details/_(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://videos.tva.ca/details/_5596811470001',
        'info_dict': {
            'id': '5596811470001',
            'ext': 'mp4',
            'title': 'Un extrait de l\'épisode du dimanche 8 octobre 2017 !',
            'uploader_id': '5481942443001',
            'upload_date': '20171003',
            'timestamp': 1507064617,
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
        'skip': 'HTTP Error 404: Not Found',
    }, {
        'url': 'https://video.tva.ca/details/_5596811470001',
        'only_matching': True,
    }]
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/5481942443001/default_default/index.html?videoId=%s'

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

        return {
            '_type': 'url_transparent',
            'id': video_id,
            'url': smuggle_url(self.BRIGHTCOVE_URL_TEMPLATE % video_id, {'geo_countries': ['CA']}),
            'ie_key': 'BrightcoveNew',
        }

class QubIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?qub\.ca/(?:[^/]+/)*[0-9a-z-]+-(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://www.qub.ca/tvaplus/tva/alerte-amber/saison-1/episode-01-1000036619',
        'md5': '949490fd0e7aee11d0543777611fbd53',
        'info_dict': {
            'id': '6084352463001',
            'ext': 'mp4',
            'title': 'Épisode 01',
            'uploader_id': '5481942443001',
            'upload_date': '20190907',
            'timestamp': 1567899756,
            'description': 'md5:9c0d7fbb90939420c651fd977df90145',
        },
    }, {
        'url': 'https://www.qub.ca/tele/video/lcn-ca-vous-regarde-rev-30s-ap369664-1009357943',
        'only_matching': True,
    }]
    # reference_id also works with old account_id(5481942443001)
    # BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/5813221784001/default_default/index.html?videoId=ref:%s'

    def _real_extract(self, url):
        entity_id = self._match_id(url)
        entity = self._download_json(
            'https://www.qub.ca/proxy/pfu/content-delivery-service/v1/entities',
            entity_id, query={'id': entity_id})
        video_id = entity['videoId']
        episode = strip_or_none(entity.get('name'))

        return {
            '_type': 'url_transparent',
            'id': video_id,
            'title': episode,
            # 'url': self.BRIGHTCOVE_URL_TEMPLATE % entity['referenceId'],
            'url': 'https://videos.tva.ca/details/_' + video_id,
            'description': entity.get('longDescription'),
            'duration': float_or_none(entity.get('durationMillis'), 1000),
            'episode': episode,
            'episode_number': int_or_none(entity.get('episodeNumber')),
            # 'ie_key': 'BrightcoveNew',
            'ie_key': TVAIE.ie_key(),
        }


code:
import re

from .common import InfoExtractor
from .brightcove import BrightcoveNewIE

class TVANouvellesIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tvanouvelles\.ca/videos/(?P<id>\d+)'
    _TEST = {
        'url': 'http://www.tvanouvelles.ca/videos/5117035533001',
        'info_dict': {
            'id': '5117035533001',
            'ext': 'mp4',
            'title': 'L’industrie du taxi dénonce l’entente entre Québec et Uber: explications',
            'description': 'md5:479653b7c8cf115747bf5118066bd8b3',
            'uploader_id': '1741764581',
            'timestamp': 1473352030,
            'upload_date': '20160908',
        },
        'add_ie': ['BrightcoveNew'],
    }
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/1741764581/default_default/index.html?videoId=%s'

    def _real_extract(self, url):
        brightcove_id = self._match_id(url)
        return self.url_result(
            self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id,
            BrightcoveNewIE.ie_key(), brightcove_id)

class TVANouvellesArticleIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tvanouvelles\.ca/(?:[^/]+/)+(?P<id>[^/?#&]+)'
    _TEST = {
        'url': 'http://www.tvanouvelles.ca/2016/11/17/des-policiers-qui-ont-la-meche-un-peu-courte',
        'info_dict': {
            'id': 'des-policiers-qui-ont-la-meche-un-peu-courte',
            'title': 'Des policiers qui ont «la mèche un peu courte»?',
            'description': 'md5:92d363c8eb0f0f030de9a4a84a90a3a0',
        },
        'playlist_mincount': 4,
    }

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

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

        webpage = self._download_webpage(url, display_id)

        entries = [
            self.url_result(
                'http://www.tvanouvelles.ca/videos/%s' % mobj.group('id'),
                ie=TVANouvellesIE.ie_key(), video_id=mobj.group('id'))
            for mobj in re.finditer(
                r'data-video-id=(["\'])?(?P<id>\d+)', webpage)]

        title = self._og_search_title(webpage, fatal=False)
        description = self._og_search_description(webpage)

        return self.playlist_result(entries, display_id, title, description)


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

class TVCIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tvc\.ru/video/iframe/id/(?P<id>\d+)'
    _EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:http:)?//(?:www\.)?tvc\.ru/video/iframe/id/[^"]+)\1']
    _TEST = {
        'url': 'http://www.tvc.ru/video/iframe/id/74622/isPlay/false/id_stat/channel/?acc_video_id=/channel/brand/id/17/show/episodes/episode_id/39702',
        'md5': 'bbc5ff531d1e90e856f60fc4b3afd708',
        'info_dict': {
            'id': '74622',
            'ext': 'mp4',
            'title': 'События. "События". Эфир от 22.05.2015 14:30',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 1122,
        },
    }

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

        video = self._download_json(
            'http://www.tvc.ru/video/json/id/%s' % video_id, video_id)

        formats = []
        for info in video.get('path', {}).get('quality', []):
            video_url = info.get('url')
            if not video_url:
                continue
            format_id = self._search_regex(
                r'cdnvideo/([^/]+?)(?:-[^/]+?)?/', video_url,
                'format id', default=None)
            formats.append({
                'url': video_url,
                'format_id': format_id,
                'width': int_or_none(info.get('width')),
                'height': int_or_none(info.get('height')),
                'tbr': int_or_none(info.get('bitrate')),
            })
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': video['title'],
            'thumbnail': video.get('picture'),
            'duration': int_or_none(video.get('duration')),
            'formats': formats,
        }

class TVCArticleIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tvc\.ru/(?!video/iframe/id/)(?P<id>[^?#]+)'
    _TESTS = [{
        'url': 'http://www.tvc.ru/channel/brand/id/29/show/episodes/episode_id/39702/',
        'info_dict': {
            'id': '74622',
            'ext': 'mp4',
            'title': 'События. "События". Эфир от 22.05.2015 14:30',
            'description': 'md5:ad7aa7db22903f983e687b8a3e98c6dd',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 1122,
        },
    }, {
        'url': 'http://www.tvc.ru/news/show/id/69944',
        'info_dict': {
            'id': '75399',
            'ext': 'mp4',
            'title': 'Эксперты: в столице встал вопрос о максимально безопасных остановках',
            'description': 'md5:f2098f71e21f309e89f69b525fd9846e',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 278,
        },
    }, {
        'url': 'http://www.tvc.ru/channel/brand/id/47/show/episodes#',
        'info_dict': {
            'id': '2185',
            'ext': 'mp4',
            'title': 'Ещё не поздно. Эфир от 03.08.2013',
            'description': 'md5:51fae9f3f8cfe67abce014e428e5b027',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 3316,
        },
    }]

    def _real_extract(self, url):
        webpage = self._download_webpage(url, self._match_id(url))
        return {
            '_type': 'url_transparent',
            'ie_key': 'TVC',
            'url': self._og_search_video_url(webpage),
            'title': clean_html(self._og_search_title(webpage)),
            'description': clean_html(self._og_search_description(webpage)),
            'thumbnail': self._og_search_thumbnail(webpage),
        }


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

class TVerIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tver\.jp/(?:(?P<type>lp|corner|series|episodes?|feature|tokyo2020/video)/)+(?P<id>[a-zA-Z0-9]+)'
    _TESTS = [{
        'skip': 'videos are only available for 7 days',
        'url': 'https://tver.jp/episodes/ep83nf3w4p',
        'info_dict': {
            'title': '家事ヤロウ!!! 売り場席巻のチーズSP＆財前直見×森泉親子の脱東京暮らし密着！',
            'description': 'md5:dc2c06b6acc23f1e7c730c513737719b',
            'series': '家事ヤロウ!!!',
            'episode': '売り場席巻のチーズSP＆財前直見×森泉親子の脱東京暮らし密着！',
            'alt_title': '売り場席巻のチーズSP＆財前直見×森泉親子の脱東京暮らし密着！',
            'channel': 'テレビ朝日',
            'onair_label': '5月3日(火)放送分',
            'ext_title': '家事ヤロウ!!! 売り場席巻のチーズSP＆財前直見×森泉親子の脱東京暮らし密着！ テレビ朝日 5月3日(火)放送分',
        },
        'add_ie': ['BrightcoveNew'],
    }, {
        'url': 'https://tver.jp/corner/f0103888',
        'only_matching': True,
    }, {
        'url': 'https://tver.jp/lp/f0033031',
        'only_matching': True,
    }]
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
    _PLATFORM_UID = None
    _PLATFORM_TOKEN = None

    def _real_initialize(self):
        create_response = self._download_json(
            'https://platform-api.tver.jp/v2/api/platform_users/browser/create', None,
            note='Creating session', data=b'device_type=pc', headers={
                'Origin': 'https://s.tver.jp',
                'Referer': 'https://s.tver.jp/',
                'Content-Type': 'application/x-www-form-urlencoded',
            })
        self._PLATFORM_UID = traverse_obj(create_response, ('result', 'platform_uid'))
        self._PLATFORM_TOKEN = traverse_obj(create_response, ('result', 'platform_token'))

    def _real_extract(self, url):
        video_id, video_type = self._match_valid_url(url).group('id', 'type')
        if video_type not in {'series', 'episodes'}:
            webpage = self._download_webpage(url, video_id, note='Resolving to new URL')
            video_id = self._match_id(self._search_regex(
                (r'canonical"\s*href="(https?://tver\.jp/[^"]+)"', r'&link=(https?://tver\.jp/[^?&]+)[?&]'),
                webpage, 'url regex'))

        episode_info = self._download_json(
            f'https://platform-api.tver.jp/service/api/v1/callEpisode/{video_id}?require_data=mylist,later[epefy106ur],good[epefy106ur],resume[epefy106ur]',
            video_id, fatal=False,
            query={
                'platform_uid': self._PLATFORM_UID,
                'platform_token': self._PLATFORM_TOKEN,
            }, headers={
                'x-tver-platform-type': 'web'
            })
        episode_content = traverse_obj(
            episode_info, ('result', 'episode', 'content')) or {}

        video_info = self._download_json(
            f'https://statics.tver.jp/content/episode/{video_id}.json', video_id,
            query={
                'v': str_or_none(episode_content.get('version')) or '5',
            }, headers={
                'Origin': 'https://tver.jp',
                'Referer': 'https://tver.jp/',
            })
        p_id = video_info['video']['accountID']
        r_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID')), get_all=False)
        if not r_id:
            raise ExtractorError('Failed to extract reference ID for Brightcove')
        if not r_id.isdigit():
            r_id = f'ref:{r_id}'

        episode = strip_or_none(episode_content.get('title'))
        series = str_or_none(episode_content.get('seriesTitle'))
        title = (
            join_nonempty(series, episode, delim=' ')
            or str_or_none(video_info.get('title')))
        provider = str_or_none(episode_content.get('productionProviderName'))
        onair_label = str_or_none(episode_content.get('broadcastDateLabel'))

        return {
            '_type': 'url_transparent',
            'title': title,
            'series': series,
            'episode': episode,
            # an another title which is considered "full title" for some viewers
            'alt_title': join_nonempty(title, provider, onair_label, delim=' '),
            'channel': provider,
            'onair_label': onair_label,
            'description': str_or_none(video_info.get('description')),
            'url': smuggle_url(
                self.BRIGHTCOVE_URL_TEMPLATE % (p_id, r_id), {'geo_countries': ['JP']}),
            'ie_key': 'BrightcoveNew',
        }


code:
from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    float_or_none,
    int_or_none,
    parse_age_limit,
    try_get,
    url_or_none,
)

class TvigleIE(InfoExtractor):
    IE_NAME = 'tvigle'
    IE_DESC = 'Интернет-телевидение Tvigle.ru'
    _VALID_URL = r'https?://(?:www\.)?(?:tvigle\.ru/(?:[^/]+/)+(?P<display_id>[^/]+)/$|cloud\.tvigle\.ru/video/(?P<id>\d+))'
    _EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//cloud\.tvigle\.ru/video/.+?)\1']

    _GEO_BYPASS = False
    _GEO_COUNTRIES = ['RU']

    _TESTS = [
        {
            'url': 'http://www.tvigle.ru/video/sokrat/',
            'info_dict': {
                'id': '1848932',
                'display_id': 'sokrat',
                'ext': 'mp4',
                'title': 'Сократ',
                'description': 'md5:d6b92ffb7217b4b8ebad2e7665253c17',
                'duration': 6586,
                'age_limit': 12,
            },
            'skip': 'georestricted',
        },
        {
            'url': 'http://www.tvigle.ru/video/vladimir-vysotskii/vedushchii-teleprogrammy-60-minut-ssha-o-vladimire-vysotskom/',
            'info_dict': {
                'id': '5142516',
                'ext': 'flv',
                'title': 'Ведущий телепрограммы «60 минут» (США) о Владимире Высоцком',
                'description': 'md5:027f7dc872948f14c96d19b4178428a4',
                'duration': 186.080,
                'age_limit': 0,
            },
            'skip': 'georestricted',
        }, {
            'url': 'https://cloud.tvigle.ru/video/5267604/',
            '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')

        if not video_id:
            webpage = self._download_webpage(url, display_id)
            video_id = self._html_search_regex(
                (r'<div[^>]+class=["\']player["\'][^>]+id=["\'](\d+)',
                 r'cloudId\s*=\s*["\'](\d+)',
                 r'class="video-preview current_playing" id="(\d+)"'),
                webpage, 'video id')

        video_data = self._download_json(
            'http://cloud.tvigle.ru/api/play/video/%s/' % video_id, display_id)

        item = video_data['playlist']['items'][0]

        videos = item.get('videos')

        error_message = item.get('errorMessage')
        if not videos and error_message:
            if item.get('isGeoBlocked') is True:
                self.raise_geo_restricted(
                    msg=error_message, countries=self._GEO_COUNTRIES)
            else:
                raise ExtractorError(
                    '%s returned error: %s' % (self.IE_NAME, error_message),
                    expected=True)

        title = item['title']
        description = item.get('description')
        thumbnail = item.get('thumbnail')
        duration = float_or_none(item.get('durationMilliseconds'), 1000)
        age_limit = parse_age_limit(item.get('ageRestrictions'))

        formats = []
        for vcodec, url_or_fmts in item['videos'].items():
            if vcodec == 'hls':
                m3u8_url = url_or_none(url_or_fmts)
                if not m3u8_url:
                    continue
                formats.extend(self._extract_m3u8_formats(
                    m3u8_url, video_id, ext='mp4', entry_protocol='m3u8_native',
                    m3u8_id='hls', fatal=False))
            elif vcodec == 'dash':
                mpd_url = url_or_none(url_or_fmts)
                if not mpd_url:
                    continue
                formats.extend(self._extract_mpd_formats(
                    mpd_url, video_id, mpd_id='dash', fatal=False))
            else:
                if not isinstance(url_or_fmts, dict):
                    continue
                for format_id, video_url in url_or_fmts.items():
                    if format_id == 'm3u8':
                        continue
                    video_url = url_or_none(video_url)
                    if not video_url:
                        continue
                    height = self._search_regex(
                        r'^(\d+)[pP]$', format_id, 'height', default=None)
                    filesize = int_or_none(try_get(
                        item, lambda x: x['video_files_size'][vcodec][format_id]))
                    formats.append({
                        'url': video_url,
                        'format_id': '%s-%s' % (vcodec, format_id),
                        'vcodec': vcodec,
                        'height': int_or_none(height),
                        'filesize': filesize,
                    })
        self._sort_formats(formats)

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


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

class TVIPlayerIE(InfoExtractor):
    _VALID_URL = r'https?://tviplayer\.iol\.pt(/programa/[\w-]+/[a-f0-9]+)?/\w+/(?P<id>\w+)'
    _TESTS = [{
        'url': 'https://tviplayer.iol.pt/programa/jornal-das-8/53c6b3903004dc006243d0cf/video/61c8e8b90cf2c7ea0f0f71a9',
        'info_dict': {
            'id': '61c8e8b90cf2c7ea0f0f71a9',
            'ext': 'mp4',
            'duration': 4167,
            'title': 'Jornal das 8 - 26 de dezembro de 2021',
            'thumbnail': 'https://www.iol.pt/multimedia/oratvi/multimedia/imagem/id/61c8ee630cf2cc58e7d98d9f/',
            'season_number': 8,
            'season': 'Season 8',
        }
    }, {
        'url': 'https://tviplayer.iol.pt/programa/isabel/62b471090cf26256cd2a8594/video/62be445f0cf2ea4f0a5218e5',
        'info_dict': {
            'id': '62be445f0cf2ea4f0a5218e5',
            'ext': 'mp4',
            'duration': 3255,
            'season': 'Season 1',
            'title': 'Isabel - Episódio 1',
            'thumbnail': 'https://www.iol.pt/multimedia/oratvi/multimedia/imagem/id/62beac200cf2f9a86eab856b/',
            'season_number': 1,
        }
    }, {
        # no /programa/
        'url': 'https://tviplayer.iol.pt/video/62c4131c0cf2f9a86eac06bb',
        'info_dict': {
            'id': '62c4131c0cf2f9a86eac06bb',
            'ext': 'mp4',
            'title': 'David e Mickael Carreira respondem: «Qual é o próximo a ser pai?»',
            'thumbnail': 'https://www.iol.pt/multimedia/oratvi/multimedia/imagem/id/62c416490cf2ea367d4433fd/',
            'season': 'Season 2',
            'duration': 148,
            'season_number': 2,
        }
    }, {
        # episodio url
        'url': 'https://tviplayer.iol.pt/programa/para-sempre/61716c360cf2365a5ed894c4/episodio/t1e187',
        'info_dict': {
            'id': 't1e187',
            'ext': 'mp4',
            'season': 'Season 1',
            'title': 'Quem denunciou Pedro?',
            'thumbnail': 'https://www.iol.pt/multimedia/oratvi/multimedia/imagem/id/62eda30b0cf2ea367d48973b/',
            'duration': 1250,
            'season_number': 1,
        }
    }]

    def _real_initialize(self):
        self.wms_auth_sign_token = self._download_webpage(
            'https://services.iol.pt/matrix?userId=', 'wmsAuthSign',
            note='Trying to get wmsAuthSign token')

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

        json_data = self._search_json(
            r'<script>\s*jsonData\s*=\s*', webpage, 'json_data', video_id)

        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
            f'{json_data["videoUrl"]}?wmsAuthSign={self.wms_auth_sign_token}',
            video_id, ext='mp4')
        return {
            'id': video_id,
            'title': json_data.get('title') or self._og_search_title(webpage),
            'thumbnail': json_data.get('cover') or self._og_search_thumbnail(webpage),
            'duration': json_data.get('duration'),
            'formats': formats,
            'subtitles': subtitles,
            'season_number': traverse_obj(json_data, ('program', 'seasonNum')),
        }


code:
from .mtv import MTVServicesInfoExtractor

# TODO: Remove - Reason not used anymore - Service moved to youtube

class TVLandIE(MTVServicesInfoExtractor):
    IE_NAME = 'tvland.com'
    _VALID_URL = r'https?://(?:www\.)?tvland\.com/(?:video-clips|(?:full-)?episodes)/(?P<id>[^/?#.]+)'
    _FEED_URL = 'http://www.tvland.com/feeds/mrss/'
    _TESTS = [{
        # Geo-restricted. Without a proxy metadata are still there. With a
        # proxy it redirects to http://m.tvland.com/app/
        'url': 'https://www.tvland.com/episodes/s04pzf/everybody-loves-raymond-the-dog-season-1-ep-19',
        'info_dict': {
            'description': 'md5:84928e7a8ad6649371fbf5da5e1ad75a',
            'title': 'The Dog',
        },
        'playlist_mincount': 5,
        'skip': '404 Not found',
    }, {
        'url': 'https://www.tvland.com/video-clips/4n87f2/younger-a-first-look-at-younger-season-6',
        'md5': 'e2c6389401cf485df26c79c247b08713',
        'info_dict': {
            'id': '891f7d3c-5b5b-4753-b879-b7ba1a601757',
            'ext': 'mp4',
            'title': 'Younger|April 30, 2019|6|NO-EPISODE#|A First Look at Younger Season 6',
            'description': 'md5:595ea74578d3a888ae878dfd1c7d4ab2',
            'upload_date': '20190430',
            'timestamp': 1556658000,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.tvland.com/full-episodes/iu0hz6/younger-a-kiss-is-just-a-kiss-season-3-ep-301',
        'only_matching': True,
    }]


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

class TVN24IE(InfoExtractor):
    _VALID_URL = r'https?://(?:(?:[^/]+)\.)?tvn24(?:bis)?\.pl/(?:[^/]+/)*(?P<id>[^/]+)'
    _TESTS = [{
        'url': 'http://www.tvn24.pl/wiadomosci-z-kraju,3/oredzie-artura-andrusa,702428.html',
        'md5': 'fbdec753d7bc29d96036808275f2130c',
        'info_dict': {
            'id': '1584444',
            'ext': 'mp4',
            'title': '"Święta mają być wesołe, dlatego, ludziska, wszyscy pod jemiołę"',
            'description': 'Wyjątkowe orędzie Artura Andrusa, jednego z gości Szkła kontaktowego.',
            'thumbnail': 're:https?://.*[.]jpeg',
        }
    }, {
        # different layout
        'url': 'https://tvnmeteo.tvn24.pl/magazyny/maja-w-ogrodzie,13/odcinki-online,1,4,1,0/pnacza-ptaki-i-iglaki-odc-691-hgtv-odc-29,1771763.html',
        'info_dict': {
            'id': '1771763',
            'ext': 'mp4',
            'title': 'Pnącza, ptaki i iglaki (odc. 691 /HGTV odc. 29)',
            'thumbnail': 're:https?://.*',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://fakty.tvn24.pl/ogladaj-online,60/53-konferencja-bezpieczenstwa-w-monachium,716431.html',
        'only_matching': True,
    }, {
        'url': 'http://sport.tvn24.pl/pilka-nozna,105/ligue-1-kamil-glik-rozcial-glowe-monaco-tylko-remisuje-z-bastia,716522.html',
        'only_matching': True,
    }, {
        'url': 'http://tvn24bis.pl/poranek,146,m/gen-koziej-w-tvn24-bis-wracamy-do-czasow-zimnej-wojny,715660.html',
        'only_matching': True,
    }, {
        'url': 'https://www.tvn24.pl/magazyn-tvn24/angie-w-jednej-czwartej-polka-od-szarej-myszki-do-cesarzowej-europy,119,2158',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        title = self._og_search_title(
            webpage, default=None) or self._search_regex(
            r'<h\d+[^>]+class=["\']magazineItemHeader[^>]+>(.+?)</h',
            webpage, 'title')

        def extract_json(attr, name, default=NO_DEFAULT, fatal=True):
            return self._parse_json(
                self._search_regex(
                    r'\b%s=(["\'])(?P<json>(?!\1).+?)\1' % attr, webpage,
                    name, group='json', default=default, fatal=fatal) or '{}',
                display_id, transform_source=unescapeHTML, fatal=fatal)

        quality_data = extract_json('data-quality', 'formats')

        formats = []
        for format_id, url in quality_data.items():
            formats.append({
                'url': url,
                'format_id': format_id,
                'height': int_or_none(format_id.rstrip('p')),
            })
        self._sort_formats(formats)

        description = self._og_search_description(webpage, default=None)
        thumbnail = self._og_search_thumbnail(
            webpage, default=None) or self._html_search_regex(
            r'\bdata-poster=(["\'])(?P<url>(?!\1).+?)\1', webpage,
            'thumbnail', group='url')

        video_id = None

        share_params = extract_json(
            'data-share-params', 'share params', default=None)
        if isinstance(share_params, dict):
            video_id = share_params.get('id')

        if not video_id:
            video_id = self._search_regex(
                r'data-vid-id=["\'](\d+)', webpage, 'video id',
                default=None) or self._search_regex(
                r',(\d+)\.html', url, 'video id', default=display_id)

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


code:
import re

from .common import InfoExtractor
from ..utils import (
    int_or_none,
    unescapeHTML,
    url_or_none,
)

class TVNetIE(InfoExtractor):
    _VALID_URL = r'https?://(?:[^/]+)\.tvnet\.gov\.vn/[^/]+/(?:\d+/)?(?P<id>\d+)(?:/|$)'
    _TESTS = [{
        # video
        'url': 'http://de.tvnet.gov.vn/video/109788/vtv1---bac-tuyet-tai-lao-cai-va-ha-giang/tin-nong-24h',
        'md5': 'b4d7abe0252c9b47774760b7519c7558',
        'info_dict': {
            'id': '109788',
            'ext': 'mp4',
            'title': 'VTV1 - Bắc tuyết tại Lào Cai và Hà Giang',
            'thumbnail': r're:(?i)https?://.*\.(?:jpg|png)',
            'is_live': False,
            'view_count': int,
        },
    }, {
        # audio
        'url': 'http://vn.tvnet.gov.vn/radio/27017/vov1---ban-tin-chieu-10062018/doi-song-va-xa-hoi',
        'md5': 'b5875ce9b0a2eecde029216d0e6db2ae',
        'info_dict': {
            'id': '27017',
            'ext': 'm4a',
            'title': 'VOV1 - Bản tin chiều (10/06/2018)',
            'thumbnail': r're:(?i)https?://.*\.(?:jpg|png)',
            'is_live': False,
        },
    }, {
        'url': 'http://us.tvnet.gov.vn/video/118023/129999/ngay-0705',
        'info_dict': {
            'id': '129999',
            'ext': 'mp4',
            'title': 'VTV1 - Quốc hội với cử tri (11/06/2018)',
            'thumbnail': r're:(?i)https?://.*\.(?:jpg|png)',
            'is_live': False,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # live stream
        'url': 'http://us.tvnet.gov.vn/kenh-truyen-hinh/1011/vtv1',
        'info_dict': {
            'id': '1011',
            'ext': 'mp4',
            'title': r're:^VTV1 \| LiveTV [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'thumbnail': r're:(?i)https?://.*\.(?:jpg|png)',
            'is_live': True,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # radio live stream
        'url': 'http://vn.tvnet.gov.vn/kenh-truyen-hinh/1014',
        'info_dict': {
            'id': '1014',
            'ext': 'm4a',
            'title': r're:VOV1 \| LiveTV [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'thumbnail': r're:(?i)https?://.*\.(?:jpg|png)',
            'is_live': True,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://us.tvnet.gov.vn/phim/6136/25510/vtv3---ca-mot-doi-an-oan-tap-1-50/phim-truyen-hinh',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, video_id)

        title = self._og_search_title(
            webpage, default=None) or self._html_search_meta(
            'title', webpage, default=None) or self._search_regex(
            r'<title>([^<]+)<', webpage, 'title')
        title = re.sub(r'\s*-\s*TV Net\s*$', '', title)

        if '/video/' in url or '/radio/' in url:
            is_live = False
        elif '/kenh-truyen-hinh/' in url:
            is_live = True
        else:
            is_live = None

        data_file = unescapeHTML(self._search_regex(
            r'data-file=(["\'])(?P<url>(?:https?:)?//.+?)\1', webpage,
            'data file', group='url'))

        stream_urls = set()
        formats = []
        for stream in self._download_json(data_file, video_id):
            if not isinstance(stream, dict):
                continue
            stream_url = url_or_none(stream.get('url'))
            if stream_url in stream_urls or not stream_url:
                continue
            stream_urls.add(stream_url)
            formats.extend(self._extract_m3u8_formats(
                stream_url, video_id, 'mp4', live=is_live, m3u8_id='hls', fatal=False))
        self._sort_formats(formats)

        # better support for radio streams
        if title.startswith('VOV'):
            for f in formats:
                f.update({
                    'ext': 'm4a',
                    'vcodec': 'none',
                })

        thumbnail = self._og_search_thumbnail(
            webpage, default=None) or unescapeHTML(
            self._search_regex(
                r'data-image=(["\'])(?P<url>(?:https?:)?//.+?)\1', webpage,
                'thumbnail', default=None, group='url'))

        view_count = int_or_none(self._search_regex(
            r'(?s)<div[^>]+\bclass=["\'].*?view-count[^>]+>.*?(\d+).*?</div>',
            webpage, 'view count', default=None))

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


code:
from .common import InfoExtractor
from ..utils import (
    clean_html,
    get_element_by_class,
    js_to_json,
)

class TVNoeIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tvnoe\.cz/video/(?P<id>[0-9]+)'
    _TEST = {
        'url': 'http://www.tvnoe.cz/video/10362',
        'md5': 'aee983f279aab96ec45ab6e2abb3c2ca',
        'info_dict': {
            'id': '10362',
            'ext': 'mp4',
            'series': 'Noční univerzita',
            'title': 'prof. Tomáš Halík, Th.D. - Návrat náboženství a střet civilizací',
            'description': 'md5:f337bae384e1a531a52c55ebc50fff41',
        }
    }

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

        iframe_url = self._search_regex(
            r'<iframe[^>]+src="([^"]+)"', webpage, 'iframe URL')

        ifs_page = self._download_webpage(iframe_url, video_id)
        jwplayer_data = self._find_jwplayer_data(
            ifs_page, video_id, transform_source=js_to_json)
        info_dict = self._parse_jwplayer_data(
            jwplayer_data, video_id, require_title=False, base_url=iframe_url)

        info_dict.update({
            'id': video_id,
            'title': clean_html(get_element_by_class(
                'field-name-field-podnazev', webpage)),
            'description': clean_html(get_element_by_class(
                'field-name-body', webpage)),
            'series': clean_html(get_element_by_class('title', webpage))
        })

        return info_dict


code:
import re

from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    ExtractorError,
    get_element_by_id,
    int_or_none,
    parse_iso8601,
    parse_duration,
    str_or_none,
    try_get,
    update_url_query,
    urljoin,
)

class TVNowBaseIE(InfoExtractor):
    _VIDEO_FIELDS = (
        'id', 'title', 'free', 'geoblocked', 'articleLong', 'articleShort',
        'broadcastStartDate', 'isDrm', 'duration', 'season', 'episode',
        'manifest.dashclear', 'manifest.hlsclear', 'manifest.smoothclear',
        'format.title', 'format.defaultImage169Format', 'format.defaultImage169Logo')

    def _call_api(self, path, video_id, query):
        return self._download_json(
            'https://api.tvnow.de/v3/' + path, video_id, query=query)

    def _extract_video(self, info, display_id):
        video_id = compat_str(info['id'])
        title = info['title']

        paths = []
        for manifest_url in (info.get('manifest') or {}).values():
            if not manifest_url:
                continue
            manifest_url = update_url_query(manifest_url, {'filter': ''})
            path = self._search_regex(r'https?://[^/]+/(.+?)\.ism/', manifest_url, 'path')
            if path in paths:
                continue
            paths.append(path)

            def url_repl(proto, suffix):
                return re.sub(
                    r'(?:hls|dash|hss)([.-])', proto + r'\1', re.sub(
                        r'\.ism/(?:[^.]*\.(?:m3u8|mpd)|[Mm]anifest)',
                        '.ism/' + suffix, manifest_url))

            def make_urls(proto, suffix):
                urls = [url_repl(proto, suffix)]
                hd_url = urls[0].replace('/manifest/', '/ngvod/')
                if hd_url != urls[0]:
                    urls.append(hd_url)
                return urls

            for man_url in make_urls('dash', '.mpd'):
                formats = self._extract_mpd_formats(
                    man_url, video_id, mpd_id='dash', fatal=False)
            for man_url in make_urls('hss', 'Manifest'):
                formats.extend(self._extract_ism_formats(
                    man_url, video_id, ism_id='mss', fatal=False))
            for man_url in make_urls('hls', '.m3u8'):
                formats.extend(self._extract_m3u8_formats(
                    man_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls',
                    fatal=False))
            if formats:
                break
        else:
            if not self.get_param('allow_unplayable_formats') and info.get('isDrm'):
                raise ExtractorError(
                    'Video %s is DRM protected' % video_id, expected=True)
            if info.get('geoblocked'):
                raise self.raise_geo_restricted()
            if not info.get('free', True):
                raise ExtractorError(
                    'Video %s is not available for free' % video_id, expected=True)
        self._sort_formats(formats)

        description = info.get('articleLong') or info.get('articleShort')
        timestamp = parse_iso8601(info.get('broadcastStartDate'), ' ')
        duration = parse_duration(info.get('duration'))

        f = info.get('format', {})

        thumbnails = [{
            'url': 'https://aistvnow-a.akamaihd.net/tvnow/movie/%s' % video_id,
        }]
        thumbnail = f.get('defaultImage169Format') or f.get('defaultImage169Logo')
        if thumbnail:
            thumbnails.append({
                'url': thumbnail,
            })

        return {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'thumbnails': thumbnails,
            'timestamp': timestamp,
            'duration': duration,
            'series': f.get('title'),
            'season_number': int_or_none(info.get('season')),
            'episode_number': int_or_none(info.get('episode')),
            'episode': title,
            'formats': formats,
        }

class TVNowIE(TVNowBaseIE):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:www\.)?tvnow\.(?:de|at|ch)/(?P<station>[^/]+)/
                        (?P<show_id>[^/]+)/
                        (?!(?:list|jahr)(?:/|$))(?P<id>[^/?\#&]+)
                    '''

    @classmethod
    def suitable(cls, url):
        return (False if TVNowNewIE.suitable(url) or TVNowSeasonIE.suitable(url) or TVNowAnnualIE.suitable(url) or TVNowShowIE.suitable(url)
                else super(TVNowIE, cls).suitable(url))

    _TESTS = [{
        'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/der-neue-porsche-911-gt-3/player',
        'info_dict': {
            'id': '331082',
            'display_id': 'grip-das-motormagazin/der-neue-porsche-911-gt-3',
            'ext': 'mp4',
            'title': 'Der neue Porsche 911 GT 3',
            'description': 'md5:6143220c661f9b0aae73b245e5d898bb',
            'timestamp': 1495994400,
            'upload_date': '20170528',
            'duration': 5283,
            'series': 'GRIP - Das Motormagazin',
            'season_number': 14,
            'episode_number': 405,
            'episode': 'Der neue Porsche 911 GT 3',
        },
    }, {
        # rtl2
        'url': 'https://www.tvnow.de/rtl2/armes-deutschland/episode-0008/player',
        'only_matching': True,
    }, {
        # rtlnitro
        'url': 'https://www.tvnow.de/nitro/alarm-fuer-cobra-11-die-autobahnpolizei/auf-eigene-faust-pilot/player',
        'only_matching': True,
    }, {
        # superrtl
        'url': 'https://www.tvnow.de/superrtl/die-lustigsten-schlamassel-der-welt/u-a-ketchup-effekt/player',
        'only_matching': True,
    }, {
        # ntv
        'url': 'https://www.tvnow.de/ntv/startup-news/goetter-in-weiss/player',
        'only_matching': True,
    }, {
        # vox
        'url': 'https://www.tvnow.de/vox/auto-mobil/neues-vom-automobilmarkt-2017-11-19-17-00-00/player',
        'only_matching': True,
    }, {
        # rtlplus
        'url': 'https://www.tvnow.de/rtlplus/op-ruft-dr-bruckner/die-vernaehte-frau/player',
        'only_matching': True,
    }, {
        'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/der-neue-porsche-911-gt-3',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        display_id = '%s/%s' % mobj.group(2, 3)

        info = self._call_api(
            'movies/' + display_id, display_id, query={
                'fields': ','.join(self._VIDEO_FIELDS),
            })

        return self._extract_video(info, display_id)

class TVNowNewIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    (?P<base_url>https?://
                        (?:www\.)?tvnow\.(?:de|at|ch)/
                        (?:shows|serien))/
                        (?P<show>[^/]+)-\d+/
                        [^/]+/
                        episode-\d+-(?P<episode>[^/?$&]+)-(?P<id>\d+)
                    '''

    _TESTS = [{
        'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05/episode-405-der-neue-porsche-911-gt-3-331082',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        base_url = re.sub(r'(?:shows|serien)', '_', mobj.group('base_url'))
        show, episode = mobj.group('show', 'episode')
        return self.url_result(
            # Rewrite new URLs to the old format and use extraction via old API
            # at api.tvnow.de as a loophole for bypassing premium content checks
            '%s/%s/%s' % (base_url, show, episode),
            ie=TVNowIE.ie_key(), video_id=mobj.group('id'))

class TVNowFilmIE(TVNowBaseIE):
    _VALID_URL = r'''(?x)
                    (?P<base_url>https?://
                        (?:www\.)?tvnow\.(?:de|at|ch)/
                        (?:filme))/
                        (?P<title>[^/?$&]+)-(?P<id>\d+)
                    '''
    _TESTS = [{
        'url': 'https://www.tvnow.de/filme/lord-of-war-haendler-des-todes-7959',
        'info_dict': {
            'id': '1426690',
            'display_id': 'lord-of-war-haendler-des-todes',
            'ext': 'mp4',
            'title': 'Lord of War',
            'description': 'md5:5eda15c0d5b8cb70dac724c8a0ff89a9',
            'timestamp': 1550010000,
            'upload_date': '20190212',
            'duration': 7016,
        },
    }, {
        'url': 'https://www.tvnow.de/filme/the-machinist-12157',
        'info_dict': {
            'id': '328160',
            'display_id': 'the-machinist',
            'ext': 'mp4',
            'title': 'The Machinist',
            'description': 'md5:9a0e363fdd74b3a9e1cdd9e21d0ecc28',
            'timestamp': 1496469720,
            'upload_date': '20170603',
            'duration': 5836,
        },
    }, {
        'url': 'https://www.tvnow.de/filme/horst-schlaemmer-isch-kandidiere-17777',
        'only_matching': True,  # DRM protected
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        display_id = mobj.group('title')

        webpage = self._download_webpage(url, display_id, fatal=False)
        if not webpage:
            raise ExtractorError('Cannot download "%s"' % url, expected=True)

        json_text = get_element_by_id('now-web-state', webpage)
        if not json_text:
            raise ExtractorError('Cannot read video data', expected=True)

        json_data = self._parse_json(
            json_text,
            display_id,
            transform_source=lambda x: x.replace('&q;', '"'),
            fatal=False)
        if not json_data:
            raise ExtractorError('Cannot read video data', expected=True)

        player_key = next(
            (key for key in json_data.keys() if 'module/player' in key),
            None)
        page_key = next(
            (key for key in json_data.keys() if 'page/filme' in key),
            None)
        movie_id = try_get(
            json_data,
            [
                lambda x: x[player_key]['body']['id'],
                lambda x: x[page_key]['body']['modules'][0]['id'],
                lambda x: x[page_key]['body']['modules'][1]['id']],
            int)
        if not movie_id:
            raise ExtractorError('Cannot extract movie ID', expected=True)

        info = self._call_api(
            'movies/%d' % movie_id,
            display_id,
            query={'fields': ','.join(self._VIDEO_FIELDS)})

        return self._extract_video(info, display_id)

class TVNowNewBaseIE(InfoExtractor):
    def _call_api(self, path, video_id, query={}):
        result = self._download_json(
            'https://apigw.tvnow.de/module/' + path, video_id, query=query)
        error = result.get('error')
        if error:
            raise ExtractorError(
                '%s said: %s' % (self.IE_NAME, error), expected=True)
        return result

r"""
TODO: new apigw.tvnow.de based version of TVNowIE. Replace old TVNowIE with it
when api.tvnow.de is shut down. This version can't bypass premium checks though.
class TVNowIE(TVNowNewBaseIE):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:www\.)?tvnow\.(?:de|at|ch)/
                        (?:shows|serien)/[^/]+/
                        (?:[^/]+/)+
                        (?P<display_id>[^/?$&]+)-(?P<id>\d+)
                    '''

    _TESTS = [{
        # episode with annual navigation
        'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05/episode-405-der-neue-porsche-911-gt-3-331082',
        'info_dict': {
            'id': '331082',
            'display_id': 'grip-das-motormagazin/der-neue-porsche-911-gt-3',
            'ext': 'mp4',
            'title': 'Der neue Porsche 911 GT 3',
            'description': 'md5:6143220c661f9b0aae73b245e5d898bb',
            'thumbnail': r're:^https?://.*\.jpg$',
            'timestamp': 1495994400,
            'upload_date': '20170528',
            'duration': 5283,
            'series': 'GRIP - Das Motormagazin',
            'season_number': 14,
            'episode_number': 405,
            'episode': 'Der neue Porsche 911 GT 3',
        },
    }, {
        # rtl2, episode with season navigation
        'url': 'https://www.tvnow.de/shows/armes-deutschland-11471/staffel-3/episode-14-bernd-steht-seit-der-trennung-von-seiner-frau-allein-da-526124',
        'only_matching': True,
    }, {
        # rtlnitro
        'url': 'https://www.tvnow.de/serien/alarm-fuer-cobra-11-die-autobahnpolizei-1815/staffel-13/episode-5-auf-eigene-faust-pilot-366822',
        'only_matching': True,
    }, {
        # superrtl
        'url': 'https://www.tvnow.de/shows/die-lustigsten-schlamassel-der-welt-1221/staffel-2/episode-14-u-a-ketchup-effekt-364120',
        'only_matching': True,
    }, {
        # ntv
        'url': 'https://www.tvnow.de/shows/startup-news-10674/staffel-2/episode-39-goetter-in-weiss-387630',
        'only_matching': True,
    }, {
        # vox
        'url': 'https://www.tvnow.de/shows/auto-mobil-174/2017-11/episode-46-neues-vom-automobilmarkt-2017-11-19-17-00-00-380072',
        'only_matching': True,
    }, {
        'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05/episode-405-der-neue-porsche-911-gt-3-331082',
        'only_matching': True,
    }]

    def _extract_video(self, info, url, display_id):
        config = info['config']
        source = config['source']

        video_id = compat_str(info.get('id') or source['videoId'])
        title = source['title'].strip()

        paths = []
        for manifest_url in (info.get('manifest') or {}).values():
            if not manifest_url:
                continue
            manifest_url = update_url_query(manifest_url, {'filter': ''})
            path = self._search_regex(r'https?://[^/]+/(.+?)\.ism/', manifest_url, 'path')
            if path in paths:
                continue
            paths.append(path)

            def url_repl(proto, suffix):
                return re.sub(
                    r'(?:hls|dash|hss)([.-])', proto + r'\1', re.sub(
                        r'\.ism/(?:[^.]*\.(?:m3u8|mpd)|[Mm]anifest)',
                        '.ism/' + suffix, manifest_url))

            formats = self._extract_mpd_formats(
                url_repl('dash', '.mpd'), video_id,
                mpd_id='dash', fatal=False)
            formats.extend(self._extract_ism_formats(
                url_repl('hss', 'Manifest'),
                video_id, ism_id='mss', fatal=False))
            formats.extend(self._extract_m3u8_formats(
                url_repl('hls', '.m3u8'), video_id, 'mp4',
                'm3u8_native', m3u8_id='hls', fatal=False))
            if formats:
                break
        else:
            if try_get(info, lambda x: x['rights']['isDrm']):
                raise ExtractorError(
                    'Video %s is DRM protected' % video_id, expected=True)
            if try_get(config, lambda x: x['boards']['geoBlocking']['block']):
                raise self.raise_geo_restricted()
            if not info.get('free', True):
                raise ExtractorError(
                    'Video %s is not available for free' % video_id, expected=True)
        self._sort_formats(formats)

        description = source.get('description')
        thumbnail = url_or_none(source.get('poster'))
        timestamp = unified_timestamp(source.get('previewStart'))
        duration = parse_duration(source.get('length'))

        series = source.get('format')
        season_number = int_or_none(self._search_regex(
            r'staffel-(\d+)', url, 'season number', default=None))
        episode_number = int_or_none(self._search_regex(
            r'episode-(\d+)', url, 'episode number', default=None))

        return {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'timestamp': timestamp,
            'duration': duration,
            'series': series,
            'season_number': season_number,
            'episode_number': episode_number,
            'episode': title,
            'formats': formats,
        }

    def _real_extract(self, url):
        display_id, video_id = self._match_valid_url(url).groups()
        info = self._call_api('player/' + video_id, video_id)
        return self._extract_video(info, video_id, display_id)

class TVNowFilmIE(TVNowIE):
    _VALID_URL = r'''(?x)
                    (?P<base_url>https?://
                        (?:www\.)?tvnow\.(?:de|at|ch)/
                        (?:filme))/
                        (?P<title>[^/?$&]+)-(?P<id>\d+)
                    '''
    _TESTS = [{
        'url': 'https://www.tvnow.de/filme/lord-of-war-haendler-des-todes-7959',
        'info_dict': {
            'id': '1426690',
            'display_id': 'lord-of-war-haendler-des-todes',
            'ext': 'mp4',
            'title': 'Lord of War',
            'description': 'md5:5eda15c0d5b8cb70dac724c8a0ff89a9',
            'timestamp': 1550010000,
            'upload_date': '20190212',
            'duration': 7016,
        },
    }, {
        'url': 'https://www.tvnow.de/filme/the-machinist-12157',
        'info_dict': {
            'id': '328160',
            'display_id': 'the-machinist',
            'ext': 'mp4',
            'title': 'The Machinist',
            'description': 'md5:9a0e363fdd74b3a9e1cdd9e21d0ecc28',
            'timestamp': 1496469720,
            'upload_date': '20170603',
            'duration': 5836,
        },
    }, {
        'url': 'https://www.tvnow.de/filme/horst-schlaemmer-isch-kandidiere-17777',
        'only_matching': True,  # DRM protected
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        display_id = mobj.group('title')

        webpage = self._download_webpage(url, display_id, fatal=False)
        if not webpage:
            raise ExtractorError('Cannot download "%s"' % url, expected=True)

        json_text = get_element_by_id('now-web-state', webpage)
        if not json_text:
            raise ExtractorError('Cannot read video data', expected=True)

        json_data = self._parse_json(
            json_text,
            display_id,
            transform_source=lambda x: x.replace('&q;', '"'),
            fatal=False)
        if not json_data:
            raise ExtractorError('Cannot read video data', expected=True)

        player_key = next(
            (key for key in json_data.keys() if 'module/player' in key),
            None)
        page_key = next(
            (key for key in json_data.keys() if 'page/filme' in key),
            None)
        movie_id = try_get(
            json_data,
            [
                lambda x: x[player_key]['body']['id'],
                lambda x: x[page_key]['body']['modules'][0]['id'],
                lambda x: x[page_key]['body']['modules'][1]['id']],
            int)
        if not movie_id:
            raise ExtractorError('Cannot extract movie ID', expected=True)

        info = self._call_api('player/%d' % movie_id, display_id)
        return self._extract_video(info, url, display_id)
"""

class TVNowListBaseIE(TVNowNewBaseIE):
    _SHOW_VALID_URL = r'''(?x)
                    (?P<base_url>
                        https?://
                            (?:www\.)?tvnow\.(?:de|at|ch)/(?:shows|serien)/
                            [^/?#&]+-(?P<show_id>\d+)
                    )
                    '''

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

    def _extract_items(self, url, show_id, list_id, query):
        items = self._call_api(
            'teaserrow/format/episode/' + show_id, list_id,
            query=query)['items']

        entries = []
        for item in items:
            if not isinstance(item, dict):
                continue
            item_url = urljoin(url, item.get('url'))
            if not item_url:
                continue
            video_id = str_or_none(item.get('id') or item.get('videoId'))
            item_title = item.get('subheadline') or item.get('text')
            entries.append(self.url_result(
                item_url, ie=TVNowNewIE.ie_key(), video_id=video_id,
                video_title=item_title))

        return self.playlist_result(entries, '%s/%s' % (show_id, list_id))

class TVNowSeasonIE(TVNowListBaseIE):
    _VALID_URL = r'%s/staffel-(?P<id>\d+)' % TVNowListBaseIE._SHOW_VALID_URL
    _TESTS = [{
        'url': 'https://www.tvnow.de/serien/alarm-fuer-cobra-11-die-autobahnpolizei-1815/staffel-13',
        'info_dict': {
            'id': '1815/13',
        },
        'playlist_mincount': 22,
    }]

    def _real_extract(self, url):
        _, show_id, season_id = self._match_valid_url(url).groups()
        return self._extract_items(
            url, show_id, season_id, {'season': season_id})

class TVNowAnnualIE(TVNowListBaseIE):
    _VALID_URL = r'%s/(?P<year>\d{4})-(?P<month>\d{2})' % TVNowListBaseIE._SHOW_VALID_URL
    _TESTS = [{
        'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05',
        'info_dict': {
            'id': '1669/2017-05',
        },
        'playlist_mincount': 2,
    }]

    def _real_extract(self, url):
        _, show_id, year, month = self._match_valid_url(url).groups()
        return self._extract_items(
            url, show_id, '%s-%s' % (year, month), {
                'year': int(year),
                'month': int(month),
            })

class TVNowShowIE(TVNowListBaseIE):
    _VALID_URL = TVNowListBaseIE._SHOW_VALID_URL
    _TESTS = [{
        # annual navigationType
        'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669',
        'info_dict': {
            'id': '1669',
        },
        'playlist_mincount': 73,
    }, {
        # season navigationType
        'url': 'https://www.tvnow.de/shows/armes-deutschland-11471',
        'info_dict': {
            'id': '11471',
        },
        'playlist_mincount': 3,
    }]

    @classmethod
    def suitable(cls, url):
        return (False if TVNowNewIE.suitable(url) or TVNowSeasonIE.suitable(url) or TVNowAnnualIE.suitable(url)
                else super(TVNowShowIE, cls).suitable(url))

    def _real_extract(self, url):
        base_url, show_id = self._match_valid_url(url).groups()

        result = self._call_api(
            'teaserrow/format/navigation/' + show_id, show_id)

        items = result['items']

        entries = []
        navigation = result.get('navigationType')
        if navigation == 'annual':
            for item in items:
                if not isinstance(item, dict):
                    continue
                year = int_or_none(item.get('year'))
                if year is None:
                    continue
                months = item.get('months')
                if not isinstance(months, list):
                    continue
                for month_dict in months:
                    if not isinstance(month_dict, dict) or not month_dict:
                        continue
                    month_number = int_or_none(list(month_dict.keys())[0])
                    if month_number is None:
                        continue
                    entries.append(self.url_result(
                        '%s/%04d-%02d' % (base_url, year, month_number),
                        ie=TVNowAnnualIE.ie_key()))
        elif navigation == 'season':
            for item in items:
                if not isinstance(item, dict):
                    continue
                season_number = int_or_none(item.get('season'))
                if season_number is None:
                    continue
                entries.append(self.url_result(
                    '%s/staffel-%d' % (base_url, season_number),
                    ie=TVNowSeasonIE.ie_key()))
        else:
            raise ExtractorError('Unknown navigationType')

        return self.playlist_result(entries, show_id)


code:
from .common import InfoExtractor
from ..utils import (
    determine_ext,
    get_elements_text_and_html_by_attribute,
    scale_thumbnails_to_max_format_width,
)

class TVOpenGrBaseIE(InfoExtractor):
    def _return_canonical_url(self, url, video_id):
        webpage = self._download_webpage(url, video_id)
        canonical_url = self._og_search_url(webpage)
        title = self._og_search_title(webpage)
        return self.url_result(canonical_url, ie=TVOpenGrWatchIE.ie_key(), video_id=video_id, video_title=title)

class TVOpenGrWatchIE(TVOpenGrBaseIE):
    IE_NAME = 'tvopengr:watch'
    IE_DESC = 'tvopen.gr (and ethnos.gr) videos'
    _VALID_URL = r'https?://(?P<netloc>(?:www\.)?(?:tvopen|ethnos)\.gr)/watch/(?P<id>\d+)/(?P<slug>[^/]+)'
    _API_ENDPOINT = 'https://www.tvopen.gr/templates/data/player'

    _TESTS = [{
        'url': 'https://www.ethnos.gr/watch/101009/nikoskaprabelosdenexoymekanenanasthenhsemethmethmetallaxhomikron',
        'md5': '8728570e3a72e0f8d9475ba94859fdc1',
        'info_dict': {
            'id': '101009',
            'title': 'md5:51f68773dcb6c70498cd326f45fefdf0',
            'display_id': 'nikoskaprabelosdenexoymekanenanasthenhsemethmethmetallaxhomikron',
            'description': 'md5:78fff49f18fb3effe41b070e5c7685d6',
            'thumbnail': 'https://opentv-static.siliconweb.com/imgHandler/1920/d573ba71-ec5f-43c6-b4cb-d181f327d3a8.jpg',
            'ext': 'mp4',
            'upload_date': '20220109',
            'timestamp': 1641686400,
        },
    }, {
        'url': 'https://www.tvopen.gr/watch/100979/se28099agapaomenalla7cepeisodio267cmhthrargiapashskakias',
        'md5': '38f98a1be0c577db4ea2d1b1c0770c48',
        'info_dict': {
            'id': '100979',
            'title': 'md5:e021f3001e16088ee40fa79b20df305b',
            'display_id': 'se28099agapaomenalla7cepeisodio267cmhthrargiapashskakias',
            'description': 'md5:ba17db53954134eb8d625d199e2919fb',
            'thumbnail': 'https://opentv-static.siliconweb.com/imgHandler/1920/9bb71cf1-21da-43a9-9d65-367950fde4e3.jpg',
            'ext': 'mp4',
            'upload_date': '20220108',
            'timestamp': 1641600000,
        },
    }]

    def _extract_formats_and_subs(self, response, video_id):
        formats, subs = [], {}
        for format_id, format_url in response.items():
            if format_id not in ('stream', 'httpstream', 'mpegdash'):
                continue
            ext = determine_ext(format_url)
            if ext == 'm3u8':
                formats_, subs_ = self._extract_m3u8_formats_and_subtitles(
                    format_url, video_id, 'mp4', m3u8_id=format_id,
                    fatal=False)
            elif ext == 'mpd':
                formats_, subs_ = self._extract_mpd_formats_and_subtitles(
                    format_url, video_id, 'mp4', fatal=False)
            else:
                formats.append({
                    'url': format_url,
                    'format_id': format_id,
                })
                continue
            formats.extend(formats_)
            self._merge_subtitles(subs_, target=subs)
        self._sort_formats(formats)
        return formats, subs

    def _real_extract(self, url):
        netloc, video_id, display_id = self._match_valid_url(url).group('netloc', 'id', 'slug')
        if netloc.find('tvopen.gr') == -1:
            return self._return_canonical_url(url, video_id)
        webpage = self._download_webpage(url, video_id)
        info = self._search_json_ld(webpage, video_id, expected_type='VideoObject')
        info['formats'], info['subtitles'] = self._extract_formats_and_subs(
            self._download_json(self._API_ENDPOINT, video_id, query={'cid': video_id}),
            video_id)
        info['thumbnails'] = scale_thumbnails_to_max_format_width(
            info['formats'], info['thumbnails'], r'(?<=/imgHandler/)\d+')
        description, _html = next(get_elements_text_and_html_by_attribute('class', 'description', webpage))
        if description and _html.startswith('<span '):
            info['description'] = description
        info['id'] = video_id
        info['display_id'] = display_id
        return info

class TVOpenGrEmbedIE(TVOpenGrBaseIE):
    IE_NAME = 'tvopengr:embed'
    IE_DESC = 'tvopen.gr embedded videos'
    _VALID_URL = r'(?:https?:)?//(?:www\.|cdn\.|)(?:tvopen|ethnos).gr/embed/(?P<id>\d+)'
    _EMBED_REGEX = [rf'''<iframe[^>]+?src=(?P<_q1>["'])(?P<url>{_VALID_URL})(?P=_q1)''']

    _TESTS = [{
        'url': 'https://cdn.ethnos.gr/embed/100963',
        'md5': '2da147881f45571d81662d94d086628b',
        'info_dict': {
            'id': '100963',
            'display_id': 'koronoiosapotoysdieythyntestonsxoleionselftestgiaosoysdenbrhkan',
            'title': 'md5:2c71876fadf0cda6043da0da5fca2936',
            'description': 'md5:17482b4432e5ed30eccd93b05d6ea509',
            'thumbnail': 'https://opentv-static.siliconweb.com/imgHandler/1920/5804e07f-799a-4247-a696-33842c94ca37.jpg',
            'ext': 'mp4',
            'upload_date': '20220108',
            'timestamp': 1641600000,
        },
    }]

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