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

class FourZeroStudioArchiveIE(InfoExtractor):
    _VALID_URL = r'https?://0000\.studio/(?P<uploader_id>[^/]+)/broadcasts/(?P<id>[^/]+)/archive'
    IE_NAME = '0000studio:archive'
    _TESTS = [{
        'url': 'https://0000.studio/mumeijiten/broadcasts/1290f433-fce0-4909-a24a-5f7df09665dc/archive',
        'info_dict': {
            'id': '1290f433-fce0-4909-a24a-5f7df09665dc',
            'title': 'noteで『canape』様へのファンレターを執筆します。（数秘術その2）',
            'timestamp': 1653802534,
            'release_timestamp': 1653796604,
            'thumbnails': 'count:1',
            'comments': 'count:7',
            'uploader': '『中崎雄心』の執務室。',
            'uploader_id': 'mumeijiten',
        }
    }]

    def _real_extract(self, url):
        video_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
        webpage = self._download_webpage(url, video_id)
        nuxt_data = self._search_nuxt_data(webpage, video_id, traverse=None)

        pcb = traverse_obj(nuxt_data, ('ssrRefs', lambda _, v: v['__typename'] == 'PublicCreatorBroadcast'), get_all=False)
        uploader_internal_id = traverse_obj(nuxt_data, (
            'ssrRefs', lambda _, v: v['__typename'] == 'PublicUser', 'id'), get_all=False)

        formats, subs = self._extract_m3u8_formats_and_subtitles(pcb['archiveUrl'], video_id, ext='mp4')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': pcb.get('title'),
            'age_limit': 18 if pcb.get('isAdult') else None,
            'timestamp': unified_timestamp(pcb.get('finishTime')),
            'release_timestamp': unified_timestamp(pcb.get('createdAt')),
            'thumbnails': [{
                'url': pcb['thumbnailUrl'],
                'ext': 'png',
            }] if pcb.get('thumbnailUrl') else None,
            'formats': formats,
            'subtitles': subs,
            'comments': [{
                'author': c.get('username'),
                'author_id': c.get('postedUserId'),
                'author_thumbnail': c.get('userThumbnailUrl'),
                'id': c.get('id'),
                'text': c.get('body'),
                'timestamp': unified_timestamp(c.get('createdAt')),
                'like_count': c.get('likeCount'),
                'is_favorited': c.get('isLikedByOwner'),
                'author_is_uploader': c.get('postedUserId') == uploader_internal_id,
            } for c in traverse_obj(nuxt_data, (
                'ssrRefs', ..., lambda _, v: v['__typename'] == 'PublicCreatorBroadcastComment')) or []],
            'uploader_id': uploader_id,
            'uploader': traverse_obj(nuxt_data, (
                'ssrRefs', lambda _, v: v['__typename'] == 'PublicUser', 'username'), get_all=False),
        }

class FourZeroStudioClipIE(InfoExtractor):
    _VALID_URL = r'https?://0000\.studio/(?P<uploader_id>[^/]+)/archive-clip/(?P<id>[^/]+)'
    IE_NAME = '0000studio:clip'
    _TESTS = [{
        'url': 'https://0000.studio/soeji/archive-clip/e46b0278-24cd-40a8-92e1-b8fc2b21f34f',
        'info_dict': {
            'id': 'e46b0278-24cd-40a8-92e1-b8fc2b21f34f',
            'title': 'わたベーさんからイラスト差し入れいただきました。ありがとうございました！',
            'timestamp': 1652109105,
            'like_count': 1,
            'uploader': 'ソエジマケイタ',
            'uploader_id': 'soeji',
        }
    }]

    def _real_extract(self, url):
        video_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
        webpage = self._download_webpage(url, video_id)
        nuxt_data = self._search_nuxt_data(webpage, video_id, traverse=None)

        clip_info = traverse_obj(nuxt_data, ('ssrRefs', lambda _, v: v['__typename'] == 'PublicCreatorArchivedClip'), get_all=False)

        info = next((
            m for m in self._parse_html5_media_entries(url, webpage, video_id)
            if 'mp4' in traverse_obj(m, ('formats', ..., 'ext'))
        ), None)
        if not info:
            self.report_warning('Failed to find a desired media element. Falling back to using NUXT data.')
            info = {
                'formats': [{
                    'ext': 'mp4',
                    'url': url,
                } for url in clip_info.get('mediaFiles') or [] if url],
            }
        return {
            **info,
            'id': video_id,
            'title': clip_info.get('clipComment'),
            'timestamp': unified_timestamp(clip_info.get('createdAt')),
            'like_count': clip_info.get('likeCount'),
            'uploader_id': uploader_id,
            'uploader': traverse_obj(nuxt_data, (
                'ssrRefs', lambda _, v: v['__typename'] == 'PublicUser', 'username'), get_all=False),
        }


code:
import json
import uuid

from .common import InfoExtractor
from ..compat import (
    compat_HTTPError,
    compat_str,
    compat_urllib_parse_unquote,
)
from ..utils import (
    ExtractorError,
    int_or_none,
    parse_age_limit,
    parse_duration,
    try_get,
    unified_timestamp,
)

class FOXIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?fox\.com/watch/(?P<id>[\da-fA-F]+)'
    _TESTS = [{
        # clip
        'url': 'https://www.fox.com/watch/4b765a60490325103ea69888fb2bd4e8/',
        'md5': 'ebd296fcc41dd4b19f8115d8461a3165',
        'info_dict': {
            'id': '4b765a60490325103ea69888fb2bd4e8',
            'ext': 'mp4',
            'title': 'Aftermath: Bruce Wayne Develops Into The Dark Knight',
            'description': 'md5:549cd9c70d413adb32ce2a779b53b486',
            'duration': 102,
            'timestamp': 1504291893,
            'upload_date': '20170901',
            'creator': 'FOX',
            'series': 'Gotham',
            'age_limit': 14,
            'episode': 'Aftermath: Bruce Wayne Develops Into The Dark Knight'
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # episode, geo-restricted
        'url': 'https://www.fox.com/watch/087036ca7f33c8eb79b08152b4dd75c1/',
        'only_matching': True,
    }, {
        # sports event, geo-restricted
        'url': 'https://www.fox.com/watch/b057484dade738d1f373b3e46216fa2c/',
        'only_matching': True,
    }]
    _GEO_BYPASS = False
    _HOME_PAGE_URL = 'https://www.fox.com/'
    _API_KEY = '6E9S4bmcoNnZwVLOHywOv8PJEdu76cM9'
    _access_token = None
    _device_id = compat_str(uuid.uuid4())

    def _call_api(self, path, video_id, data=None):
        headers = {
            'X-Api-Key': self._API_KEY,
        }
        if self._access_token:
            headers['Authorization'] = 'Bearer ' + self._access_token
        try:
            return self._download_json(
                'https://api3.fox.com/v2.0/' + path,
                video_id, data=data, headers=headers)
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
                entitlement_issues = self._parse_json(
                    e.cause.read().decode(), video_id)['entitlementIssues']
                for e in entitlement_issues:
                    if e.get('errorCode') == 1005:
                        raise ExtractorError(
                            'This video is only available via cable service provider '
                            'subscription. You may want to use --cookies.', expected=True)
                messages = ', '.join([e['message'] for e in entitlement_issues])
                raise ExtractorError(messages, expected=True)
            raise

    def _real_initialize(self):
        if not self._access_token:
            mvpd_auth = self._get_cookies(self._HOME_PAGE_URL).get('mvpd-auth')
            if mvpd_auth:
                self._access_token = (self._parse_json(compat_urllib_parse_unquote(
                    mvpd_auth.value), None, fatal=False) or {}).get('accessToken')
            if not self._access_token:
                self._access_token = self._call_api(
                    'login', None, json.dumps({
                        'deviceId': self._device_id,
                    }).encode())['accessToken']

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

        self._access_token = self._call_api(
            'previewpassmvpd?device_id=%s&mvpd_id=TempPass_fbcfox_60min' % self._device_id,
            video_id)['accessToken']

        video = self._call_api('watch', video_id, data=json.dumps({
            'capabilities': ['drm/widevine', 'fsdk/yo'],
            'deviceWidth': 1280,
            'deviceHeight': 720,
            'maxRes': '720p',
            'os': 'macos',
            'osv': '',
            'provider': {
                'freewheel': {'did': self._device_id},
                'vdms': {'rays': ''},
                'dmp': {'kuid': '', 'seg': ''}
            },
            'playlist': '',
            'privacy': {'us': '1---'},
            'siteSection': '',
            'streamType': 'vod',
            'streamId': video_id}).encode('utf-8'))

        title = video['name']
        release_url = video['url']

        try:
            m3u8_url = self._download_json(release_url, video_id)['playURL']
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
                error = self._parse_json(e.cause.read().decode(), video_id)
                if error.get('exception') == 'GeoLocationBlocked':
                    self.raise_geo_restricted(countries=['US'])
                raise ExtractorError(error['description'], expected=True)
            raise
        formats = self._extract_m3u8_formats(
            m3u8_url, video_id, 'mp4',
            entry_protocol='m3u8_native', m3u8_id='hls')
        self._sort_formats(formats)

        data = try_get(
            video, lambda x: x['trackingData']['properties'], dict) or {}

        duration = int_or_none(video.get('durationInSeconds')) or int_or_none(
            video.get('duration')) or parse_duration(video.get('duration'))
        timestamp = unified_timestamp(video.get('datePublished'))
        creator = data.get('brand') or data.get('network') or video.get('network')
        series = video.get('seriesName') or data.get(
            'seriesName') or data.get('show')

        subtitles = {}
        for doc_rel in video.get('documentReleases', []):
            rel_url = doc_rel.get('url')
            if not url or doc_rel.get('format') != 'SCC':
                continue
            subtitles['en'] = [{
                'url': rel_url,
                'ext': 'scc',
            }]
            break

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'description': video.get('description'),
            'duration': duration,
            'timestamp': timestamp,
            'age_limit': parse_age_limit(video.get('contentRating')),
            'creator': creator,
            'series': series,
            'season_number': int_or_none(video.get('seasonNumber')),
            'episode': video.get('name'),
            'episode_number': int_or_none(video.get('episodeNumber')),
            'release_year': int_or_none(video.get('releaseYear')),
            'subtitles': subtitles,
        }

repo_name: Lesmiscore/ytdl


code:
from .common import InfoExtractor

class FOX9IE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?fox9\.com/video/(?P<id>\d+)'

    def _real_extract(self, url):
        video_id = self._match_id(url)
        return self.url_result(
            'anvato:anvato_epfox_app_web_prod_b3373168e12f423f41504f207000188daf88251b:' + video_id,
            'Anvato', video_id)

