code:
import re

from .common import InfoExtractor
from ..utils import ExtractorError

class EpiconIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?epicon\.in/(?:documentaries|movies|tv-shows/[^/?#]+/[^/?#]+)/(?P<id>[^/?#]+)'
    _TESTS = [{
        'url': 'https://www.epicon.in/documentaries/air-battle-of-srinagar',
        'info_dict': {
            'id': 'air-battle-of-srinagar',
            'ext': 'mp4',
            'title': 'Air Battle of Srinagar',
            'description': 'md5:c4de2013af9bc05ae4392e4115d518d7',
            'thumbnail': r're:^https?://.*\.jpg$',
        }
    }, {
        'url': 'https://www.epicon.in/movies/krit',
        'info_dict': {
            'id': 'krit',
            'ext': 'mp4',
            'title': 'Krit',
            'description': 'md5:c12b35dad915d48ccff7f013c79bab4a',
            'thumbnail': r're:^https?://.*\.jpg$',
        }
    }, {
        'url': 'https://www.epicon.in/tv-shows/paapnaashini-ganga/season-1/vardaan',
        'info_dict': {
            'id': 'vardaan',
            'ext': 'mp4',
            'title': 'Paapnaashini Ganga - Season 1 - Ep 1 - VARDAAN',
            'description': 'md5:f517058c3d0402398eefa6242f4dd6ae',
            'thumbnail': r're:^https?://.*\.jpg$',
        }
    }, {
        'url': 'https://www.epicon.in/movies/jayadev',
        'info_dict': {
            'id': 'jayadev',
            'ext': 'mp4',
            'title': 'Jayadev',
            'description': 'md5:09e349eecd8e585a3b6466904f19df6c',
            'thumbnail': r're:^https?://.*\.jpg$',
        }
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        cid = self._search_regex(r'class=\"mylist-icon\ iconclick\"\ id=\"(\d+)', webpage, 'cid')
        headers = {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'}
        data = f'cid={cid}&action=st&type=video'.encode()
        data_json = self._parse_json(self._download_json('https://www.epicon.in/ajaxplayer/', id, headers=headers, data=data), id)

        if not data_json['success']:
            raise ExtractorError(data_json['message'], expected=True)

        title = self._search_regex(r'setplaytitle=\"([^\"]+)', webpage, 'title')
        description = self._og_search_description(webpage) or None
        thumbnail = self._og_search_thumbnail(webpage) or None
        formats = self._extract_m3u8_formats(data_json['url']['video_url'], id)
        self._sort_formats(formats)

        subtitles = {}
        for subtitle in data_json.get('subtitles', []):
            sub_url = subtitle.get('file')
            if not sub_url:
                continue
            subtitles.setdefault(subtitle.get('lang', 'English'), []).append({
                'url': self._proto_relative_url(sub_url),
            })

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

class EpiconSeriesIE(InfoExtractor):
    _VALID_URL = r'(?!.*season)https?://(?:www\.)?epicon\.in/tv-shows/(?P<id>[^/?#]+)'
    _TESTS = [{
        'url': 'https://www.epicon.in/tv-shows/1-of-something',
        'playlist_mincount': 5,
        'info_dict': {
            'id': '1-of-something',
        },
    }, {
        'url': 'https://www.epicon.in/tv-shows/eco-india-english',
        'playlist_mincount': 76,
        'info_dict': {
            'id': 'eco-india-english',
        },
    }, {
        'url': 'https://www.epicon.in/tv-shows/s/',
        'playlist_mincount': 25,
        'info_dict': {
            'id': 's',
        },
    }, {
        'url': 'https://www.epicon.in/tv-shows/ekaant',
        'playlist_mincount': 38,
        'info_dict': {
            'id': 'ekaant',
        },
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        episodes = re.findall(r'ct-tray-url=\"(tv-shows/%s/[^\"]+)' % id, webpage)
        entries = [self.url_result('https://www.epicon.in/%s' % episode, ie=EpiconIE.ie_key()) for episode in episodes]
        return self.playlist_result(entries, playlist_id=id)


code:
from .common import InfoExtractor

class EpochIE(InfoExtractor):
    _VALID_URL = r'https?://www.theepochtimes\.com/[\w-]+_(?P<id>\d+).html'
    _TESTS = [
        {
            'url': 'https://www.theepochtimes.com/they-can-do-audio-video-physical-surveillance-on-you-24h-365d-a-year-rex-lee-on-intrusive-apps_4661688.html',
            'info_dict': {
                'id': 'a3dd732c-4750-4bc8-8156-69180668bda1',
                'ext': 'mp4',
                'title': '‘They Can Do Audio, Video, Physical Surveillance on You 24H/365D a Year’: Rex Lee on Intrusive Apps',
            }
        },
        {
            'url': 'https://www.theepochtimes.com/the-communist-partys-cyberattacks-on-america-explained-rex-lee-talks-tech-hybrid-warfare_4342413.html',
            'info_dict': {
                'id': '276c7f46-3bbf-475d-9934-b9bbe827cf0a',
                'ext': 'mp4',
                'title': 'The Communist Party’s Cyberattacks on America Explained; Rex Lee Talks Tech Hybrid Warfare',
            }
        },
        {
            'url': 'https://www.theepochtimes.com/kash-patel-a-6-year-saga-of-government-corruption-from-russiagate-to-mar-a-lago_4690250.html',
            'info_dict': {
                'id': 'aa9ceecd-a127-453d-a2de-7153d6fd69b6',
                'ext': 'mp4',
                'title': 'Kash Patel: A ‘6-Year-Saga’ of Government Corruption, From Russiagate to Mar-a-Lago',
            }
        },
    ]

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

        youmaker_video_id = self._search_regex(r'data-trailer="[\w-]+" data-id="([\w-]+)"', webpage, 'url')
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
            f'http://vs1.youmaker.com/assets/{youmaker_video_id}/playlist.m3u8', video_id, 'mp4', m3u8_id='hls')

        return {
            'id': youmaker_video_id,
            'formats': formats,
            'subtitles': subtitles,
            'title': self._html_extract_title(webpage)
        }

repo_name: Lesmiscore/ytdl


code:
from .common import InfoExtractor
from ..utils import (
    encode_base_n,
    ExtractorError,
    int_or_none,
    merge_dicts,
    parse_duration,
    str_to_int,
    url_or_none,
)

class EpornerIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?eporner\.com/(?:(?:hd-porn|embed)/|video-)(?P<id>\w+)(?:/(?P<display_id>[\w-]+))?'
    _TESTS = [{
        'url': 'http://www.eporner.com/hd-porn/95008/Infamous-Tiffany-Teen-Strip-Tease-Video/',
        'md5': '39d486f046212d8e1b911c52ab4691f8',
        'info_dict': {
            'id': 'qlDUmNsj6VS',
            'display_id': 'Infamous-Tiffany-Teen-Strip-Tease-Video',
            'ext': 'mp4',
            'title': 'Infamous Tiffany Teen Strip Tease Video',
            'description': 'md5:764f39abf932daafa37485eb46efa152',
            'timestamp': 1232520922,
            'upload_date': '20090121',
            'duration': 1838,
            'view_count': int,
            'age_limit': 18,
        },
        'params': {
            'proxy': '127.0.0.1:8118'
        }
    }, {
        # New (May 2016) URL layout
        'url': 'http://www.eporner.com/hd-porn/3YRUtzMcWn0/Star-Wars-XXX-Parody/',
        'only_matching': True,
    }, {
        'url': 'http://www.eporner.com/hd-porn/3YRUtzMcWn0',
        'only_matching': True,
    }, {
        'url': 'http://www.eporner.com/embed/3YRUtzMcWn0',
        'only_matching': True,
    }, {
        'url': 'https://www.eporner.com/video-FJsA19J3Y3H/one-of-the-greats/',
        'only_matching': True,
    }]

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

        webpage, urlh = self._download_webpage_handle(url, display_id)

        video_id = self._match_id(urlh.geturl())

        hash = self._search_regex(
            r'hash\s*[:=]\s*["\']([\da-f]{32})', webpage, 'hash')

        title = self._og_search_title(webpage, default=None) or self._html_search_regex(
            r'<title>(.+?) - EPORNER', webpage, 'title')

        # Reverse engineered from vjs.js
        def calc_hash(s):
            return ''.join((encode_base_n(int(s[lb:lb + 8], 16), 36) for lb in range(0, 32, 8)))

        video = self._download_json(
            'http://www.eporner.com/xhr/video/%s' % video_id,
            display_id, note='Downloading video JSON',
            query={
                'hash': calc_hash(hash),
                'device': 'generic',
                'domain': 'www.eporner.com',
                'fallback': 'false',
            })

        if video.get('available') is False:
            raise ExtractorError(
                '%s said: %s' % (self.IE_NAME, video['message']), expected=True)

        sources = video['sources']

        formats = []
        for kind, formats_dict in sources.items():
            if not isinstance(formats_dict, dict):
                continue
            for format_id, format_dict in formats_dict.items():
                if not isinstance(format_dict, dict):
                    continue
                src = url_or_none(format_dict.get('src'))
                if not src or not src.startswith('http'):
                    continue
                if kind == 'hls':
                    formats.extend(self._extract_m3u8_formats(
                        src, display_id, 'mp4', entry_protocol='m3u8_native',
                        m3u8_id=kind, fatal=False))
                else:
                    height = int_or_none(self._search_regex(
                        r'(\d+)[pP]', format_id, 'height', default=None))
                    fps = int_or_none(self._search_regex(
                        r'(\d+)fps', format_id, 'fps', default=None))

                    formats.append({
                        'url': src,
                        'format_id': format_id,
                        'height': height,
                        'fps': fps,
                    })
        self._sort_formats(formats)

        json_ld = self._search_json_ld(webpage, display_id, default={})

        duration = parse_duration(self._html_search_meta(
            'duration', webpage, default=None))
        view_count = str_to_int(self._search_regex(
            r'id=["\']cinemaviews1["\'][^>]*>\s*([0-9,]+)',
            webpage, 'view count', default=None))

        return merge_dicts(json_ld, {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'duration': duration,
            'view_count': view_count,
            'formats': formats,
            'age_limit': 18,
        })


code:
import re

from .common import InfoExtractor
from ..compat import compat_urllib_parse_urlencode
from ..utils import (
    ExtractorError,
    merge_dicts,
)

class EroProfileIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?eroprofile\.com/m/videos/view/(?P<id>[^/]+)'
    _LOGIN_URL = 'http://www.eroprofile.com/auth/auth.php?'
    _NETRC_MACHINE = 'eroprofile'
    _TESTS = [{
        'url': 'http://www.eroprofile.com/m/videos/view/sexy-babe-softcore',
        'md5': 'c26f351332edf23e1ea28ce9ec9de32f',
        'info_dict': {
            'id': '3733775',
            'display_id': 'sexy-babe-softcore',
            'ext': 'm4v',
            'title': 'sexy babe softcore',
            'thumbnail': r're:https?://.*\.jpg',
            'age_limit': 18,
        },
        'skip': 'Video not found',
    }, {
        'url': 'http://www.eroprofile.com/m/videos/view/Try-It-On-Pee_cut_2-wmv-4shared-com-file-sharing-download-movie-file',
        'md5': '1baa9602ede46ce904c431f5418d8916',
        'info_dict': {
            'id': '1133519',
            'ext': 'm4v',
            'title': 'Try It On Pee_cut_2.wmv - 4shared.com - file sharing - download movie file',
            'thumbnail': r're:https?://.*\.jpg',
            'age_limit': 18,
        },
        'skip': 'Requires login',
    }]

    def _perform_login(self, username, password):
        query = compat_urllib_parse_urlencode({
            'username': username,
            'password': password,
            'url': 'http://www.eroprofile.com/',
        })
        login_url = self._LOGIN_URL + query
        login_page = self._download_webpage(login_url, None, False)

        m = re.search(r'Your username or password was incorrect\.', login_page)
        if m:
            raise ExtractorError(
                'Wrong username and/or password.', expected=True)

        self.report_login()
        redirect_url = self._search_regex(
            r'


code:
import json
import re

from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    clean_html,
    determine_ext,
    ExtractorError,
    dict_get,
    int_or_none,
    merge_dicts,
    parse_qs,
    parse_age_limit,
    parse_iso8601,
    str_or_none,
    try_get,
    url_or_none,
    variadic,
)

class ERTFlixBaseIE(InfoExtractor):
    def _call_api(
            self, video_id, method='Player/AcquireContent', api_version=1,
            param_headers=None, data=None, headers=None, **params):
        platform_codename = {'platformCodename': 'www'}
        headers_as_param = {'X-Api-Date-Format': 'iso', 'X-Api-Camel-Case': False}
        headers_as_param.update(param_headers or {})
        headers = headers or {}
        if data:
            headers['Content-Type'] = headers_as_param['Content-Type'] = 'application/json;charset=utf-8'
            data = json.dumps(merge_dicts(platform_codename, data)).encode('utf-8')
        query = merge_dicts(
            {} if data else platform_codename,
            {'$headers': json.dumps(headers_as_param)},
            params)
        response = self._download_json(
            'https://api.app.ertflix.gr/v%s/%s' % (str(api_version), method),
            video_id, fatal=False, query=query, data=data, headers=headers)
        if try_get(response, lambda x: x['Result']['Success']) is True:
            return response

    def _call_api_get_tiles(self, video_id, *tile_ids):
        requested_tile_ids = [video_id] + list(tile_ids)
        requested_tiles = [{'Id': tile_id} for tile_id in requested_tile_ids]
        tiles_response = self._call_api(
            video_id, method='Tile/GetTiles', api_version=2,
            data={'RequestedTiles': requested_tiles})
        tiles = try_get(tiles_response, lambda x: x['Tiles'], list) or []
        if tile_ids:
            if sorted([tile['Id'] for tile in tiles]) != sorted(requested_tile_ids):
                raise ExtractorError('Requested tiles not found', video_id=video_id)
            return tiles
        try:
            return next(tile for tile in tiles if tile['Id'] == video_id)
        except StopIteration:
            raise ExtractorError('No matching tile found', video_id=video_id)

class ERTFlixCodenameIE(ERTFlixBaseIE):
    IE_NAME = 'ertflix:codename'
    IE_DESC = 'ERTFLIX videos by codename'
    _VALID_URL = r'ertflix:(?P<id>[\w-]+)'
    _TESTS = [{
        'url': 'ertflix:monogramma-praxitelis-tzanoylinos',
        'md5': '5b9c2cd171f09126167e4082fc1dd0ef',
        'info_dict': {
            'id': 'monogramma-praxitelis-tzanoylinos',
            'ext': 'mp4',
            'title': 'md5:ef0b439902963d56c43ac83c3f41dd0e',
        },
    },
    ]

    def _extract_formats_and_subs(self, video_id, allow_none=True):
        media_info = self._call_api(video_id, codename=video_id)
        formats, subs = [], {}
        for media_file in try_get(media_info, lambda x: x['MediaFiles'], list) or []:
            for media in try_get(media_file, lambda x: x['Formats'], list) or []:
                fmt_url = url_or_none(try_get(media, lambda x: x['Url']))
                if not fmt_url:
                    continue
                ext = determine_ext(fmt_url)
                if ext == 'm3u8':
                    formats_, subs_ = self._extract_m3u8_formats_and_subtitles(
                        fmt_url, video_id, m3u8_id='hls', ext='mp4', fatal=False)
                elif ext == 'mpd':
                    formats_, subs_ = self._extract_mpd_formats_and_subtitles(
                        fmt_url, video_id, mpd_id='dash', fatal=False)
                else:
                    formats.append({
                        'url': fmt_url,
                        'format_id': str_or_none(media.get('Id')),
                    })
                    continue
                formats.extend(formats_)
                self._merge_subtitles(subs_, target=subs)

        if formats or not allow_none:
            self._sort_formats(formats)
        return formats, subs

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

        formats, subs = self._extract_formats_and_subs(video_id)

        if formats:
            return {
                'id': video_id,
                'formats': formats,
                'subtitles': subs,
                'title': self._generic_title(url),
            }

class ERTFlixIE(ERTFlixBaseIE):
    IE_NAME = 'ertflix'
    IE_DESC = 'ERTFLIX videos'
    _VALID_URL = r'https?://www\.ertflix\.gr/(?:[^/]+/)?(?:series|vod)/(?P<id>[a-z]{3}\.\d+)'
    _TESTS = [{
        'url': 'https://www.ertflix.gr/vod/vod.173258-aoratoi-ergates',
        'md5': '6479d5e60fd7e520b07ba5411dcdd6e7',
        'info_dict': {
            'id': 'aoratoi-ergates',
            'ext': 'mp4',
            'title': 'md5:c1433d598fbba0211b0069021517f8b4',
            'description': 'md5:01a64d113c31957eb7eb07719ab18ff4',
            'thumbnail': r're:https?://.+\.jpg',
            'episode_id': 'vod.173258',
            'timestamp': 1639648800,
            'upload_date': '20211216',
            'duration': 3166,
            'age_limit': 8,
        },
    }, {
        'url': 'https://www.ertflix.gr/series/ser.3448-monogramma',
        'info_dict': {
            'id': 'ser.3448',
            'age_limit': 8,
            'description': 'Η εκπομπή σαράντα ετών που σημάδεψε τον πολιτισμό μας.',
            'title': 'Μονόγραμμα',
        },
        'playlist_mincount': 64,
    }, {
        'url': 'https://www.ertflix.gr/series/ser.3448-monogramma?season=1',
        'info_dict': {
            'id': 'ser.3448',
            'age_limit': 8,
            'description': 'Η εκπομπή σαράντα ετών που σημάδεψε τον πολιτισμό μας.',
            'title': 'Μονόγραμμα',
        },
        'playlist_count': 22,
    }, {
        'url': 'https://www.ertflix.gr/series/ser.3448-monogramma?season=1&season=2021%20-%202022',
        'info_dict': {
            'id': 'ser.3448',
            'age_limit': 8,
            'description': 'Η εκπομπή σαράντα ετών που σημάδεψε τον πολιτισμό μας.',
            'title': 'Μονόγραμμα',
        },
        'playlist_mincount': 36,
    }, {
        'url': 'https://www.ertflix.gr/series/ser.164991-to-diktuo-1?season=1-9',
        'info_dict': {
            'id': 'ser.164991',
            'age_limit': 8,
            'description': 'Η πρώτη ελληνική εκπομπή με θεματολογία αποκλειστικά γύρω από το ίντερνετ.',
            'title': 'Το δίκτυο',
        },
        'playlist_mincount': 9,
    }, {
        'url': 'https://www.ertflix.gr/en/vod/vod.127652-ta-kalytera-mas-chronia-ep1-mia-volta-sto-feggari',
        'only_matching': True,
    }]

    def _extract_episode(self, episode):
        codename = try_get(episode, lambda x: x['Codename'], compat_str)
        title = episode.get('Title')
        description = clean_html(dict_get(episode, ('ShortDescription', 'TinyDescription', )))
        if not codename or not title or not episode.get('HasPlayableStream', True):
            return
        thumbnail = next((
            url_or_none(thumb.get('Url'))
            for thumb in variadic(dict_get(episode, ('Images', 'Image')) or {})
            if thumb.get('IsMain')),
            None)
        return {
            '_type': 'url_transparent',
            'thumbnail': thumbnail,
            'id': codename,
            'episode_id': episode.get('Id'),
            'title': title,
            'alt_title': episode.get('Subtitle'),
            'description': description,
            'timestamp': parse_iso8601(episode.get('PublishDate')),
            'duration': episode.get('DurationSeconds'),
            'age_limit': self._parse_age_rating(episode),
            'url': 'ertflix:%s' % (codename, ),
        }

    @staticmethod
    def _parse_age_rating(info_dict):
        return parse_age_limit(
            info_dict.get('AgeRating')
            or (info_dict.get('IsAdultContent') and 18)
            or (info_dict.get('IsKidsContent') and 0))

    def _extract_series(self, video_id, season_titles=None, season_numbers=None):
        media_info = self._call_api(video_id, method='Tile/GetSeriesDetails', id=video_id)

        series = try_get(media_info, lambda x: x['Series'], dict) or {}
        series_info = {
            'age_limit': self._parse_age_rating(series),
            'title': series.get('Title'),
            'description': dict_get(series, ('ShortDescription', 'TinyDescription', )),
        }
        if season_numbers:
            season_titles = season_titles or []
            for season in try_get(series, lambda x: x['Seasons'], list) or []:
                if season.get('SeasonNumber') in season_numbers and season.get('Title'):
                    season_titles.append(season['Title'])

        def gen_episode(m_info, season_titles):
            for episode_group in try_get(m_info, lambda x: x['EpisodeGroups'], list) or []:
                if season_titles and episode_group.get('Title') not in season_titles:
                    continue
                episodes = try_get(episode_group, lambda x: x['Episodes'], list)
                if not episodes:
                    continue
                season_info = {
                    'season': episode_group.get('Title'),
                    'season_number': int_or_none(episode_group.get('SeasonNumber')),
                }
                try:
                    episodes = [(int(ep['EpisodeNumber']), ep) for ep in episodes]
                    episodes.sort()
                except (KeyError, ValueError):
                    episodes = enumerate(episodes, 1)
                for n, episode in episodes:
                    info = self._extract_episode(episode)
                    if info is None:
                        continue
                    info['episode_number'] = n
                    info.update(season_info)
                    yield info

        return self.playlist_result(
            gen_episode(media_info, season_titles), playlist_id=video_id, **series_info)

    def _real_extract(self, url):
        video_id = self._match_id(url)
        if video_id.startswith('ser.'):
            param_season = parse_qs(url).get('season', [None])
            param_season = [
                (have_number, int_or_none(v) if have_number else str_or_none(v))
                for have_number, v in
                [(int_or_none(ps) is not None, ps) for ps in param_season]
                if v is not None
            ]
            season_kwargs = {
                k: [v for is_num, v in param_season if is_num is c] or None
                for k, c in
                [('season_titles', False), ('season_numbers', True)]
            }
            return self._extract_series(video_id, **season_kwargs)

        return self._extract_episode(self._call_api_get_tiles(video_id))

class ERTWebtvEmbedIE(InfoExtractor):
    IE_NAME = 'ertwebtv:embed'
    IE_DESC = 'ert.gr webtv embedded videos'
    _BASE_PLAYER_URL_RE = re.escape('//www.ert.gr/webtv/live-uni/vod/dt-uni-vod.php')
    _VALID_URL = rf'https?:{_BASE_PLAYER_URL_RE}\?([^#]+&)?f=(?P<id>[^#&]+)'
    _EMBED_REGEX = [rf'<iframe[^>]+?src=(?P<_q1>["\'])(?P<url>(?:https?:)?{_BASE_PLAYER_URL_RE}\?(?:(?!(?P=_q1)).)+)(?P=_q1)']

    _TESTS = [{
        'url': 'https://www.ert.gr/webtv/live-uni/vod/dt-uni-vod.php?f=trailers/E2251_TO_DIKTYO_E09_16-01_1900.mp4&bgimg=/photos/2022/1/to_diktio_ep09_i_istoria_tou_diadiktiou_stin_Ellada_1021x576.jpg',
        'md5': 'f9e9900c25c26f4ecfbddbb4b6305854',
        'info_dict': {
            'id': 'trailers/E2251_TO_DIKTYO_E09_16-01_1900.mp4',
            'title': 'md5:914f06a73cd8b62fbcd6fb90c636e497',
            'ext': 'mp4',
            'thumbnail': 'https://program.ert.gr/photos/2022/1/to_diktio_ep09_i_istoria_tou_diadiktiou_stin_Ellada_1021x576.jpg'
        },
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        formats, subs = self._extract_m3u8_formats_and_subtitles(
            f'https://mediastream.ert.gr/vodedge/_definst_/mp4:dvrorigin/{video_id}/playlist.m3u8',
            video_id, 'mp4')
        self._sort_formats(formats)
        thumbnail_id = parse_qs(url).get('bgimg', [None])[0]
        if thumbnail_id and not thumbnail_id.startswith('http'):
            thumbnail_id = f'https://program.ert.gr{thumbnail_id}'
        return {
            'id': video_id,
            'title': f'VOD - {video_id}',
            'thumbnail': thumbnail_id,
            'formats': formats,
            'subtitles': subs,
        }


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

def _decrypt_config(key, string):
    a = ''
    i = ''
    r = ''

    while len(a) < (len(string) / 2):
        a += key

    a = a[0:int(len(string) / 2)]

    t = 0
    while t < len(string):
        i += chr(int(string[t] + string[t + 1], 16))
        t += 2

    icko = [s for s in i]

    for t, c in enumerate(a):
        r += chr(ord(c) ^ ord(icko[t]))

    return r

class EscapistIE(InfoExtractor):
    _VALID_URL = r'https?://?(?:(?:www|v1)\.)?escapistmagazine\.com/videos/view/[^/]+/(?P<id>[0-9]+)'
    _TESTS = [{
        'url': 'http://www.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate',
        'md5': 'ab3a706c681efca53f0a35f1415cf0d1',
        'info_dict': {
            'id': '6618',
            'ext': 'mp4',
            'description': "Baldur's Gate: Original, Modded or Enhanced Edition? I'll break down what you can expect from the new Baldur's Gate: Enhanced Edition.",
            'title': "Breaking Down Baldur's Gate",
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 264,
            'uploader': 'The Escapist',
        }
    }, {
        'url': 'http://www.escapistmagazine.com/videos/view/zero-punctuation/10044-Evolve-One-vs-Multiplayer',
        'md5': '9e8c437b0dbb0387d3bd3255ca77f6bf',
        'info_dict': {
            'id': '10044',
            'ext': 'mp4',
            'description': 'This week, Zero Punctuation reviews Evolve.',
            'title': 'Evolve - One vs Multiplayer',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 304,
            'uploader': 'The Escapist',
        }
    }, {
        'url': 'http://escapistmagazine.com/videos/view/the-escapist-presents/6618',
        'only_matching': True,
    }, {
        'url': 'https://v1.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate',
        'only_matching': True,
    }]

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

        ims_video = self._parse_json(
            self._search_regex(
                r'imsVideo\.play\(({.+?})\);', webpage, 'imsVideo'),
            video_id)
        video_id = ims_video['videoID']
        key = ims_video['hash']

        config = self._download_webpage(
            'http://www.escapistmagazine.com/videos/vidconfig.php',
            video_id, 'Downloading video config', headers={
                'Referer': url,
            }, query={
                'videoID': video_id,
                'hash': key,
            })

        data = self._parse_json(_decrypt_config(key, config), video_id)

        video_data = data['videoData']

        title = clean_html(video_data['title'])

        formats = [{
            'url': video['src'],
            'format_id': '%s-%sp' % (determine_ext(video['src']), video['res']),
            'height': int_or_none(video.get('res')),
        } for video in data['files']['videos']]
        self._sort_formats(formats)

        return {
            'id': video_id,
            'formats': formats,
            'title': title,
            'thumbnail': self._og_search_thumbnail(webpage) or data.get('poster'),
            'description': self._og_search_description(webpage),
            'duration': float_or_none(video_data.get('duration'), 1000),
            'uploader': video_data.get('publisher'),
            'series': video_data.get('show'),
        }


code:
import base64
import json
import re
import urllib.parse

from .adobepass import AdobePassIE
from .common import InfoExtractor
from .once import OnceIE
from ..utils import (
    determine_ext,
    dict_get,
    int_or_none,
    traverse_obj,
    unified_strdate,
    unified_timestamp,
)

class ESPNIE(OnceIE):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            (?:
                                (?:
                                    (?:(?:\w+\.)+)?espn\.go|
                                    (?:www\.)?espn
                                )\.com/
                                (?:
                                    (?:
                                        video/(?:clip|iframe/twitter)|
                                    )
                                    (?:
                                        .*?\?.*?\bid=|
                                        /_/id/
                                    )|
                                    [^/]+/video/
                                )
                            )|
                            (?:www\.)espnfc\.(?:com|us)/(?:video/)?[^/]+/\d+/video/
                        )
                        (?P<id>\d+)
                    '''

    _TESTS = [{
        'url': 'http://espn.go.com/video/clip?id=10365079',
        'info_dict': {
            'id': '10365079',
            'ext': 'mp4',
            'title': '30 for 30 Shorts: Judging Jewell',
            'description': 'md5:39370c2e016cb4ecf498ffe75bef7f0f',
            'timestamp': 1390936111,
            'upload_date': '20140128',
            'duration': 1302,
            'thumbnail': r're:https://.+\.jpg',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://broadband.espn.go.com/video/clip?id=18910086',
        'info_dict': {
            'id': '18910086',
            'ext': 'mp4',
            'title': 'Kyrie spins around defender for two',
            'description': 'md5:2b0f5bae9616d26fba8808350f0d2b9b',
            'timestamp': 1489539155,
            'upload_date': '20170315',
        },
        'params': {
            'skip_download': True,
        },
        'expected_warnings': ['Unable to download f4m manifest'],
    }, {
        'url': 'http://nonredline.sports.espn.go.com/video/clip?id=19744672',
        'only_matching': True,
    }, {
        'url': 'https://cdn.espn.go.com/video/clip/_/id/19771774',
        'only_matching': True,
    }, {
        'url': 'http://www.espn.com/video/clip?id=10365079',
        'only_matching': True,
    }, {
        'url': 'http://www.espn.com/video/clip/_/id/17989860',
        'only_matching': True,
    }, {
        'url': 'https://espn.go.com/video/iframe/twitter/?cms=espn&id=10365079',
        'only_matching': True,
    }, {
        'url': 'http://www.espnfc.us/video/espn-fc-tv/86/video/3319154/nashville-unveiled-as-the-newest-club-in-mls',
        'only_matching': True,
    }, {
        'url': 'http://www.espnfc.com/english-premier-league/23/video/3324163/premier-league-in-90-seconds-golden-tweets',
        'only_matching': True,
    }, {
        'url': 'http://www.espn.com/espnw/video/26066627/arkansas-gibson-completes-hr-cycle-four-innings',
        'only_matching': True,
    }, {
        'url': 'http://www.espn.com/watch/player?id=19141491',
        'only_matching': True,
    }, {
        'url': 'http://www.espn.com/watch/player?bucketId=257&id=19505875',
        'only_matching': True,
    }, ]

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

        clip = self._download_json(
            'http://api-app.espn.com/v1/video/clips/%s' % video_id,
            video_id)['videos'][0]

        title = clip['headline']

        format_urls = set()
        formats = []

        def traverse_source(source, base_source_id=None):
            for source_id, source in source.items():
                if source_id == 'alert':
                    continue
                elif isinstance(source, str):
                    extract_source(source, base_source_id)
                elif isinstance(source, dict):
                    traverse_source(
                        source,
                        '%s-%s' % (base_source_id, source_id)
                        if base_source_id else source_id)

        def extract_source(source_url, source_id=None):
            if source_url in format_urls:
                return
            format_urls.add(source_url)
            ext = determine_ext(source_url)
            if OnceIE.suitable(source_url):
                formats.extend(self._extract_once_formats(source_url))
            elif ext == 'smil':
                formats.extend(self._extract_smil_formats(
                    source_url, video_id, fatal=False))
            elif ext == 'f4m':
                formats.extend(self._extract_f4m_formats(
                    source_url, video_id, f4m_id=source_id, fatal=False))
            elif ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    source_url, video_id, 'mp4', entry_protocol='m3u8_native',
                    m3u8_id=source_id, fatal=False))
            else:
                f = {
                    'url': source_url,
                    'format_id': source_id,
                }
                mobj = re.search(r'(\d+)p(\d+)_(\d+)k\.', source_url)
                if mobj:
                    f.update({
                        'height': int(mobj.group(1)),
                        'fps': int(mobj.group(2)),
                        'tbr': int(mobj.group(3)),
                    })
                if source_id == 'mezzanine':
                    f['quality'] = 1
                formats.append(f)

        links = clip.get('links', {})
        traverse_source(links.get('source', {}))
        traverse_source(links.get('mobile', {}))
        self._sort_formats(formats)

        description = clip.get('caption') or clip.get('description')
        thumbnail = clip.get('thumbnail')
        duration = int_or_none(clip.get('duration'))
        timestamp = unified_timestamp(clip.get('originalPublishDate'))

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

class ESPNArticleIE(InfoExtractor):
    _VALID_URL = r'https?://(?:espn\.go|(?:www\.)?espn)\.com/(?:[^/]+/)*(?P<id>[^/]+)'
    _TESTS = [{
        'url': 'http://espn.go.com/nba/recap?gameId=400793786',
        'only_matching': True,
    }, {
        'url': 'http://espn.go.com/blog/golden-state-warriors/post/_/id/593/how-warriors-rapidly-regained-a-winning-edge',
        'only_matching': True,
    }, {
        'url': 'http://espn.go.com/sports/endurance/story/_/id/12893522/dzhokhar-tsarnaev-sentenced-role-boston-marathon-bombings',
        'only_matching': True,
    }, {
        'url': 'http://espn.go.com/nba/playoffs/2015/story/_/id/12887571/john-wall-washington-wizards-no-swelling-left-hand-wrist-game-5-return',
        'only_matching': True,
    }]

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

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

        webpage = self._download_webpage(url, video_id)

        video_id = self._search_regex(
            r'class=(["\']).*?video-play-button.*?\1[^>]+data-id=["\'](?P<id>\d+)',
            webpage, 'video id', group='id')

        return self.url_result(
            'http://espn.go.com/video/clip?id=%s' % video_id, ESPNIE.ie_key())

class FiveThirtyEightIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?fivethirtyeight\.com/features/(?P<id>[^/?#]+)'
    _TEST = {
        'url': 'http://fivethirtyeight.com/features/how-the-6-8-raiders-can-still-make-the-playoffs/',
        'info_dict': {
            'id': '56032156',
            'ext': 'flv',
            'title': 'FiveThirtyEight: The Raiders can still make the playoffs',
            'description': 'Neil Paine breaks down the simplest scenario that will put the Raiders into the playoffs at 8-8.',
        },
        'params': {
            'skip_download': True,
        },
    }

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

        webpage = self._download_webpage(url, video_id)

        embed_url = self._search_regex(
            r'<iframe[^>]+src=["\'](https?://fivethirtyeight\.abcnews\.go\.com/video/embed/\d+/\d+)',
            webpage, 'embed url')

        return self.url_result(embed_url, 'AbcNewsVideo')

class ESPNCricInfoIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?espncricinfo\.com/video/[^#$&?/]+-(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://www.espncricinfo.com/video/finch-chasing-comes-with-risks-despite-world-cup-trend-1289135',
        'info_dict': {
            'id': '1289135',
            'ext': 'mp4',
            'title': 'Finch: Chasing comes with \'risks\' despite World Cup trend',
            'description': 'md5:ea32373303e25efbb146efdfc8a37829',
            'upload_date': '20211113',
            'duration': 96,
        },
        'params': {'skip_download': True}
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        data_json = self._download_json(f'https://hs-consumer-api.espncricinfo.com/v1/pages/video/video-details?videoId={id}', id)['video']
        formats, subtitles = [], {}
        for item in data_json.get('playbacks') or []:
            if item.get('type') == 'HLS' and item.get('url'):
                m3u8_frmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(item['url'], id)
                formats.extend(m3u8_frmts)
                subtitles = self._merge_subtitles(subtitles, m3u8_subs)
            elif item.get('type') == 'AUDIO' and item.get('url'):
                formats.append({
                    'url': item['url'],
                    'vcodec': 'none',
                })
        self._sort_formats(formats)
        return {
            'id': id,
            'title': data_json.get('title'),
            'description': data_json.get('summary'),
            'upload_date': unified_strdate(dict_get(data_json, ('publishedAt', 'recordedAt'))),
            'duration': data_json.get('duration'),
            'formats': formats,
            'subtitles': subtitles,
        }

class WatchESPNIE(AdobePassIE):
    _VALID_URL = r'https?://(?:www\.)?espn\.com/(?:watch|espnplus)/player/_/id/(?P<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
    _TESTS = [{
        'url': 'https://www.espn.com/watch/player/_/id/dbbc6b1d-c084-4b47-9878-5f13c56ce309',
        'info_dict': {
            'id': 'dbbc6b1d-c084-4b47-9878-5f13c56ce309',
            'ext': 'mp4',
            'title': 'Huddersfield vs. Burnley',
            'duration': 7500,
            'thumbnail': 'https://artwork.api.espn.com/artwork/collections/media/dbbc6b1d-c084-4b47-9878-5f13c56ce309/default?width=640&apikey=1ngjw23osgcis1i1vbj96lmfqs',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://www.espn.com/watch/player/_/id/a049a56e-a7ce-477e-aef3-c7e48ef8221c',
        'info_dict': {
            'id': 'a049a56e-a7ce-477e-aef3-c7e48ef8221c',
            'ext': 'mp4',
            'title': 'Dynamo Dresden vs. VfB Stuttgart (Round #1) (German Cup)',
            'duration': 8335,
            'thumbnail': 'https://s.secure.espncdn.com/stitcher/artwork/collections/media/bd1f3d12-0654-47d9-852e-71b85ea695c7/16x9.jpg?timestamp=202201112217&showBadge=true&cb=12&package=ESPN_PLUS',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://www.espn.com/espnplus/player/_/id/317f5fd1-c78a-4ebe-824a-129e0d348421',
        'info_dict': {
            'id': '317f5fd1-c78a-4ebe-824a-129e0d348421',
            'ext': 'mp4',
            'title': 'The Wheel - Episode 10',
            'duration': 3352,
            'thumbnail': 'https://s.secure.espncdn.com/stitcher/artwork/collections/media/317f5fd1-c78a-4ebe-824a-129e0d348421/16x9.jpg?timestamp=202205031523&showBadge=true&cb=12&package=ESPN_PLUS',
        },
        'params': {
            'skip_download': True,
        },
    }]

    _API_KEY = 'ZXNwbiZicm93c2VyJjEuMC4w.ptUt7QxsteaRruuPmGZFaJByOoqKvDP2a5YkInHrc7c'

    def _call_bamgrid_api(self, path, video_id, payload=None, headers={}):
        if 'Authorization' not in headers:
            headers['Authorization'] = f'Bearer {self._API_KEY}'
        parse = urllib.parse.urlencode if path == 'token' else json.dumps
        return self._download_json(
            f'https://espn.api.edge.bamgrid.com/{path}', video_id, headers=headers, data=parse(payload).encode())

    def _real_extract(self, url):
        video_id = self._match_id(url)
        cdn_data = self._download_json(
            f'https://watch-cdn.product.api.espn.com/api/product/v3/watchespn/web/playback/event?id={video_id}',
            video_id)
        video_data = cdn_data['playbackState']

        # ESPN+ subscription required, through cookies
        if 'DTC' in video_data.get('sourceId'):
            cookie = self._get_cookies(url).get('ESPN-ONESITE.WEB-PROD.token')
            if not cookie:
                self.raise_login_required(method='cookies')

            assertion = self._call_bamgrid_api(
                'devices', video_id,
                headers={'Content-Type': 'application/json; charset=UTF-8'},
                payload={
                    'deviceFamily': 'android',
                    'applicationRuntime': 'android',
                    'deviceProfile': 'tv',
                    'attributes': {},
                })['assertion']
            token = self._call_bamgrid_api(
                'token', video_id, payload={
                    'subject_token': assertion,
                    'subject_token_type': 'urn:bamtech:params:oauth:token-type:device',
                    'platform': 'android',
                    'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange'
                })['access_token']

            assertion = self._call_bamgrid_api(
                'accounts/grant', video_id, payload={'id_token': cookie.value.split('|')[1]},
                headers={
                    'Authorization': token,
                    'Content-Type': 'application/json; charset=UTF-8'
                })['assertion']
            token = self._call_bamgrid_api(
                'token', video_id, payload={
                    'subject_token': assertion,
                    'subject_token_type': 'urn:bamtech:params:oauth:token-type:account',
                    'platform': 'android',
                    'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange'
                })['access_token']

            playback = self._download_json(
                video_data['videoHref'].format(scenario='browser~ssai'), video_id,
                headers={
                    'Accept': 'application/vnd.media-service+json; version=5',
                    'Authorization': token
                })
            m3u8_url, headers = playback['stream']['complete'][0]['url'], {'authorization': token}

        # No login required
        elif video_data.get('sourceId') == 'ESPN_FREE':
            asset = self._download_json(
                f'https://watch.auth.api.espn.com/video/auth/media/{video_id}/asset?apikey=uiqlbgzdwuru14v627vdusswb',
                video_id)
            m3u8_url, headers = asset['stream'], {}

        # TV Provider required
        else:
            resource = self._get_mvpd_resource('ESPN', video_data['name'], video_id, None)
            auth = self._extract_mvpd_auth(url, video_id, 'ESPN', resource).encode()

            asset = self._download_json(
                f'https://watch.auth.api.espn.com/video/auth/media/{video_id}/asset?apikey=uiqlbgzdwuru14v627vdusswb',
                video_id, data=f'adobeToken={urllib.parse.quote_plus(base64.b64encode(auth))}&drmSupport=HLS'.encode())
            m3u8_url, headers = asset['stream'], {}

        formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4', m3u8_id='hls')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'duration': traverse_obj(cdn_data, ('tracking', 'duration')),
            'title': video_data.get('name'),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnail': video_data.get('posterHref'),
            'http_headers': headers,
        }


code:
import re

from .common import InfoExtractor
from ..compat import compat_urlparse
from ..utils import (
    int_or_none,
    parse_filesize,
    unified_strdate,
)

class EsriVideoIE(InfoExtractor):
    _VALID_URL = r'https?://video\.esri\.com/watch/(?P<id>[0-9]+)'
    _TEST = {
        'url': 'https://video.esri.com/watch/1124/arcgis-online-_dash_-developing-applications',
        'md5': 'd4aaf1408b221f1b38227a9bbaeb95bc',
        'info_dict': {
            'id': '1124',
            'ext': 'mp4',
            'title': 'ArcGIS Online - Developing Applications',
            'description': 'Jeremy Bartley demonstrates how to develop applications with ArcGIS Online.',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 185,
            'upload_date': '20120419',
        }
    }

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

        webpage = self._download_webpage(url, video_id)

        formats = []
        for width, height, content in re.findall(
                r'(?s)
(\d+)x(\d+):(.+?)
', webpage):
            for video_url, ext, filesize in re.findall(
                    r']+href="([^"]+)">([^<]+) \(([^<]+)\)', content):
                formats.append({
                    'url': compat_urlparse.urljoin(url, video_url),
                    'ext': ext.lower(),
                    'format_id': '%s-%s' % (ext.lower(), height),
                    'width': int(width),
                    'height': int(height),
                    'filesize_approx': parse_filesize(filesize),
                })
        self._sort_formats(formats)

        title = self._html_search_meta('title', webpage, 'title')
        description = self._html_search_meta(
            'description', webpage, 'description', fatal=False)

        thumbnail = self._html_search_meta('thumbnail', webpage, 'thumbnail', fatal=False)
        if thumbnail:
            thumbnail = re.sub(r'_[st]\.jpg$', '_x.jpg', thumbnail)

        duration = int_or_none(self._search_regex(
            [r'var\s+videoSeconds\s*=\s*(\d+)', r"'duration'\s*:\s*(\d+)"],
            webpage, 'duration', fatal=False))

        upload_date = unified_strdate(self._html_search_meta(
            'last-modified', webpage, 'upload date', fatal=False))

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


code:
from .common import InfoExtractor
from ..utils import (
    int_or_none,
    orderedSet,
    parse_duration,
    parse_qs,
    qualities,
    unified_strdate,
    xpath_text
)

class EuropaIE(InfoExtractor):
    _VALID_URL = r'https?://ec\.europa\.eu/avservices/(?:video/player|audio/audioDetails)\.cfm\?.*?\bref=(?P<id>[A-Za-z0-9-]+)'
    _TESTS = [{
        'url': 'http://ec.europa.eu/avservices/video/player.cfm?ref=I107758',
        'md5': '574f080699ddd1e19a675b0ddf010371',
        'info_dict': {
            'id': 'I107758',
            'ext': 'mp4',
            'title': 'TRADE - Wikileaks on TTIP',
            'description': 'NEW  LIVE EC Midday press briefing of 11/08/2015',
            'thumbnail': r're:^https?://.*\.jpg$',
            'upload_date': '20150811',
            'duration': 34,
            'view_count': int,
            'formats': 'mincount:3',
        }
    }, {
        'url': 'http://ec.europa.eu/avservices/video/player.cfm?sitelang=en&ref=I107786',
        'only_matching': True,
    }, {
        'url': 'http://ec.europa.eu/avservices/audio/audioDetails.cfm?ref=I-109295&sitelang=en',
        'only_matching': True,
    }]

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

        playlist = self._download_xml(
            'http://ec.europa.eu/avservices/video/player/playlist.cfm?ID=%s' % video_id, video_id)

        def get_item(type_, preference):
            items = {}
            for item in playlist.findall('./info/%s/item' % type_):
                lang, label = xpath_text(item, 'lg', default=None), xpath_text(item, 'label', default=None)
                if lang and label:
                    items[lang] = label.strip()
            for p in preference:
                if items.get(p):
                    return items[p]

        query = parse_qs(url)
        preferred_lang = query.get('sitelang', ('en', ))[0]

        preferred_langs = orderedSet((preferred_lang, 'en', 'int'))

        title = get_item('title', preferred_langs) or video_id
        description = get_item('description', preferred_langs)
        thumbnail = xpath_text(playlist, './info/thumburl', 'thumbnail')
        upload_date = unified_strdate(xpath_text(playlist, './info/date', 'upload date'))
        duration = parse_duration(xpath_text(playlist, './info/duration', 'duration'))
        view_count = int_or_none(xpath_text(playlist, './info/views', 'views'))

        language_preference = qualities(preferred_langs[::-1])

        formats = []
        for file_ in playlist.findall('./files/file'):
            video_url = xpath_text(file_, './url')
            if not video_url:
                continue
            lang = xpath_text(file_, './lg')
            formats.append({
                'url': video_url,
                'format_id': lang,
                'format_note': xpath_text(file_, './lglabel'),
                'language_preference': language_preference(lang)
            })
        self._sort_formats(formats)

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


code:
import re

from .common import InfoExtractor

class EuropeanTourIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?europeantour\.com/dpworld-tour/news/video/(?P<id>[^/&?#$]+)'

    _TESTS = [{
        'url': 'https://www.europeantour.com/dpworld-tour/news/video/the-best-shots-of-the-2021-seasons/',
        'info_dict': {
            'id': '6287788195001',
            'ext': 'mp4',
            'title': 'The best shots of the 2021 seasons',
            'duration': 2416.512,
            'timestamp': 1640010141,
            'uploader_id': '5136026580001',
            'tags': ['prod-imported'],
            'thumbnail': 'md5:fdac52bc826548860edf8145ee74e71a',
            'upload_date': '20211220'
        },
        'params': {'skip_download': True}
    }]

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

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        vid, aid = re.search(r'(?s)brightcove-player\s?video-id="([^"]+)".*"ACCOUNT_ID":"([^"]+)"', webpage).groups()
        if not aid:
            aid = '5136026580001'
        return self.url_result(
            self.BRIGHTCOVE_URL_TEMPLATE % (aid, vid), 'BrightcoveNew')


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

class EurosportIE(InfoExtractor):
    _VALID_URL = r'https?://www\.eurosport\.com/\w+/[\w-]+/\d+/[\w-]+_(?P<id>vid\d+)'
    _TESTS = [{
        'url': 'https://www.eurosport.com/tennis/roland-garros/2022/highlights-rafael-nadal-brushes-aside-caper-ruud-to-win-record-extending-14th-french-open-title_vid1694147/video.shtml',
        'info_dict': {
            'id': '2480939',
            'ext': 'mp4',
            'title': 'Highlights: Rafael Nadal brushes aside Caper Ruud to win record-extending 14th French Open title',
            'description': 'md5:b564db73ecfe4b14ebbd8e62a3692c76',
            'thumbnail': 'https://imgresizer.eurosport.com/unsafe/1280x960/smart/filters:format(jpeg)/origin-imgresizer.eurosport.com/2022/06/05/3388285-69245968-2560-1440.png',
            'duration': 195.0,
            'display_id': 'vid1694147',
            'timestamp': 1654446698,
            'upload_date': '20220605',
        }
    }, {
        'url': 'https://www.eurosport.com/tennis/roland-garros/2022/watch-the-top-five-shots-from-men-s-final-as-rafael-nadal-beats-casper-ruud-to-seal-14th-french-open_vid1694283/video.shtml',
        'info_dict': {
            'id': '2481254',
            'ext': 'mp4',
            'title': 'md5:149dcc5dfb38ab7352acc008cc9fb071',
            'duration': 130.0,
            'thumbnail': 'https://imgresizer.eurosport.com/unsafe/1280x960/smart/filters:format(jpeg)/origin-imgresizer.eurosport.com/2022/06/05/3388422-69248708-2560-1440.png',
            'description': 'md5:a0c8a7f6b285e48ae8ddbe7aa85cfee6',
            'display_id': 'vid1694283',
            'timestamp': 1654456090,
            'upload_date': '20220605',
        }
    }, {
        # geo-fence but can bypassed by xff
        'url': 'https://www.eurosport.com/cycling/tour-de-france-femmes/2022/incredible-ride-marlen-reusser-storms-to-stage-4-win-at-tour-de-france-femmes_vid1722221/video.shtml',
        'info_dict': {
            'id': '2582552',
            'ext': 'mp4',
            'title': '‘Incredible ride!’ - Marlen Reusser storms to Stage 4 win at Tour de France Femmes',
            'duration': 188.0,
            'display_id': 'vid1722221',
            'timestamp': 1658936167,
            'thumbnail': 'https://imgresizer.eurosport.com/unsafe/1280x960/smart/filters:format(jpeg)/origin-imgresizer.eurosport.com/2022/07/27/3423347-69852108-2560-1440.jpg',
            'description': 'md5:32bbe3a773ac132c57fb1e8cca4b7c71',
            'upload_date': '20220727',
        }
    }]

    _TOKEN = None

    # actually defined in https://netsport.eurosport.io/?variables={"databaseId":<databaseId>,"playoutType":"VDP"}&extensions={"persistedQuery":{"version":1 ..
    # but this method require to get sha256 hash
    _GEO_COUNTRIES = ['DE', 'NL', 'EU', 'IT', 'FR']  # Not complete list but it should work

    def _real_initialize(self):
        if EurosportIE._TOKEN is None:
            EurosportIE._TOKEN = self._download_json(
                'https://eu3-prod-direct.eurosport.com/token?realm=eurosport', None,
                'Trying to get token')['data']['attributes']['token']

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

        json_data = self._download_json(
            f'https://eu3-prod-direct.eurosport.com/playback/v2/videoPlaybackInfo/sourceSystemId/eurosport-{display_id}',
            display_id, query={'usePreAuth': True}, headers={'Authorization': f'Bearer {EurosportIE._TOKEN}'})['data']

        json_ld_data = self._search_json_ld(webpage, display_id)

        formats, subtitles = [], {}
        for stream_type in json_data['attributes']['streaming']:
            if stream_type == 'hls':
                fmts, subs = self._extract_m3u8_formats_and_subtitles(
                    traverse_obj(json_data, ('attributes', 'streaming', stream_type, 'url')), display_id, ext='mp4')
            elif stream_type == 'dash':
                fmts, subs = self._extract_mpd_formats_and_subtitles(
                    traverse_obj(json_data, ('attributes', 'streaming', stream_type, 'url')), display_id)
            elif stream_type == 'mss':
                fmts, subs = self._extract_ism_formats_and_subtitles(
                    traverse_obj(json_data, ('attributes', 'streaming', stream_type, 'url')), display_id)

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

        self._sort_formats(formats)

        return {
            'id': json_data['id'],
            'title': json_ld_data.get('title') or self._og_search_title(webpage),
            'display_id': display_id,
            'formats': formats,
            'subtitles': subtitles,
            'thumbnails': json_ld_data.get('thumbnails'),
            'description': (json_ld_data.get('description')
                            or self._html_search_meta(['og:description', 'description'], webpage)),
            'duration': json_ld_data.get('duration'),
            'timestamp': json_ld_data.get('timestamp'),
        }


code:
from .common import InfoExtractor

from ..utils import (
    parse_duration,
    js_to_json,
)

class EUScreenIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?euscreen\.eu/item.html\?id=(?P<id>[^&?$/]+)'

    _TESTS = [{
        'url': 'https://euscreen.eu/item.html?id=EUS_0EBCBF356BFC4E12A014023BA41BD98C',
        'info_dict': {
            'id': 'EUS_0EBCBF356BFC4E12A014023BA41BD98C',
            'ext': 'mp4',
            'title': "L'effondrement du stade du Heysel",
            'alt_title': 'Collapse of the Heysel Stadium',
            'duration': 318.0,
            'description': 'md5:f0ffffdfce6821139357a1b8359d6152',
            'series': 'JA2 DERNIERE',
            'episode': '-',
            'uploader': 'INA /  France',
            'thumbnail': 'http://images3.noterik.com/domain/euscreenxl/user/eu_ina/video/EUS_0EBCBF356BFC4E12A014023BA41BD98C/image.jpg'
        },
        'params': {'skip_download': True}
    }]

    _payload = b'<fsxml><screen><properties><screenId>-1</screenId></properties><capabilities id="1"><properties><platform>Win32</platform><appcodename>Mozilla</appcodename><appname>Netscape</appname><appversion>5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36</appversion><useragent>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36</useragent><cookiesenabled>true</cookiesenabled><screenwidth>784</screenwidth><screenheight>758</screenheight><orientation>undefined</orientation><smt_browserid>Sat, 07 Oct 2021 08:56:50 GMT</smt_browserid><smt_sessionid>1633769810758</smt_sessionid></properties></capabilities></screen></fsxml>'

    def _real_extract(self, url):
        id = self._match_id(url)
        args_for_js_request = self._download_webpage(
            'https://euscreen.eu/lou/LouServlet/domain/euscreenxl/html5application/euscreenxlitem',
            id, data=self._payload, query={'actionlist': 'itempage', 'id': id})
        info_js = self._download_webpage(
            'https://euscreen.eu/lou/LouServlet/domain/euscreenxl/html5application/euscreenxlitem',
            id, data=args_for_js_request.replace('screenid', 'screenId').encode())
        video_json = self._parse_json(
            self._search_regex(r'setVideo\(({.+})\)\(\$end\$\)put', info_js, 'Video JSON'),
            id, transform_source=js_to_json)
        meta_json = self._parse_json(
            self._search_regex(r'setData\(({.+})\)\(\$end\$\)', info_js, 'Metadata JSON'),
            id, transform_source=js_to_json)
        formats = [{
            'url': source['src'],
        } for source in video_json.get('sources', [])]
        self._sort_formats(formats)

        return {
            'id': id,
            'title': meta_json.get('originalTitle'),
            'alt_title': meta_json.get('title'),
            'duration': parse_duration(meta_json.get('duration')),
            'description': '%s\n%s' % (meta_json.get('summaryOriginal', ''), meta_json.get('summaryEnglish', '')),
            'series': meta_json.get('series') or meta_json.get('seriesEnglish'),
            'episode': meta_json.get('episodeNumber'),
            'uploader': meta_json.get('provider'),
            'thumbnail': meta_json.get('screenshot') or video_json.get('screenshot'),
            'formats': formats,
        }


code:
# coding: utf-8
from __future__ import unicode_literals

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

class EvoLoadBaseIE(InfoExtractor):
    IE_DESC = False  # Do not list

class EvoLoadIE(EvoLoadBaseIE):
    IE_NAME = 'evoload'
    # https://evoload.io/v/58AtgpYn5tAKEp
    _VALID_URL = r'https?://evoload\.io/[ev]/(?P<id>[a-zA-Z0-9]+)/?'
    _TEST = {}
    TITLE_RE = (
        r'<h3\s+class="kt-subheader__title">\s*(.+)\s*</h3>',
        r'<title>(?:Evoload|EvoLOAD.io) - Play\s(.+)\s*</title>'
    )
    VIDEO_URL = 'https://evoload.io/v/%s/'
    EMBED_URL = 'https://evoload.io/e/%s/'

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage_video = self._download_webpage(self.VIDEO_URL % video_id, video_id, note='Downloading video page')
        webpage_embed = self._download_webpage(self.EMBED_URL % video_id, video_id, note='Downloading embed page')

        title = try_get(self.TITLE_RE, (
            lambda x: self._search_regex(x, webpage_video, 'video title'),
            lambda x: self._search_regex(x, webpage_embed, 'video title'),
        ), None)
        if not title:
            raise ExtractorError('Failed to extract video title')

        entry = self._parse_html5_media_entries(url, webpage_embed, video_id, m3u8_id='hls')[0]
        self._sort_formats(entry['formats'])
        entry.update({
            'id': video_id,
            'title': title,
            'age_limit': 18,
        })
        return entry


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

class ExpoTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?expotv\.com/videos/[^?#]*/(?P<id>[0-9]+)($|[?#])'
    _TEST = {
        'url': 'http://www.expotv.com/videos/reviews/3/40/NYX-Butter-lipstick/667916',
        'md5': 'fe1d728c3a813ff78f595bc8b7a707a8',
        'info_dict': {
            'id': '667916',
            'ext': 'mp4',
            'title': 'NYX Butter Lipstick Little Susie',
            'description': 'Goes on like butter, but looks better!',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'Stephanie S.',
            'upload_date': '20150520',
            'view_count': int,
        }
    }

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

        webpage = self._download_webpage(url, video_id)
        player_key = self._search_regex(
            r'<param name="playerKey" value="([^"]+)"', webpage, 'player key')
        config = self._download_json(
            'http://client.expotv.com/video/config/%s/%s' % (video_id, player_key),
            video_id, 'Downloading video configuration')

        formats = []
        for fcfg in config['sources']:
            media_url = fcfg.get('file')
            if not media_url:
                continue
            if fcfg.get('type') == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    media_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls'))
            else:
                formats.append({
                    'url': media_url,
                    'height': int_or_none(fcfg.get('height')),
                    'format_id': fcfg.get('label'),
                    'ext': self._search_regex(
                        r'filename=.*\.([a-z0-9_A-Z]+)&', media_url,
                        'file extension', default=None) or fcfg.get('type'),
                })
        self._sort_formats(formats)

        title = self._og_search_title(webpage)
        description = self._og_search_description(webpage)
        thumbnail = config.get('image')
        view_count = int_or_none(self._search_regex(
            r'
Plays: ([0-9]+)
', webpage, 'view counts'))
        uploader = self._search_regex(
            r'
\s*<img alt="([^"]+)"', webpage, 'uploader',
            fatal=False)
        upload_date = unified_strdate(self._search_regex(
            r'
Reviewed on ([0-9/.]+)
', webpage, 'upload date',
            fatal=False), day_first=False)

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


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

class ExpressenIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:www\.)?(?:expressen|di)\.se/
                        (?:(?:tvspelare/video|videoplayer/embed)/)?
                        tv/(?:[^/]+/)*
                        (?P<id>[^/?#&]+)
                    '''
    _EMBED_REGEX = [r'<iframe[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//(?:www\.)?(?:expressen|di)\.se/(?:tvspelare/video|videoplayer/embed)/tv/.+?)\1']
    _TESTS = [{
        'url': 'https://www.expressen.se/tv/ledare/ledarsnack/ledarsnack-om-arbetslosheten-bland-kvinnor-i-speciellt-utsatta-omraden/',
        'md5': 'deb2ca62e7b1dcd19fa18ba37523f66e',
        'info_dict': {
            'id': 'ba90f5a9-78d1-4511-aa02-c177b9c99136',
            'display_id': 'ledarsnack-om-arbetslosheten-bland-kvinnor-i-speciellt-utsatta-omraden',
            'ext': 'mp4',
            'title': 'Ledarsnack: Om arbetslösheten bland kvinnor i speciellt utsatta områden',
            'description': 'md5:f38c81ff69f3de4d269bbda012fcbbba',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 788,
            'timestamp': 1526639109,
            'upload_date': '20180518',
        },
    }, {
        'url': 'https://www.expressen.se/tv/kultur/kulturdebatt-med-expressens-karin-olsson/',
        'only_matching': True,
    }, {
        'url': 'https://www.expressen.se/tvspelare/video/tv/ditv/ekonomistudion/experterna-har-ar-fragorna-som-avgor-valet/?embed=true&external=true&autoplay=true&startVolume=0&partnerId=di',
        'only_matching': True,
    }, {
        'url': 'https://www.expressen.se/videoplayer/embed/tv/ditv/ekonomistudion/experterna-har-ar-fragorna-som-avgor-valet/?embed=true&external=true&autoplay=true&startVolume=0&partnerId=di',
        'only_matching': True,
    }, {
        'url': 'https://www.di.se/videoplayer/embed/tv/ditv/borsmorgon/implantica-rusar-70--under-borspremiaren-hor-styrelsemedlemmen/?embed=true&external=true&autoplay=true&startVolume=0&partnerId=di',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        def extract_data(name):
            return self._parse_json(
                self._search_regex(
                    r'data-%s=(["\'])(?P<value>(?:(?!\1).)+)\1' % name,
                    webpage, 'info', group='value'),
                display_id, transform_source=unescapeHTML)

        info = extract_data('video-tracking-info')
        video_id = info['contentId']

        data = extract_data('article-data')
        stream = data['stream']

        if determine_ext(stream) == 'm3u8':
            formats = self._extract_m3u8_formats(
                stream, display_id, 'mp4', entry_protocol='m3u8_native',
                m3u8_id='hls')
        else:
            formats = [{
                'url': stream,
            }]
        self._sort_formats(formats)

        title = info.get('titleRaw') or data['title']
        description = info.get('descriptionRaw')
        thumbnail = info.get('socialMediaImage') or data.get('image')
        duration = int_or_none(info.get('videoTotalSecondsDuration')
                               or data.get('totalSecondsDuration'))
        timestamp = unified_timestamp(info.get('publishDate'))

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


code:
import contextlib
import os
from ..utils import load_plugins

# NB: Must be before other imports so that plugins can be correctly injected
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', {})

_LAZY_LOADER = False
if not os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
    with contextlib.suppress(ImportError):
        from .lazy_extractors import *  # noqa: F403
        from .lazy_extractors import _ALL_CLASSES
        _SELFHOSTED_CLASSES = []
        _LAZY_LOADER = True

if not _LAZY_LOADER:
    from ._extractors import *  # noqa: F403
    _ALL_CLASSES = [  # noqa: F811
        klass
        for name, klass in globals().items()
        if name.endswith('IE') and name != 'GenericIE'
    ]
    _SELFHOSTED_CLASSES = [
        ie for ie in _ALL_CLASSES if ie._SELF_HOSTED
    ]
    _ALL_CLASSES.append(GenericIE)  # noqa: F405

globals().update(_PLUGIN_CLASSES)
_ALL_CLASSES[:0] = _PLUGIN_CLASSES.values()


code:
from ..utils import str_to_int
from .keezmovies import KeezMoviesIE

class ExtremeTubeIE(KeezMoviesIE):
    _VALID_URL = r'https?://(?:www\.)?extremetube\.com/(?:[^/]+/)?video/(?P<id>[^/#?&]+)'
    _TESTS = [{
        'url': 'http://www.extremetube.com/video/music-video-14-british-euro-brit-european-cumshots-swallow-652431',
        'md5': '92feaafa4b58e82f261e5419f39c60cb',
        'info_dict': {
            'id': 'music-video-14-british-euro-brit-european-cumshots-swallow-652431',
            'ext': 'mp4',
            'title': 'Music Video 14 british euro brit european cumshots swallow',
            'uploader': 'anonim',
            'view_count': int,
            'age_limit': 18,
        }
    }, {
        'url': 'http://www.extremetube.com/gay/video/abcde-1234',
        'only_matching': True,
    }, {
        'url': 'http://www.extremetube.com/video/latina-slut-fucked-by-fat-black-dick',
        'only_matching': True,
    }, {
        'url': 'http://www.extremetube.com/video/652431',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        webpage, info = self._extract_info(url)

        if not info['title']:
            info['title'] = self._search_regex(
                r'<h1[^>]+title="([^"]+)"[^>]*>', webpage, 'title')

        uploader = self._html_search_regex(
            r'Uploaded by:\s*</[^>]+>\s*<a[^>]+>(.+?)</a>',
            webpage, 'uploader', fatal=False)
        view_count = str_to_int(self._search_regex(
            r'Views:\s*</[^>]+>\s*<[^>]+>([\d,\.]+)</',
            webpage, 'view count', fatal=False))

        info.update({
            'uploader': uploader,
            'view_count': view_count,
        })

        return info


code:
from .common import InfoExtractor
from ..utils import (
    xpath_text,
    parse_duration,
    ExtractorError,
)

class EyedoTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?eyedo\.tv/[^/]+/(?:#!/)?Live/Detail/(?P<id>[0-9]+)'
    _TEST = {
        'url': 'https://www.eyedo.tv/en-US/#!/Live/Detail/16301',
        'md5': 'ba14f17995cdfc20c36ba40e21bf73f7',
        'info_dict': {
            'id': '16301',
            'ext': 'mp4',
            'title': 'Journée du conseil scientifique de l\'Afnic 2015',
            'description': 'md5:4abe07293b2f73efc6e1c37028d58c98',
            'uploader': 'Afnic Live',
            'uploader_id': '8023',
        }
    }
    _ROOT_URL = 'http://live.eyedo.net:1935/'

    def _real_extract(self, url):
        video_id = self._match_id(url)
        video_data = self._download_xml('http://eyedo.tv/api/live/GetLive/%s' % video_id, video_id)

        def _add_ns(path):
            return self._xpath_ns(path, 'http://schemas.datacontract.org/2004/07/EyeDo.Core.Implementation.Web.ViewModels.Api')

        title = xpath_text(video_data, _add_ns('Titre'), 'title', True)
        state_live_code = xpath_text(video_data, _add_ns('StateLiveCode'), 'title', True)
        if state_live_code == 'avenir':
            raise ExtractorError(
                '%s said: We\'re sorry, but this video is not yet available.' % self.IE_NAME,
                expected=True)

        is_live = state_live_code == 'live'
        m3u8_url = None
        # http://eyedo.tv/Content/Html5/Scripts/html5view.js
        if is_live:
            if xpath_text(video_data, 'Cdn') == 'true':
                m3u8_url = 'http://rrr.sz.xlcdn.com/?account=eyedo&file=A%s&type=live&service=wowza&protocol=http&output=playlist.m3u8' % video_id
            else:
                m3u8_url = self._ROOT_URL + 'w/%s/eyedo_720p/playlist.m3u8' % video_id
        else:
            m3u8_url = self._ROOT_URL + 'replay-w/%s/mp4:%s.mp4/playlist.m3u8' % (video_id, video_id)

        return {
            'id': video_id,
            'title': title,
            'formats': self._extract_m3u8_formats(
                m3u8_url, video_id, 'mp4', 'm3u8_native'),
            'description': xpath_text(video_data, _add_ns('Description')),
            'duration': parse_duration(xpath_text(video_data, _add_ns('Duration'))),
            'uploader': xpath_text(video_data, _add_ns('Createur')),
            'uploader_id': xpath_text(video_data, _add_ns('CreateurId')),
            'chapter': xpath_text(video_data, _add_ns('ChapitreTitre')),
            'chapter_id': xpath_text(video_data, _add_ns('ChapitreId')),
        }


code:
# coding: utf-8
from __future__ import unicode_literals

import re

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
)

class EynyBaseIE(InfoExtractor):
    IE_DESC = False  # Do not list

class EynyIE(EynyBaseIE):
    IE_NAME = 'eyny'
    _VALID_URL = r'https?:\/\/(?:www\.)?eyny\.com\/\d+\/watch\?v=(?P<id>[^#?&]+)(?:&[^&]+)*'
    _TEST = {}
    _TITLE_REGEX = re.compile(r'(?ms)<title>(.+) -  Free Videos \& Sex Movies - XXX Tube - EYNY<\/title>')

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

        title = self._TITLE_REGEX.search(webpage).group(1)

        entries = self._parse_html5_media_entries(url, webpage, video_id, m3u8_id='hls')
        if not entries:
            raise ExtractorError('Private video', expected=True)
        entry = entries[0]
        self._sort_formats(entry['formats'])
        entry.update({
            'id': video_id,
            'title': title,
            'age_limit': 18,
        })
        return entry