code:
import functools
import random
import re
import string
import time

from .common import InfoExtractor
from ..aes import aes_cbc_encrypt_bytes
from ..utils import (
    ExtractorError,
    determine_ext,
    int_or_none,
    js_to_json,
    traverse_obj,
    urljoin,
)

class TencentBaseIE(InfoExtractor):
    """Subclasses must set _API_URL, _APP_VERSION, _PLATFORM, _HOST, _REFERER"""

    def _get_ckey(self, video_id, url, guid):
        ua = self.get_param('http_headers')['User-Agent']

        payload = (f'{video_id}|{int(time.time())}|mg3c3b04ba|{self._APP_VERSION}|{guid}|'
                   f'{self._PLATFORM}|{url[:48]}|{ua.lower()[:48]}||Mozilla|Netscape|Windows x86_64|00|')

        return aes_cbc_encrypt_bytes(
            bytes(f'|{sum(map(ord, payload))}|{payload}', 'utf-8'),
            b'Ok\xda\xa3\x9e/\x8c\xb0\x7f^r-\x9e\xde\xf3\x14',
            b'\x01PJ\xf3V\xe6\x19\xcf.B\xbb\xa6\x8c?p\xf9',
            padding_mode='whitespace').hex().upper()

    def _get_video_api_response(self, video_url, video_id, series_id, subtitle_format, video_format, video_quality):
        guid = ''.join([random.choice(string.digits + string.ascii_lowercase) for _ in range(16)])
        ckey = self._get_ckey(video_id, video_url, guid)
        query = {
            'vid': video_id,
            'cid': series_id,
            'cKey': ckey,
            'encryptVer': '8.1',
            'spcaptiontype': '1' if subtitle_format == 'vtt' else '0',
            'sphls': '2' if video_format == 'hls' else '0',
            'dtype': '3' if video_format == 'hls' else '0',
            'defn': video_quality,
            'spsrt': '2',  # Enable subtitles
            'sphttps': '1',  # Enable HTTPS
            'otype': 'json',
            'spwm': '1',
            # For SHD
            'host': self._HOST,
            'referer': self._REFERER,
            'ehost': video_url,
            'appVer': self._APP_VERSION,
            'platform': self._PLATFORM,
            # For VQQ
            'guid': guid,
            'flowid': ''.join(random.choice(string.digits + string.ascii_lowercase) for _ in range(32)),
        }

        return self._search_json(r'QZOutputJson=', self._download_webpage(
            self._API_URL, video_id, query=query), 'api_response', video_id)

    def _extract_video_formats_and_subtitles(self, api_response, video_id):
        video_response = api_response['vl']['vi'][0]
        video_width, video_height = video_response.get('vw'), video_response.get('vh')

        formats, subtitles = [], {}
        for video_format in video_response['ul']['ui']:
            if video_format.get('hls'):
                fmts, subs = self._extract_m3u8_formats_and_subtitles(
                    video_format['url'] + video_format['hls']['pt'], video_id, 'mp4', fatal=False)
                for f in fmts:
                    f.update({'width': video_width, 'height': video_height})

                formats.extend(fmts)
                self._merge_subtitles(subs, target=subtitles)
            else:
                formats.append({
                    'url': f'{video_format["url"]}{video_response["fn"]}?vkey={video_response["fvkey"]}',
                    'width': video_width,
                    'height': video_height,
                    'ext': 'mp4',
                })

        return formats, subtitles

    def _extract_video_native_subtitles(self, api_response, subtitles_format):
        subtitles = {}
        for subtitle in traverse_obj(api_response, ('sfl', 'fi')) or ():
            subtitles.setdefault(subtitle['lang'].lower(), []).append({
                'url': subtitle['url'],
                'ext': subtitles_format,
                'protocol': 'm3u8_native' if determine_ext(subtitle['url']) == 'm3u8' else 'http',
            })

        return subtitles

    def _extract_all_video_formats_and_subtitles(self, url, video_id, series_id):
        formats, subtitles = [], {}
        for video_format, subtitle_format, video_quality in (
                # '': 480p, 'shd': 720p, 'fhd': 1080p
                ('mp4', 'srt', ''), ('hls', 'vtt', 'shd'), ('hls', 'vtt', 'fhd')):
            api_response = self._get_video_api_response(
                url, video_id, series_id, subtitle_format, video_format, video_quality)

            if api_response.get('em') != 0 and api_response.get('exem') != 0:
                if '您所在区域暂无此内容版权' in api_response.get('msg'):
                    self.raise_geo_restricted()
                raise ExtractorError(f'Tencent said: {api_response.get("msg")}')

            fmts, subs = self._extract_video_formats_and_subtitles(api_response, video_id)
            native_subtitles = self._extract_video_native_subtitles(api_response, subtitle_format)

            formats.extend(fmts)
            self._merge_subtitles(subs, native_subtitles, target=subtitles)

        self._sort_formats(formats)
        return formats, subtitles

    def _get_clean_title(self, title):
        return re.sub(
            r'\s*[_\-]\s*(?:Watch online|腾讯视频|(?:高清)?1080P在线观看平台).*?$',
            '', title or '').strip() or None

class VQQBaseIE(TencentBaseIE):
    _VALID_URL_BASE = r'https?://v\.qq\.com'

    _API_URL = 'https://h5vv6.video.qq.com/getvinfo'
    _APP_VERSION = '3.5.57'
    _PLATFORM = '10901'
    _HOST = 'v.qq.com'
    _REFERER = 'v.qq.com'

    def _get_webpage_metadata(self, webpage, video_id):
        return self._parse_json(
            self._search_regex(
                r'(?s)<script[^>]*>[^<]*window\.__pinia\s*=\s*([^<]+)</script>',
                webpage, 'pinia data', fatal=False),
            video_id, transform_source=js_to_json, fatal=False)

class VQQVideoIE(VQQBaseIE):
    IE_NAME = 'vqq:video'
    _VALID_URL = VQQBaseIE._VALID_URL_BASE + r'/x/(?:page|cover/(?P<series_id>\w+))/(?P<id>\w+)'

    _TESTS = [{
        'url': 'https://v.qq.com/x/page/q326831cny0.html',
        'md5': '826ef93682df09e3deac4a6e6e8cdb6e',
        'info_dict': {
            'id': 'q326831cny0',
            'ext': 'mp4',
            'title': '我是选手：雷霆裂阵，终极时刻',
            'description': 'md5:e7ed70be89244017dac2a835a10aeb1e',
            'thumbnail': r're:^https?://[^?#]+q326831cny0',
        },
    }, {
        'url': 'https://v.qq.com/x/page/o3013za7cse.html',
        'md5': 'b91cbbeada22ef8cc4b06df53e36fa21',
        'info_dict': {
            'id': 'o3013za7cse',
            'ext': 'mp4',
            'title': '欧阳娜娜VLOG',
            'description': 'md5:29fe847497a98e04a8c3826e499edd2e',
            'thumbnail': r're:^https?://[^?#]+o3013za7cse',
        },
    }, {
        'url': 'https://v.qq.com/x/cover/7ce5noezvafma27/a00269ix3l8.html',
        'md5': '71459c5375c617c265a22f083facce67',
        'info_dict': {
            'id': 'a00269ix3l8',
            'ext': 'mp4',
            'title': '鸡毛飞上天 第01集',
            'description': 'md5:8cae3534327315b3872fbef5e51b5c5b',
            'thumbnail': r're:^https?://[^?#]+7ce5noezvafma27',
            'series': '鸡毛飞上天',
        },
    }, {
        'url': 'https://v.qq.com/x/cover/mzc00200p29k31e/s0043cwsgj0.html',
        'md5': '96b9fd4a189fdd4078c111f21d7ac1bc',
        'info_dict': {
            'id': 's0043cwsgj0',
            'ext': 'mp4',
            'title': '第1集：如何快乐吃糖？',
            'description': 'md5:1d8c3a0b8729ae3827fa5b2d3ebd5213',
            'thumbnail': r're:^https?://[^?#]+s0043cwsgj0',
            'series': '青年理工工作者生活研究所',
        },
    }]

    def _real_extract(self, url):
        video_id, series_id = self._match_valid_url(url).group('id', 'series_id')
        webpage = self._download_webpage(url, video_id)
        webpage_metadata = self._get_webpage_metadata(webpage, video_id)

        formats, subtitles = self._extract_all_video_formats_and_subtitles(url, video_id, series_id)
        return {
            'id': video_id,
            'title': self._get_clean_title(self._og_search_title(webpage)
                                           or traverse_obj(webpage_metadata, ('global', 'videoInfo', 'title'))),
            'description': (self._og_search_description(webpage)
                            or traverse_obj(webpage_metadata, ('global', 'videoInfo', 'desc'))),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnail': (self._og_search_thumbnail(webpage)
                          or traverse_obj(webpage_metadata, ('global', 'videoInfo', 'pic160x90'))),
            'series': traverse_obj(webpage_metadata, ('global', 'coverInfo', 'title')),
        }