class FOX9NewsIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?fox9\.com/news/(?P<id>[^/?&#]+)'
    _TEST = {
        'url': 'https://www.fox9.com/news/black-bear-in-tree-draws-crowd-in-downtown-duluth-minnesota',
        'md5': 'd6e1b2572c3bab8a849c9103615dd243',
        'info_dict': {
            'id': '314473',
            'ext': 'mp4',
            'title': 'Bear climbs tree in downtown Duluth',
            'description': 'md5:6a36bfb5073a411758a752455408ac90',
            'duration': 51,
            'timestamp': 1478123580,
            'upload_date': '20161102',
            'uploader': 'EPFOX',
            'categories': ['News', 'Sports'],
            'tags': ['news', 'video'],
        },
    }

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        anvato_id = self._search_regex(
            r'anvatoId\s*:\s*[\'"](\d+)', webpage, 'anvato id')
        return self.url_result('https://www.fox9.com/video/' + anvato_id, 'FOX9')


code:
import itertools

from .common import InfoExtractor
from ..utils import (
    get_element_by_id,
    int_or_none,
    remove_end,
)

class FoxgayIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?foxgay\.com/videos/(?:\S+-)?(?P<id>\d+)\.shtml'
    _TEST = {
        'url': 'http://foxgay.com/videos/fuck-turkish-style-2582.shtml',
        'md5': '344558ccfea74d33b7adbce22e577f54',
        'info_dict': {
            'id': '2582',
            'ext': 'mp4',
            'title': 'Fuck Turkish-style',
            'description': 'md5:6ae2d9486921891efe89231ace13ffdf',
            'age_limit': 18,
            'thumbnail': r're:https?://.*\.jpg$',
        },
    }

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

        title = remove_end(self._html_extract_title(webpage), ' - Foxgay.com')
        description = get_element_by_id('inf_tit', webpage)

        # The default user-agent with foxgay cookies leads to pages without videos
        self.cookiejar.clear('.foxgay.com')
        # Find the URL for the iFrame which contains the actual video.
        iframe_url = self._html_search_regex(
            r'<iframe[^>]+src=([\'"])(?P<url>[^\'"]+)\1', webpage,
            'video frame', group='url')
        iframe = self._download_webpage(
            iframe_url, video_id, headers={'User-Agent': 'curl/7.50.1'},
            note='Downloading video frame')
        video_data = self._parse_json(self._search_regex(
            r'video_data\s*=\s*([^;]+);', iframe, 'video data'), video_id)

        formats = [{
            'url': source,
            'height': int_or_none(resolution),
        } for source, resolution in zip(
            video_data['sources'], video_data.get('resolutions', itertools.repeat(None)))]

        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'description': description,
            'thumbnail': video_data.get('act_vid', {}).get('thumb'),
            'age_limit': 18,
        }


code:
import re

from .amp import AMPIE
from .common import InfoExtractor

class FoxNewsIE(AMPIE):
    IE_NAME = 'foxnews'
    IE_DESC = 'Fox News and Fox Business Video'
    _VALID_URL = r'https?://(?P<host>video\.(?:insider\.)?fox(?:news|business)\.com)/v/(?:video-embed\.html\?video_id=)?(?P<id>\d+)'
    _TESTS = [
        {
            'url': 'http://video.foxnews.com/v/3937480/frozen-in-time/#sp=show-clips',
            'md5': '32aaded6ba3ef0d1c04e238d01031e5e',
            'info_dict': {
                'id': '3937480',
                'ext': 'flv',
                'title': 'Frozen in Time',
                'description': '16-year-old girl is size of toddler',
                'duration': 265,
                'timestamp': 1304411491,
                'upload_date': '20110503',
                'thumbnail': r're:^https?://.*\.jpg$',
            },
        },
        {
            'url': 'http://video.foxnews.com/v/3922535568001/rep-luis-gutierrez-on-if-obamas-immigration-plan-is-legal/#sp=show-clips',
            'md5': '5846c64a1ea05ec78175421b8323e2df',
            'info_dict': {
                'id': '3922535568001',
                'ext': 'mp4',
                'title': "Rep. Luis Gutierrez on if Obama's immigration plan is legal",
                'description': "Congressman discusses president's plan",
                'duration': 292,
                'timestamp': 1417662047,
                'upload_date': '20141204',
                'thumbnail': r're:^https?://.*\.jpg$',
            },
            'params': {
                # m3u8 download
                'skip_download': True,
            },
        },
        {
            'url': 'http://video.foxnews.com/v/video-embed.html?video_id=3937480&d=video.foxnews.com',
            'only_matching': True,
        },
        {
            'url': 'http://video.foxbusiness.com/v/4442309889001',
            'only_matching': True,
        },
        {
            # From http://insider.foxnews.com/2016/08/25/univ-wisconsin-student-group-pushing-silence-certain-words
            'url': 'http://video.insider.foxnews.com/v/video-embed.html?video_id=5099377331001&autoplay=true&share_url=http://insider.foxnews.com/2016/08/25/univ-wisconsin-student-group-pushing-silence-certain-words&share_title=Student%20Group:%20Saying%20%27Politically%20Correct,%27%20%27Trash%27%20and%20%27Lame%27%20Is%20Offensive&share=true',
            'only_matching': True,
        },
    ]

    @classmethod
    def _extract_embed_urls(cls, url, webpage):
        for mobj in re.finditer(
                r'''(?x)
                    <(?:script|(?:amp-)?iframe)[^>]+\bsrc=["\']
                    (?:https?:)?//video\.foxnews\.com/v/(?:video-embed\.html|embed\.js)\?
                    (?:[^>"\']+&)?(?:video_)?id=(?P<video_id>\d+)
                ''', webpage):
            yield f'https://video.foxnews.com/v/video-embed.html?video_id={mobj.group("video_id")}'

    def _real_extract(self, url):
        host, video_id = self._match_valid_url(url).groups()

        info = self._extract_feed_info(
            'http://%s/v/feed/video/%s.js?template=fox' % (host, video_id))
        info['id'] = video_id
        return info

class FoxNewsArticleIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?(?:insider\.)?foxnews\.com/(?!v)([^/]+/)+(?P<id>[a-z-]+)'
    IE_NAME = 'foxnews:article'

    _TESTS = [{
        # data-video-id
        'url': 'http://www.foxnews.com/politics/2016/09/08/buzz-about-bud-clinton-camp-denies-claims-wore-earpiece-at-forum.html',
        'md5': '83d44e1aff1433e7a29a7b537d1700b5',
        'info_dict': {
            'id': '5116295019001',
            'ext': 'mp4',
            'title': 'Trump and Clinton asked to defend positions on Iraq War',
            'description': 'Veterans react on \'The Kelly File\'',
            'timestamp': 1473301045,
            'upload_date': '20160908',
        },
    }, {
        # iframe embed
        'url': 'http://www.foxnews.com/us/2018/03/09/parkland-survivor-kyle-kashuv-on-meeting-trump-his-app-to-prevent-another-school-shooting.amp.html?__twitter_impression=true',
        'info_dict': {
            'id': '5748266721001',
            'ext': 'flv',
            'title': 'Kyle Kashuv has a positive message for the Trump White House',
            'description': 'Marjory Stoneman Douglas student disagrees with classmates.',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 229,
            'timestamp': 1520594670,
            'upload_date': '20180309',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://insider.foxnews.com/2016/08/25/univ-wisconsin-student-group-pushing-silence-certain-words',
        'only_matching': True,
    }]

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

        video_id = self._html_search_regex(
            r'data-video-id=([\'"])(?P<id>[^\'"]+)\1',
            webpage, 'video ID', group='id', default=None)
        if video_id:
            return self.url_result(
                'http://video.foxnews.com/v/' + video_id, FoxNewsIE.ie_key())

        return self.url_result(
            next(FoxNewsIE._extract_embed_urls(url, webpage)), FoxNewsIE.ie_key())


code:
from .common import InfoExtractor

class FoxSportsIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?foxsports\.com/(?:[^/]+/)*video/(?P<id>\d+)'

    _TEST = {
        'url': 'http://www.foxsports.com/tennessee/video/432609859715',
        'md5': 'b49050e955bebe32c301972e4012ac17',
        'info_dict': {
            'id': '432609859715',
            'ext': 'mp4',
            'title': 'Courtney Lee on going up 2-0 in series vs. Blazers',
            'description': 'Courtney Lee talks about Memphis being focused.',
            # TODO: fix timestamp
            'upload_date': '19700101',  # '20150423',
            # 'timestamp': 1429761109,
            'uploader': 'NEWA-FNG-FOXSPORTS',
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
        'add_ie': ['ThePlatform'],
    }

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

        return self.url_result(
            'https://feed.theplatform.com/f/BKQ29B/foxsports-all?byId=' + video_id, 'ThePlatformFeed')


code:
import hashlib
import time
import urllib.parse

from .common import InfoExtractor
from ..utils import (
    clean_html,
    join_nonempty,
    strip_or_none,
)

class FptplayIE(InfoExtractor):
    _VALID_URL = r'https?://fptplay\.vn/xem-video/[^/]+\-(?P<id>\w+)(?:/tap-(?P<episode>\d+)?/?(?:[?#]|$)|)'
    _GEO_COUNTRIES = ['VN']
    IE_NAME = 'fptplay'
    IE_DESC = 'fptplay.vn'
    _TESTS = [{
        'url': 'https://fptplay.vn/xem-video/nhan-duyen-dai-nhan-xin-dung-buoc-621a123016f369ebbde55945',
        'md5': 'ca0ee9bc63446c0c3e9a90186f7d6b33',
        'info_dict': {
            'id': '621a123016f369ebbde55945',
            'ext': 'mp4',
            'title': 'Nhân Duyên Đại Nhân Xin Dừng Bước - Tập 1A',
            'description': 'md5:23cf7d1ce0ade8e21e76ae482e6a8c6c',
        },
    }, {
        'url': 'https://fptplay.vn/xem-video/ma-toi-la-dai-gia-61f3aa8a6b3b1d2e73c60eb5/tap-3',
        'md5': 'b35be968c909b3e4e1e20ca45dd261b1',
        'info_dict': {
            'id': '61f3aa8a6b3b1d2e73c60eb5',
            'ext': 'mp4',
            'title': 'Má Tôi Là Đại Gia - Tập 3',
            'description': 'md5:ff8ba62fb6e98ef8875c42edff641d1c',
        },
    }, {
        'url': 'https://fptplay.vn/xem-video/lap-toi-do-giam-under-the-skin-6222d9684ec7230fa6e627a2/tap-4',
        'md5': 'bcb06c55ec14786d7d4eda07fa1ccbb9',
        'info_dict': {
            'id': '6222d9684ec7230fa6e627a2',
            'ext': 'mp4',
            'title': 'Lạp Tội Đồ Giám - Tập 2B',
            'description': 'md5:e5a47e9d35fbf7e9479ca8a77204908b',
        },
    }, {
        'url': 'https://fptplay.vn/xem-video/nha-co-chuyen-hi-alls-well-ends-well-1997-6218995f6af792ee370459f0',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id, slug_episode = self._match_valid_url(url).group('id', 'episode')
        webpage = self._download_webpage(url, video_id=video_id, fatal=False) or ''
        title = self._search_regex(
            r'(?s)
]*>(.+)
', webpage, 'title', fatal=False)
        real_episode = slug_episode if not title else self._search_regex(
            r'<p.+title="(?P<episode>[^">]+)"\s+class="epi-title active"', webpage, 'episode', fatal=False)
        title = strip_or_none(title) or self._html_search_meta(('og:title', 'twitter:title'), webpage)

        info = self._download_json(
            self.get_api_with_st_token(video_id, int(slug_episode) - 1 if slug_episode else 0), video_id)
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(info['data']['url'], video_id, 'mp4')
        self._sort_formats(formats)
        return {
            'id': video_id,
            'title': join_nonempty(title, real_episode, delim=' - '),
            'description': (
                clean_html(self._search_regex(r'
]*>(.+)
', webpage, 'description'))
                or self._html_search_meta(('og:description', 'twitter:description'), webpage)),
            'formats': formats,
            'subtitles': subtitles,
        }

    def get_api_with_st_token(self, video_id, episode):
        path = f'/api/v6.2_w/stream/vod/{video_id}/{episode}/auto_vip'
        timestamp = int(time.time()) + 10800

        t = hashlib.md5(f'WEBv6Dkdsad90dasdjlALDDDS{timestamp}{path}'.encode()).hexdigest().upper()
        r = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
        n = [int(f'0x{t[2 * o: 2 * o + 2]}', 16) for o in range(len(t) // 2)]

        def convert(e):
            t = ''
            n = 0
            i = [0, 0, 0]
            a = [0, 0, 0, 0]
            s = len(e)
            c = 0
            for z in range(s, 0, -1):
                if n <= 3:
                    i[n] = e[c]
                n += 1
                c += 1
                if 3 == n:
                    a[0] = (252 & i[0]) >> 2
                    a[1] = ((3 & i[0]) << 4) + ((240 & i[1]) >> 4)
                    a[2] = ((15 & i[1]) << 2) + ((192 & i[2]) >> 6)
                    a[3] = (63 & i[2])
                    for v in range(4):
                        t += r[a[v]]
                    n = 0
            if n:
                for o in range(n, 3):
                    i[o] = 0

                for o in range(n + 1):
                    a[0] = (252 & i[0]) >> 2
                    a[1] = ((3 & i[0]) << 4) + ((240 & i[1]) >> 4)
                    a[2] = ((15 & i[1]) << 2) + ((192 & i[2]) >> 6)
                    a[3] = (63 & i[2])
                    t += r[a[o]]
                n += 1
                while n < 3:
                    t += ''
                    n += 1
            return t

        st_token = convert(n).replace('+', '-').replace('/', '_').replace('=', '')
        return f'https://api.fptplay.net{path}?{urllib.parse.urlencode({"st": st_token, "e": timestamp})}'


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

class FranceInterIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?franceinter\.fr/emissions/(?P<id>[^?#]+)'

    _TEST = {
        'url': 'https://www.franceinter.fr/emissions/affaires-sensibles/affaires-sensibles-07-septembre-2016',
        'md5': '9e54d7bdb6fdc02a841007f8a975c094',
        'info_dict': {
            'id': 'affaires-sensibles/affaires-sensibles-07-septembre-2016',
            'ext': 'mp3',
            'title': 'Affaire Cahuzac : le contentieux du compte en Suisse',
            'description': 'md5:401969c5d318c061f86bda1fa359292b',
            'thumbnail': r're:^https?://.*\.jpg',
            'upload_date': '20160907',
        },
    }

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

        webpage = self._download_webpage(url, video_id)

        video_url = self._search_regex(
            r'(?s)<div[^>]+class=["\']page-diffusion["\'][^>]*>.*?<button[^>]+data-url=(["\'])(?P<url>(?:(?!\1).)+)\1',
            webpage, 'video url', group='url')

        title = self._og_search_title(webpage)
        description = self._og_search_description(webpage)
        thumbnail = self._html_search_meta(['og:image', 'twitter:image'], webpage)

        upload_date_str = self._search_regex(
            r'class=["\']\s*cover-emission-period\s*["\'][^>]*>[^<]+\s+(\d{1,2}\s+[^\s]+\s+\d{4})<',
            webpage, 'upload date', fatal=False)
        if upload_date_str:
            upload_date_list = upload_date_str.split()
            upload_date_list.reverse()
            upload_date_list[1] = '%02d' % (month_by_name(upload_date_list[1], lang='fr') or 0)
            upload_date_list[2] = '%02d' % int(upload_date_list[2])
            upload_date = ''.join(upload_date_list)
        else:
            upload_date = None

        return {
            'id': video_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'upload_date': upload_date,
            'formats': [{
                'url': video_url,
                'vcodec': 'none',
            }],
        }


code:
from .common import InfoExtractor
from ..utils import (
    determine_ext,
    ExtractorError,
    format_field,
    parse_iso8601,
    parse_qs,
)
from .dailymotion import DailymotionIE

class FranceTVBaseInfoExtractor(InfoExtractor):
    def _make_url_result(self, video_or_full_id, catalog=None):
        full_id = 'francetv:%s' % video_or_full_id
        if '@' not in video_or_full_id and catalog:
            full_id += '@%s' % catalog
        return self.url_result(
            full_id, ie=FranceTVIE.ie_key(),
            video_id=video_or_full_id.split('@')[0])

class FranceTVIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    (?:
                        https?://
                            sivideo\.webservices\.francetelevisions\.fr/tools/getInfosOeuvre/v2/\?
                            .*?\bidDiffusion=[^&]+|
                        (?:
                            https?://videos\.francetv\.fr/video/|
                            francetv:
                        )
                        (?P<id>[^@]+)(?:@(?P<catalog>.+))?
                    )
                    '''
    _EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?://)?embed\.francetv\.fr/\?ue=.+?)\1']

    _TESTS = [{
        # without catalog
        'url': 'https://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion=162311093&callback=_jsonp_loader_callback_request_0',
        'md5': 'c2248a8de38c4e65ea8fae7b5df2d84f',
        'info_dict': {
            'id': '162311093',
            'ext': 'mp4',
            'title': '13h15, le dimanche... - Les mystères de Jésus',
            'description': 'md5:75efe8d4c0a8205e5904498ffe1e1a42',
            'timestamp': 1502623500,
            'upload_date': '20170813',
        },
    }, {
        # with catalog
        'url': 'https://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion=NI_1004933&catalogue=Zouzous&callback=_jsonp_loader_callback_request_4',
        'only_matching': True,
    }, {
        'url': 'http://videos.francetv.fr/video/NI_657393@Regions',
        'only_matching': True,
    }, {
        'url': 'francetv:162311093',
        'only_matching': True,
    }, {
        'url': 'francetv:NI_1004933@Zouzous',
        'only_matching': True,
    }, {
        'url': 'francetv:NI_983319@Info-web',
        'only_matching': True,
    }, {
        'url': 'francetv:NI_983319',
        'only_matching': True,
    }, {
        'url': 'francetv:NI_657393@Regions',
        'only_matching': True,
    }, {
        # france-3 live
        'url': 'francetv:SIM_France3',
        'only_matching': True,
    }]

    def _extract_video(self, video_id, catalogue=None):
        # Videos are identified by idDiffusion so catalogue part is optional.
        # However when provided, some extra formats may be returned so we pass
        # it if available.
        is_live = None
        videos = []
        title = None
        subtitle = None
        image = None
        duration = None
        timestamp = None
        spritesheets = None

        for device_type in ('desktop', 'mobile'):
            dinfo = self._download_json(
                'https://player.webservices.francetelevisions.fr/v1/videos/%s' % video_id,
                video_id, 'Downloading %s video JSON' % device_type, query={
                    'device_type': device_type,
                    'browser': 'chrome',
                }, fatal=False)

            if not dinfo:
                continue

            video = dinfo.get('video')
            if video:
                videos.append(video)
                if duration is None:
                    duration = video.get('duration')
                if is_live is None:
                    is_live = video.get('is_live')
                if spritesheets is None:
                    spritesheets = video.get('spritesheets')

            meta = dinfo.get('meta')
            if meta:
                if title is None:
                    title = meta.get('title')
                # XXX: what is meta['pre_title']?
                if subtitle is None:
                    subtitle = meta.get('additional_title')
                if image is None:
                    image = meta.get('image_url')
                if timestamp is None:
                    timestamp = parse_iso8601(meta.get('broadcasted_at'))

        formats = []
        subtitles = {}
        for video in videos:
            format_id = video.get('format')

            video_url = None
            if video.get('workflow') == 'token-akamai':
                token_url = video.get('token')
                if token_url:
                    token_json = self._download_json(
                        token_url, video_id,
                        'Downloading signed %s manifest URL' % format_id)
                    if token_json:
                        video_url = token_json.get('url')
            if not video_url:
                video_url = video.get('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':
                fmts, subs = self._extract_m3u8_formats_and_subtitles(
                    video_url, video_id, 'mp4',
                    entry_protocol='m3u8_native', m3u8_id=format_id,
                    fatal=False)
                formats.extend(fmts)
                self._merge_subtitles(subs, target=subtitles)
            elif ext == 'mpd':
                fmts, subs = self._extract_mpd_formats_and_subtitles(
                    video_url, video_id, mpd_id=format_id, fatal=False)
                formats.extend(fmts)
                self._merge_subtitles(subs, target=subtitles)
            elif video_url.startswith('rtmp'):
                formats.append({
                    'url': video_url,
                    'format_id': 'rtmp-%s' % format_id,
                    'ext': 'flv',
                })
            else:
                if self._is_valid_url(video_url, video_id, format_id):
                    formats.append({
                        'url': video_url,
                        'format_id': format_id,
                    })

            # XXX: what is video['captions']?

        for f in formats:
            if f.get('acodec') != 'none' and f.get('language') in ('qtz', 'qad'):
                f['language_preference'] = -10
                f['format_note'] = 'audio description%s' % format_field(f, 'format_note', ', %s')

        if spritesheets:
            formats.append({
                'format_id': 'spritesheets',
                'format_note': 'storyboard',
                'acodec': 'none',
                'vcodec': 'none',
                'ext': 'mhtml',
                'protocol': 'mhtml',
                'url': 'about:invalid',
                'fragments': [{
                    'url': sheet,
                    # XXX: not entirely accurate; each spritesheet seems to be
                    # a 10×10 grid of thumbnails corresponding to approximately
                    # 2 seconds of the video; the last spritesheet may be shorter
                    'duration': 200,
                } for sheet in spritesheets]
            })

        self._sort_formats(formats)

        if subtitle:
            title += ' - %s' % subtitle
        title = title.strip()

        return {
            'id': video_id,
            'title': title,
            'thumbnail': image,
            'duration': duration,
            'timestamp': timestamp,
            'is_live': is_live,
            'formats': formats,
            'subtitles': subtitles,
        }

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

        if not video_id:
            qs = parse_qs(url)
            video_id = qs.get('idDiffusion', [None])[0]
            catalog = qs.get('catalogue', [None])[0]
            if not video_id:
                raise ExtractorError('Invalid URL', expected=True)

        return self._extract_video(video_id, catalog)

class FranceTVSiteIE(FranceTVBaseInfoExtractor):
    _VALID_URL = r'https?://(?:(?:www\.)?france\.tv|mobile\.france\.tv)/(?:[^/]+/)*(?P<id>[^/]+)\.html'

    _TESTS = [{
        'url': 'https://www.france.tv/france-2/13h15-le-dimanche/140921-les-mysteres-de-jesus.html',
        'info_dict': {
            'id': 'ec217ecc-0733-48cf-ac06-af1347b849d1',
            'ext': 'mp4',
            'title': '13h15, le dimanche... - Les mystères de Jésus',
            'description': 'md5:75efe8d4c0a8205e5904498ffe1e1a42',
            'timestamp': 1502623500,
            'upload_date': '20170813',
        },
        'params': {
            'skip_download': True,
        },
        'add_ie': [FranceTVIE.ie_key()],
    }, {
        # france3
        'url': 'https://www.france.tv/france-3/des-chiffres-et-des-lettres/139063-emission-du-mardi-9-mai-2017.html',
        'only_matching': True,
    }, {
        # france4
        'url': 'https://www.france.tv/france-4/hero-corp/saison-1/134151-apres-le-calme.html',
        'only_matching': True,
    }, {
        # france5
        'url': 'https://www.france.tv/france-5/c-a-dire/saison-10/137013-c-a-dire.html',
        'only_matching': True,
    }, {
        # franceo
        'url': 'https://www.france.tv/france-o/archipels/132249-mon-ancetre-l-esclave.html',
        'only_matching': True,
    }, {
        # france2 live
        'url': 'https://www.france.tv/france-2/direct.html',
        'only_matching': True,
    }, {
        'url': 'https://www.france.tv/documentaires/histoire/136517-argentine-les-500-bebes-voles-de-la-dictature.html',
        'only_matching': True,
    }, {
        'url': 'https://www.france.tv/jeux-et-divertissements/divertissements/133965-le-web-contre-attaque.html',
        'only_matching': True,
    }, {
        'url': 'https://mobile.france.tv/france-5/c-dans-l-air/137347-emission-du-vendredi-12-mai-2017.html',
        'only_matching': True,
    }, {
        'url': 'https://www.france.tv/142749-rouge-sang.html',
        'only_matching': True,
    }, {
        # france-3 live
        'url': 'https://www.france.tv/france-3/direct.html',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        catalogue = None
        video_id = self._search_regex(
            r'(?:data-main-video\s*=|videoId["\']?\s*[:=])\s*(["\'])(?P<id>(?:(?!\1).)+)\1',
            webpage, 'video id', default=None, group='id')

        if not video_id:
            video_id, catalogue = self._html_search_regex(
                r'(?:href=|player\.setVideo\(\s*)"http://videos?\.francetv\.fr/video/([^@]+@[^"]+)"',
                webpage, 'video ID').split('@')

        return self._make_url_result(video_id, catalogue)

class FranceTVInfoIE(FranceTVBaseInfoExtractor):
    IE_NAME = 'francetvinfo.fr'
    _VALID_URL = r'https?://(?:www|mobile|france3-regions)\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&.]+)'

    _TESTS = [{
        'url': 'https://www.francetvinfo.fr/replay-jt/france-3/soir-3/jt-grand-soir-3-jeudi-22-aout-2019_3561461.html',
        'info_dict': {
            'id': 'd12458ee-5062-48fe-bfdd-a30d6a01b793',
            'ext': 'mp4',
            'title': 'Soir 3',
            'upload_date': '20190822',
            'timestamp': 1566510900,
            'description': 'md5:72d167097237701d6e8452ff03b83c00',
            'subtitles': {
                'fr': 'mincount:2',
            },
        },
        'params': {
            'skip_download': True,
        },
        'add_ie': [FranceTVIE.ie_key()],
    }, {
        'note': 'Only an image exists in initial webpage instead of the video',
        'url': 'https://www.francetvinfo.fr/sante/maladie/coronavirus/covid-19-en-inde-une-situation-catastrophique-a-new-dehli_4381095.html',
        'info_dict': {
            'id': '7d204c9e-a2d3-11eb-9e4c-000d3a23d482',
            'ext': 'mp4',
            'title': 'Covid-19 : une situation catastrophique à New Dehli',
            'thumbnail': str,
            'duration': 76,
            'timestamp': 1619028518,
            'upload_date': '20210421',
        },
        'params': {
            'skip_download': True,
        },
        'add_ie': [FranceTVIE.ie_key()],
    }, {
        'url': 'http://www.francetvinfo.fr/elections/europeennes/direct-europeennes-regardez-le-debat-entre-les-candidats-a-la-presidence-de-la-commission_600639.html',
        'only_matching': True,
    }, {
        'url': 'http://www.francetvinfo.fr/economie/entreprises/les-entreprises-familiales-le-secret-de-la-reussite_933271.html',
        'only_matching': True,
    }, {
        'url': 'http://france3-regions.francetvinfo.fr/bretagne/cotes-d-armor/thalassa-echappee-breizh-ce-venredi-dans-les-cotes-d-armor-954961.html',
        'only_matching': True,
    }, {
        # Dailymotion embed
        'url': 'http://www.francetvinfo.fr/politique/notre-dame-des-landes/video-sur-france-inter-cecile-duflot-denonce-le-regard-meprisant-de-patrick-cohen_1520091.html',
        'md5': 'ee7f1828f25a648addc90cb2687b1f12',
        'info_dict': {
            'id': 'x4iiko0',
            'ext': 'mp4',
            'title': 'NDDL, référendum, Brexit : Cécile Duflot répond à Patrick Cohen',
            'description': 'Au lendemain de la victoire du "oui" au référendum sur l\'aéroport de Notre-Dame-des-Landes, l\'ancienne ministre écologiste est l\'invitée de Patrick Cohen. Plus d\'info : https://www.franceinter.fr/emissions/le-7-9/le-7-9-27-juin-2016',
            'timestamp': 1467011958,
            'upload_date': '20160627',
            'uploader': 'France Inter',
            'uploader_id': 'x2q2ez',
        },
        'add_ie': ['Dailymotion'],
    }, {
        'url': 'http://france3-regions.francetvinfo.fr/limousin/emissions/jt-1213-limousin',
        'only_matching': True,
    }, {
        # "<figure id=" pattern (#28792)
        'url': 'https://www.francetvinfo.fr/culture/patrimoine/incendie-de-notre-dame-de-paris/notre-dame-de-paris-de-l-incendie-de-la-cathedrale-a-sa-reconstruction_4372291.html',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        dailymotion_urls = tuple(DailymotionIE._extract_embed_urls(url, webpage))
        if dailymotion_urls:
            return self.playlist_result([
                self.url_result(dailymotion_url, DailymotionIE.ie_key())
                for dailymotion_url in dailymotion_urls])

        video_id = self._search_regex(
            (r'player\.load[^;]+src:\s*["\']([^"\']+)',
             r'id-video=([^@]+@[^"]+)',
             r'<a[^>]+href="(?:https?:)?//videos\.francetv\.fr/video/([^@]+@[^"]+)"',
             r'(?:data-id|<figure[^<]+\bid)=["\']([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'),
            webpage, 'video id')

        return self._make_url_result(video_id)


code:
import re

from .common import InfoExtractor
from ..utils import (
    float_or_none,
    get_element_by_class,
    get_element_by_id,
    unified_strdate,
)

class FreesoundIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?freesound\.org/people/[^/]+/sounds/(?P<id>[^/]+)'
    _TEST = {
        'url': 'http://www.freesound.org/people/miklovan/sounds/194503/',
        'md5': '12280ceb42c81f19a515c745eae07650',
        'info_dict': {
            'id': '194503',
            'ext': 'mp3',
            'title': 'gulls in the city.wav',
            'description': 'the sounds of seagulls in the city',
            'duration': 130.233,
            'uploader': 'miklovan',
            'upload_date': '20130715',
            'tags': list,
        }
    }

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

        webpage = self._download_webpage(url, audio_id)

        audio_url = self._og_search_property('audio', webpage, 'song url')
        title = self._og_search_property('audio:title', webpage, 'song title')

        description = self._html_search_regex(
            r'(?s)id=["\']sound_description["\'][^>]*>(.+?)
',
            webpage, 'description', fatal=False)

        duration = float_or_none(
            get_element_by_class('duration', webpage), scale=1000)

        upload_date = unified_strdate(get_element_by_id('sound_date', webpage))
        uploader = self._og_search_property(
            'audio:artist', webpage, 'uploader', fatal=False)

        channels = self._html_search_regex(
            r'Channels

(.+?)
', webpage,
            'channels info', fatal=False)

        tags_str = get_element_by_class('tags', webpage)
        tags = re.findall(r']+>([^<]+)', tags_str) if tags_str else None

        audio_urls = [audio_url]

        LQ_FORMAT = '-lq.mp3'
        if LQ_FORMAT in audio_url:
            audio_urls.append(audio_url.replace(LQ_FORMAT, '-hq.mp3'))

        formats = [{
            'url': format_url,
            'format_note': channels,
            'quality': quality,
        } for quality, format_url in enumerate(audio_urls)]
        self._sort_formats(formats)

        return {
            'id': audio_id,
            'title': title,
            'description': description,
            'duration': duration,
            'uploader': uploader,
            'upload_date': upload_date,
            'tags': tags,
            'formats': formats,
        }


code:
from .common import InfoExtractor
from .youtube import YoutubeIE

class FreespeechIE(InfoExtractor):
    IE_NAME = 'freespeech.org'
    _VALID_URL = r'https?://(?:www\.)?freespeech\.org/stories/(?P<id>.+)'
    _TEST = {
        'add_ie': ['Youtube'],
        'url': 'http://www.freespeech.org/stories/fcc-announces-net-neutrality-rollback-whats-stake/',
        'info_dict': {
            'id': 'waRk6IPqyWM',
            'ext': 'mp4',
            'title': 'What\'s At Stake - Net Neutrality Special',
            'description': 'Presented by MNN and FSTV',
            'upload_date': '20170728',
            'uploader_id': 'freespeechtv',
            'uploader': 'freespeechtv',
        },
    }

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        youtube_url = self._search_regex(
            r'data-video-url="([^"]+)"',
            webpage, 'youtube url')

        return self.url_result(youtube_url, YoutubeIE.ie_key())


code:
import itertools
import re

from .common import InfoExtractor
from ..utils import int_or_none, traverse_obj, urlencode_postdata

class FreeTvBaseIE(InfoExtractor):
    def _get_api_response(self, content_id, resource_type, postdata):
        return self._download_json(
            'https://www.freetv.com/wordpress/wp-admin/admin-ajax.php',
            content_id, data=urlencode_postdata(postdata),
            note=f'Downloading {content_id} {resource_type} JSON')['data']

class FreeTvMoviesIE(FreeTvBaseIE):
    _VALID_URL = r'https?://(?:www\.)?freetv\.com/peliculas/(?P<id>[^/]+)'
    _TESTS = [{
        'url': 'https://www.freetv.com/peliculas/atrapame-si-puedes/',
        'md5': 'dc62d5abf0514726640077cd1591aa92',
        'info_dict': {
            'id': '428021',
            'title': 'Atrápame Si Puedes',
            'description': 'md5:ca63bc00898aeb2f64ec87c6d3a5b982',
            'ext': 'mp4',
        }
    }, {
        'url': 'https://www.freetv.com/peliculas/monstruoso/',
        'md5': '509c15c68de41cb708d1f92d071f20aa',
        'info_dict': {
            'id': '377652',
            'title': 'Monstruoso',
            'description': 'md5:333fc19ee327b457b980e54a911ea4a3',
            'ext': 'mp4',
        }
    }]

    def _extract_video(self, content_id, action='olyott_video_play'):
        api_response = self._get_api_response(content_id, 'video', {
            'action': action,
            'contentID': content_id,
        })

        video_id, video_url = api_response['displayMeta']['contentID'], api_response['displayMeta']['streamURLVideo']
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(video_url, video_id, 'mp4')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': traverse_obj(api_response, ('displayMeta', 'title')),
            'description': traverse_obj(api_response, ('displayMeta', 'desc')),
            'formats': formats,
            'subtitles': subtitles,
        }

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

        return self._extract_video(
            self._search_regex((
                r'class=["\'][^>]+postid-(?P<video_id>\d+)',
                r'<link[^>]+freetv.com/\?p=(?P<video_id>\d+)',
                r'<div[^>]+data-params=["\'][^>]+post_id=(?P<video_id>\d+)',
            ), webpage, 'video id', group='video_id'))

class FreeTvIE(FreeTvBaseIE):
    IE_NAME = 'freetv:series'
    _VALID_URL = r'https?://(?:www\.)?freetv\.com/series/(?P<id>[^/]+)'
    _TESTS = [{
        'url': 'https://www.freetv.com/series/el-detective-l/',
        'info_dict': {
            'id': 'el-detective-l',
            'title': 'El Detective L',
            'description': 'md5:f9f1143bc33e9856ecbfcbfb97a759be'
        },
        'playlist_count': 24,
    }, {
        'url': 'https://www.freetv.com/series/esmeraldas/',
        'info_dict': {
            'id': 'esmeraldas',
            'title': 'Esmeraldas',
            'description': 'md5:43d7ec45bd931d8268a4f5afaf4c77bf'
        },
        'playlist_count': 62,
    }, {
        'url': 'https://www.freetv.com/series/las-aventuras-de-leonardo/',
        'info_dict': {
            'id': 'las-aventuras-de-leonardo',
            'title': 'Las Aventuras de Leonardo',
            'description': 'md5:0c47130846c141120a382aca059288f6'
        },
        'playlist_count': 13,
    },
    ]

    def _extract_series_season(self, season_id, series_title):
        episodes = self._get_api_response(season_id, 'series', {
            'contentID': season_id,
            'action': 'olyott_get_dynamic_series_content',
            'type': 'list',
            'perPage': '1000',
        })['1']

        for episode in episodes:
            video_id = str(episode['contentID'])
            formats, subtitles = self._extract_m3u8_formats_and_subtitles(episode['streamURL'], video_id, 'mp4')
            self._sort_formats(formats)

            yield {
                'id': video_id,
                'title': episode.get('fullTitle'),
                'description': episode.get('description'),
                'formats': formats,
                'subtitles': subtitles,
                'thumbnail': episode.get('thumbnail'),
                'series': series_title,
                'series_id': traverse_obj(episode, ('contentMeta', 'displayMeta', 'seriesID')),
                'season_id': traverse_obj(episode, ('contentMeta', 'displayMeta', 'seasonID')),
                'season_number': traverse_obj(
                    episode, ('contentMeta', 'displayMeta', 'seasonNum'), expected_type=int_or_none),
                'episode_number': traverse_obj(
                    episode, ('contentMeta', 'displayMeta', 'episodeNum'), expected_type=int_or_none),
            }

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

        title = self._html_search_regex(
            r'<h1[^>]+class=["\']synopis[^>]>(?P<title>[^<]+)', webpage, 'title', group='title', fatal=False)
        description = self._html_search_regex(
            r'<div[^>]+class=["\']+synopis content[^>]><p>(?P<description>[^<]+)',
            webpage, 'description', group='description', fatal=False)

        return self.playlist_result(
            itertools.chain.from_iterable(
                self._extract_series_season(season_id, title)
                for season_id in re.findall(r'<option[^>]+value=["\'](\d+)["\']', webpage)),
            display_id, title, description)


code:
import re

from .common import InfoExtractor
from ..compat import (
    compat_str,
    compat_urlparse,
)
from ..utils import (
    ExtractorError,
    parse_duration,
    url_or_none,
    urlencode_postdata,
)

class FrontendMastersBaseIE(InfoExtractor):
    _API_BASE = 'https://api.frontendmasters.com/v1/kabuki'
    _LOGIN_URL = 'https://frontendmasters.com/login/'

    _NETRC_MACHINE = 'frontendmasters'

    _QUALITIES = {
        'low': {'width': 480, 'height': 360},
        'mid': {'width': 1280, 'height': 720},
        'high': {'width': 1920, 'height': 1080}
    }

    def _perform_login(self, username, password):
        login_page = self._download_webpage(
            self._LOGIN_URL, None, 'Downloading login page')

        login_form = self._hidden_inputs(login_page)

        login_form.update({
            'username': username,
            'password': password
        })

        post_url = self._search_regex(
            r'<form[^>]+action=(["\'])(?P<url>.+?)\1', login_page,
            'post_url', default=self._LOGIN_URL, group='url')

        if not post_url.startswith('http'):
            post_url = compat_urlparse.urljoin(self._LOGIN_URL, post_url)

        response = self._download_webpage(
            post_url, None, 'Logging in', data=urlencode_postdata(login_form),
            headers={'Content-Type': 'application/x-www-form-urlencoded'})

        # Successful login
        if any(p in response for p in (
                'wp-login.php?action=logout', '>Logout')):
            return

        error = self._html_search_regex(
            r'class=(["\'])(?:(?!\1).)*\bMessageAlert\b(?:(?!\1).)*\1[^>]*>(?P<error>[^<]+)<',
            response, 'error message', default=None, group='error')
        if error:
            raise ExtractorError('Unable to login: %s' % error, expected=True)
        raise ExtractorError('Unable to log in')

class FrontendMastersPageBaseIE(FrontendMastersBaseIE):
    def _download_course(self, course_name, url):
        return self._download_json(
            '%s/courses/%s' % (self._API_BASE, course_name), course_name,
            'Downloading course JSON', headers={'Referer': url})

    @staticmethod
    def _extract_chapters(course):
        chapters = []
        lesson_elements = course.get('lessonElements')
        if isinstance(lesson_elements, list):
            chapters = [url_or_none(e) for e in lesson_elements if url_or_none(e)]
        return chapters

    @staticmethod
    def _extract_lesson(chapters, lesson_id, lesson):
        title = lesson.get('title') or lesson_id
        display_id = lesson.get('slug')
        description = lesson.get('description')
        thumbnail = lesson.get('thumbnail')

        chapter_number = None
        index = lesson.get('index')
        element_index = lesson.get('elementIndex')
        if (isinstance(index, int) and isinstance(element_index, int)
                and index < element_index):
            chapter_number = element_index - index
        chapter = (chapters[chapter_number - 1]
                   if chapter_number - 1 < len(chapters) else None)

        duration = None
        timestamp = lesson.get('timestamp')
        if isinstance(timestamp, compat_str):
            mobj = re.search(
                r'(?P<start>\d{1,2}:\d{1,2}:\d{1,2})\s*-(?P<end>\s*\d{1,2}:\d{1,2}:\d{1,2})',
                timestamp)
            if mobj:
                duration = parse_duration(mobj.group('end')) - parse_duration(
                    mobj.group('start'))

        return {
            '_type': 'url_transparent',
            'url': 'frontendmasters:%s' % lesson_id,
            'ie_key': FrontendMastersIE.ie_key(),
            'id': lesson_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'duration': duration,
            'chapter': chapter,
            'chapter_number': chapter_number,
        }

class FrontendMastersIE(FrontendMastersBaseIE):
    _VALID_URL = r'(?:frontendmasters:|https?://api\.frontendmasters\.com/v\d+/kabuki/video/)(?P<id>[^/]+)'
    _TESTS = [{
        'url': 'https://api.frontendmasters.com/v1/kabuki/video/a2qogef6ba',
        'md5': '7f161159710d6b7016a4f4af6fcb05e2',
        'info_dict': {
            'id': 'a2qogef6ba',
            'ext': 'mp4',
            'title': 'a2qogef6ba',
        },
        'skip': 'Requires FrontendMasters account credentials',
    }, {
        'url': 'frontendmasters:a2qogef6ba',
        'only_matching': True,
    }]

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

        source_url = '%s/video/%s/source' % (self._API_BASE, lesson_id)

        formats = []
        for ext in ('webm', 'mp4'):
            for quality in ('low', 'mid', 'high'):
                resolution = self._QUALITIES[quality].copy()
                format_id = '%s-%s' % (ext, quality)
                format_url = self._download_json(
                    source_url, lesson_id,
                    'Downloading %s source JSON' % format_id, query={
                        'f': ext,
                        'r': resolution['height'],
                    }, headers={
                        'Referer': url,
                    }, fatal=False)['url']

                if not format_url:
                    continue

                f = resolution.copy()
                f.update({
                    'url': format_url,
                    'ext': ext,
                    'format_id': format_id,
                })
                formats.append(f)
        self._sort_formats(formats)

        subtitles = {
            'en': [{
                'url': '%s/transcripts/%s.vtt' % (self._API_BASE, lesson_id),
            }]
        }

        return {
            'id': lesson_id,
            'title': lesson_id,
            'formats': formats,
            'subtitles': subtitles
        }

class FrontendMastersLessonIE(FrontendMastersPageBaseIE):
    _VALID_URL = r'https?://(?:www\.)?frontendmasters\.com/courses/(?P<course_name>[^/]+)/(?P<lesson_name>[^/]+)'
    _TEST = {
        'url': 'https://frontendmasters.com/courses/web-development/tools',
        'info_dict': {
            'id': 'a2qogef6ba',
            'display_id': 'tools',
            'ext': 'mp4',
            'title': 'Tools',
            'description': 'md5:82c1ea6472e88ed5acd1829fe992e4f7',
            'thumbnail': r're:^https?://.*\.jpg$',
            'chapter': 'Introduction',
            'chapter_number': 1,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'Requires FrontendMasters account credentials',
    }

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        course_name, lesson_name = mobj.group('course_name', 'lesson_name')

        course = self._download_course(course_name, url)

        lesson_id, lesson = next(
            (video_id, data)
            for video_id, data in course['lessonData'].items()
            if data.get('slug') == lesson_name)

        chapters = self._extract_chapters(course)
        return self._extract_lesson(chapters, lesson_id, lesson)

class FrontendMastersCourseIE(FrontendMastersPageBaseIE):
    _VALID_URL = r'https?://(?:www\.)?frontendmasters\.com/courses/(?P<id>[^/]+)'
    _TEST = {
        'url': 'https://frontendmasters.com/courses/web-development/',
        'info_dict': {
            'id': 'web-development',
            'title': 'Introduction to Web Development',
            'description': 'md5:9317e6e842098bf725d62360e52d49a6',
        },
        'playlist_count': 81,
        'skip': 'Requires FrontendMasters account credentials',
    }

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

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

        course = self._download_course(course_name, url)

        chapters = self._extract_chapters(course)

        lessons = sorted(
            course['lessonData'].values(), key=lambda data: data['index'])

        entries = []
        for lesson in lessons:
            lesson_name = lesson.get('slug')
            lesson_id = lesson.get('hash') or lesson.get('statsId')
            if not lesson_id or not lesson_name:
                continue
            entries.append(self._extract_lesson(chapters, lesson_id, lesson))

        title = course.get('title')
        description = course.get('description')

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


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

class FujiTVFODPlus7IE(InfoExtractor):
    _VALID_URL = r'https?://fod\.fujitv\.co\.jp/title/(?P<sid>[0-9a-z]{4})/(?P<id>[0-9a-z]+)'
    _BASE_URL = 'https://i.fod.fujitv.co.jp/'
    _BITRATE_MAP = {
        300: (320, 180),
        800: (640, 360),
        1200: (1280, 720),
        2000: (1280, 720),
        4000: (1920, 1080),
    }

    _TESTS = [{
        'url': 'https://fod.fujitv.co.jp/title/5d40/5d40110076',
        'info_dict': {
            'id': '5d40110076',
            'ext': 'ts',
            'title': '#1318 『まる子、まぼろしの洋館を見る』の巻',
            'series': 'ちびまる子ちゃん',
            'series_id': '5d40',
            'description': 'md5:b3f51dbfdda162ac4f789e0ff4d65750',
            'thumbnail': 'https://i.fod.fujitv.co.jp/img/program/5d40/episode/5d40110076_a.jpg',
        },
    }, {
        'url': 'https://fod.fujitv.co.jp/title/5d40/5d40810083',
        'info_dict': {
            'id': '5d40810083',
            'ext': 'ts',
            'title': '#1324 『まる子とオニの子』の巻／『結成！2月をムダにしない会』の巻',
            'description': 'md5:3972d900b896adc8ab1849e310507efa',
            'series': 'ちびまる子ちゃん',
            'series_id': '5d40',
            'thumbnail': 'https://i.fod.fujitv.co.jp/img/program/5d40/episode/5d40810083_a.jpg'},
        'skip': 'Video available only in one week'
    }]

    def _real_extract(self, url):
        series_id, video_id = self._match_valid_url(url).groups()
        self._request_webpage(HEADRequest(url), video_id)
        json_info = {}
        token = self._get_cookies(url).get('CT')
        if token:
            json_info = self._download_json('https://fod-sp.fujitv.co.jp/apps/api/episode/detail/?ep_id=%s&is_premium=false' % video_id, video_id, headers={'x-authorization': f'Bearer {token.value}'}, fatal=False)
        else:
            self.report_warning(f'The token cookie is needed to extract video metadata. {self._login_hint("cookies")}')
        formats, subtitles = [], {}
        src_json = self._download_json(f'{self._BASE_URL}abrjson_v2/tv_android/{video_id}', video_id)
        for src in src_json['video_selector']:
            if not src.get('url'):
                continue
            fmt, subs = self._extract_m3u8_formats_and_subtitles(src['url'], video_id, 'ts')
            for f in fmt:
                f.update(dict(zip(('height', 'width'),
                                  self._BITRATE_MAP.get(f.get('tbr'), ()))))
            formats.extend(fmt)
            subtitles = self._merge_subtitles(subtitles, subs)
        self._sort_formats(formats, ['tbr'])

        return {
            'id': video_id,
            'title': json_info.get('ep_title'),
            'series': json_info.get('lu_title'),
            'series_id': series_id,
            'description': json_info.get('ep_description'),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnail': f'{self._BASE_URL}img/program/{series_id}/episode/{video_id}_a.jpg',
        }


code:
import random
import re
import string

from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    ExtractorError,
    determine_ext,
    int_or_none,
    join_nonempty,
    js_to_json,
    make_archive_id,
    orderedSet,
    qualities,
    str_or_none,
    traverse_obj,
    try_get,
    urlencode_postdata,
)

class FunimationBaseIE(InfoExtractor):
    _NETRC_MACHINE = 'funimation'
    _REGION = None
    _TOKEN = None

    def _get_region(self):
        region_cookie = self._get_cookies('https://www.funimation.com').get('region')
        region = region_cookie.value if region_cookie else self.get_param('geo_bypass_country')
        return region or traverse_obj(
            self._download_json(
                'https://geo-service.prd.funimationsvc.com/geo/v1/region/check', None, fatal=False,
                note='Checking geo-location', errnote='Unable to fetch geo-location information'),
            'region') or 'US'

    def _perform_login(self, username, password):
        if self._TOKEN:
            return
        try:
            data = self._download_json(
                'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
                None, 'Logging in', data=urlencode_postdata({
                    'username': username,
                    'password': password,
                }))
            FunimationBaseIE._TOKEN = data['token']
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
                error = self._parse_json(e.cause.read().decode(), None)['error']
                raise ExtractorError(error, expected=True)
            raise

class FunimationPageIE(FunimationBaseIE):
    IE_NAME = 'funimation:page'
    _VALID_URL = r'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:(?P<lang>[^/]+)/)?(?:shows|v)/(?P<show>[^/]+)/(?P<episode>[^/?#&]+)'

    _TESTS = [{
        'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/',
        'info_dict': {
            'id': '210050',
            'ext': 'mp4',
            'title': 'Broadcast Dub Preview',
            # Other metadata is tested in FunimationIE
        },
        'params': {
            'skip_download': 'm3u8',
        },
        'add_ie': ['Funimation'],
    }, {
        # Not available in US
        'url': 'https://www.funimation.com/shows/hacksign/role-play/',
        'only_matching': True,
    }, {
        # with lang code
        'url': 'https://www.funimation.com/en/shows/hacksign/role-play/',
        'only_matching': True,
    }, {
        'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/',
        'only_matching': True,
    }, {
        'url': 'https://www.funimation.com/v/a-certain-scientific-railgun/super-powered-level-5',
        'only_matching': True,
    }]

    def _real_initialize(self):
        if not self._REGION:
            FunimationBaseIE._REGION = self._get_region()

    def _real_extract(self, url):
        locale, show, episode = self._match_valid_url(url).group('lang', 'show', 'episode')

        video_id = traverse_obj(self._download_json(
            f'https://title-api.prd.funimationsvc.com/v1/shows/{show}/episodes/{episode}',
            f'{show}_{episode}', query={
                'deviceType': 'web',
                'region': self._REGION,
                'locale': locale or 'en'
            }), ('videoList', ..., 'id'), get_all=False)

        return self.url_result(f'https://www.funimation.com/player/{video_id}', FunimationIE.ie_key(), video_id)

class FunimationIE(FunimationBaseIE):
    _VALID_URL = r'https?://(?:www\.)?funimation\.com/player/(?P<id>\d+)'

    _TESTS = [{
        'url': 'https://www.funimation.com/player/210051',
        'info_dict': {
            'id': '210050',
            'display_id': 'broadcast-dub-preview',
            'ext': 'mp4',
            'title': 'Broadcast Dub Preview',
            'thumbnail': r're:https?://.*\.(?:jpg|png)',
            'episode': 'Broadcast Dub Preview',
            'episode_id': '210050',
            'season': 'Extras',
            'season_id': '166038',
            'season_number': 99,
            'series': 'Attack on Titan: Junior High',
            'description': '',
            'duration': 155,
        },
        'params': {
            'skip_download': 'm3u8',
        },
    }, {
        'note': 'player_id should be extracted with the relevent compat-opt',
        'url': 'https://www.funimation.com/player/210051',
        'info_dict': {
            'id': '210051',
            'display_id': 'broadcast-dub-preview',
            'ext': 'mp4',
            'title': 'Broadcast Dub Preview',
            'thumbnail': r're:https?://.*\.(?:jpg|png)',
            'episode': 'Broadcast Dub Preview',
            'episode_id': '210050',
            'season': 'Extras',
            'season_id': '166038',
            'season_number': 99,
            'series': 'Attack on Titan: Junior High',
            'description': '',
            'duration': 155,
        },
        'params': {
            'skip_download': 'm3u8',
            'compat_opts': ['seperate-video-versions'],
        },
    }]

    @staticmethod
    def _get_experiences(episode):
        for lang, lang_data in episode.get('languages', {}).items():
            for video_data in lang_data.values():
                for version, f in video_data.items():
                    yield lang, version.title(), f

    def _get_episode(self, webpage, experience_id=None, episode_id=None, fatal=True):
        ''' Extract the episode, season and show objects given either episode/experience id '''
        show = self._parse_json(
            self._search_regex(
                r'show\s*=\s*({.+?})\s*;', webpage, 'show data', fatal=fatal),
            experience_id, transform_source=js_to_json, fatal=fatal) or []
        for season in show.get('seasons', []):
            for episode in season.get('episodes', []):
                if episode_id is not None:
                    if str(episode.get('episodePk')) == episode_id:
                        return episode, season, show
                    continue
                for _, _, f in self._get_experiences(episode):
                    if f.get('experienceId') == experience_id:
                        return episode, season, show
        if fatal:
            raise ExtractorError('Unable to find episode information')
        else:
            self.report_warning('Unable to find episode information')
        return {}, {}, {}

    def _real_extract(self, url):
        initial_experience_id = self._match_id(url)
        webpage = self._download_webpage(
            url, initial_experience_id, note=f'Downloading player webpage for {initial_experience_id}')
        episode, season, show = self._get_episode(webpage, experience_id=int(initial_experience_id))
        episode_id = str(episode['episodePk'])
        display_id = episode.get('slug') or episode_id

        formats, subtitles, thumbnails, duration = [], {}, [], 0
        requested_languages, requested_versions = self._configuration_arg('language'), self._configuration_arg('version')
        language_preference = qualities((requested_languages or [''])[::-1])
        source_preference = qualities((requested_versions or ['uncut', 'simulcast'])[::-1])
        only_initial_experience = 'seperate-video-versions' in self.get_param('compat_opts', [])

        for lang, version, fmt in self._get_experiences(episode):
            experience_id = str(fmt['experienceId'])
            if (only_initial_experience and experience_id != initial_experience_id
                    or requested_languages and lang.lower() not in requested_languages
                    or requested_versions and version.lower() not in requested_versions):
                continue
            thumbnails.append({'url': fmt.get('poster')})
            duration = max(duration, fmt.get('duration', 0))
            format_name = '%s %s (%s)' % (version, lang, experience_id)
            self.extract_subtitles(
                subtitles, experience_id, display_id=display_id, format_name=format_name,
                episode=episode if experience_id == initial_experience_id else episode_id)

            headers = {}
            if self._TOKEN:
                headers['Authorization'] = 'Token %s' % self._TOKEN
            page = self._download_json(
                'https://www.funimation.com/api/showexperience/%s/' % experience_id,
                display_id, headers=headers, expected_status=403, query={
                    'pinst_id': ''.join([random.choice(string.digits + string.ascii_letters) for _ in range(8)]),
                }, note=f'Downloading {format_name} JSON')
            sources = page.get('items') or []
            if not sources:
                error = try_get(page, lambda x: x['errors'][0], dict)
                if error:
                    self.report_warning('%s said: Error %s - %s' % (
                        self.IE_NAME, error.get('code'), error.get('detail') or error.get('title')))
                else:
                    self.report_warning('No sources found for format')

            current_formats = []
            for source in sources:
                source_url = source.get('src')
                source_type = source.get('videoType') or determine_ext(source_url)
                if source_type == 'm3u8':
                    current_formats.extend(self._extract_m3u8_formats(
                        source_url, display_id, 'mp4', m3u8_id='%s-%s' % (experience_id, 'hls'), fatal=False,
                        note=f'Downloading {format_name} m3u8 information'))
                else:
                    current_formats.append({
                        'format_id': '%s-%s' % (experience_id, source_type),
                        'url': source_url,
                    })
                for f in current_formats:
                    # TODO: Convert language to code
                    f.update({
                        'language': lang,
                        'format_note': version,
                        'source_preference': source_preference(version.lower()),
                        'language_preference': language_preference(lang.lower()),
                    })
                formats.extend(current_formats)
        if not formats and (requested_languages or requested_versions):
            self.raise_no_formats(
                'There are no video formats matching the requested languages/versions', expected=True, video_id=display_id)
        self._remove_duplicate_formats(formats)
        self._sort_formats(formats, ('lang', 'source'))

        return {
            'id': episode_id,
            '_old_archive_ids': [make_archive_id(self, initial_experience_id)],
            'display_id': display_id,
            'duration': duration,
            'title': episode['episodeTitle'],
            'description': episode.get('episodeSummary'),
            'episode': episode.get('episodeTitle'),
            'episode_number': int_or_none(episode.get('episodeId')),
            'episode_id': episode_id,
            'season': season.get('seasonTitle'),
            'season_number': int_or_none(season.get('seasonId')),
            'season_id': str_or_none(season.get('seasonPk')),
            'series': show.get('showTitle'),
            'formats': formats,
            'thumbnails': thumbnails,
            'subtitles': subtitles,
        }

    def _get_subtitles(self, subtitles, experience_id, episode, display_id, format_name):
        if isinstance(episode, str):
            webpage = self._download_webpage(
                f'https://www.funimation.com/player/{experience_id}/', display_id,
                fatal=False, note=f'Downloading player webpage for {format_name}')
            episode, _, _ = self._get_episode(webpage, episode_id=episode, fatal=False)

        for _, version, f in self._get_experiences(episode):
            for source in f.get('sources'):
                for text_track in source.get('textTracks'):
                    if not text_track.get('src'):
                        continue
                    sub_type = text_track.get('type').upper()
                    sub_type = sub_type if sub_type != 'FULL' else None
                    current_sub = {
                        'url': text_track['src'],
                        'name': join_nonempty(version, text_track.get('label'), sub_type, delim=' ')
                    }
                    lang = join_nonempty(text_track.get('language', 'und'),
                                         version if version != 'Simulcast' else None,
                                         sub_type, delim='_')
                    if current_sub not in subtitles.get(lang, []):
                        subtitles.setdefault(lang, []).append(current_sub)
        return subtitles

class FunimationShowIE(FunimationBaseIE):
    IE_NAME = 'funimation:show'
    _VALID_URL = r'(?P<url>https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?P<locale>[^/]+)?/?shows/(?P<id>[^/?#&]+))/?(?:[?#]|$)'

    _TESTS = [{
        'url': 'https://www.funimation.com/en/shows/sk8-the-infinity',
        'info_dict': {
            'id': 1315000,
            'title': 'SK8 the Infinity'
        },
        'playlist_count': 13,
        'params': {
            'skip_download': True,
        },
    }, {
        # without lang code
        'url': 'https://www.funimation.com/shows/ouran-high-school-host-club/',
        'info_dict': {
            'id': 39643,
            'title': 'Ouran High School Host Club'
        },
        'playlist_count': 26,
        'params': {
            'skip_download': True,
        },
    }]

    def _real_initialize(self):
        if not self._REGION:
            FunimationBaseIE._REGION = self._get_region()

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

        show_info = self._download_json(
            'https://title-api.prd.funimationsvc.com/v2/shows/%s?region=%s&deviceType=web&locale=%s'
            % (display_id, self._REGION, locale or 'en'), display_id)
        items_info = self._download_json(
            'https://prod-api-funimationnow.dadcdigital.com/api/funimation/episodes/?limit=99999&title_id=%s'
            % show_info.get('id'), display_id)

        vod_items = traverse_obj(items_info, ('items', ..., lambda k, _: re.match(r'(?i)mostRecent[AS]vod', k), 'item'))

        return {
            '_type': 'playlist',
            'id': show_info['id'],
            'title': show_info['name'],
            'entries': orderedSet(
                self.url_result(
                    '%s/%s' % (base_url, vod_item.get('episodeSlug')), FunimationPageIE.ie_key(),
                    vod_item.get('episodeId'), vod_item.get('episodeName'))
                for vod_item in sorted(vod_items, key=lambda x: x.get('episodeOrder', -1))),
        }


code:
from .common import InfoExtractor
from .nexx import NexxIE
from ..utils import (
    int_or_none,
    str_or_none,
)

class FunkIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.|origin\.)?funk\.net/(?:channel|playlist)/[^/]+/(?P<display_id>[0-9a-z-]+)-(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://www.funk.net/channel/ba-793/die-lustigsten-instrumente-aus-dem-internet-teil-2-1155821',
        'md5': '8dd9d9ab59b4aa4173b3197f2ea48e81',
        'info_dict': {
            'id': '1155821',
            'ext': 'mp4',
            'title': 'Die LUSTIGSTEN INSTRUMENTE aus dem Internet - Teil 2',
            'description': 'md5:a691d0413ef4835588c5b03ded670c1f',
            'timestamp': 1514507395,
            'upload_date': '20171229',
        },

    }, {
        'url': 'https://www.funk.net/playlist/neuesteVideos/kameras-auf-dem-fusion-festival-1618699',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        display_id, nexx_id = self._match_valid_url(url).groups()
        video = self._download_json(
            'https://www.funk.net/api/v4.0/videos/' + nexx_id, nexx_id)
        return {
            '_type': 'url_transparent',
            'url': 'nexx:741:' + nexx_id,
            'ie_key': NexxIE.ie_key(),
            'id': nexx_id,
            'title': video.get('title'),
            'description': video.get('description'),
            'duration': int_or_none(video.get('duration')),
            'channel_id': str_or_none(video.get('channelId')),
            'display_id': display_id,
            'tags': video.get('tags'),
            'thumbnail': video.get('imageUrlLandscape'),
        }


code:
from .common import InfoExtractor
from ..utils import (
    determine_ext,
    int_or_none,
    mimetype2ext,
    parse_iso8601,
)

class FusionIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?fusion\.(?:net|tv)/(?:video/|show/.+?\bvideo=)(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://fusion.tv/video/201781/u-s-and-panamanian-forces-work-together-to-stop-a-vessel-smuggling-drugs/',
        'info_dict': {
            'id': '3145868',
            'ext': 'mp4',
            'title': 'U.S. and Panamanian forces work together to stop a vessel smuggling drugs',
            'description': 'md5:0cc84a9943c064c0f46b128b41b1b0d7',
            'duration': 140.0,
            'timestamp': 1442589635,
            'uploader': 'UNIVISON',
            'upload_date': '20150918',
        },
        'params': {
            'skip_download': True,
        },
        'add_ie': ['Anvato'],
    }, {
        'url': 'http://fusion.tv/video/201781',
        'only_matching': True,
    }, {
        'url': 'https://fusion.tv/show/food-exposed-with-nelufar-hedayat/?ancla=full-episodes&video=588644',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        video = self._download_json(
            'https://platform.fusion.net/wp-json/fusiondotnet/v1/video/' + video_id, video_id)

        info = {
            'id': video_id,
            'title': video['title'],
            'description': video.get('excerpt'),
            'timestamp': parse_iso8601(video.get('published')),
            'series': video.get('show'),
        }

        formats = []
        src = video.get('src') or {}
        for f_id, f in src.items():
            for q_id, q in f.items():
                q_url = q.get('url')
                if not q_url:
                    continue
                ext = determine_ext(q_url, mimetype2ext(q.get('type')))
                if ext == 'smil':
                    formats.extend(self._extract_smil_formats(q_url, video_id, fatal=False))
                elif f_id == 'm3u8-variant' or (ext == 'm3u8' and q_id == 'Variant'):
                    formats.extend(self._extract_m3u8_formats(
                        q_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False))
                else:
                    formats.append({
                        'format_id': '-'.join([f_id, q_id]),
                        'url': q_url,
                        'width': int_or_none(q.get('width')),
                        'height': int_or_none(q.get('height')),
                        'tbr': int_or_none(self._search_regex(r'_(\d+)\.m(?:p4|3u8)', q_url, 'bitrate')),
                        'ext': 'mp4' if ext == 'm3u8' else ext,
                        'protocol': 'm3u8_native' if ext == 'm3u8' else 'https',
                    })
        if formats:
            self._sort_formats(formats)
            info['formats'] = formats
        else:
            info.update({
                '_type': 'url',
                'url': 'anvato:uni:' + video['video_ids']['anvato'],
                'ie_key': 'Anvato',
            })

        return info


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

class FuyinTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?fuyin\.tv/html/(?:\d+)/(?P<id>\d+)\.html'
    _TESTS = [{
        'url': 'https://www.fuyin.tv/html/2733/44129.html',
        'info_dict': {
            'id': '44129',
            'ext': 'mp4',
            'title': '第1集',
            'description': 'md5:21a3d238dc8d49608e1308e85044b9c3',
        }
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        json_data = self._download_json(
            'https://www.fuyin.tv/api/api/tv.movie/url',
            video_id, query={'urlid': f'{video_id}'})
        webpage = self._download_webpage(url, video_id, fatal=False)

        return {
            'id': video_id,
            'title': traverse_obj(json_data, ('data', 'title')),
            'url': json_data['data']['url'],
            'ext': 'mp4',
            'description': self._html_search_meta('description', webpage),
        }


code:
import re

from .common import InfoExtractor
from ..utils import (
    clean_html,
    str_to_int,
)

class GabTVIE(InfoExtractor):
    _VALID_URL = r'https?://tv\.gab\.com/channel/[^/]+/view/(?P<id>[a-z0-9-]+)'
    _TESTS = [{
        'url': 'https://tv.gab.com/channel/wurzelroot/view/why-was-america-in-afghanistan-61217eacea5665de450d0488',
        'info_dict': {
            'id': '61217eacea5665de450d0488',
            'ext': 'mp4',
            'title': 'WHY WAS AMERICA IN AFGHANISTAN - AMERICA FIRST AGAINST AMERICAN OLIGARCHY',
            'description': None,
            'uploader': 'Wurzelroot',
            'uploader_id': '608fb0a85738fd1974984f7d',
            'thumbnail': 'https://tv.gab.com/image/61217eacea5665de450d0488',
        }
    }]

    def _real_extract(self, url):
        id = self._match_id(url).split('-')[-1]
        webpage = self._download_webpage(url, id)
        channel_id = self._search_regex(r'data-channel-id=\"(?P<channel_id>[^\"]+)', webpage, 'channel_id')
        channel_name = self._search_regex(r'data-channel-name=\"(?P<channel_id>[^\"]+)', webpage, 'channel_name')
        title = self._search_regex(r'data-episode-title=\"(?P<channel_id>[^\"]+)', webpage, 'title')
        view_key = self._search_regex(r'data-view-key=\"(?P<channel_id>[^\"]+)', webpage, 'view_key')
        description = clean_html(
            self._html_search_regex(self._meta_regex('description'), webpage, 'description', group='content')) or None
        available_resolutions = re.findall(r'<a\ data-episode-id=\"%s\"\ data-resolution=\"(?P<resolution>[^\"]+)' % id,
                                           webpage)

        formats = []
        for resolution in available_resolutions:
            frmt = {
                'url': f'https://tv.gab.com/media/{id}?viewKey={view_key}&r={resolution}',
                'format_id': resolution,
                'vcodec': 'h264',
                'acodec': 'aac',
                'ext': 'mp4'
            }
            if 'audio-' in resolution:
                frmt['abr'] = str_to_int(resolution.replace('audio-', ''))
                frmt['height'] = 144
                frmt['quality'] = -10
            else:
                frmt['height'] = str_to_int(resolution.replace('p', ''))
            formats.append(frmt)
        self._sort_formats(formats)

        return {
            'id': id,
            'title': title,
            'formats': formats,
            'description': description,
            'uploader': channel_name,
            'uploader_id': channel_id,
            'thumbnail': f'https://tv.gab.com/image/{id}',
        }


code:
from .common import InfoExtractor
from ..compat import (
    compat_str,
    compat_urllib_parse_unquote,
)
from ..utils import (
    ExtractorError,
    int_or_none,
    str_or_none,
    strip_or_none,
    try_get,
    urlencode_postdata,
)

class GaiaIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?gaia\.com/video/(?P<id>[^/?]+).*?\bfullplayer=(?P<type>feature|preview)'
    _TESTS = [{
        'url': 'https://www.gaia.com/video/connecting-universal-consciousness?fullplayer=feature',
        'info_dict': {
            'id': '89356',
            'ext': 'mp4',
            'title': 'Connecting with Universal Consciousness',
            'description': 'md5:844e209ad31b7d31345f5ed689e3df6f',
            'upload_date': '20151116',
            'timestamp': 1447707266,
            'duration': 936,
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
    }, {
        'url': 'https://www.gaia.com/video/connecting-universal-consciousness?fullplayer=preview',
        'info_dict': {
            'id': '89351',
            'ext': 'mp4',
            'title': 'Connecting with Universal Consciousness',
            'description': 'md5:844e209ad31b7d31345f5ed689e3df6f',
            'upload_date': '20151116',
            'timestamp': 1447707266,
            'duration': 53,
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
    }]
    _NETRC_MACHINE = 'gaia'
    _jwt = None

    def _real_initialize(self):
        auth = self._get_cookies('https://www.gaia.com/').get('auth')
        if auth:
            auth = self._parse_json(compat_urllib_parse_unquote(auth.value), None, fatal=False)
            self._jwt = auth.get('jwt')

    def _perform_login(self, username, password):
        if self._jwt:
            return
        auth = self._download_json(
            'https://auth.gaia.com/v1/login',
            None, data=urlencode_postdata({
                'username': username,
                'password': password
            }))
        if auth.get('success') is False:
            raise ExtractorError(', '.join(auth['messages']), expected=True)
        self._jwt = auth.get('jwt')

    def _real_extract(self, url):
        display_id, vtype = self._match_valid_url(url).groups()
        node_id = self._download_json(
            'https://brooklyn.gaia.com/pathinfo', display_id, query={
                'path': 'video/' + display_id,
            })['id']
        node = self._download_json(
            'https://brooklyn.gaia.com/node/%d' % node_id, node_id)
        vdata = node[vtype]
        media_id = compat_str(vdata['nid'])
        title = node['title']

        headers = None
        if self._jwt:
            headers = {'Authorization': 'Bearer ' + self._jwt}
        media = self._download_json(
            'https://brooklyn.gaia.com/media/' + media_id,
            media_id, headers=headers)
        formats = self._extract_m3u8_formats(
            media['mediaUrls']['bcHLS'], media_id, 'mp4')
        self._sort_formats(formats)

        subtitles = {}
        text_tracks = media.get('textTracks', {})
        for key in ('captions', 'subtitles'):
            for lang, sub_url in text_tracks.get(key, {}).items():
                subtitles.setdefault(lang, []).append({
                    'url': sub_url,
                })

        fivestar = node.get('fivestar', {})
        fields = node.get('fields', {})

        def get_field_value(key, value_key='value'):
            return try_get(fields, lambda x: x[key][0][value_key])

        return {
            'id': media_id,
            'display_id': display_id,
            'title': title,
            'formats': formats,
            'description': strip_or_none(get_field_value('body') or get_field_value('teaser')),
            'timestamp': int_or_none(node.get('created')),
            'subtitles': subtitles,
            'duration': int_or_none(vdata.get('duration')),
            'like_count': int_or_none(try_get(fivestar, lambda x: x['up_count']['value'])),
            'dislike_count': int_or_none(try_get(fivestar, lambda x: x['down_count']['value'])),
            'comment_count': int_or_none(node.get('comment_count')),
            'series': try_get(node, lambda x: x['series']['title'], compat_str),
            'season_number': int_or_none(get_field_value('season')),
            'season_id': str_or_none(get_field_value('series_nid', 'nid')),
            'episode_number': int_or_none(get_field_value('episode')),
        }


code:
from .brightcove import BrightcoveNewIE
from .common import InfoExtractor
from ..utils import (
    clean_html,
    get_element_by_class,
    get_element_by_id,
)

class GameInformerIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?gameinformer\.com/(?:[^/]+/)*(?P<id>[^.?&#]+)'
    _TESTS = [{
        # normal Brightcove embed code extracted with BrightcoveNewIE._extract_url
        'url': 'http://www.gameinformer.com/b/features/archive/2015/09/26/replay-animal-crossing.aspx',
        'md5': '292f26da1ab4beb4c9099f1304d2b071',
        'info_dict': {
            'id': '4515472681001',
            'ext': 'mp4',
            'title': 'Replay - Animal Crossing',
            'description': 'md5:2e211891b215c85d061adc7a4dd2d930',
            'timestamp': 1443457610,
            'upload_date': '20150928',
            'uploader_id': '694940074001',
        },
    }, {
        # Brightcove id inside unique element with field--name-field-brightcove-video-id class
        'url': 'https://www.gameinformer.com/video-feature/new-gameplay-today/2019/07/09/new-gameplay-today-streets-of-rogue',
        'info_dict': {
            'id': '6057111913001',
            'ext': 'mp4',
            'title': 'New Gameplay Today – Streets Of Rogue',
            'timestamp': 1562699001,
            'upload_date': '20190709',
            'uploader_id': '694940074001',

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

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(
            url, display_id, headers=self.geo_verification_headers())
        brightcove_id = clean_html(get_element_by_class('field--name-field-brightcove-video-id', webpage) or get_element_by_id('video-source-content', webpage))
        brightcove_url = self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id if brightcove_id else BrightcoveNewIE._extract_url(self, webpage)
        return self.url_result(brightcove_url, 'BrightcoveNew', brightcove_id)