class VQQSeriesIE(VQQBaseIE):
    IE_NAME = 'vqq:series'
    _VALID_URL = VQQBaseIE._VALID_URL_BASE + r'/x/cover/(?P<id>\w+)\.html/?(?:[?#]|$)'

    _TESTS = [{
        'url': 'https://v.qq.com/x/cover/7ce5noezvafma27.html',
        'info_dict': {
            'id': '7ce5noezvafma27',
            'title': '鸡毛飞上天',
            'description': 'md5:8cae3534327315b3872fbef5e51b5c5b',
        },
        'playlist_count': 55,
    }, {
        'url': 'https://v.qq.com/x/cover/oshd7r0vy9sfq8e.html',
        'info_dict': {
            'id': 'oshd7r0vy9sfq8e',
            'title': '恋爱细胞2',
            'description': 'md5:9d8a2245679f71ca828534b0f95d2a03',
        },
        'playlist_count': 12,
    }]

    def _real_extract(self, url):
        series_id = self._match_id(url)
        webpage = self._download_webpage(url, series_id)
        webpage_metadata = self._get_webpage_metadata(webpage, series_id)

        episode_paths = [f'/x/cover/{series_id}/{video_id}.html' for video_id in re.findall(
            r'<div[^>]+data-vid="(?P<video_id>[^"]+)"[^>]+class="[^"]+episode-item-rect--number',
            webpage)]

        return self.playlist_from_matches(
            episode_paths, series_id, ie=VQQVideoIE, getter=functools.partial(urljoin, url),
            title=self._get_clean_title(traverse_obj(webpage_metadata, ('coverInfo', 'title'))
                                        or self._og_search_title(webpage)),
            description=(traverse_obj(webpage_metadata, ('coverInfo', 'description'))
                         or self._og_search_description(webpage)))

class WeTvBaseIE(TencentBaseIE):
    _VALID_URL_BASE = r'https?://(?:www\.)?wetv\.vip/(?:[^?#]+/)?play'

    _API_URL = 'https://play.wetv.vip/getvinfo'
    _APP_VERSION = '3.5.57'
    _PLATFORM = '4830201'
    _HOST = 'wetv.vip'
    _REFERER = 'wetv.vip'

    def _get_webpage_metadata(self, webpage, video_id):
        return self._parse_json(
            traverse_obj(self._search_nextjs_data(webpage, video_id), ('props', 'pageProps', 'data')),
            video_id, fatal=False)

class WeTvEpisodeIE(WeTvBaseIE):
    IE_NAME = 'wetv:episode'
    _VALID_URL = WeTvBaseIE._VALID_URL_BASE + r'/(?P<series_id>\w+)(?:-[^?#]+)?/(?P<id>\w+)(?:-[^?#]+)?'

    _TESTS = [{
        'url': 'https://wetv.vip/en/play/air11ooo2rdsdi3-Cute-Programmer/v0040pr89t9-EP1-Cute-Programmer',
        'md5': '0c70fdfaa5011ab022eebc598e64bbbe',
        'info_dict': {
            'id': 'v0040pr89t9',
            'ext': 'mp4',
            'title': 'EP1: Cute Programmer',
            'description': 'md5:e87beab3bf9f392d6b9e541a63286343',
            'thumbnail': r're:^https?://[^?#]+air11ooo2rdsdi3',
            'series': 'Cute Programmer',
            'episode': 'Episode 1',
            'episode_number': 1,
            'duration': 2835,
        },
    }, {
        'url': 'https://wetv.vip/en/play/u37kgfnfzs73kiu/p0039b9nvik',
        'md5': '3b3c15ca4b9a158d8d28d5aa9d7c0a49',
        'info_dict': {
            'id': 'p0039b9nvik',
            'ext': 'mp4',
            'title': 'EP1: You Are My Glory',
            'description': 'md5:831363a4c3b4d7615e1f3854be3a123b',
            'thumbnail': r're:^https?://[^?#]+u37kgfnfzs73kiu',
            'series': 'You Are My Glory',
            'episode': 'Episode 1',
            'episode_number': 1,
            'duration': 2454,
        },
    }, {
        'url': 'https://wetv.vip/en/play/lcxgwod5hapghvw-WeTV-PICK-A-BOO/i0042y00lxp-Zhao-Lusi-Describes-The-First-Experiences-She-Had-In-Who-Rules-The-World-%7C-WeTV-PICK-A-BOO',
        'md5': '71133f5c2d5d6cad3427e1b010488280',
        'info_dict': {
            'id': 'i0042y00lxp',
            'ext': 'mp4',
            'title': 'md5:f7a0857dbe5fbbe2e7ad630b92b54e6a',
            'description': 'md5:76260cb9cdc0ef76826d7ca9d92fadfa',
            'thumbnail': r're:^https?://[^?#]+lcxgwod5hapghvw',
            'series': 'WeTV PICK-A-BOO',
            'episode': 'Episode 0',
            'episode_number': 0,
            'duration': 442,
        },
    }]

    def _real_extract(self, url):
        video_id, series_id = self._match_valid_url(url).group('id', 'series_id')
        webpage = self._download_webpage(url, video_id)
        webpage_metadata = self._get_webpage_metadata(webpage, video_id)

        formats, subtitles = self._extract_all_video_formats_and_subtitles(url, video_id, series_id)
        return {
            'id': video_id,
            'title': self._get_clean_title(self._og_search_title(webpage)
                                           or traverse_obj(webpage_metadata, ('coverInfo', 'title'))),
            'description': (traverse_obj(webpage_metadata, ('coverInfo', 'description'))
                            or self._og_search_description(webpage)),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnail': self._og_search_thumbnail(webpage),
            'duration': int_or_none(traverse_obj(webpage_metadata, ('videoInfo', 'duration'))),
            'series': traverse_obj(webpage_metadata, ('coverInfo', 'title')),
            'episode_number': int_or_none(traverse_obj(webpage_metadata, ('videoInfo', 'episode'))),
        }

class WeTvSeriesIE(WeTvBaseIE):
    _VALID_URL = WeTvBaseIE._VALID_URL_BASE + r'/(?P<id>\w+)(?:-[^/?#]+)?/?(?:[?#]|$)'

    _TESTS = [{
        'url': 'https://wetv.vip/play/air11ooo2rdsdi3-Cute-Programmer',
        'info_dict': {
            'id': 'air11ooo2rdsdi3',
            'title': 'Cute Programmer',
            'description': 'md5:e87beab3bf9f392d6b9e541a63286343',
        },
        'playlist_count': 30,
    }, {
        'url': 'https://wetv.vip/en/play/u37kgfnfzs73kiu-You-Are-My-Glory',
        'info_dict': {
            'id': 'u37kgfnfzs73kiu',
            'title': 'You Are My Glory',
            'description': 'md5:831363a4c3b4d7615e1f3854be3a123b',
        },
        'playlist_count': 32,
    }]

    def _real_extract(self, url):
        series_id = self._match_id(url)
        webpage = self._download_webpage(url, series_id)
        webpage_metadata = self._get_webpage_metadata(webpage, series_id)

        episode_paths = ([f'/play/{series_id}/{episode["vid"]}' for episode in webpage_metadata.get('videoList')]
                         or re.findall(r'<a[^>]+class="play-video__link"[^>]+href="(?P<path>[^"]+)', webpage))

        return self.playlist_from_matches(
            episode_paths, series_id, ie=WeTvEpisodeIE, getter=functools.partial(urljoin, url),
            title=self._get_clean_title(traverse_obj(webpage_metadata, ('coverInfo', 'title'))
                                        or self._og_search_title(webpage)),
            description=(traverse_obj(webpage_metadata, ('coverInfo', 'description'))
                         or self._og_search_description(webpage)))


code:
import urllib.parse

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    random_uuidv4,
    unified_timestamp,
    urlencode_postdata,
)

class TennisTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tennistv\.com/videos/(?P<id>[-a-z0-9]+)'
    _TESTS = [{
        'url': 'https://www.tennistv.com/videos/indian-wells-2018-verdasco-fritz',
        'info_dict': {
            'id': 'indian-wells-2018-verdasco-fritz',
            'ext': 'mp4',
            'title': 'Fernando Verdasco v Taylor Fritz',
            'description': 're:^After his stunning victory.{174}$',
            'thumbnail': 'https://atp-prod.akamaized.net/api/images/v1/images/112831/landscape/1242/0',
            'timestamp': 1521017381,
            'upload_date': '20180314',
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'Requires email and password of a subscribed account',
    }, {
        'url': 'https://www.tennistv.com/videos/2650480/best-matches-of-2022-part-5',
        'info_dict': {
            'id': '2650480',
            'ext': 'mp4',
            'title': 'Best Matches of 2022 - Part 5',
            'description': 'md5:36dec3bfae7ed74bd79e48045b17264c',
            'thumbnail': 'https://open.http.mp.streamamg.com/p/3001482/sp/300148200/thumbnail/entry_id/0_myef18pd/version/100001/height/1920',
        },
        'params': {'skip_download': 'm3u8'},
        'skip': 'Requires email and password of a subscribed account',
    }]
    _NETRC_MACHINE = 'tennistv'

    access_token, refresh_token = None, None
    _PARTNER_ID = 3001482
    _FORMAT_URL = 'https://open.http.mp.streamamg.com/p/{partner}/sp/{partner}00/playManifest/entryId/{entry}/format/applehttp/protocol/https/a.m3u8?ks={session}'
    _AUTH_BASE_URL = 'https://sso.tennistv.com/auth/realms/TennisTV/protocol/openid-connect'
    _HEADERS = {
        'origin': 'https://www.tennistv.com',
        'referer': 'https://www.tennistv.com/',
        'content-Type': 'application/x-www-form-urlencoded'
    }

    def _perform_login(self, username, password):
        login_page = self._download_webpage(
            f'{self._AUTH_BASE_URL}/auth', None, 'Downloading login page',
            query={
                'client_id': 'tennis-tv-web',
                'redirect_uri': 'https://tennistv.com',
                'response_mode': 'fragment',
                'response_type': 'code',
                'scope': 'openid'
            })

        post_url = self._html_search_regex(r'action=["\']([^"\']+?)["\']\s+method=["\']post["\']', login_page, 'login POST url')
        temp_page = self._download_webpage(
            post_url, None, 'Sending login data', 'Unable to send login data',
            headers=self._HEADERS, data=urlencode_postdata({
                'username': username,
                'password': password,
                'submitAction': 'Log In'
            }))
        if 'Your username or password was incorrect' in temp_page:
            raise ExtractorError('Your username or password was incorrect', expected=True)

        handle = self._request_webpage(
            f'{self._AUTH_BASE_URL}/auth', None, 'Logging in', headers=self._HEADERS,
            query={
                'client_id': 'tennis-tv-web',
                'redirect_uri': 'https://www.tennistv.com/resources/v1.1.10/html/silent-check-sso.html',
                'state': random_uuidv4(),
                'response_mode': 'fragment',
                'response_type': 'code',
                'scope': 'openid',
                'nonce': random_uuidv4(),
                'prompt': 'none'
            })

        self.get_token(None, {
            'code': urllib.parse.parse_qs(handle.geturl())['code'][-1],
            'grant_type': 'authorization_code',
            'client_id': 'tennis-tv-web',
            'redirect_uri': 'https://www.tennistv.com/resources/v1.1.10/html/silent-check-sso.html'
        })

    def get_token(self, video_id, payload):
        res = self._download_json(
            f'{self._AUTH_BASE_URL}/token', video_id, 'Fetching tokens',
            'Unable to fetch tokens', headers=self._HEADERS, data=urlencode_postdata(payload))

        self.access_token = res.get('access_token') or self.access_token
        self.refresh_token = res.get('refresh_token') or self.refresh_token

    def _real_initialize(self):
        if self.access_token and self.refresh_token:
            return

        cookies = self._get_cookies('https://www.tennistv.com/')
        if not cookies.get('access_token') or not cookies.get('refresh_token'):
            self.raise_login_required()
        self.access_token, self.refresh_token = cookies['access_token'].value, cookies['refresh_token'].value

    def _download_session_json(self, video_id, entryid,):
        return self._download_json(
            f'https://atppayments.streamamg.com/api/v1/session/ksession/?lang=en&apijwttoken={self.access_token}&entryId={entryid}',
            video_id, 'Downloading ksession token', 'Failed to download ksession token', headers=self._HEADERS)

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

        entryid = self._search_regex(r'data-entry-id=["\']([^"\']+)', webpage, 'entryID')
        session_json = self._download_session_json(video_id, entryid)

        k_session = session_json.get('KSession')
        if k_session is None:
            self.get_token(video_id, {
                'grant_type': 'refresh_token',
                'refresh_token': self.refresh_token,
                'client_id': 'tennis-tv-web'
            })
            k_session = self._download_session_json(video_id, entryid).get('KSession')
            if k_session is None:
                raise ExtractorError('Failed to get KSession, possibly a premium video', expected=True)

        if session_json.get('ErrorMessage'):
            self.report_warning(session_json['ErrorMessage'])

        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
            self._FORMAT_URL.format(partner=self._PARTNER_ID, entry=entryid, session=k_session), video_id)

        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': self._html_extract_title(webpage) or self._og_search_title(webpage),
            'description': self._html_search_regex(
                (r'', *self._og_regexes('description')),
                webpage, 'description', fatal=False),
            'thumbnail': f'https://open.http.mp.streamamg.com/p/{self._PARTNER_ID}/sp/{self._PARTNER_ID}00/thumbnail/entry_id/{entryid}/version/100001/height/1920',
            'timestamp': unified_timestamp(self._html_search_regex(
                r'', webpage, 'upload time')),
            'series': self._html_search_regex(r'data-series\s*?=\s*?"(.*?)"', webpage, 'series', fatal=False) or None,
            'season': self._html_search_regex(r'data-tournament-city\s*?=\s*?"(.*?)"', webpage, 'season', fatal=False) or None,
            'episode': self._html_search_regex(r'data-round\s*?=\s*?"(.*?)"', webpage, 'round', fatal=False) or None,
            'formats': formats,
            'subtitles': subtitles,
        }

repo_name: Lesmiscore/ytdl


code:
from datetime import datetime
import base64

from .common import InfoExtractor
from ..utils import (
    HEADRequest,
    int_or_none,
    urlencode_postdata,
)

class TenPlayIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?10play\.com\.au/(?:[^/]+/)+(?P<id>tpv\d{6}[a-z]{5})'
    _NETRC_MACHINE = '10play'
    _TESTS = [{
        'url': 'https://10play.com.au/neighbours/web-extras/season-39/nathan-borg-is-the-first-aussie-actor-with-a-cochlear-implant-to-join-neighbours/tpv210128qupwd',
        'info_dict': {
            'id': '6226844312001',
            'ext': 'mp4',
            'title': 'Nathan Borg Is The First Aussie Actor With A Cochlear Implant To Join Neighbours',
            'alt_title': 'Nathan Borg Is The First Aussie Actor With A Cochlear Implant To Join Neighbours',
            'description': 'md5:a02d0199c901c2dd4c796f1e7dd0de43',
            'duration': 186,
            'season': 39,
            'series': 'Neighbours',
            'thumbnail': r're:https://.*\.jpg',
            'uploader': 'Channel 10',
            'age_limit': 15,
            'timestamp': 1611810000,
            'upload_date': '20210128',
            'uploader_id': '2199827728001',
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'Only available in Australia',
    }, {
        'url': 'https://10play.com.au/todd-sampsons-body-hack/episodes/season-4/episode-7/tpv200921kvngh',
        'info_dict': {
            'id': '6192880312001',
            'ext': 'mp4',
            'title': "Todd Sampson's Body Hack - S4 Ep. 2",
            'description': 'md5:fa278820ad90f08ea187f9458316ac74',
            'age_limit': 15,
            'timestamp': 1600770600,
            'upload_date': '20200922',
            'uploader': 'Channel 10',
            'uploader_id': '2199827728001'
        },
        'params': {
            'skip_download': True,
        }
    }, {
        'url': 'https://10play.com.au/how-to-stay-married/web-extras/season-1/terrys-talks-ep-1-embracing-change/tpv190915ylupc',
        'only_matching': True,
    }]
    _GEO_BYPASS = False

    _AUS_AGES = {
        'G': 0,
        'PG': 15,
        'M': 15,
        'MA': 15,
        'MA15+': 15,
        'R': 18,
        'X': 18
    }

    def _get_bearer_token(self, video_id):
        username, password = self._get_login_info()
        if username is None or password is None:
            self.raise_login_required('Your 10play account\'s details must be provided with --username and --password.')
        _timestamp = datetime.now().strftime('%Y%m%d000000')
        _auth_header = base64.b64encode(_timestamp.encode('ascii')).decode('ascii')
        data = self._download_json('https://10play.com.au/api/user/auth', video_id, 'Getting bearer token', headers={
            'X-Network-Ten-Auth': _auth_header,
        }, data=urlencode_postdata({
            'email': username,
            'password': password,
        }))
        return 'Bearer ' + data['jwt']['accessToken']

    def _real_extract(self, url):
        content_id = self._match_id(url)
        data = self._download_json(
            'https://10play.com.au/api/v1/videos/' + content_id, content_id)
        headers = {}

        if data.get('memberGated') is True:
            _token = self._get_bearer_token(content_id)
            headers = {'Authorization': _token}

        _video_url = self._download_json(
            data.get('playbackApiEndpoint'), content_id, 'Downloading video JSON',
            headers=headers).get('source')
        m3u8_url = self._request_webpage(HEADRequest(
            _video_url), content_id).geturl()
        if '10play-not-in-oz' in m3u8_url:
            self.raise_geo_restricted(countries=['AU'])
        formats = self._extract_m3u8_formats(m3u8_url, content_id, 'mp4')
        self._sort_formats(formats)

        return {
            'formats': formats,
            'subtitles': {'en': [{'url': data.get('captionUrl')}]} if data.get('captionUrl') else None,
            'id': data.get('altId') or content_id,
            'duration': data.get('duration'),
            'title': data.get('subtitle'),
            'alt_title': data.get('title'),
            'description': data.get('description'),
            'age_limit': self._AUS_AGES.get(data.get('classification')),
            'series': data.get('tvShow'),
            'season': int_or_none(data.get('season')),
            'episode_number': int_or_none(data.get('episode')),
            'timestamp': data.get('published'),
            'thumbnail': data.get('imageUrl'),
            'uploader': 'Channel 10',
            'uploader_id': '2199827728001',
        }


code:
import re

from .common import InfoExtractor
from ..utils import ExtractorError

class TestURLIE(InfoExtractor):
    """ Allows addressing of the test cases as test:yout.*be_1 """

    IE_DESC = False  # Do not list
    _VALID_URL = r'test(?:url)?:(?P<extractor>.*?)(?:_(?P<num>[0-9]+))?$'

    def _real_extract(self, url):
        from . import gen_extractor_classes

        extractor_id, num = self._match_valid_url(url).group('extractor', 'num')
        if not extractor_id:
            return {'id': ':test', 'title': '', 'url': url}

        rex = re.compile(extractor_id, flags=re.IGNORECASE)
        matching_extractors = [e for e in gen_extractor_classes() if rex.search(e.IE_NAME)]

        if len(matching_extractors) == 0:
            raise ExtractorError('No extractors matching {extractor_id!r} found', expected=True)
        elif len(matching_extractors) > 1:
            try:  # Check for exact match
                extractor = next(
                    ie for ie in matching_extractors
                    if ie.IE_NAME.lower() == extractor_id.lower())
            except StopIteration:
                raise ExtractorError(
                    'Found multiple matching extractors: %s' % ' '.join(ie.IE_NAME for ie in matching_extractors),
                    expected=True)
        else:
            extractor = matching_extractors[0]

        testcases = tuple(extractor.get_testcases(True))
        try:
            tc = testcases[int(num or 0)]
        except IndexError:
            raise ExtractorError(
                f'Test case {num or 0} not found, got only {len(testcases)} tests', expected=True)

        self.to_screen(f'Test URL: {tc["url"]}')
        return self.url_result(tc['url'])

class TestInfoDictIE(InfoExtractor):
    """ IE to carry out some tests """

    IE_DESC = False  # Do not list
    _VALID_URL = r'^testdict$'

    def _real_extract(self, url):
        return {
            'id': 'testdict',
            'title': 'taitoru',
            'formats': [{
                'format_id': ' space',
                'url': 'https://example.com/'
            }, {
                'format_id': 'middle space',
                'url': 'https://example.com/'
            }, {
                'format_id': 'what*the!fuck',
                'url': 'https://example.com/'
            }, {
                'format_id': 'some(more)thing',
                'url': 'https://example.com/'
            }],
        }


code:
import json

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

class TF1IE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?tf1\.fr/[^/]+/(?P<program_slug>[^/]+)/videos/(?P<id>[^/?&#]+)\.html'
    _TESTS = [{
        'url': 'https://www.tf1.fr/tmc/quotidien-avec-yann-barthes/videos/quotidien-premiere-partie-11-juin-2019.html',
        'info_dict': {
            'id': '13641379',
            'ext': 'mp4',
            'title': 'md5:f392bc52245dc5ad43771650c96fb620',
            'description': 'md5:a02cdb217141fb2d469d6216339b052f',
            'upload_date': '20190611',
            'timestamp': 1560273989,
            'duration': 1738,
            'series': 'Quotidien avec Yann Barthès',
            'tags': ['intégrale', 'quotidien', 'Replay'],
        },
        'params': {
            # Sometimes wat serves the whole file with the --test option
            'skip_download': True,
        },
    }, {
        'url': 'http://www.tf1.fr/tf1/koh-lanta/videos/replay-koh-lanta-22-mai-2015.html',
        'only_matching': True,
    }, {
        'url': 'http://www.tf1.fr/hd1/documentaire/videos/mylene-farmer-d-une-icone.html',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        program_slug, slug = self._match_valid_url(url).groups()
        video = self._download_json(
            'https://www.tf1.fr/graphql/web', slug, query={
                'id': '9b80783950b85247541dd1d851f9cc7fa36574af015621f853ab111a679ce26f',
                'variables': json.dumps({
                    'programSlug': program_slug,
                    'slug': slug,
                })
            })['data']['videoBySlug']
        wat_id = video['streamId']

        tags = []
        for tag in (video.get('tags') or []):
            label = tag.get('label')
            if not label:
                continue
            tags.append(label)

        decoration = video.get('decoration') or {}

        thumbnails = []
        for source in (try_get(decoration, lambda x: x['image']['sources'], list) or []):
            source_url = source.get('url')
            if not source_url:
                continue
            thumbnails.append({
                'url': source_url,
                'width': int_or_none(source.get('width')),
            })

        return {
            '_type': 'url_transparent',
            'id': wat_id,
            'url': 'wat:' + wat_id,
            'title': video.get('title'),
            'thumbnails': thumbnails,
            'description': decoration.get('description'),
            'timestamp': parse_iso8601(video.get('date')),
            'duration': int_or_none(try_get(video, lambda x: x['publicPlayingInfos']['duration'])),
            'tags': tags,
            'series': decoration.get('programLabel'),
            'season_number': int_or_none(video.get('season')),
            'episode_number': int_or_none(video.get('episode')),
        }


code:
import json

from .common import InfoExtractor
from ..utils import (
    HEADRequest,
    ExtractorError,
    int_or_none,
    clean_html,
)

class TFOIE(InfoExtractor):
    _GEO_COUNTRIES = ['CA']
    _VALID_URL = r'https?://(?:www\.)?tfo\.org/(?:en|fr)/(?:[^/]+/){2}(?P<id>\d+)'
    _TEST = {
        'url': 'http://www.tfo.org/en/universe/tfo-247/100463871/video-game-hackathon',
        'md5': 'cafbe4f47a8dae0ca0159937878100d6',
        'info_dict': {
            'id': '7da3d50e495c406b8fc0b997659cc075',
            'ext': 'mp4',
            'title': 'Video Game Hackathon',
            'description': 'md5:558afeba217c6c8d96c60e5421795c07',
        }
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        self._request_webpage(HEADRequest('http://www.tfo.org/'), video_id)
        infos = self._download_json(
            'http://www.tfo.org/api/web/video/get_infos', video_id, data=json.dumps({
                'product_id': video_id,
            }).encode(), headers={
                'X-tfo-session': self._get_cookies('http://www.tfo.org/')['tfo-session'].value,
            })
        if infos.get('success') == 0:
            if infos.get('code') == 'ErrGeoBlocked':
                self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
            raise ExtractorError('%s said: %s' % (self.IE_NAME, clean_html(infos['msg'])), expected=True)
        video_data = infos['data']

        return {
            '_type': 'url_transparent',
            'id': video_id,
            'url': 'limelight:media:' + video_data['llid'],
            'title': video_data['title'],
            'description': video_data.get('description'),
            'series': video_data.get('collection'),
            'season_number': int_or_none(video_data.get('season')),
            'episode_number': int_or_none(video_data.get('episode')),
            'duration': int_or_none(video_data.get('duration')),
            'ie_key': 'LimelightMedia',
        }


code:
from .common import InfoExtractor
from ..utils import extract_attributes, remove_end

class TheHoleTvIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?the-hole\.tv/episodes/(?P<id>[\w-]+)'
    _TESTS = [{
        'url': 'https://the-hole.tv/episodes/gromkii-vopros-sergey-orlov',
        'md5': 'fea6682f47786f3ae5a6cbd635ec4bf9',
        'info_dict': {
            'id': 'gromkii-vopros-sergey-orlov',
            'ext': 'mp4',
            'title': 'Сергей Орлов — Громкий вопрос',
            'thumbnail': 'https://assets-cdn.the-hole.tv/images/t8gan4n6zn627e7wni11b2uemqts',
            'description': 'md5:45741a9202331f995d9fb76996759379'
        }
    }]

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

        player_attrs = extract_attributes(self._search_regex(
            r'(<div[^>]*\bdata-controller="player"[^>]*>)', webpage, 'video player'))
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
            player_attrs['data-player-source-value'], video_id, 'mp4')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': remove_end(self._html_extract_title(webpage), ' — The Hole'),
            'description': self._og_search_description(webpage),
            'thumbnail': player_attrs.get('data-player-poster-value'),
            'formats': formats,
            'subtitles': subtitles
        }


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

class TheInterceptIE(InfoExtractor):
    _VALID_URL = r'https?://theintercept\.com/fieldofvision/(?P<id>[^/?#]+)'
    _TESTS = [{
        'url': 'https://theintercept.com/fieldofvision/thisisacoup-episode-four-surrender-or-die/',
        'md5': '145f28b41d44aab2f87c0a4ac8ec95bd',
        'info_dict': {
            'id': '46214',
            'ext': 'mp4',
            'title': '#ThisIsACoup – Episode Four: Surrender or Die',
            'description': 'md5:74dd27f0e2fbd50817829f97eaa33140',
            'timestamp': 1450429239,
            'upload_date': '20151218',
            'comment_count': int,
        }
    }]

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

        json_data = self._parse_json(self._search_regex(
            r'initialStoreTree\s*=\s*(?P<json_data>{.+})', webpage,
            'initialStoreTree'), display_id)

        for post in json_data['resources']['posts'].values():
            if post['slug'] == display_id:
                return {
                    '_type': 'url_transparent',
                    'url': 'jwplatform:%s' % post['fov_videoid'],
                    'id': compat_str(post['ID']),
                    'display_id': display_id,
                    'title': post['title'],
                    'description': post.get('excerpt'),
                    'timestamp': parse_iso8601(post.get('date')),
                    'comment_count': int_or_none(post.get('comments_number')),
                }
        raise ExtractorError('Unable to find the current post')


code:
import re
import time
import hmac
import binascii
import hashlib

from .once import OnceIE
from .adobepass import AdobePassIE
from ..utils import (
    determine_ext,
    ExtractorError,
    float_or_none,
    int_or_none,
    parse_qs,
    sanitized_Request,
    unsmuggle_url,
    update_url_query,
    xpath_with_ns,
    mimetype2ext,
    find_xpath_attr,
)

default_ns = 'http://www.w3.org/2005/SMIL21/Language'
_x = lambda p: xpath_with_ns(p, {'smil': default_ns})

class ThePlatformBaseIE(OnceIE):
    _TP_TLD = 'com'

    def _extract_theplatform_smil(self, smil_url, video_id, note='Downloading SMIL data'):
        meta = self._download_xml(
            smil_url, video_id, note=note, query={'format': 'SMIL'},
            headers=self.geo_verification_headers())
        error_element = find_xpath_attr(meta, _x('.//smil:ref'), 'src')
        if error_element is not None:
            exception = find_xpath_attr(
                error_element, _x('.//smil:param'), 'name', 'exception')
            if exception is not None:
                if exception.get('value') == 'GeoLocationBlocked':
                    self.raise_geo_restricted(error_element.attrib['abstract'])
                elif error_element.attrib['src'].startswith(
                        'http://link.theplatform.%s/s/errorFiles/Unavailable.'
                        % self._TP_TLD):
                    raise ExtractorError(
                        error_element.attrib['abstract'], expected=True)

        smil_formats = self._parse_smil_formats(
            meta, smil_url, video_id, namespace=default_ns,
            # the parameters are from syfy.com, other sites may use others,
            # they also work for nbc.com
            f4m_params={'g': 'UXWGVKRWHFSP', 'hdcore': '3.0.3'},
            transform_rtmp_url=lambda streamer, src: (streamer, 'mp4:' + src))

        formats = []
        for _format in smil_formats:
            if OnceIE.suitable(_format['url']):
                formats.extend(self._extract_once_formats(_format['url']))
            else:
                media_url = _format['url']
                if determine_ext(media_url) == 'm3u8':
                    hdnea2 = self._get_cookies(media_url).get('hdnea2')
                    if hdnea2:
                        _format['url'] = update_url_query(media_url, {'hdnea3': hdnea2.value})

                formats.append(_format)

        subtitles = self._parse_smil_subtitles(meta, default_ns)

        return formats, subtitles

    def _download_theplatform_metadata(self, path, video_id):
        info_url = 'http://link.theplatform.%s/s/%s?format=preview' % (self._TP_TLD, path)
        return self._download_json(info_url, video_id)

    def _parse_theplatform_metadata(self, info):
        subtitles = {}
        captions = info.get('captions')
        if isinstance(captions, list):
            for caption in captions:
                lang, src, mime = caption.get('lang', 'en'), caption.get('src'), caption.get('type')
                subtitles.setdefault(lang, []).append({
                    'ext': mimetype2ext(mime),
                    'url': src,
                })

        duration = info.get('duration')
        tp_chapters = info.get('chapters', [])
        chapters = []
        if tp_chapters:
            def _add_chapter(start_time, end_time):
                start_time = float_or_none(start_time, 1000)
                end_time = float_or_none(end_time, 1000)
                if start_time is None or end_time is None:
                    return
                chapters.append({
                    'start_time': start_time,
                    'end_time': end_time,
                })

            for chapter in tp_chapters[:-1]:
                _add_chapter(chapter.get('startTime'), chapter.get('endTime'))
            _add_chapter(tp_chapters[-1].get('startTime'), tp_chapters[-1].get('endTime') or duration)

        return {
            'title': info['title'],
            'subtitles': subtitles,
            'description': info['description'],
            'thumbnail': info['defaultThumbnailUrl'],
            'duration': float_or_none(duration, 1000),
            'timestamp': int_or_none(info.get('pubDate'), 1000) or None,
            'uploader': info.get('billingCode'),
            'chapters': chapters,
        }

    def _extract_theplatform_metadata(self, path, video_id):
        info = self._download_theplatform_metadata(path, video_id)
        return self._parse_theplatform_metadata(info)

class ThePlatformIE(ThePlatformBaseIE, AdobePassIE):
    _VALID_URL = r'''(?x)
        (?:https?://(?:link|player)\.theplatform\.com/[sp]/(?P<provider_id>[^/]+)/
           (?:(?:(?:[^/]+/)+select/)?(?P<media>media/(?:guid/\d+/)?)?|(?P<config>(?:[^/\?]+/(?:swf|config)|onsite)/select/))?
         |theplatform:)(?P<id>[^/\?&]+)'''
    _EMBED_REGEX = [
        r'''(?x)
            <meta\s+
                property=(["'])(?:og:video(?::(?:secure_)?url)?|twitter:player)\1\s+
                content=(["'])(?P<url>https?://player\.theplatform\.com/p/.+?)\2''',
        r'(?s)<(?:iframe|script)[^>]+src=(["\'])(?P<url>(?:https?:)?//player\.theplatform\.com/p/.+?)\1'
    ]

    _TESTS = [{
        # from http://www.metacafe.com/watch/cb-e9I_cZgTgIPd/blackberrys_big_bold_z30/
        'url': 'http://link.theplatform.com/s/dJ5BDC/e9I_cZgTgIPd/meta.smil?format=smil&Tracking=true&mbr=true',
        'info_dict': {
            'id': 'e9I_cZgTgIPd',
            'ext': 'flv',
            'title': 'Blackberry\'s big, bold Z30',
            'description': 'The Z30 is Blackberry\'s biggest, baddest mobile messaging device yet.',
            'duration': 247,
            'timestamp': 1383239700,
            'upload_date': '20131031',
            'uploader': 'CBSI-NEW',
        },
        'params': {
            # rtmp download
            'skip_download': True,
        },
        'skip': '404 Not Found',
    }, {
        # from http://www.cnet.com/videos/tesla-model-s-a-second-step-towards-a-cleaner-motoring-future/
        'url': 'http://link.theplatform.com/s/kYEXFC/22d_qsQ6MIRT',
        'info_dict': {
            'id': '22d_qsQ6MIRT',
            'ext': 'flv',
            'description': 'md5:ac330c9258c04f9d7512cf26b9595409',
            'title': 'Tesla Model S: A second step towards a cleaner motoring future',
            'timestamp': 1426176191,
            'upload_date': '20150312',
            'uploader': 'CBSI-NEW',
        },
        'params': {
            # rtmp download
            'skip_download': True,
        }
    }, {
        'url': 'https://player.theplatform.com/p/D6x-PC/pulse_preview/embed/select/media/yMBg9E8KFxZD',
        'info_dict': {
            'id': 'yMBg9E8KFxZD',
            'ext': 'mp4',
            'description': 'md5:644ad9188d655b742f942bf2e06b002d',
            'title': 'HIGHLIGHTS: USA bag first ever series Cup win',
            'uploader': 'EGSM',
        }
    }, {
        'url': 'http://player.theplatform.com/p/NnzsPC/widget/select/media/4Y0TlYUr_ZT7',
        'only_matching': True,
    }, {
        'url': 'http://player.theplatform.com/p/2E2eJC/nbcNewsOffsite?guid=tdy_or_siri_150701',
        'md5': 'fb96bb3d85118930a5b055783a3bd992',
        'info_dict': {
            'id': 'tdy_or_siri_150701',
            'ext': 'mp4',
            'title': 'iPhone Siri’s sassy response to a math question has people talking',
            'description': 'md5:a565d1deadd5086f3331d57298ec6333',
            'duration': 83.0,
            'thumbnail': r're:^https?://.*\.jpg$',
            'timestamp': 1435752600,
            'upload_date': '20150701',
            'uploader': 'NBCU-NEWS',
        },
    }, {
        # From http://www.nbc.com/the-blacklist/video/sir-crispin-crandall/2928790?onid=137781#vc137781=1
        # geo-restricted (US), HLS encrypted with AES-128
        'url': 'http://player.theplatform.com/p/NnzsPC/onsite_universal/select/media/guid/2410887629/2928790?fwsitesection=nbc_the_blacklist_video_library&autoPlay=true&carouselID=137781',
        'only_matching': True,
    }]

    @classmethod
    def _extract_embed_urls(cls, url, webpage):
        # Are whitespaces ignored in URLs?
        # https://github.com/ytdl-org/youtube-dl/issues/12044
        for embed_url in super()._extract_embed_urls(url, webpage):
            yield re.sub(r'\s', '', embed_url)

    @staticmethod
    def _sign_url(url, sig_key, sig_secret, life=600, include_qs=False):
        flags = '10' if include_qs else '00'
        expiration_date = '%x' % (int(time.time()) + life)

        def str_to_hex(str):
            return binascii.b2a_hex(str.encode('ascii')).decode('ascii')

        def hex_to_bytes(hex):
            return binascii.a2b_hex(hex.encode('ascii'))

        relative_path = re.match(r'https?://link\.theplatform\.com/s/([^?]+)', url).group(1)
        clear_text = hex_to_bytes(flags + expiration_date + str_to_hex(relative_path))
        checksum = hmac.new(sig_key.encode('ascii'), clear_text, hashlib.sha1).hexdigest()
        sig = flags + expiration_date + checksum + str_to_hex(sig_secret)
        return '%s&sig=%s' % (url, sig)

    def _real_extract(self, url):
        url, smuggled_data = unsmuggle_url(url, {})
        self._initialize_geo_bypass({
            'countries': smuggled_data.get('geo_countries'),
        })

        mobj = self._match_valid_url(url)
        provider_id = mobj.group('provider_id')
        video_id = mobj.group('id')

        if not provider_id:
            provider_id = 'dJ5BDC'

        path = provider_id + '/'
        if mobj.group('media'):
            path += mobj.group('media')
        path += video_id

        qs_dict = parse_qs(url)
        if 'guid' in qs_dict:
            webpage = self._download_webpage(url, video_id)
            scripts = re.findall(r'<script[^>]+src="([^"]+)"', webpage)
            feed_id = None
            # feed id usually locates in the last script.
            # Seems there's no pattern for the interested script filename, so
            # I try one by one
            for script in reversed(scripts):
                feed_script = self._download_webpage(
                    self._proto_relative_url(script, 'http:'),
                    video_id, 'Downloading feed script')
                feed_id = self._search_regex(
                    r'defaultFeedId\s*:\s*"([^"]+)"', feed_script,
                    'default feed id', default=None)
                if feed_id is not None:
                    break
            if feed_id is None:
                raise ExtractorError('Unable to find feed id')
            return self.url_result('http://feed.theplatform.com/f/%s/%s?byGuid=%s' % (
                provider_id, feed_id, qs_dict['guid'][0]))

        if smuggled_data.get('force_smil_url', False):
            smil_url = url
        # Explicitly specified SMIL (see https://github.com/ytdl-org/youtube-dl/issues/7385)
        elif '/guid/' in url:
            headers = {}
            source_url = smuggled_data.get('source_url')
            if source_url:
                headers['Referer'] = source_url
            request = sanitized_Request(url, headers=headers)
            webpage = self._download_webpage(request, video_id)
            smil_url = self._search_regex(
                r'<link[^>]+href=(["\'])(?P<url>.+?)\1[^>]+type=["\']application/smil\+xml',
                webpage, 'smil url', group='url')
            path = self._search_regex(
                r'link\.theplatform\.com/s/((?:[^/?#&]+/)+[^/?#&]+)', smil_url, 'path')
            smil_url += '?' if '?' not in smil_url else '&' + 'formats=m3u,mpeg4'
        elif mobj.group('config'):
            config_url = url + '&form=json'
            config_url = config_url.replace('swf/', 'config/')
            config_url = config_url.replace('onsite/', 'onsite/config/')
            config = self._download_json(config_url, video_id, 'Downloading config')
            if 'releaseUrl' in config:
                release_url = config['releaseUrl']
            else:
                release_url = 'http://link.theplatform.com/s/%s?mbr=true' % path
            smil_url = release_url + '&formats=MPEG4&manifest=f4m'
        else:
            smil_url = 'http://link.theplatform.com/s/%s?mbr=true' % path

        sig = smuggled_data.get('sig')
        if sig:
            smil_url = self._sign_url(smil_url, sig['key'], sig['secret'])

        formats, subtitles = self._extract_theplatform_smil(smil_url, video_id)
        self._sort_formats(formats)

        ret = self._extract_theplatform_metadata(path, video_id)
        combined_subtitles = self._merge_subtitles(ret.get('subtitles', {}), subtitles)
        ret.update({
            'id': video_id,
            'formats': formats,
            'subtitles': combined_subtitles,
        })

        return ret

class ThePlatformFeedIE(ThePlatformBaseIE):
    _URL_TEMPLATE = '%s//feed.theplatform.com/f/%s/%s?form=json&%s'
    _VALID_URL = r'https?://feed\.theplatform\.com/f/(?P<provider_id>[^/]+)/(?P<feed_id>[^?/]+)\?(?:[^&]+&)*(?P<filter>by(?:Gui|I)d=(?P<id>[^&]+))'
    _TESTS = [{
        # From http://player.theplatform.com/p/7wvmTC/MSNBCEmbeddedOffSite?guid=n_hardball_5biden_140207
        'url': 'http://feed.theplatform.com/f/7wvmTC/msnbc_video-p-test?form=json&pretty=true&range=-40&byGuid=n_hardball_5biden_140207',
        'md5': '6e32495b5073ab414471b615c5ded394',
        'info_dict': {
            'id': 'n_hardball_5biden_140207',
            'ext': 'mp4',
            'title': 'The Biden factor: will Joe run in 2016?',
            'description': 'Could Vice President Joe Biden be preparing a 2016 campaign? Mark Halperin and Sam Stein weigh in.',
            'thumbnail': r're:^https?://.*\.jpg$',
            'upload_date': '20140208',
            'timestamp': 1391824260,
            'duration': 467.0,
            'categories': ['MSNBC/Issues/Democrats', 'MSNBC/Issues/Elections/Election 2016'],
            'uploader': 'NBCU-NEWS',
        },
    }, {
        'url': 'http://feed.theplatform.com/f/2E2eJC/nnd_NBCNews?byGuid=nn_netcast_180306.Copy.01',
        'only_matching': True,
    }]

    def _extract_feed_info(self, provider_id, feed_id, filter_query, video_id, custom_fields=None, asset_types_query={}, account_id=None):
        real_url = self._URL_TEMPLATE % (self.http_scheme(), provider_id, feed_id, filter_query)
        entry = self._download_json(real_url, video_id)['entries'][0]
        main_smil_url = 'http://link.theplatform.com/s/%s/media/guid/%d/%s' % (provider_id, account_id, entry['guid']) if account_id else entry.get('plmedia$publicUrl')

        formats = []
        subtitles = {}
        first_video_id = None
        duration = None
        asset_types = []
        for item in entry['media$content']:
            smil_url = item['plfile$url']
            cur_video_id = ThePlatformIE._match_id(smil_url)
            if first_video_id is None:
                first_video_id = cur_video_id
                duration = float_or_none(item.get('plfile$duration'))
            file_asset_types = item.get('plfile$assetTypes') or parse_qs(smil_url)['assetTypes']
            for asset_type in file_asset_types:
                if asset_type in asset_types:
                    continue
                asset_types.append(asset_type)
                query = {
                    'mbr': 'true',
                    'formats': item['plfile$format'],
                    'assetTypes': asset_type,
                }
                if asset_type in asset_types_query:
                    query.update(asset_types_query[asset_type])
                cur_formats, cur_subtitles = self._extract_theplatform_smil(update_url_query(
                    main_smil_url or smil_url, query), video_id, 'Downloading SMIL data for %s' % asset_type)
                formats.extend(cur_formats)
                subtitles = self._merge_subtitles(subtitles, cur_subtitles)

        self._sort_formats(formats)

        thumbnails = [{
            'url': thumbnail['plfile$url'],
            'width': int_or_none(thumbnail.get('plfile$width')),
            'height': int_or_none(thumbnail.get('plfile$height')),
        } for thumbnail in entry.get('media$thumbnails', [])]

        timestamp = int_or_none(entry.get('media$availableDate'), scale=1000)
        categories = [item['media$name'] for item in entry.get('media$categories', [])]

        ret = self._extract_theplatform_metadata('%s/%s' % (provider_id, first_video_id), video_id)
        subtitles = self._merge_subtitles(subtitles, ret['subtitles'])
        ret.update({
            'id': video_id,
            'formats': formats,
            'subtitles': subtitles,
            'thumbnails': thumbnails,
            'duration': duration,
            'timestamp': timestamp,
            'categories': categories,
        })
        if custom_fields:
            ret.update(custom_fields(entry))

        return ret

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)

        video_id = mobj.group('id')
        provider_id = mobj.group('provider_id')
        feed_id = mobj.group('feed_id')
        filter_query = mobj.group('filter')

        return self._extract_feed_info(provider_id, feed_id, filter_query, video_id)


code:
from .common import InfoExtractor

class TheStarIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?thestar\.com/(?:[^/]+/)*(?P<id>.+)\.html'
    _TEST = {
        'url': 'http://www.thestar.com/life/2016/02/01/mankind-why-this-woman-started-a-men-s-skincare-line.html',
        'md5': '2c62dd4db2027e35579fefb97a8b6554',
        'info_dict': {
            'id': '4732393888001',
            'ext': 'mp4',
            'title': 'Mankind: Why this woman started a men\'s skin care line',
            'description': 'Robert Cribb talks to Young Lee, the founder of Uncle Peter\'s MAN.',
            'uploader_id': '794267642001',
            'timestamp': 1454353482,
            'upload_date': '20160201',
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        }
    }
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/794267642001/default_default/index.html?videoId=%s'

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        brightcove_id = self._search_regex(
            r'mainartBrightcoveVideoId["\']?\s*:\s*["\']?(\d+)',
            webpage, 'brightcove id')
        return self.url_result(
            self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id,
            'BrightcoveNew', brightcove_id)


code:
import re

from .common import InfoExtractor
from ..utils import extract_attributes

class TheSunIE(InfoExtractor):
    _VALID_URL = r'https://(?:www\.)?thesun\.co\.uk/[^/]+/(?P<id>\d+)'
    _TEST = {
        'url': 'https://www.thesun.co.uk/tvandshowbiz/2261604/orlando-bloom-and-katy-perry-post-adorable-instagram-video-together-celebrating-thanksgiving-after-split-rumours/',
        'info_dict': {
            'id': '2261604',
            'title': 'md5:cba22f48bad9218b64d5bbe0e16afddf',
        },
        'playlist_count': 2,
    }
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'

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

        webpage = self._download_webpage(url, article_id)

        entries = []
        for video in re.findall(
                r'<video[^>]+data-video-id-pending=[^>]+>',
                webpage):
            attrs = extract_attributes(video)
            video_id = attrs['data-video-id-pending']
            account_id = attrs.get('data-account', '5067014667001')
            entries.append(self.url_result(
                self.BRIGHTCOVE_URL_TEMPLATE % (account_id, video_id),
                'BrightcoveNew', video_id))

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


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

class ThetaStreamIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?theta\.tv/(?!video/)(?P<id>[a-z0-9-]+)'
    _TESTS = [{
        'url': 'https://www.theta.tv/davirus',
        'skip': 'The live may have ended',
        'info_dict': {
            'id': 'DaVirus',
            'ext': 'mp4',
            'title': 'I choose you - My Community is King -👀 - YO HABLO ESPANOL - CODE DAVIRUS',
            'thumbnail': r're:https://live-thumbnails-prod-theta-tv\.imgix\.net/thumbnail/.+\.jpg',
        }
    }, {
        'url': 'https://www.theta.tv/mst3k',
        'note': 'This channel is live 24/7',
        'info_dict': {
            'id': 'MST3K',
            'ext': 'mp4',
            'title': 'Mystery Science Theatre 3000 24/7 Powered by the THETA Network.',
            'thumbnail': r're:https://user-prod-theta-tv\.imgix\.net/.+\.jpg',
        }
    }, {
        'url': 'https://www.theta.tv/contv-anime',
        'info_dict': {
            'id': 'ConTVAnime',
            'ext': 'mp4',
            'title': 'CONTV ANIME 24/7. Powered by THETA Network.',
            'thumbnail': r're:https://user-prod-theta-tv\.imgix\.net/.+\.jpg',
        }
    }]

    def _real_extract(self, url):
        channel_id = self._match_id(url)
        info = self._download_json(f'https://api.theta.tv/v1/channel?alias={channel_id}', channel_id)['body']

        m3u8_playlist = next(
            data['url'] for data in info['live_stream']['video_urls']
            if data.get('type') != 'embed' and data.get('resolution') in ('master', 'source'))

        formats = self._extract_m3u8_formats(m3u8_playlist, channel_id, 'mp4', m3u8_id='hls', live=True)
        self._sort_formats(formats)

        channel = try_get(info, lambda x: x['user']['username'])  # using this field instead of channel_id due to capitalization

        return {
            'id': channel,
            'title': try_get(info, lambda x: x['live_stream']['title']),
            'channel': channel,
            'view_count': try_get(info, lambda x: x['live_stream']['view_count']),
            'is_live': True,
            'formats': formats,
            'thumbnail': try_get(info, lambda x: x['live_stream']['thumbnail_url']),
        }

class ThetaVideoIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?theta\.tv/video/(?P<id>vid[a-z0-9]+)'
    _TEST = {
        'url': 'https://www.theta.tv/video/vidiq6aaet3kzf799p0',
        'md5': '633d8c29eb276bb38a111dbd591c677f',
        'info_dict': {
            'id': 'vidiq6aaet3kzf799p0',
            'ext': 'mp4',
            'title': 'Theta EdgeCast Tutorial',
            'uploader': 'Pixiekittie',
            'description': 'md5:e316253f5bdced8b5a46bb50ae60a09f',
            'thumbnail': r're:https://user-prod-theta-tv\.imgix\.net/.+/vod_thumb/.+.jpg',
        }
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        info = self._download_json(f'https://api.theta.tv/v1/video/{video_id}/raw', video_id)['body']

        m3u8_playlist = try_get(info, lambda x: x['video_urls'][0]['url'])

        formats = self._extract_m3u8_formats(m3u8_playlist, video_id, 'mp4', m3u8_id='hls')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': info.get('title'),
            'uploader': try_get(info, lambda x: x['user']['username']),
            'description': info.get('description'),
            'view_count': info.get('view_count'),
            'like_count': info.get('like_count'),
            'formats': formats,
            'thumbnail': info.get('thumbnail_url'),
        }


code:
import json

from .theplatform import ThePlatformIE
from ..utils import (
    determine_ext,
    parse_duration,
    parse_iso8601,
)

class TheWeatherChannelIE(ThePlatformIE):
    _VALID_URL = r'https?://(?:www\.)?weather\.com(?P<asset_name>(?:/(?P<locale>[a-z]{2}-[A-Z]{2}))?/(?:[^/]+/)*video/(?P<id>[^/?#]+))'
    _TESTS = [{
        'url': 'https://weather.com/series/great-outdoors/video/ice-climber-is-in-for-a-shock',
        'md5': 'c4cbe74c9c17c5676b704b950b73dd92',
        'info_dict': {
            'id': 'cc82397e-cc3f-4d11-9390-a785add090e8',
            'ext': 'mp4',
            'title': 'Ice Climber Is In For A Shock',
            'description': 'md5:55606ce1378d4c72e6545e160c9d9695',
            'uploader': 'TWC - Digital (No Distro)',
            'uploader_id': '6ccd5455-16bb-46f2-9c57-ff858bb9f62c',
            'upload_date': '20160720',
            'timestamp': 1469018835,
        }
    }, {
        'url': 'https://weather.com/en-CA/international/videos/video/unidentified-object-falls-from-sky-in-india',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        asset_name, locale, display_id = self._match_valid_url(url).groups()
        if not locale:
            locale = 'en-US'
        video_data = list(self._download_json(
            'https://weather.com/api/v1/p/redux-dal', display_id, data=json.dumps([{
                'name': 'getCMSAssetsUrlConfig',
                'params': {
                    'language': locale.replace('-', '_'),
                    'query': {
                        'assetName': {
                            '$in': asset_name,
                        },
                    },
                }
            }]).encode(), headers={
                'Content-Type': 'application/json',
            })['dal']['getCMSAssetsUrlConfig'].values())[0]['data'][0]
        video_id = video_data['id']
        seo_meta = video_data.get('seometa', {})
        title = video_data.get('title') or seo_meta['title']

        urls = []
        thumbnails = []
        formats = []
        for variant_id, variant_url in video_data.get('variants', []).items():
            variant_url = variant_url.strip()
            if not variant_url or variant_url in urls:
                continue
            urls.append(variant_url)
            ext = determine_ext(variant_url)
            if ext == 'jpg':
                thumbnails.append({
                    'url': variant_url,
                    'id': variant_id,
                })
            elif ThePlatformIE.suitable(variant_url):
                tp_formats, _ = self._extract_theplatform_smil(variant_url, video_id)
                formats.extend(tp_formats)
            elif ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    variant_url, video_id, 'mp4', 'm3u8_native',
                    m3u8_id=variant_id, fatal=False))
            elif ext == 'f4m':
                formats.extend(self._extract_f4m_formats(
                    variant_url, video_id, f4m_id=variant_id, fatal=False))
            else:
                formats.append({
                    'url': variant_url,
                    'format_id': variant_id,
                })
        self._sort_formats(formats)

        cc_url = video_data.get('cc_url')

        return {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': video_data.get('description') or seo_meta.get('description') or seo_meta.get('og:description'),
            'duration': parse_duration(video_data.get('duration')),
            'uploader': video_data.get('providername'),
            'uploader_id': video_data.get('providerid'),
            'timestamp': parse_iso8601(video_data.get('publishdate')),
            'subtitles': {locale[:2]: [{'url': cc_url}]} if cc_url else None,
            'thumbnails': thumbnails,
            'formats': formats,
        }


code:
from .common import InfoExtractor

class ThisAmericanLifeIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?thisamericanlife\.org/(?:radio-archives/episode/|play_full\.php\?play=)(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://www.thisamericanlife.org/radio-archives/episode/487/harper-high-school-part-one',
        'md5': '8f7d2da8926298fdfca2ee37764c11ce',
        'info_dict': {
            'id': '487',
            'ext': 'm4a',
            'title': '487: Harper High School, Part One',
            'description': 'md5:ee40bdf3fb96174a9027f76dbecea655',
            'thumbnail': r're:^https?://.*\.jpg$',
        },
    }, {
        'url': 'http://www.thisamericanlife.org/play_full.php?play=487',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(
            'http://www.thisamericanlife.org/radio-archives/episode/%s' % video_id, video_id)

        return {
            'id': video_id,
            'url': 'http://stream.thisamericanlife.org/{0}/stream/{0}_64k.m3u8'.format(video_id),
            'protocol': 'm3u8_native',
            'ext': 'm4a',
            'acodec': 'aac',
            'vcodec': 'none',
            'abr': 64,
            'title': self._html_search_meta(r'twitter:title', webpage, 'title', fatal=True),
            'description': self._html_search_meta(r'description', webpage, 'description'),
            'thumbnail': self._og_search_thumbnail(webpage),
        }


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

class ThisAVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?thisav\.com/video/(?P<id>[0-9]+)/.*'
    _TESTS = [{
        # jwplayer
        'url': 'http://www.thisav.com/video/47734/%98%26sup1%3B%83%9E%83%82---just-fit.html',
        'md5': '0480f1ef3932d901f0e0e719f188f19b',
        'info_dict': {
            'id': '47734',
            'ext': 'flv',
            'title': '高樹マリア - Just fit',
            'uploader': 'dj7970',
            'uploader_id': 'dj7970'
        }
    }, {
        # html5 media
        'url': 'http://www.thisav.com/video/242352/nerdy-18yo-big-ass-tattoos-and-glasses.html',
        'md5': 'ba90c076bd0f80203679e5b60bf523ee',
        'info_dict': {
            'id': '242352',
            'ext': 'mp4',
            'title': 'Nerdy 18yo Big Ass Tattoos and Glasses',
            'uploader': 'cybersluts',
            'uploader_id': 'cybersluts',
        },
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)

        video_id = mobj.group('id')
        webpage = self._download_webpage(url, video_id)
        title = remove_end(self._html_extract_title(webpage), ' - 視頻 - ThisAV.com-世界第一中文成人娛樂網站')
        video_url = self._html_search_regex(
            r"addVariable\('file','([^']+)'\);", webpage, 'video url', default=None)
        if video_url:
            info_dict = {
                'formats': [{
                    'url': video_url,
                }],
            }
        else:
            entries = self._parse_html5_media_entries(url, webpage, video_id)
            if entries:
                info_dict = entries[0]
            else:
                info_dict = self._extract_jwplayer_data(
                    webpage, video_id, require_title=False)
        uploader = self._html_search_regex(
            r': ([^<]+)',
            webpage, 'uploader name', fatal=False)
        uploader_id = self._html_search_regex(
            r': (?:[^<]+)',
            webpage, 'uploader id', fatal=False)

        info_dict.update({
            'id': video_id,
            'uploader': uploader,
            'uploader_id': uploader_id,
            'title': title,
        })

        return info_dict


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

class ThisOldHouseIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?thisoldhouse\.com/(?:watch|how-to|tv-episode|(?:[^/]+/)?\d+)/(?P<id>[^/?#]+)'
    _TESTS = [{
        'url': 'https://www.thisoldhouse.com/how-to/how-to-build-storage-bench',
        'info_dict': {
            'id': '5dcdddf673c3f956ef5db202',
            'ext': 'mp4',
            'title': 'How to Build a Storage Bench',
            'description': 'In the workshop, Tom Silva and Kevin O\'Connor build a storage bench for an entryway.',
            'timestamp': 1442548800,
            'upload_date': '20150918',
            'duration': 674,
            'view_count': int,
            'average_rating': 0,
            'thumbnail': r're:^https?://.*\.jpg\?\d+$',
            'display_id': 'how-to-build-a-storage-bench',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://www.thisoldhouse.com/watch/arlington-arts-crafts-arts-and-crafts-class-begins',
        'only_matching': True,
    }, {
        'url': 'https://www.thisoldhouse.com/tv-episode/ask-toh-shelf-rough-electric',
        'only_matching': True,
    }, {
        'url': 'https://www.thisoldhouse.com/furniture/21017078/how-to-build-a-storage-bench',
        'only_matching': True,
    }, {
        'url': 'https://www.thisoldhouse.com/21113884/s41-e13-paradise-lost',
        'only_matching': True,
    }, {
        # iframe www.thisoldhouse.com
        'url': 'https://www.thisoldhouse.com/21083431/seaside-transformation-the-westerly-project',
        'only_matching': True,
    }]
    _ZYPE_TMPL = 'https://player.zype.com/embed/%s.html?api_key=hsOk_yMSPYNrT22e9pu8hihLXjaZf0JW5jsOWv4ZqyHJFvkJn6rtToHl09tbbsbe'

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        if 'To Unlock This content' in webpage:
            self.raise_login_required(method='cookies')
        video_url = self._search_regex(
            r'<iframe[^>]+src=[\'"]((?:https?:)?//(?:www\.)?thisoldhouse\.(?:chorus\.build|com)/videos/zype/([0-9a-f]{24})[^\'"]*)[\'"]',
            webpage, 'video url')
        if 'subscription_required=true' in video_url or 'c-entry-group-labels__image' in webpage:
            return self.url_result(self._request_webpage(HEADRequest(video_url), display_id).geturl(), 'Zype', display_id)
        video_id = self._search_regex(r'(?:https?:)?//(?:www\.)?thisoldhouse\.(?:chorus\.build|com)/videos/zype/([0-9a-f]{24})', video_url, 'video id')
        return self.url_result(self._ZYPE_TMPL % video_id, 'Zype', video_id)


code:
from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    determine_ext,
    ExtractorError,
    float_or_none,
    int_or_none,
    join_nonempty,
    parse_iso8601,
)

class ThreeQSDNIE(InfoExtractor):
    IE_NAME = '3qsdn'
    IE_DESC = '3Q SDN'
    _VALID_URL = r'https?://playout\.3qsdn\.com/(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
    _EMBED_REGEX = [r'<iframe[^>]+\b(?:data-)?src=(["\'])(?P<url>%s.*?)\1' % _VALID_URL]
    _TESTS = [{
        # https://player.3qsdn.com/demo.html
        'url': 'https://playout.3qsdn.com/7201c779-6b3c-11e7-a40e-002590c750be',
        'md5': '64a57396b16fa011b15e0ea60edce918',
        'info_dict': {
            'id': '7201c779-6b3c-11e7-a40e-002590c750be',
            'ext': 'mp4',
            'title': 'Video Ads',
            'is_live': False,
            'description': 'Video Ads Demo',
            'timestamp': 1500334803,
            'upload_date': '20170717',
            'duration': 888.032,
            'subtitles': {
                'eng': 'count:1',
            },
        },
        'expected_warnings': ['Unknown MIME type application/mp4 in DASH manifest'],
    }, {
        # live video stream
        'url': 'https://playout.3qsdn.com/66e68995-11ca-11e8-9273-002590c750be',
        'info_dict': {
            'id': '66e68995-11ca-11e8-9273-002590c750be',
            'ext': 'mp4',
            'title': 're:^66e68995-11ca-11e8-9273-002590c750be [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'is_live': True,
        },
        'params': {
            'skip_download': True,  # m3u8 downloads
        },
    }, {
        # live audio stream
        'url': 'http://playout.3qsdn.com/9edf36e0-6bf2-11e2-a16a-9acf09e2db48',
        'only_matching': True,
    }, {
        # live audio stream with some 404 URLs
        'url': 'http://playout.3qsdn.com/ac5c3186-777a-11e2-9c30-9acf09e2db48',
        'only_matching': True,
    }, {
        # geo restricted with 'This content is not available in your country'
        'url': 'http://playout.3qsdn.com/d63a3ffe-75e8-11e2-9c30-9acf09e2db48',
        'only_matching': True,
    }, {
        # geo restricted with 'playout.3qsdn.com/forbidden'
        'url': 'http://playout.3qsdn.com/8e330f26-6ae2-11e2-a16a-9acf09e2db48',
        'only_matching': True,
    }, {
        # live video with rtmp link
        'url': 'https://playout.3qsdn.com/6092bb9e-8f72-11e4-a173-002590c750be',
        'only_matching': True,
    }, {
        # ondemand from http://www.philharmonie.tv/veranstaltung/26/
        'url': 'http://playout.3qsdn.com/0280d6b9-1215-11e6-b427-0cc47a188158?protocol=http',
        'only_matching': True,
    }, {
        # live video stream
        'url': 'https://playout.3qsdn.com/d755d94b-4ab9-11e3-9162-0025907ad44f?js=true',
        'only_matching': True,
    }]

    def _extract_from_webpage(self, url, webpage):
        for res in super()._extract_from_webpage(url, webpage):
            yield {
                **res,
                '_type': 'url_transparent',
                'uploader': self._search_regex(r'^(?:https?://)?([^/]*)/.*', url, 'video uploader'),
            }

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

        try:
            config = self._download_json(
                url.replace('://playout.3qsdn.com/', '://playout.3qsdn.com/config/'), video_id)
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
                self.raise_geo_restricted()
            raise

        live = config.get('streamContent') == 'live'
        aspect = float_or_none(config.get('aspect'))

        formats = []
        subtitles = {}
        for source_type, source in (config.get('sources') or {}).items():
            if not source:
                continue
            if source_type == 'dash':
                fmts, subs = self._extract_mpd_formats_and_subtitles(
                    source, video_id, mpd_id='mpd', fatal=False)
                formats.extend(fmts)
                subtitles = self._merge_subtitles(subtitles, subs)
            elif source_type == 'hls':
                fmts, subs = self._extract_m3u8_formats_and_subtitles(
                    source, video_id, 'mp4', live=live, m3u8_id='hls', fatal=False)
                formats.extend(fmts)
                subtitles = self._merge_subtitles(subtitles, subs)
            elif source_type == 'progressive':
                for s in source:
                    src = s.get('src')
                    if not (src and self._is_valid_url(src, video_id)):
                        continue
                    ext = determine_ext(src)
                    height = int_or_none(s.get('height'))
                    formats.append({
                        'ext': ext,
                        'format_id': join_nonempty('http', ext, height and '%dp' % height),
                        'height': height,
                        'source_preference': 0,
                        'url': src,
                        'vcodec': 'none' if height == 0 else None,
                        'width': int(height * aspect) if height and aspect else None,
                    })
        # It seems like this would be correctly handled by default
        # However, unless someone can confirm this, the old
        # behaviour is being kept as-is
        self._sort_formats(formats, ('res', 'source_preference'))

        for subtitle in (config.get('subtitles') or []):
            src = subtitle.get('src')
            if not src:
                continue
            subtitles.setdefault(subtitle.get('label') or 'eng', []).append({
                'url': src,
            })

        title = config.get('title') or video_id

        return {
            'id': video_id,
            'title': title,
            'thumbnail': config.get('poster') or None,
            'description': config.get('description') or None,
            'timestamp': parse_iso8601(config.get('upload_date')),
            'duration': float_or_none(config.get('vlength')) or None,
            'is_live': live,
            'formats': formats,
            'subtitles': subtitles,
        }


code:
import re

from .common import InfoExtractor
from ..utils import (
    try_get,
    unified_strdate,
)

class ThreeSpeakIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?3speak\.tv/watch\?v\=[^/]+/(?P<id>[^/$&#?]+)'

    _TESTS = [{
        'url': 'https://3speak.tv/watch?v=dannyshine/wjgoxyfy',
        'info_dict': {
            'id': 'wjgoxyfy',
            'ext': 'mp4',
            'title': 'Can People who took the Vax think Critically',
            'uploader': 'dannyshine',
            'description': 'md5:181aa7ccb304afafa089b5af3bca7a10',
            'tags': ['sex', 'covid', 'antinatalism', 'comedy', 'vaccines'],
            'thumbnail': 'https://img.3speakcontent.co/wjgoxyfy/thumbnails/default.png',
            'upload_date': '20211021',
            'duration': 2703.867833,
            'filesize': 1620054781,
        },
        'params': {'skip_download': True}
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        json_str = self._html_search_regex(r'JSON\.parse\(\'([^\']+)\'\)', webpage, 'json')
        # The json string itself is escaped. Hence the double parsing
        data_json = self._parse_json(self._parse_json(f'"{json_str}"', id), id)
        video_json = self._parse_json(data_json['json_metadata'], id)
        formats, subtitles = [], {}
        og_m3u8 = self._html_search_regex(r'<meta\s?property=\"ogvideo\"\s?content=\"([^\"]+)\">', webpage, 'og m3u8', fatal=False)
        if og_m3u8:
            https_frmts, https_subs = self._extract_m3u8_formats_and_subtitles(og_m3u8, id, fatal=False, m3u8_id='https')
            formats.extend(https_frmts)
            subtitles = self._merge_subtitles(subtitles, https_subs)
        ipfs_m3u8 = try_get(video_json, lambda x: x['video']['info']['ipfs'])
        if ipfs_m3u8:
            ipfs_frmts, ipfs_subs = self._extract_m3u8_formats_and_subtitles(f'https://ipfs.3speak.tv/ipfs/{ipfs_m3u8}',
                                                                             id, fatal=False, m3u8_id='ipfs')
            formats.extend(ipfs_frmts)
            subtitles = self._merge_subtitles(subtitles, ipfs_subs)
        mp4_file = try_get(video_json, lambda x: x['video']['info']['file'])
        if mp4_file:
            formats.append({
                'url': f'https://threespeakvideo.b-cdn.net/{id}/{mp4_file}',
                'ext': 'mp4',
                'format_id': 'https-mp4',
                'duration': try_get(video_json, lambda x: x['video']['info']['duration']),
                'filesize': try_get(video_json, lambda x: x['video']['info']['filesize']),
                'quality': 11,
                'format_note': 'Original file',
            })
        self._sort_formats(formats)
        return {
            'id': id,
            'title': data_json.get('title') or data_json.get('root_title'),
            'uploader': data_json.get('author'),
            'description': try_get(video_json, lambda x: x['video']['content']['description']),
            'tags': try_get(video_json, lambda x: x['video']['content']['tags']),
            'thumbnail': try_get(video_json, lambda x: x['image'][0]),
            'upload_date': unified_strdate(data_json.get('created')),
            'formats': formats,
            'subtitles': subtitles,
        }

class ThreeSpeakUserIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?3speak\.tv/user/(?P<id>[^/$&?#]+)'

    _TESTS = [{
        'url': 'https://3speak.tv/user/theycallmedan',
        'info_dict': {
            'id': 'theycallmedan',
        },
        'playlist_mincount': 115,
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        entries = [
            self.url_result(
                'https://3speak.tv/watch?v=%s' % video,
                ie=ThreeSpeakIE.ie_key())
            for video in re.findall(r'data-payout\s?\=\s?\"([^\"]+)\"', webpage) if video
        ]
        return self.playlist_result(entries, id)