code:
import json

from .common import InfoExtractor
from ..utils import int_or_none

class PodomaticIE(InfoExtractor):
    IE_NAME = 'podomatic'
    _VALID_URL = r'''(?x)
                    (?P<proto>https?)://
                        (?:
                            (?P<channel>[^.]+)\.podomatic\.com/entry|
                            (?:www\.)?podomatic\.com/podcasts/(?P<channel_2>[^/]+)/episodes
                        )/
                        (?P<id>[^/?#&]+)
                '''

    _TESTS = [{
        'url': 'http://scienceteachingtips.podomatic.com/entry/2009-01-02T16_03_35-08_00',
        'md5': '84bb855fcf3429e6bf72460e1eed782d',
        'info_dict': {
            'id': '2009-01-02T16_03_35-08_00',
            'ext': 'mp3',
            'uploader': 'Science Teaching Tips',
            'uploader_id': 'scienceteachingtips',
            'title': '64.  When the Moon Hits Your Eye',
            'duration': 446,
        }
    }, {
        'url': 'http://ostbahnhof.podomatic.com/entry/2013-11-15T16_31_21-08_00',
        'md5': 'd2cf443931b6148e27638650e2638297',
        'info_dict': {
            'id': '2013-11-15T16_31_21-08_00',
            'ext': 'mp3',
            'uploader': 'Ostbahnhof / Techno Mix',
            'uploader_id': 'ostbahnhof',
            'title': 'Einunddreizig',
            'duration': 3799,
        }
    }, {
        'url': 'https://www.podomatic.com/podcasts/scienceteachingtips/episodes/2009-01-02T16_03_35-08_00',
        'only_matching': True,
    }]

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

        json_url = (('%s://%s.podomatic.com/entry/embed_params/%s'
                     + '?permalink=true&rtmp=0') %
                    (mobj.group('proto'), channel, video_id))
        data_json = self._download_webpage(
            json_url, video_id, 'Downloading video info')
        data = json.loads(data_json)

        video_url = data['downloadLink']
        if not video_url:
            video_url = '%s/%s' % (data['streamer'].replace('rtmp', 'http'), data['mediaLocation'])
        uploader = data['podcast']
        title = data['title']
        thumbnail = data['imageLocation']
        duration = int_or_none(data.get('length'), 1000)

        return {
            'id': video_id,
            'url': video_url,
            'title': title,
            'uploader': uploader,
            'uploader_id': channel,
            'thumbnail': thumbnail,
            'duration': duration,
        }


code:
from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    extract_attributes,
    int_or_none,
    js_to_json,
    merge_dicts,
)

class PokemonIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?pokemon\.com/[a-z]{2}(?:.*?play=(?P<id>[a-z0-9]{32})|/(?:[^/]+/)+(?P<display_id>[^/?#&]+))'
    _TESTS = [{
        'url': 'https://www.pokemon.com/us/pokemon-episodes/20_30-the-ol-raise-and-switch/',
        'md5': '2fe8eaec69768b25ef898cda9c43062e',
        'info_dict': {
            'id': 'afe22e30f01c41f49d4f1d9eab5cd9a4',
            'ext': 'mp4',
            'title': 'The Ol’ Raise and Switch!',
            'description': 'md5:7db77f7107f98ba88401d3adc80ff7af',
        },
        'add_id': ['LimelightMedia'],
    }, {
        # no data-video-title
        'url': 'https://www.pokemon.com/fr/episodes-pokemon/films-pokemon/pokemon-lascension-de-darkrai-2008',
        'info_dict': {
            'id': 'dfbaf830d7e54e179837c50c0c6cc0e1',
            'ext': 'mp4',
            'title': "Pokémon : L'ascension de Darkrai",
            'description': 'md5:d1dbc9e206070c3e14a06ff557659fb5',
        },
        'add_id': ['LimelightMedia'],
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.pokemon.com/uk/pokemon-episodes/?play=2e8b5c761f1d4a9286165d7748c1ece2',
        'only_matching': True,
    }, {
        'url': 'http://www.pokemon.com/fr/episodes-pokemon/18_09-un-hiver-inattendu/',
        'only_matching': True,
    }, {
        'url': 'http://www.pokemon.com/de/pokemon-folgen/01_20-bye-bye-smettbo/',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id, display_id = self._match_valid_url(url).groups()
        webpage = self._download_webpage(url, video_id or display_id)
        video_data = extract_attributes(self._search_regex(
            r'(<[^>]+data-video-id="%s"[^>]*>)' % (video_id if video_id else '[a-z0-9]{32}'),
            webpage, 'video data element'))
        video_id = video_data['data-video-id']
        title = video_data.get('data-video-title') or self._html_search_meta(
            'pkm-title', webpage, ' title', default=None) or self._search_regex(
            r'<h1[^>]+\bclass=["\']us-title[^>]+>([^<]+)', webpage, 'title')
        return {
            '_type': 'url_transparent',
            'id': video_id,
            'url': 'limelight:media:%s' % video_id,
            'title': title,
            'description': video_data.get('data-video-summary'),
            'thumbnail': video_data.get('data-video-poster'),
            'series': 'Pokémon',
            'season_number': int_or_none(video_data.get('data-video-season')),
            'episode': title,
            'episode_number': int_or_none(video_data.get('data-video-episode')),
            'ie_key': 'LimelightMedia',
        }

class PokemonWatchIE(InfoExtractor):
    _VALID_URL = r'https?://watch\.pokemon\.com/[a-z]{2}-[a-z]{2}/(?:#/)?player(?:\.html)?\?id=(?P<id>[a-z0-9]{32})'
    _API_URL = 'https://www.pokemon.com/api/pokemontv/v2/channels/{0:}'
    _TESTS = [{
        'url': 'https://watch.pokemon.com/en-us/player.html?id=8309a40969894a8e8d5bc1311e9c5667',
        'md5': '62833938a31e61ab49ada92f524c42ff',
        'info_dict': {
            'id': '8309a40969894a8e8d5bc1311e9c5667',
            'ext': 'mp4',
            'title': 'Lillier and the Staff!',
            'description': 'md5:338841b8c21b283d24bdc9b568849f04',
        }
    }, {
        'url': 'https://watch.pokemon.com/en-us/#/player?id=3fe7752ba09141f0b0f7756d1981c6b2',
        'only_matching': True
    }, {
        'url': 'https://watch.pokemon.com/de-de/player.html?id=b3c402e111a4459eb47e12160ab0ba07',
        'only_matching': True
    }]

    def _extract_media(self, channel_array, video_id):
        for channel in channel_array:
            for media in channel.get('media'):
                if media.get('id') == video_id:
                    return media
        return None

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

        info = {
            '_type': 'url',
            'id': video_id,
            'url': 'limelight:media:%s' % video_id,
            'ie_key': 'LimelightMedia',
        }

        # API call can be avoided entirely if we are listing formats
        if self.get_param('listformats', False):
            return info

        webpage = self._download_webpage(url, video_id)
        build_vars = self._parse_json(self._search_regex(
            r'(?s)buildVars\s*=\s*({.*?})', webpage, 'build vars'),
            video_id, transform_source=js_to_json)
        region = build_vars.get('region')
        channel_array = self._download_json(self._API_URL.format(region), video_id)
        video_data = self._extract_media(channel_array, video_id)

        if video_data is None:
            raise ExtractorError(
                'Video %s does not exist' % video_id, expected=True)

        info['_type'] = 'url_transparent'
        images = video_data.get('images')

        return merge_dicts(info, {
            'title': video_data.get('title'),
            'description': video_data.get('description'),
            'thumbnail': images.get('medium') or images.get('small'),
            'series': 'Pokémon',
            'season_number': int_or_none(video_data.get('season')),
            'episode': video_data.get('title'),
            'episode_number': int_or_none(video_data.get('episode')),
        })

repo_name: Lesmiscore/ytdl


code:
import base64

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

class PokerGoBaseIE(InfoExtractor):
    _NETRC_MACHINE = 'pokergo'
    _AUTH_TOKEN = None
    _PROPERTY_ID = '1dfb3940-7d53-4980-b0b0-f28b369a000d'

    def _perform_login(self, username, password):
        if self._AUTH_TOKEN:
            return
        self.report_login()
        PokerGoBaseIE._AUTH_TOKEN = self._download_json(
            f'https://subscription.pokergo.com/properties/{self._PROPERTY_ID}/sign-in', None,
            headers={'authorization': f'Basic {base64.b64encode(f"{username}:{password}".encode()).decode()}'},
            data=b'')['meta']['token']
        if not self._AUTH_TOKEN:
            raise ExtractorError('Unable to get Auth Token.', expected=True)

    def _real_initialize(self):
        if not self._AUTH_TOKEN:
            self.raise_login_required(method='password')

class PokerGoIE(PokerGoBaseIE):
    _VALID_URL = r'https?://(?:www\.)?pokergo\.com/videos/(?P<id>[^&$#/?]+)'

    _TESTS = [{
        'url': 'https://www.pokergo.com/videos/2a70ec4e-4a80-414b-97ec-725d9b72a7dc',
        'info_dict': {
            'id': 'aVLOxDzY',
            'ext': 'mp4',
            'title': 'Poker After Dark | Season 12 (2020) | Cry Me a River | Episode 2',
            'description': 'md5:c7a8c29556cbfb6eb3c0d5d622251b71',
            'thumbnail': 'https://cdn.jwplayer.com/v2/media/aVLOxDzY/poster.jpg?width=720',
            'timestamp': 1608085715,
            'duration': 2700.12,
            'season_number': 12,
            'episode_number': 2,
            'series': 'poker after dark',
            'upload_date': '20201216',
            'season': 'Season 12',
            'episode': 'Episode 2',
            'display_id': '2a70ec4e-4a80-414b-97ec-725d9b72a7dc',
        },
        'params': {'skip_download': True}
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        data_json = self._download_json(f'https://api.pokergo.com/v2/properties/{self._PROPERTY_ID}/videos/{id}', id,
                                        headers={'authorization': f'Bearer {self._AUTH_TOKEN}'})['data']
        v_id = data_json['source']

        thumbnails = [{
            'url': image['url'],
            'id': image.get('label'),
            'width': image.get('width'),
            'height': image.get('height')
        } for image in data_json.get('images') or [] if image.get('url')]
        series_json = next(dct for dct in data_json.get('show_tags') or [] if dct.get('video_id') == id) or {}

        return {
            '_type': 'url_transparent',
            'display_id': id,
            'title': data_json.get('title'),
            'description': data_json.get('description'),
            'duration': data_json.get('duration'),
            'thumbnails': thumbnails,
            'season_number': series_json.get('season'),
            'episode_number': series_json.get('episode_number'),
            'series': try_get(series_json, lambda x: x['tag']['name']),
            'url': f'https://cdn.jwplayer.com/v2/media/{v_id}'
        }

class PokerGoCollectionIE(PokerGoBaseIE):
    _VALID_URL = r'https?://(?:www\.)?pokergo\.com/collections/(?P<id>[^&$#/?]+)'

    _TESTS = [{
        'url': 'https://www.pokergo.com/collections/19ffe481-5dae-481a-8869-75cc0e3c4700',
        'playlist_mincount': 13,
        'info_dict': {
            'id': '19ffe481-5dae-481a-8869-75cc0e3c4700',
        },
    }]

    def _entries(self, id):
        data_json = self._download_json(f'https://api.pokergo.com/v2/properties/{self._PROPERTY_ID}/collections/{id}?include=entities',
                                        id, headers={'authorization': f'Bearer {self._AUTH_TOKEN}'})['data']
        for video in data_json.get('collection_video') or []:
            video_id = video.get('id')
            if video_id:
                yield self.url_result(
                    f'https://www.pokergo.com/videos/{video_id}',
                    ie=PokerGoIE.ie_key(), video_id=video_id)

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


code:
from uuid import uuid4
import json

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

class PolsatGoIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?polsat(?:box)?go\.pl/.+/(?P<id>[0-9a-fA-F]+)(?:[/#?]|$)'
    _TESTS = [{
        'url': 'https://polsatgo.pl/wideo/seriale/swiat-wedlug-kiepskich/5024045/sezon-1/5028300/swiat-wedlug-kiepskich-odcinek-88/4121',
        'info_dict': {
            'id': '4121',
            'ext': 'mp4',
            'title': 'Świat według Kiepskich - Odcinek 88',
            'age_limit': 12,
        },
    }]

    def _extract_formats(self, sources, video_id):
        for source in sources or []:
            if not source.get('id'):
                continue
            url = url_or_none(self._call_api(
                'drm', video_id, 'getPseudoLicense',
                {'mediaId': video_id, 'sourceId': source['id']}).get('url'))
            if not url:
                continue
            yield {
                'url': url,
                'height': int_or_none(try_get(source, lambda x: x['quality'][:-1]))
            }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        media = self._call_api('navigation', video_id, 'prePlayData', {'mediaId': video_id})['mediaItem']

        formats = list(self._extract_formats(
            try_get(media, lambda x: x['playback']['mediaSources']), video_id))
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': media['displayInfo']['title'],
            'formats': formats,
            'age_limit': int_or_none(media['displayInfo']['ageGroup'])
        }

    def _call_api(self, endpoint, media_id, method, params):
        rand_uuid = str(uuid4())
        res = self._download_json(
            f'https://b2c-mobile.redefine.pl/rpc/{endpoint}/', media_id,
            note=f'Downloading {method} JSON metadata',
            data=json.dumps({
                'method': method,
                'id': '2137',
                'jsonrpc': '2.0',
                'params': {
                    **params,
                    'userAgentData': {
                        'deviceType': 'mobile',
                        'application': 'native',
                        'os': 'android',
                        'build': 10003,
                        'widevine': False,
                        'portal': 'pg',
                        'player': 'cpplayer',
                    },
                    'deviceId': {
                        'type': 'other',
                        'value': rand_uuid,
                    },
                    'clientId': rand_uuid,
                    'cpid': 1,
                },
            }).encode('utf-8'),
            headers={'Content-type': 'application/json'})
        if not res.get('result'):
            if res['error']['code'] == 13404:
                raise ExtractorError('This video is either unavailable in your region or is DRM protected', expected=True)
            raise ExtractorError(f'Solorz said: {res["error"]["message"]} - {res["error"]["data"]["userMessage"]}')
        return res['result']


code:
import itertools
import json
import math
import re

from .common import InfoExtractor
from ..compat import (
    compat_str,
    compat_urllib_parse_unquote,
    compat_urlparse
)
from ..utils import (
    extract_attributes,
    ExtractorError,
    InAdvancePagedList,
    int_or_none,
    js_to_json,
    parse_iso8601,
    strip_or_none,
    unified_timestamp,
    unescapeHTML,
    url_or_none,
)

class PolskieRadioBaseExtractor(InfoExtractor):
    def _extract_webpage_player_entries(self, webpage, playlist_id, base_data):
        media_urls = set()

        for data_media in re.findall(r'<[^>]+data-media="?({[^>]+})"?', webpage):
            media = self._parse_json(data_media, playlist_id, transform_source=unescapeHTML, fatal=False)
            if not media.get('file') or not media.get('desc'):
                continue
            media_url = self._proto_relative_url(media['file'])
            if media_url in media_urls:
                continue
            media_urls.add(media_url)
            entry = base_data.copy()
            entry.update({
                'id': compat_str(media['id']),
                'url': media_url,
                'duration': int_or_none(media.get('length')),
                'vcodec': 'none' if media.get('provider') == 'audio' else None,
            })
            entry_title = compat_urllib_parse_unquote(media['desc'])
            if entry_title:
                entry['title'] = entry_title
            yield entry

class PolskieRadioIE(PolskieRadioBaseExtractor):
    _VALID_URL = r'https?://(?:www\.)?polskieradio(?:24)?\.pl/\d+/\d+/Artykul/(?P<id>[0-9]+)'
    _TESTS = [{  # Old-style single broadcast.
        'url': 'http://www.polskieradio.pl/7/5102/Artykul/1587943,Prof-Andrzej-Nowak-o-historii-nie-da-sie-myslec-beznamietnie',
        'info_dict': {
            'id': '1587943',
            'title': 'Prof. Andrzej Nowak: o historii nie da się myśleć beznamiętnie',
            'description': 'md5:12f954edbf3120c5e7075e17bf9fc5c5',
        },
        'playlist': [{
            'md5': '2984ee6ce9046d91fc233bc1a864a09a',
            'info_dict': {
                'id': '1540576',
                'ext': 'mp3',
                'title': 'md5:d4623290d4ac983bf924061c75c23a0d',
                'timestamp': 1456594200,
                'upload_date': '20160227',
                'duration': 2364,
                'thumbnail': r're:^https?://static\.prsa\.pl/images/.*\.jpg$'
            },
        }],
    }, {  # New-style single broadcast.
        'url': 'https://www.polskieradio.pl/8/2382/Artykul/2534482,Zagarysci-Poezja-jak-spoiwo',
        'info_dict': {
            'id': '2534482',
            'title': 'Żagaryści. Poezja jak spoiwo',
            'description': 'md5:f18d95d5dcba747a09b635e21a4c0695',
        },
        'playlist': [{
            'md5': 'd07559829f61d5a93a75755987ded760',
            'info_dict': {
                'id': '2516679',
                'ext': 'mp3',
                'title': 'md5:c6e1234e0b747ad883cb91b7ad06b98c',
                'timestamp': 1592654400,
                'upload_date': '20200620',
                'duration': 1430,
                'thumbnail': r're:^https?://static\.prsa\.pl/images/.*\.jpg$'
            },
        }],
    }, {
        # PR4 audition - other frontend
        'url': 'https://www.polskieradio.pl/10/6071/Artykul/2610977,Poglos-29-pazdziernika-godz-2301',
        'info_dict': {
            'id': '2610977',
            'ext': 'mp3',
            'title': 'Pogłos 29 października godz. 23:01',
        },
    }, {
        'url': 'http://polskieradio.pl/9/305/Artykul/1632955,Bardzo-popularne-slowo-remis',
        'only_matching': True,
    }, {
        'url': 'http://www.polskieradio.pl/7/5102/Artykul/1587943',
        'only_matching': True,
    }, {
        # with mp4 video
        'url': 'http://www.polskieradio.pl/9/299/Artykul/1634903,Brexit-Leszek-Miller-swiat-sie-nie-zawali-Europa-bedzie-trwac-dalej',
        'only_matching': True,
    }, {
        'url': 'https://polskieradio24.pl/130/4503/Artykul/2621876,Narusza-nasza-suwerennosc-Publicysci-o-uzaleznieniu-funduszy-UE-od-praworzadnosci',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, playlist_id)

        content = self._search_regex(
            r'(?s)
]+class="\s*this-article\s*"[^>]*>(.+?)
]+class="tags"[^>]*>',
            webpage, 'content', default=None)

        timestamp = unified_timestamp(self._html_search_regex(
            r'(?s)]+id="datetime2"[^>]*>(.+?)',
            webpage, 'timestamp', default=None))

        thumbnail_url = self._og_search_thumbnail(webpage, default=None)

        title = self._og_search_title(webpage).strip()

        description = strip_or_none(self._og_search_description(webpage, default=None))
        description = description.replace('\xa0', ' ') if description is not None else None

        if not content:
            return {
                'id': playlist_id,
                'url': self._proto_relative_url(
                    self._search_regex(
                        r"source:\s*'(//static\.prsa\.pl/[^']+)'",
                        webpage, 'audition record url')),
                'title': title,
                'description': description,
                'timestamp': timestamp,
                'thumbnail': thumbnail_url,
            }

        entries = self._extract_webpage_player_entries(content, playlist_id, {
            'title': title,
            'timestamp': timestamp,
            'thumbnail': thumbnail_url,
        })

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

class PolskieRadioCategoryIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?polskieradio\.pl/\d+(?:,[^/]+)?/(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://www.polskieradio.pl/7/5102,HISTORIA-ZYWA',
        'info_dict': {
            'id': '5102',
            'title': 'HISTORIA ŻYWA',
        },
        'playlist_mincount': 38,
    }, {
        'url': 'http://www.polskieradio.pl/7/4807',
        'info_dict': {
            'id': '4807',
            'title': 'Vademecum 1050. rocznicy Chrztu Polski'
        },
        'playlist_mincount': 5
    }, {
        'url': 'http://www.polskieradio.pl/7/129,Sygnaly-dnia?ref=source',
        'only_matching': True
    }, {
        'url': 'http://www.polskieradio.pl/37,RedakcjaKatolicka/4143,Kierunek-Krakow',
        'info_dict': {
            'id': '4143',
            'title': 'Kierunek Kraków',
        },
        'playlist_mincount': 61
    }, {
        'url': 'http://www.polskieradio.pl/10,czworka/214,muzyka',
        'info_dict': {
            'id': '214',
            'title': 'Muzyka',
        },
        'playlist_mincount': 61
    }, {
        'url': 'http://www.polskieradio.pl/7,Jedynka/5102,HISTORIA-ZYWA',
        'only_matching': True,
    }, {
        'url': 'http://www.polskieradio.pl/8,Dwojka/196,Publicystyka',
        'only_matching': True,
    }]

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

    def _entries(self, url, page, category_id):
        content = page
        for page_num in itertools.count(2):
            for a_entry, entry_id in re.findall(
                    r'(?s)
]+>.*?(]+href=["\']/\d+/\d+/Artykul/(\d+)[^>]+>).*?
',
                    content):
                entry = extract_attributes(a_entry)
                href = entry.get('href')
                if not href:
                    continue
                yield self.url_result(
                    compat_urlparse.urljoin(url, href), PolskieRadioIE.ie_key(),
                    entry_id, entry.get('title'))
            mobj = re.search(
                r'
]+class=["\']next["\'][^>]*>\s*]+href=(["\'])(?P<url>(?:(?!\1).)+)\1',
                content)
            if not mobj:
                break
            next_url = compat_urlparse.urljoin(url, mobj.group('url'))
            content = self._download_webpage(
                next_url, category_id, 'Downloading page %s' % page_num)

    def _real_extract(self, url):
        category_id = self._match_id(url)
        webpage = self._download_webpage(url, category_id)
        title = self._html_search_regex(
            r'<title>([^<]+) - [^<]+ - [^<]+</title>',
            webpage, 'title', fatal=False)
        return self.playlist_result(
            self._entries(url, webpage, category_id),
            category_id, title)

class PolskieRadioPlayerIE(InfoExtractor):
    IE_NAME = 'polskieradio:player'
    _VALID_URL = r'https?://player\.polskieradio\.pl/anteny/(?P<id>[^/]+)'

    _BASE_URL = 'https://player.polskieradio.pl'
    _PLAYER_URL = 'https://player.polskieradio.pl/main.bundle.js'
    _STATIONS_API_URL = 'https://apipr.polskieradio.pl/api/stacje'

    _TESTS = [{
        'url': 'https://player.polskieradio.pl/anteny/trojka',
        'info_dict': {
            'id': '3',
            'ext': 'm4a',
            'title': 'Trójka',
        },
        'params': {
            'format': 'bestaudio',
            'skip_download': 'endless stream',
        },
    }]

    def _get_channel_list(self, channel_url='no_channel'):
        player_code = self._download_webpage(
            self._PLAYER_URL, channel_url,
            note='Downloading js player')
        channel_list = js_to_json(self._search_regex(
            r';var r="anteny",a=(\[.+?\])},', player_code, 'channel list'))
        return self._parse_json(channel_list, channel_url)

    def _real_extract(self, url):
        channel_url = self._match_id(url)
        channel_list = self._get_channel_list(channel_url)

        channel = next((c for c in channel_list if c.get('url') == channel_url), None)

        if not channel:
            raise ExtractorError('Channel not found')

        station_list = self._download_json(self._STATIONS_API_URL, channel_url,
                                           note='Downloading stream url list',
                                           headers={
                                               'Accept': 'application/json',
                                               'Referer': url,
                                               'Origin': self._BASE_URL,
                                           })
        station = next((s for s in station_list
                        if s.get('Name') == (channel.get('streamName') or channel.get('name'))), None)
        if not station:
            raise ExtractorError('Station not found even though we extracted channel')

        formats = []
        for stream_url in station['Streams']:
            stream_url = self._proto_relative_url(stream_url)
            if stream_url.endswith('/playlist.m3u8'):
                formats.extend(self._extract_m3u8_formats(stream_url, channel_url, live=True))
            elif stream_url.endswith('/manifest.f4m'):
                formats.extend(self._extract_mpd_formats(stream_url, channel_url))
            elif stream_url.endswith('/Manifest'):
                formats.extend(self._extract_ism_formats(stream_url, channel_url))
            else:
                formats.append({
                    'url': stream_url,
                })

        self._sort_formats(formats)

        return {
            'id': compat_str(channel['id']),
            'formats': formats,
            'title': channel.get('name') or channel.get('streamName'),
            'display_id': channel_url,
            'thumbnail': f'{self._BASE_URL}/images/{channel_url}-color-logo.png',
            'is_live': True,
        }

class PolskieRadioPodcastBaseExtractor(InfoExtractor):
    _API_BASE = 'https://apipodcasts.polskieradio.pl/api'

    def _parse_episode(self, data):
        return {
            'id': data['guid'],
            'formats': [{
                'url': data['url'],
                'filesize': int_or_none(data.get('fileSize')),
            }],
            'title': data['title'],
            'description': data.get('description'),
            'duration': int_or_none(data.get('length')),
            'timestamp': parse_iso8601(data.get('publishDate')),
            'thumbnail': url_or_none(data.get('image')),
            'series': data.get('podcastTitle'),
            'episode': data['title'],
        }

class PolskieRadioPodcastListIE(PolskieRadioPodcastBaseExtractor):
    IE_NAME = 'polskieradio:podcast:list'
    _VALID_URL = r'https?://podcasty\.polskieradio\.pl/podcast/(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://podcasty.polskieradio.pl/podcast/8/',
        'info_dict': {
            'id': '8',
            'title': 'Śniadanie w Trójce',
            'description': 'md5:57abcc27bc4c6a6b25baa3061975b9ef',
            'uploader': 'Beata Michniewicz',
        },
        'playlist_mincount': 714,
    }]
    _PAGE_SIZE = 10

    def _call_api(self, podcast_id, page):
        return self._download_json(
            f'{self._API_BASE}/Podcasts/{podcast_id}/?pageSize={self._PAGE_SIZE}&page={page}',
            podcast_id, f'Downloading page {page}')

    def _real_extract(self, url):
        podcast_id = self._match_id(url)
        data = self._call_api(podcast_id, 1)

        def get_page(page_num):
            page_data = self._call_api(podcast_id, page_num + 1) if page_num else data
            yield from (self._parse_episode(ep) for ep in page_data['items'])

        return {
            '_type': 'playlist',
            'entries': InAdvancePagedList(
                get_page, math.ceil(data['itemCount'] / self._PAGE_SIZE), self._PAGE_SIZE),
            'id': str(data['id']),
            'title': data['title'],
            'description': data.get('description'),
            'uploader': data.get('announcer'),
        }

class PolskieRadioPodcastIE(PolskieRadioPodcastBaseExtractor):
    IE_NAME = 'polskieradio:podcast'
    _VALID_URL = r'https?://podcasty\.polskieradio\.pl/track/(?P<id>[a-f\d]{8}(?:-[a-f\d]{4}){4}[a-f\d]{8})'
    _TESTS = [{
        'url': 'https://podcasty.polskieradio.pl/track/6eafe403-cb8f-4756-b896-4455c3713c32',
        'info_dict': {
            'id': '6eafe403-cb8f-4756-b896-4455c3713c32',
            'ext': 'mp3',
            'title': 'Theresa May rezygnuje. Co dalej z brexitem?',
            'description': 'md5:e41c409a29d022b70ef0faa61dbded60',
        },
    }]

    def _real_extract(self, url):
        podcast_id = self._match_id(url)
        data = self._download_json(
            f'{self._API_BASE}/audio',
            podcast_id, 'Downloading podcast metadata',
            data=json.dumps({
                'guids': [podcast_id],
            }).encode('utf-8'),
            headers={
                'Content-Type': 'application/json',
            })
        return self._parse_episode(data[0])

class PolskieRadioRadioKierowcowIE(PolskieRadioBaseExtractor):
    _VALID_URL = r'https?://(?:www\.)?radiokierowcow\.pl/artykul/(?P<id>[0-9]+)'
    IE_NAME = 'polskieradio:kierowcow'

    _TESTS = [{
        'url': 'https://radiokierowcow.pl/artykul/2694529',
        'info_dict': {
            'id': '2694529',
            'title': 'Zielona fala reliktem przeszłości?',
            'description': 'md5:343950a8717c9818fdfd4bd2b8ca9ff2',
        },
        'playlist_count': 3,
    }]

    def _real_extract(self, url):
        media_id = self._match_id(url)
        webpage = self._download_webpage(url, media_id)
        nextjs_build = self._search_nextjs_data(webpage, media_id)['buildId']
        article = self._download_json(
            f'https://radiokierowcow.pl/_next/data/{nextjs_build}/artykul/{media_id}.json?articleId={media_id}',
            media_id)
        data = article['pageProps']['data']
        title = data['title']
        entries = self._extract_webpage_player_entries(data['content'], media_id, {
            'title': title,
        })

        return {
            '_type': 'playlist',
            'id': media_id,
            'entries': entries,
            'title': title,
            'description': data.get('lead'),
        }


code:
from .common import InfoExtractor
from ..compat import compat_b64decode
from ..utils import int_or_none

class PopcorntimesIE(InfoExtractor):
    _VALID_URL = r'https?://popcorntimes\.tv/[^/]+/m/(?P<id>[^/]+)/(?P<display_id>[^/?#&]+)'
    _TEST = {
        'url': 'https://popcorntimes.tv/de/m/A1XCFvz/haensel-und-gretel-opera-fantasy',
        'md5': '93f210991ad94ba8c3485950a2453257',
        'info_dict': {
            'id': 'A1XCFvz',
            'display_id': 'haensel-und-gretel-opera-fantasy',
            'ext': 'mp4',
            'title': 'Hänsel und Gretel',
            'description': 'md5:1b8146791726342e7b22ce8125cf6945',
            'thumbnail': r're:^https?://.*\.jpg$',
            'creator': 'John Paul',
            'release_date': '19541009',
            'duration': 4260,
            'tbr': 5380,
            'width': 720,
            'height': 540,
        },
    }

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

        webpage = self._download_webpage(url, display_id)

        title = self._search_regex(
            r'<h1>([^<]+)', webpage, 'title',
            default=None) or self._html_search_meta(
            'ya:ovs:original_name', webpage, 'title', fatal=True)

        loc = self._search_regex(
            r'PCTMLOC\s*=\s*(["\'])(?P<value>(?:(?!\1).)+)\1', webpage, 'loc',
            group='value')

        loc_b64 = ''
        for c in loc:
            c_ord = ord(c)
            if ord('a') <= c_ord <= ord('z') or ord('A') <= c_ord <= ord('Z'):
                upper = ord('Z') if c_ord <= ord('Z') else ord('z')
                c_ord += 13
                if upper < c_ord:
                    c_ord -= 26
            loc_b64 += chr(c_ord)

        video_url = compat_b64decode(loc_b64).decode('utf-8')

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

        thumbnail = self._search_regex(
            r'<img[^>]+class=["\']video-preview[^>]+\bsrc=(["\'])(?P<value>(?:(?!\1).)+)\1',
            webpage, 'thumbnail', default=None,
            group='value') or self._og_search_thumbnail(webpage)

        creator = self._html_search_meta(
            'video:director', webpage, 'creator', default=None)

        release_date = self._html_search_meta(
            'video:release_date', webpage, default=None)
        if release_date:
            release_date = release_date.replace('-', '')

        def int_meta(name):
            return int_or_none(self._html_search_meta(
                name, webpage, default=None))

        return {
            'id': video_id,
            'display_id': display_id,
            'url': video_url,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'creator': creator,
            'release_date': release_date,
            'duration': int_meta('video:duration'),
            'tbr': int_meta('ya:ovs:bitrate'),
            'width': int_meta('og:video:width'),
            'height': int_meta('og:video:height'),
            'http_headers': {
                'Referer': url,
            },
        }


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

class PopcornTVIE(InfoExtractor):
    _VALID_URL = r'https?://[^/]+\.popcorntv\.it/guarda/(?P<display_id>[^/]+)/(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://animemanga.popcorntv.it/guarda/food-wars-battaglie-culinarie-episodio-01/9183',
        'md5': '47d65a48d147caf692ab8562fe630b45',
        'info_dict': {
            'id': '9183',
            'display_id': 'food-wars-battaglie-culinarie-episodio-01',
            'ext': 'mp4',
            'title': 'Food Wars, Battaglie Culinarie | Episodio 01',
            'description': 'md5:b8bea378faae4651d3b34c6e112463d0',
            'thumbnail': r're:^https?://.*\.jpg$',
            'timestamp': 1497610857,
            'upload_date': '20170616',
            'duration': 1440,
            'view_count': int,
        },
    }, {
        'url': 'https://cinema.popcorntv.it/guarda/smash-cut/10433',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        m3u8_url = extract_attributes(
            self._search_regex(
                r'(<link[^>]+itemprop=["\'](?:content|embed)Url[^>]*>)',
                webpage, 'content'
            ))['href']

        formats = self._extract_m3u8_formats(
            m3u8_url, display_id, 'mp4', entry_protocol='m3u8_native',
            m3u8_id='hls')

        title = self._search_regex(
            r'<h1[^>]+itemprop=["\']name[^>]*>([^<]+)', webpage,
            'title', default=None) or self._og_search_title(webpage)

        description = self._html_search_regex(
            r'(?s)<article[^>]+itemprop=["\']description[^>]*>(.+?)</article>',
            webpage, 'description', fatal=False)
        thumbnail = self._og_search_thumbnail(webpage)
        timestamp = unified_timestamp(self._html_search_meta(
            'uploadDate', webpage, 'timestamp'))
        duration = int_or_none(self._html_search_meta(
            'duration', webpage), invscale=60)
        view_count = int_or_none(self._html_search_meta(
            'interactionCount', webpage, 'view count'))

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


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

class Porn91IE(InfoExtractor):
    IE_NAME = '91porn'
    _VALID_URL = r'(?:https?://)(?:www\.|)91porn\.com/.+?\?viewkey=(?P<id>[\w\d]+)'

    _TEST = {
        'url': 'http://91porn.com/view_video.php?viewkey=7e42283b4f5ab36da134',
        'md5': '7fcdb5349354f40d41689bd0fa8db05a',
        'info_dict': {
            'id': '7e42283b4f5ab36da134',
            'title': '18岁大一漂亮学妹，水嫩性感，再爽一次！',
            'ext': 'mp4',
            'duration': 431,
            'age_limit': 18,
        }
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        self._set_cookie('91porn.com', 'language', 'cn_CN')

        webpage = self._download_webpage(
            'http://91porn.com/view_video.php?viewkey=%s' % video_id, video_id)

        if '作为游客，你每天只可观看10个视频' in webpage:
            raise ExtractorError('91 Porn says: Daily limit 10 videos exceeded', expected=True)

        title = self._search_regex(
            r'
([^<]+)
', webpage, 'title')
        title = title.replace('\n', '')

        video_link_url = self._search_regex(
            r'<textarea[^>]+id=["\']fm-video_link[^>]+>([^<]+)</textarea>',
            webpage, 'video link')
        videopage = self._download_webpage(video_link_url, video_id)

        info_dict = self._parse_html5_media_entries(url, videopage, video_id)[0]

        duration = parse_duration(self._search_regex(
            r'时长:\s*\s*(\d+:\d+)', webpage, 'duration', fatal=False))

        comment_count = int_or_none(self._search_regex(
            r'留言:\s*\s*(\d+)', webpage, 'comment count', fatal=False))

        info_dict.update({
            'id': video_id,
            'title': title,
            'duration': duration,
            'comment_count': comment_count,
            'age_limit': self._rta_search(webpage),
        })

        return info_dict


code:
import re

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

class PornComIE(InfoExtractor):
    _VALID_URL = r'https?://(?:[a-zA-Z]+\.)?porn\.com/videos/(?:(?P<display_id>[^/]+)-)?(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://www.porn.com/videos/teen-grabs-a-dildo-and-fucks-her-pussy-live-on-1hottie-i-rec-2603339',
        'md5': '3f30ce76267533cd12ba999263156de7',
        'info_dict': {
            'id': '2603339',
            'display_id': 'teen-grabs-a-dildo-and-fucks-her-pussy-live-on-1hottie-i-rec',
            'ext': 'mp4',
            'title': 'Teen grabs a dildo and fucks her pussy live on 1hottie, I rec',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 551,
            'view_count': int,
            'age_limit': 18,
            'categories': list,
            'tags': list,
        },
    }, {
        'url': 'http://se.porn.com/videos/marsha-may-rides-seth-on-top-of-his-thick-cock-2658067',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        config = self._parse_json(
            self._search_regex(
                (r'=\s*({.+?})\s*;\s*v1ar\b',
                 r'=\s*({.+?})\s*,\s*[\da-zA-Z_]+\s*='),
                webpage, 'config', default='{}'),
            display_id, transform_source=js_to_json, fatal=False)

        if config:
            title = config['title']
            formats = [{
                'url': stream['url'],
                'format_id': stream.get('id'),
                'height': int_or_none(self._search_regex(
                    r'^(\d+)[pP]', stream.get('id') or '', 'height', default=None))
            } for stream in config['streams'] if stream.get('url')]
            thumbnail = (compat_urlparse.urljoin(
                config['thumbCDN'], config['poster'])
                if config.get('thumbCDN') and config.get('poster') else None)
            duration = int_or_none(config.get('length'))
        else:
            title = self._search_regex(
                (r'<title>([^<]+)</title>', r'
]*>([^<]+)
'),
                webpage, 'title')
            formats = [{
                'url': compat_urlparse.urljoin(url, format_url),
                'format_id': '%sp' % height,
                'height': int(height),
                'filesize_approx': parse_filesize(filesize),
            } for format_url, height, filesize in re.findall(
                r']+href="(/download/[^"]+)">[^<]*?(\d+)p]*>(\d+\s*[a-zA-Z]+)<',
                webpage)]
            thumbnail = None
            duration = None

        self._sort_formats(formats)

        view_count = str_to_int(self._search_regex(
            (r'Views:\s*\s*\s*([\d,.]+)',
             r'class=["\']views["\'][^>]*>
([\d,.]+)'), webpage,
            'view count', fatal=False))

        def extract_list(kind):
            s = self._search_regex(
                (r'(?s)%s:\s*\s*(.+?)' % kind.capitalize(),
                 r'(?s)
]*>%s:(.+?)
' % kind.capitalize()),
                webpage, kind, fatal=False)
            return re.findall(r']+>([^<]+)', s or '')

        return {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'thumbnail': thumbnail,
            'duration': duration,
            'view_count': view_count,
            'formats': formats,
            'age_limit': 18,
            'categories': extract_list('categories'),
            'tags': extract_list('tags'),
        }


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

class PornezIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?pornez\.net/video(?P<id>[0-9]+)/'
    _TEST = {
        'url': 'https://pornez.net/video344819/mistresst-funny_penis_names-wmv/',
        'md5': '2e19a0a1cff3a5dbea0ef1b9e80bcbbc',
        'info_dict': {
            'id': '344819',
            'ext': 'mp4',
            'title': r'mistresst funny_penis_names wmv',
            'thumbnail': r're:^https?://.*\.jpg$',
            'age_limit': 18,
        }
    }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(url, video_id)
        iframe_src = self._html_search_regex(
            r'<iframe[^>]+src="(https?://pornez\.net/player/\?[^"]+)"', webpage, 'iframe', fatal=True)
        title = self._html_search_meta(['name', 'twitter:title', 'og:title'], webpage, 'title', default=None)
        if title is None:
            title = self._search_regex(r'<h1>(.*?)</h1>', webpage, 'title', fatal=True)
        thumbnail = self._html_search_meta(['thumbnailUrl'], webpage, 'title', default=None)
        webpage = self._download_webpage(iframe_src, video_id)
        entries = self._parse_html5_media_entries(iframe_src, webpage, video_id)[0]
        for format in entries['formats']:
            height = self._search_regex(r'_(\d+)\.m3u8', format['url'], 'height')
            format['format_id'] = '%sp' % height
            format['height'] = int_or_none(height)

        entries.update({
            'id': video_id,
            'title': title,
            'thumbnail': thumbnail,
            'age_limit': 18
        })
        return entries


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

class PornFlipIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?pornflip\.com/(?:(embed|sv|v)/)?(?P<id>[^/]+)'
    _TESTS = [
        {
            'url': 'https://www.pornflip.com/dzv9Mtw1qj2/sv/brazzers-double-dare-two-couples-fucked-jenna-reid-maya-bijou',
            'info_dict': {
                'id': 'dzv9Mtw1qj2',
                'ext': 'mp4',
                'title': 'Brazzers - Double Dare Two couples fucked Jenna Reid Maya Bijou',
                'description': 'md5:d2b69e6cc743c5fd158e162aa7f05821',
                'duration': 476,
                'like_count': int,
                'dislike_count': int,
                'view_count': int,
                'timestamp': 1617846819,
                'upload_date': '20210408',
                'uploader': 'Brazzers',
                'age_limit': 18,
            },
            'params': {
                'skip_download': True,
            },
        },
        {
            'url': 'https://www.pornflip.com/v/IrJEC40i21L',
            'only_matching': True,
        },
        {
            'url': 'https://www.pornflip.com/Z3jzbChC5-P/sexintaxi-e-sereyna-gomez-czech-naked-couple',
            'only_matching': True,
        },
        {
            'url': 'https://www.pornflip.com/embed/bLcDFxnrZnU',
            'only_matching': True,
        },
    ]
    _HOST = 'www.pornflip.com'

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(
            'https://{}/sv/{}'.format(self._HOST, video_id), video_id, headers={'host': self._HOST})
        description = self._html_search_regex(r'&p\[summary\]=(.*?)\s*&p', webpage, 'description', fatal=False)
        duration = self._search_regex(r'"duration":\s+"([^"]+)",', webpage, 'duration', fatal=False)
        view_count = self._search_regex(r'"interactionCount":\s+"([^"]+)"', webpage, 'view_count', fatal=False)
        title = self._html_search_regex(r'id="mediaPlayerTitleLink"[^>]*>(.+)', webpage, 'title', fatal=False)
        uploader = self._html_search_regex(r'class="title-chanel"[^>]*>[^<]*]*>([^<]+)<', webpage, 'uploader', fatal=False)
        upload_date = self._search_regex(r'"uploadDate":\s+"([^"]+)",', webpage, 'upload_date', fatal=False)
        likes = self._html_search_regex(
            r'class="btn btn-up-rating[^>]*>[^<]*]*>[^<]*[^>]*]*>[^0-9]*([0-9]+)[^<0-9]*<', webpage, 'like_count', fatal=False)
        dislikes = self._html_search_regex(
            r'class="btn btn-down-rating[^>]*>[^<]*]*>[^<]*[^>]*]*>[^0-9]*([0-9]+)[^<0-9]*<', webpage, 'dislike_count', fatal=False)
        mpd_url = self._search_regex(r'"([^"]+userscontent.net/dash/[0-9]+/manifest.mpd[^"]*)"', webpage, 'mpd_url').replace('&', '&')
        formats = self._extract_mpd_formats(mpd_url, video_id, mpd_id='dash')
        self._sort_formats(formats)

        return {
            'age_limit': 18,
            'description': description,
            'dislike_count': int_or_none(dislikes),
            'duration': parse_duration(duration),
            'formats': formats,
            'id': video_id,
            'like_count': int_or_none(likes),
            'timestamp': parse_iso8601(upload_date),
            'thumbnail': self._og_search_thumbnail(webpage),
            'title': title,
            'uploader': uploader,
            'view_count': int_or_none(view_count),
        }


code:
from .common import InfoExtractor
from ..utils import (
    determine_ext,
    ExtractorError,
    int_or_none,
    js_to_json,
    merge_dicts,
    urljoin,
)

class PornHdIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?pornhd\.com/(?:[a-z]{2,4}/)?videos/(?P<id>\d+)(?:/(?P<display_id>.+))?'
    _TESTS = [{
        'url': 'http://www.pornhd.com/videos/9864/selfie-restroom-masturbation-fun-with-chubby-cutie-hd-porn-video',
        'md5': '87f1540746c1d32ec7a2305c12b96b25',
        'info_dict': {
            'id': '9864',
            'display_id': 'selfie-restroom-masturbation-fun-with-chubby-cutie-hd-porn-video',
            'ext': 'mp4',
            'title': 'Restroom selfie masturbation',
            'description': 'md5:3748420395e03e31ac96857a8f125b2b',
            'thumbnail': r're:^https?://.*\.jpg',
            'view_count': int,
            'like_count': int,
            'age_limit': 18,
        },
        'skip': 'HTTP Error 404: Not Found',
    }, {
        'url': 'http://www.pornhd.com/videos/1962/sierra-day-gets-his-cum-all-over-herself-hd-porn-video',
        'md5': '1b7b3a40b9d65a8e5b25f7ab9ee6d6de',
        'info_dict': {
            'id': '1962',
            'display_id': 'sierra-day-gets-his-cum-all-over-herself-hd-porn-video',
            'ext': 'mp4',
            'title': 'md5:98c6f8b2d9c229d0f0fde47f61a1a759',
            'description': 'md5:8ff0523848ac2b8f9b065ba781ccf294',
            'thumbnail': r're:^https?://.*\.jpg',
            'view_count': int,
            'like_count': int,
            'age_limit': 18,
        },
    }]

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

        webpage = self._download_webpage(url, display_id or video_id)

        title = self._html_search_regex(
            [r']+class=["\']video-name["\'][^>]*>([^<]+)',
             r'<title>(.+?) - .*?[Pp]ornHD.*?</title>'], webpage, 'title')

        sources = self._parse_json(js_to_json(self._search_regex(
            r"(?s)sources'?\s*[:=]\s*(\{.+?\})",
            webpage, 'sources', default='{}')), video_id)

        info = {}
        if not sources:
            entries = self._parse_html5_media_entries(url, webpage, video_id)
            if entries:
                info = entries[0]

        if not sources and not info:
            message = self._html_search_regex(
                r'(?s)<(div|p)[^>]+class="no-video"[^>]*>(?P<value>.+?)</\1',
                webpage, 'error message', group='value')
            raise ExtractorError('%s said: %s' % (self.IE_NAME, message), expected=True)

        formats = []
        for format_id, video_url in sources.items():
            video_url = urljoin(url, video_url)
            if not video_url:
                continue
            height = int_or_none(self._search_regex(
                r'^(\d+)[pP]', format_id, 'height', default=None))
            formats.append({
                'url': video_url,
                'ext': determine_ext(video_url, 'mp4'),
                'format_id': format_id,
                'height': height,
            })
        if formats:
            info['formats'] = formats
        self._sort_formats(info['formats'])

        description = self._html_search_regex(
            (r'(?s)
]+class=["\']video-description[^>]+>(?P<value>.+?)
',
             r'<(div|p)[^>]+class="description"[^>]*>(?P<value>[^<]+)</\1'),
            webpage, 'description', fatal=False,
            group='value') or self._html_search_meta(
            'description', webpage, default=None) or self._og_search_description(webpage)
        view_count = int_or_none(self._html_search_regex(
            r'(\d+) views\s*<', webpage, 'view count', fatal=False))
        thumbnail = self._search_regex(
            r"poster'?\s*:\s*([\"'])(?P<url>(?:(?!\1).)+)\1", webpage,
            'thumbnail', default=None, group='url')

        like_count = int_or_none(self._search_regex(
            (r'(\d+)\s*likes',
             r'(\d+)\s*</11[^>]+>(?: |\s)*\blikes',
             r'class=["\']save-count["\'][^>]*>\s*(\d+)'),
            webpage, 'like count', fatal=False))

        return merge_dicts(info, {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'view_count': view_count,
            'like_count': like_count,
            'formats': formats,
            'age_limit': 18,
        })


code:
import functools
import itertools
import math
import operator
import re
import urllib.request

from .common import InfoExtractor
from .openload import PhantomJSwrapper
from ..compat import compat_HTTPError, compat_str
from ..utils import (
    NO_DEFAULT,
    ExtractorError,
    clean_html,
    determine_ext,
    format_field,
    int_or_none,
    merge_dicts,
    orderedSet,
    remove_quotes,
    remove_start,
    str_to_int,
    update_url_query,
    url_or_none,
    urlencode_postdata,
)

class PornHubBaseIE(InfoExtractor):
    _NETRC_MACHINE = 'pornhub'
    _PORNHUB_HOST_RE = r'(?:(?P<host>pornhub(?:premium)?\.(?:com|net|org))|pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd\.onion)'

    def _download_webpage_handle(self, *args, **kwargs):
        def dl(*args, **kwargs):
            return super(PornHubBaseIE, self)._download_webpage_handle(*args, **kwargs)

        ret = dl(*args, **kwargs)

        if not ret:
            return ret

        webpage, urlh = ret

        if any(re.search(p, webpage) for p in (
                r']+\bonload=["\']go\(\)',
                r'document\.cookie\s*=\s*["\']RNKEY=',
                r'document\.location\.reload\(true\)')):
            url_or_request = args[0]
            url = (url_or_request.get_full_url()
                   if isinstance(url_or_request, urllib.request.Request)
                   else url_or_request)
            phantom = PhantomJSwrapper(self, required_version='2.0')
            phantom.get(url, html=webpage)
            webpage, urlh = dl(*args, **kwargs)

        return webpage, urlh

    def _real_initialize(self):
        self._logged_in = False

    def _login(self, host):
        if self._logged_in:
            return

        site = host.split('.')[0]

        # Both sites pornhub and pornhubpremium have separate accounts
        # so there should be an option to provide credentials for both.
        # At the same time some videos are available under the same video id
        # on both sites so that we have to identify them as the same video.
        # For that purpose we have to keep both in the same extractor
        # but under different netrc machines.
        username, password = self._get_login_info(netrc_machine=site)
        if username is None:
            return

        login_url = 'https://www.%s/%slogin' % (host, 'premium/' if 'premium' in host else '')
        login_page = self._download_webpage(
            login_url, None, 'Downloading %s login page' % site)

        def is_logged(webpage):
            return any(re.search(p, webpage) for p in (
                r'class=["\']signOut',
                r'>Sign\s+[Oo]ut\s*<'))

        if is_logged(login_page):
            self._logged_in = True
            return

        login_form = self._hidden_inputs(login_page)

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

        response = self._download_json(
            'https://www.%s/front/authenticate' % host, None,
            'Logging in to %s' % site,
            data=urlencode_postdata(login_form),
            headers={
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'Referer': login_url,
                'X-Requested-With': 'XMLHttpRequest',
            })

        if response.get('success') == '1':
            self._logged_in = True
            return

        message = response.get('message')
        if message is not None:
            raise ExtractorError(
                'Unable to login: %s' % message, expected=True)

        raise ExtractorError('Unable to log in')

class PornHubIE(PornHubBaseIE):
    IE_DESC = 'PornHub and Thumbzilla'
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            (?:[^/]+\.)?
                            %s
                            /(?:(?:view_video\.php|video/show)\?viewkey=|embed/)|
                            (?:www\.)?thumbzilla\.com/video/
                        )
                        (?P<id>[\da-z]+)
                    ''' % PornHubBaseIE._PORNHUB_HOST_RE
    _EMBED_REGEX = [r']+?src=["\'](?P<url>(?:https?:)?//(?:www\.)?pornhub(?:premium)?\.(?:com|net|org)/embed/[\da-z]+)']
    _TESTS = [{
        'url': 'http://www.pornhub.com/view_video.php?viewkey=648719015',
        'md5': 'a6391306d050e4547f62b3f485dd9ba9',
        'info_dict': {
            'id': '648719015',
            'ext': 'mp4',
            'title': 'Seductive Indian beauty strips down and fingers her pink pussy',
            'uploader': 'Babes',
            'upload_date': '20130628',
            'timestamp': 1372447216,
            'duration': 361,
            'view_count': int,
            'like_count': int,
            'dislike_count': int,
            'comment_count': int,
            'age_limit': 18,
            'tags': list,
            'categories': list,
            'cast': list,
        },
    }, {
        # non-ASCII title
        'url': 'http://www.pornhub.com/view_video.php?viewkey=1331683002',
        'info_dict': {
            'id': '1331683002',
            'ext': 'mp4',
            'title': '重庆婷婷女王足交',
            'upload_date': '20150213',
            'timestamp': 1423804862,
            'duration': 1753,
            'view_count': int,
            'like_count': int,
            'dislike_count': int,
            'comment_count': int,
            'age_limit': 18,
            'tags': list,
            'categories': list,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'Video has been flagged for verification in accordance with our trust and safety policy',
    }, {
        # subtitles
        'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7',
        'info_dict': {
            'id': 'ph5af5fef7c2aa7',
            'ext': 'mp4',
            'title': 'BFFS - Cute Teen Girls Share Cock On the Floor',
            'uploader': 'BFFs',
            'duration': 622,
            'view_count': int,
            'like_count': int,
            'dislike_count': int,
            'comment_count': int,
            'age_limit': 18,
            'tags': list,
            'categories': list,
            'subtitles': {
                'en': [{
                    "ext": 'srt'
                }]
            },
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'This video has been disabled',
    }, {
        'url': 'http://www.pornhub.com/view_video.php?viewkey=ph601dc30bae19a',
        'info_dict': {
            'id': 'ph601dc30bae19a',
            'uploader': 'Projekt Melody',
            'uploader_id': 'projekt-melody',
            'upload_date': '20210205',
            'title': '"Welcome to My Pussy Mansion" - CB Stream (02/03/21)',
            'thumbnail': r're:https?://.+',
        },
    }, {
        'url': 'http://www.pornhub.com/view_video.php?viewkey=ph557bbb6676d2d',
        'only_matching': True,
    }, {
        # removed at the request of cam4.com
        'url': 'http://fr.pornhub.com/view_video.php?viewkey=ph55ca2f9760862',
        'only_matching': True,
    }, {
        # removed at the request of the copyright owner
        'url': 'http://www.pornhub.com/view_video.php?viewkey=788152859',
        'only_matching': True,
    }, {
        # removed by uploader
        'url': 'http://www.pornhub.com/view_video.php?viewkey=ph572716d15a111',
        'only_matching': True,
    }, {
        # private video
        'url': 'http://www.pornhub.com/view_video.php?viewkey=ph56fd731fce6b7',
        'only_matching': True,
    }, {
        'url': 'https://www.thumbzilla.com/video/ph56c6114abd99a/horny-girlfriend-sex',
        'only_matching': True,
    }, {
        'url': 'http://www.pornhub.com/video/show?viewkey=648719015',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.net/view_video.php?viewkey=203640933',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.org/view_video.php?viewkey=203640933',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5e4acdae54a82',
        'only_matching': True,
    }, {
        # Some videos are available with the same id on both premium
        # and non-premium sites (e.g. this and the following test)
        'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5f75b0f4b18e3',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5f75b0f4b18e3',
        'only_matching': True,
    }, {
        # geo restricted
        'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5a9813bfa7156',
        'only_matching': True,
    }, {
        'url': 'http://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd.onion/view_video.php?viewkey=ph5a9813bfa7156',
        'only_matching': True,
    }]

    def _extract_count(self, pattern, webpage, name):
        return str_to_int(self._search_regex(pattern, webpage, '%s count' % name, default=None))

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

        self._login(host)

        self._set_cookie(host, 'age_verified', '1')

        def dl_webpage(platform):
            self._set_cookie(host, 'platform', platform)
            return self._download_webpage(
                'https://www.%s/view_video.php?viewkey=%s' % (host, video_id),
                video_id, 'Downloading %s webpage' % platform)

        webpage = dl_webpage('pc')

        error_msg = self._html_search_regex(
            (r'(?s)
]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)
',
             r'(?s)
]+class=["\']noVideo["\'][^>]*>(?P<error>.+?)
'),
            webpage, 'error message', default=None, group='error')
        if error_msg:
            error_msg = re.sub(r'\s+', ' ', error_msg)
            raise ExtractorError(
                'PornHub said: %s' % error_msg,
                expected=True, video_id=video_id)

        if any(re.search(p, webpage) for p in (
                r'class=["\']geoBlocked["\']',
                r'>\s*This content is unavailable in your country')):
            self.raise_geo_restricted()

        # video_title from flashvars contains whitespace instead of non-ASCII (see
        # http://www.pornhub.com/view_video.php?viewkey=1331683002), not relying
        # on that anymore.
        title = self._html_search_meta(
            'twitter:title', webpage, default=None) or self._html_search_regex(
            (r'(?s)
]+class=["\']title["\'][^>]*>(?P<title>.+?)
',
             r'
]+data-video-title=(["\'])(?P<title>(?:(?!\1).)+)\1',
             r'shareTitle["\']\s*[=:]\s*(["\'])(?P<title>(?:(?!\1).)+)\1'),
            webpage, 'title', group='title')

        video_urls = []
        video_urls_set = set()
        subtitles = {}

        flashvars = self._parse_json(
            self._search_regex(
                r'var\s+flashvars_\d+\s*=\s*({.+?});', webpage, 'flashvars', default='{}'),
            video_id)
        if flashvars:
            subtitle_url = url_or_none(flashvars.get('closedCaptionsFile'))
            if subtitle_url:
                subtitles.setdefault('en', []).append({
                    'url': subtitle_url,
                    'ext': 'srt',
                })
            thumbnail = flashvars.get('image_url')
            duration = int_or_none(flashvars.get('video_duration'))
            media_definitions = flashvars.get('mediaDefinitions')
            if isinstance(media_definitions, list):
                for definition in media_definitions:
                    if not isinstance(definition, dict):
                        continue
                    video_url = definition.get('videoUrl')
                    if not video_url or not isinstance(video_url, compat_str):
                        continue
                    if video_url in video_urls_set:
                        continue
                    video_urls_set.add(video_url)
                    video_urls.append(
                        (video_url, int_or_none(definition.get('quality'))))
        else:
            thumbnail, duration = [None] * 2

        def extract_js_vars(webpage, pattern, default=NO_DEFAULT):
            assignments = self._search_regex(
                pattern, webpage, 'encoded url', default=default)
            if not assignments:
                return {}

            assignments = assignments.split(';')

            js_vars = {}

            def parse_js_value(inp):
                inp = re.sub(r'/\*(?:(?!\*/).)*?\*/', '', inp)
                if '+' in inp:
                    inps = inp.split('+')
                    return functools.reduce(
                        operator.concat, map(parse_js_value, inps))
                inp = inp.strip()
                if inp in js_vars:
                    return js_vars[inp]
                return remove_quotes(inp)

            for assn in assignments:
                assn = assn.strip()
                if not assn:
                    continue
                assn = re.sub(r'var\s+', '', assn)
                vname, value = assn.split('=', 1)
                js_vars[vname] = parse_js_value(value)
            return js_vars

        def add_video_url(video_url):
            v_url = url_or_none(video_url)
            if not v_url:
                return
            if v_url in video_urls_set:
                return
            video_urls.append((v_url, None))
            video_urls_set.add(v_url)

        def parse_quality_items(quality_items):
            q_items = self._parse_json(quality_items, video_id, fatal=False)
            if not isinstance(q_items, list):
                return
            for item in q_items:
                if isinstance(item, dict):
                    add_video_url(item.get('url'))

        if not video_urls:
            FORMAT_PREFIXES = ('media', 'quality', 'qualityItems')
            js_vars = extract_js_vars(
                webpage, r'(var\s+(?:%s)_.+)' % '|'.join(FORMAT_PREFIXES),
                default=None)
            if js_vars:
                for key, format_url in js_vars.items():
                    if key.startswith(FORMAT_PREFIXES[-1]):
                        parse_quality_items(format_url)
                    elif any(key.startswith(p) for p in FORMAT_PREFIXES[:2]):
                        add_video_url(format_url)
            if not video_urls and re.search(
                    r'<[^>]+\bid=["\']lockedPlayer', webpage):
                raise ExtractorError(
                    'Video %s is locked' % video_id, expected=True)

        if not video_urls:
            js_vars = extract_js_vars(
                dl_webpage('tv'), r'(var.+?mediastring.+?)</script>')
            add_video_url(js_vars['mediastring'])

        for mobj in re.finditer(
                r']+\bclass=["\']downloadBtn\b[^>]+\bhref=(["\'])(?P<url>(?:(?!\1).)+)\1',
                webpage):
            video_url = mobj.group('url')
            if video_url not in video_urls_set:
                video_urls.append((video_url, None))
                video_urls_set.add(video_url)

        upload_date = None
        formats = []

        def add_format(format_url, height=None):
            ext = determine_ext(format_url)
            if ext == 'mpd':
                formats.extend(self._extract_mpd_formats(
                    format_url, video_id, mpd_id='dash', fatal=False))
                return
            if ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    format_url, video_id, 'mp4', entry_protocol='m3u8_native',
                    m3u8_id='hls', fatal=False))
                return
            if not height:
                height = int_or_none(self._search_regex(
                    r'(?P<height>\d+)[pP]?_\d+[kK]', format_url, 'height',
                    default=None))
            formats.append({
                'url': format_url,
                'format_id': format_field(height, None, '%dp'),
                'height': height,
            })

        for video_url, height in video_urls:
            if not upload_date:
                upload_date = self._search_regex(
                    r'/(\d{6}/\d{2})/', video_url, 'upload data', default=None)
                if upload_date:
                    upload_date = upload_date.replace('/', '')
            if '/video/get_media' in video_url:
                medias = self._download_json(video_url, video_id, fatal=False)
                if isinstance(medias, list):
                    for media in medias:
                        if not isinstance(media, dict):
                            continue
                        video_url = url_or_none(media.get('videoUrl'))
                        if not video_url:
                            continue
                        height = int_or_none(media.get('quality'))
                        add_format(video_url, height)
                continue
            add_format(video_url)

        # field_preference is unnecessary here, but kept for code-similarity with youtube-dl
        self._sort_formats(
            formats, field_preference=('height', 'width', 'fps', 'format_id'))

        model_profile = self._search_json(
            r'var\s+MODEL_PROFILE\s*=', webpage, 'model profile', video_id, fatal=False)
        video_uploader = self._html_search_regex(
            r'(?s)From: .+?<(?:a\b[^>]+\bhref=["\']/(?:(?:user|channel)s|model|pornstar)/|span\b[^>]+\bclass=["\']username)[^>]+>(.+?)<',
            webpage, 'uploader', default=None) or model_profile.get('username')

        def extract_vote_count(kind, name):
            return self._extract_count(
                (r']+\bclass="votes%s"[^>]*>([\d,\.]+)' % kind,
                 r']+\bclass=["\']votes%s["\'][^>]*\bdata-rating=["\'](\d+)' % kind),
                webpage, name)

        view_count = self._extract_count(
            r'([\d,\.]+) [Vv]iews', webpage, 'view')
        like_count = extract_vote_count('Up', 'like')
        dislike_count = extract_vote_count('Down', 'dislike')
        comment_count = self._extract_count(
            r'All Comments\s*\(([\d,.]+)\)', webpage, 'comment')

        def extract_list(meta_key):
            div = self._search_regex(
                r'(?s)
]+\bclass=["\'].*?\b%sWrapper[^>]*>(.+?)
'
                % meta_key, webpage, meta_key, default=None)
            if div:
                return [clean_html(x).strip() for x in re.findall(r'(?s)]+\bhref=[^>]+>.+?', div)]

        info = self._search_json_ld(webpage, video_id, default={})
        # description provided in JSON-LD is irrelevant
        info['description'] = None

        return merge_dicts({
            'id': video_id,
            'uploader': video_uploader,
            'uploader_id': remove_start(model_profile.get('modelProfileLink'), '/model/'),
            'upload_date': upload_date,
            'title': title,
            'thumbnail': thumbnail,
            'duration': duration,
            'view_count': view_count,
            'like_count': like_count,
            'dislike_count': dislike_count,
            'comment_count': comment_count,
            'formats': formats,
            'age_limit': 18,
            'tags': extract_list('tags'),
            'categories': extract_list('categories'),
            'cast': extract_list('pornstars'),
            'subtitles': subtitles,

            # retry when any of the following codes is returned;
            # no more fragments or data can be downloaded anymore
            'unrecoverable_http_error': (401, 503, 472, 429),
        }, info)

class PornHubPlaylistBaseIE(PornHubBaseIE):
    def _extract_page(self, url):
        return int_or_none(self._search_regex(
            r'\bpage=(\d+)', url, 'page', default=None))

    def _extract_entries(self, webpage, host):
        # Only process container div with main playlist content skipping
        # drop-down menu that uses similar pattern for videos (see
        # https://github.com/ytdl-org/youtube-dl/issues/11594).
        container = self._search_regex(
            r'(?s)(
]+class=["\']container.+)', webpage,
            'container', default=webpage)

        return [
            self.url_result(
                'http://www.%s/%s' % (host, video_url),
                PornHubIE.ie_key(), video_title=title)
            for video_url, title in orderedSet(re.findall(
                r'href="/?(view_video\.php\?.*\bviewkey=[\da-z]+[^"]*)"[^>]*\s+title="([^"]+)"',
                container))
        ]

class PornHubUserIE(PornHubPlaylistBaseIE):
    _VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?%s/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/?#&]+))(?:[?#&]|/(?!videos)|$)' % PornHubBaseIE._PORNHUB_HOST_RE
    _TESTS = [{
        'url': 'https://www.pornhub.com/model/zoe_ph',
        'playlist_mincount': 118,
    }, {
        'url': 'https://www.pornhub.com/pornstar/liz-vicious',
        'info_dict': {
            'id': 'liz-vicious',
        },
        'playlist_mincount': 118,
    }, {
        'url': 'https://www.pornhub.com/users/russianveet69',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/channels/povd',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/model/zoe_ph?abc=1',
        'only_matching': True,
    }, {
        # Unavailable via /videos page, but available with direct pagination
        # on pornstar page (see [1]), requires premium
        # 1. https://github.com/ytdl-org/youtube-dl/issues/27853
        'url': 'https://www.pornhubpremium.com/pornstar/sienna-west',
        'only_matching': True,
    }, {
        # Same as before, multi page
        'url': 'https://www.pornhubpremium.com/pornstar/lily-labeau',
        'only_matching': True,
    }, {
        'url': 'https://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd.onion/model/zoe_ph',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        user_id = mobj.group('id')
        videos_url = '%s/videos' % mobj.group('url')
        page = self._extract_page(url)
        if page:
            videos_url = update_url_query(videos_url, {'page': page})
        return self.url_result(
            videos_url, ie=PornHubPagedVideoListIE.ie_key(), video_id=user_id)

class PornHubPagedPlaylistBaseIE(PornHubPlaylistBaseIE):
    @staticmethod
    def _has_more(webpage):
        return re.search(
            r'''(?x)

]+\bclass=["\']page_next|
                <link[^>]+\brel=["\']next|
                <button[^>]+\bid=["\']moreDataBtn
            ''', webpage) is not None

    def _entries(self, url, host, item_id):
        page = self._extract_page(url)

        VIDEOS = '/videos'

        def download_page(base_url, num, fallback=False):
            note = 'Downloading page %d%s' % (num, ' (switch to fallback)' if fallback else '')
            return self._download_webpage(
                base_url, item_id, note, query={'page': num})

        def is_404(e):
            return isinstance(e.cause, compat_HTTPError) and e.cause.code == 404

        base_url = url
        has_page = page is not None
        first_page = page if has_page else 1
        for page_num in (first_page, ) if has_page else itertools.count(first_page):
            try:
                try:
                    webpage = download_page(base_url, page_num)
                except ExtractorError as e:
                    # Some sources may not be available via /videos page,
                    # trying to fallback to main page pagination (see [1])
                    # 1. https://github.com/ytdl-org/youtube-dl/issues/27853
                    if is_404(e) and page_num == first_page and VIDEOS in base_url:
                        base_url = base_url.replace(VIDEOS, '')
                        webpage = download_page(base_url, page_num, fallback=True)
                    else:
                        raise
            except ExtractorError as e:
                if is_404(e) and page_num != first_page:
                    break
                raise
            page_entries = self._extract_entries(webpage, host)
            if not page_entries:
                break
            for e in page_entries:
                yield e
            if not self._has_more(webpage):
                break

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        host = mobj.group('host')
        item_id = mobj.group('id')

        self._login(host)

        return self.playlist_result(self._entries(url, host, item_id), item_id)

class PornHubPagedVideoListIE(PornHubPagedPlaylistBaseIE):
    _VALID_URL = r'https?://(?:[^/]+\.)?%s/(?!playlist/)(?P<id>(?:[^/]+/)*[^/?#&]+)' % PornHubBaseIE._PORNHUB_HOST_RE
    _TESTS = [{
        'url': 'https://www.pornhub.com/model/zoe_ph/videos',
        'only_matching': True,
    }, {
        'url': 'http://www.pornhub.com/users/rushandlia/videos',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos',
        'info_dict': {
            'id': 'pornstar/jenny-blighe/videos',
        },
        'playlist_mincount': 149,
    }, {
        'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos?page=3',
        'info_dict': {
            'id': 'pornstar/jenny-blighe/videos',
        },
        'playlist_mincount': 40,
    }, {
        # default sorting as Top Rated Videos
        'url': 'https://www.pornhub.com/channels/povd/videos',
        'info_dict': {
            'id': 'channels/povd/videos',
        },
        'playlist_mincount': 293,
    }, {
        # Top Rated Videos
        'url': 'https://www.pornhub.com/channels/povd/videos?o=ra',
        'only_matching': True,
    }, {
        # Most Recent Videos
        'url': 'https://www.pornhub.com/channels/povd/videos?o=da',
        'only_matching': True,
    }, {
        # Most Viewed Videos
        'url': 'https://www.pornhub.com/channels/povd/videos?o=vi',
        'only_matching': True,
    }, {
        'url': 'http://www.pornhub.com/users/zoe_ph/videos/public',
        'only_matching': True,
    }, {
        # Most Viewed Videos
        'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=mv',
        'only_matching': True,
    }, {
        # Top Rated Videos
        'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=tr',
        'only_matching': True,
    }, {
        # Longest Videos
        'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=lg',
        'only_matching': True,
    }, {
        # Newest Videos
        'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=cm',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/paid',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/fanonly',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/video',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/video?page=3',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/video/search?search=123',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/categories/teen',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/categories/teen?page=3',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/hd',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/hd?page=3',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/described-video',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/described-video?page=2',
        'only_matching': True,
    }, {
        'url': 'https://www.pornhub.com/video/incategories/60fps-1/hd-porn',
        'only_matching': True,
    }, {
        'url': 'https://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd.onion/model/zoe_ph/videos',
        'only_matching': True,
    }]

    @classmethod
    def suitable(cls, url):
        return (False
                if PornHubIE.suitable(url) or PornHubUserIE.suitable(url) or PornHubUserVideosUploadIE.suitable(url)
                else super(PornHubPagedVideoListIE, cls).suitable(url))

class PornHubUserVideosUploadIE(PornHubPagedPlaylistBaseIE):
    _VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?%s/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/]+)/videos/upload)' % PornHubBaseIE._PORNHUB_HOST_RE
    _TESTS = [{
        'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos/upload',
        'info_dict': {
            'id': 'jenny-blighe',
        },
        'playlist_mincount': 129,
    }, {
        'url': 'https://www.pornhub.com/model/zoe_ph/videos/upload',
        'only_matching': True,
    }, {
        'url': 'http://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd.onion/pornstar/jenny-blighe/videos/upload',
        'only_matching': True,
    }]

class PornHubPlaylistIE(PornHubPlaylistBaseIE):
    _VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?%s/playlist/(?P<id>[^/?#&]+))' % PornHubBaseIE._PORNHUB_HOST_RE
    _TESTS = [{
        'url': 'https://www.pornhub.com/playlist/44121572',
        'info_dict': {
            'id': '44121572',
        },
        'playlist_count': 77,
    }, {
        'url': 'https://www.pornhub.com/playlist/4667351',
        'only_matching': True,
    }, {
        'url': 'https://de.pornhub.com/playlist/4667351',
        'only_matching': True,
    }, {
        'url': 'https://de.pornhub.com/playlist/4667351?page=2',
        'only_matching': True,
    }]

    def _entries(self, url, host, item_id):
        webpage = self._download_webpage(url, item_id, 'Downloading page 1')
        playlist_id = self._search_regex(r'var\s+playlistId\s*=\s*"([^"]+)"', webpage, 'playlist_id')
        video_count = int_or_none(
            self._search_regex(r'var\s+itemsCount\s*=\s*([0-9]+)\s*\|\|', webpage, 'video_count'))
        token = self._search_regex(r'var\s+token\s*=\s*"([^"]+)"', webpage, 'token')
        page_count = math.ceil((video_count - 36) / 40.) + 1
        page_entries = self._extract_entries(webpage, host)

        def download_page(page_num):
            note = 'Downloading page {}'.format(page_num)
            page_url = 'https://www.{}/playlist/viewChunked'.format(host)
            return self._download_webpage(page_url, item_id, note, query={
                'id': playlist_id,
                'page': page_num,
                'token': token,
            })

        for page_num in range(1, page_count + 1):
            if page_num > 1:
                webpage = download_page(page_num)
                page_entries = self._extract_entries(webpage, host)
            if not page_entries:
                break
            for e in page_entries:
                yield e

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        host = mobj.group('host')
        item_id = mobj.group('id')

        self._login(host)

        return self.playlist_result(self._entries(mobj.group('url'), host, item_id), item_id)


code:
import json

from .common import InfoExtractor
from ..utils import int_or_none

class PornotubeIE(InfoExtractor):
    _VALID_URL = r'https?://(?:\w+\.)?pornotube\.com/(?:[^?#]*?)/video/(?P<id>[0-9]+)'
    _TEST = {
        'url': 'http://www.pornotube.com/orientation/straight/video/4964/title/weird-hot-and-wet-science',
        'md5': '60fc5a4f0d93a97968fc7999d98260c9',
        'info_dict': {
            'id': '4964',
            'ext': 'mp4',
            'upload_date': '20141203',
            'title': 'Weird Hot and Wet Science',
            'description': 'md5:a8304bef7ef06cb4ab476ca6029b01b0',
            'categories': ['Adult Humor', 'Blondes'],
            'uploader': 'Alpha Blue Archives',
            'thumbnail': r're:^https?://.*\.jpg$',
            'timestamp': 1417582800,
            'age_limit': 18,
        }
    }

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

        token = self._download_json(
            'https://api.aebn.net/auth/v2/origins/authenticate',
            video_id, note='Downloading token',
            data=json.dumps({'credentials': 'Clip Application'}).encode('utf-8'),
            headers={
                'Content-Type': 'application/json',
                'Origin': 'http://www.pornotube.com',
            })['tokenKey']

        video_url = self._download_json(
            'https://api.aebn.net/delivery/v1/clips/%s/MP4' % video_id,
            video_id, note='Downloading delivery information',
            headers={'Authorization': token})['mediaUrl']

        FIELDS = (
            'title', 'description', 'startSecond', 'endSecond', 'publishDate',
            'studios{name}', 'categories{name}', 'movieId', 'primaryImageNumber'
        )

        info = self._download_json(
            'https://api.aebn.net/content/v2/clips/%s?fields=%s'
            % (video_id, ','.join(FIELDS)), video_id,
            note='Downloading metadata',
            headers={'Authorization': token})

        if isinstance(info, list):
            info = info[0]

        title = info['title']

        timestamp = int_or_none(info.get('publishDate'), scale=1000)
        uploader = info.get('studios', [{}])[0].get('name')
        movie_id = info.get('movieId')
        primary_image_number = info.get('primaryImageNumber')
        thumbnail = None
        if movie_id and primary_image_number:
            thumbnail = 'http://pic.aebn.net/dis/t/%s/%s_%08d.jpg' % (
                movie_id, movie_id, primary_image_number)
        start = int_or_none(info.get('startSecond'))
        end = int_or_none(info.get('endSecond'))
        duration = end - start if start and end else None
        categories = [c['name'] for c in info.get('categories', []) if c.get('name')]

        return {
            'id': video_id,
            'url': video_url,
            'title': title,
            'description': info.get('description'),
            'duration': duration,
            'timestamp': timestamp,
            'uploader': uploader,
            'thumbnail': thumbnail,
            'categories': categories,
            'age_limit': 18,
        }


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

class PornoVoisinesIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?pornovoisines\.com/videos/show/(?P<id>\d+)/(?P<display_id>[^/.]+)'

    _TEST = {
        'url': 'http://www.pornovoisines.com/videos/show/919/recherche-appartement.html',
        'md5': '6f8aca6a058592ab49fe701c8ba8317b',
        'info_dict': {
            'id': '919',
            'display_id': 'recherche-appartement',
            'ext': 'mp4',
            'title': 'Recherche appartement',
            'description': 'md5:fe10cb92ae2dd3ed94bb4080d11ff493',
            'thumbnail': r're:^https?://.*\.jpg$',
            'upload_date': '20140925',
            'duration': 120,
            'view_count': int,
            'average_rating': float,
            'categories': ['Débutante', 'Débutantes', 'Scénario', 'Sodomie'],
            'age_limit': 18,
            'subtitles': {
                'fr': [{
                    'ext': 'vtt',
                }]
            },
        }
    }

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

        settings_url = self._download_json(
            'http://www.pornovoisines.com/api/video/%s/getsettingsurl/' % video_id,
            video_id, note='Getting settings URL')['video_settings_url']
        settings = self._download_json(settings_url, video_id)['data']

        formats = []
        for kind, data in settings['variants'].items():
            if kind == 'HLS':
                formats.extend(self._extract_m3u8_formats(
                    data, video_id, ext='mp4', entry_protocol='m3u8_native', m3u8_id='hls'))
            elif kind == 'MP4':
                for item in data:
                    formats.append({
                        'url': item['url'],
                        'height': item.get('height'),
                        'bitrate': item.get('bitrate'),
                    })
        self._sort_formats(formats)

        webpage = self._download_webpage(url, video_id)

        title = self._og_search_title(webpage)
        description = self._og_search_description(webpage)

        # The webpage has a bug - there's no space between "thumb" and src=
        thumbnail = self._html_search_regex(
            r'<img[^>]+class=([\'"])thumb\1[^>]*src=([\'"])(?P<url>[^"]+)\2',
            webpage, 'thumbnail', fatal=False, group='url')

        upload_date = unified_strdate(self._search_regex(
            r'Le\s*<b>([\d/]+)', webpage, 'upload date', fatal=False))
        duration = settings.get('main', {}).get('duration')
        view_count = int_or_none(self._search_regex(
            r'(\d+) vues', webpage, 'view count', fatal=False))
        average_rating = self._search_regex(
            r'Note\s*:\s*(\d+(?:,\d+)?)', webpage, 'average rating', fatal=False)
        if average_rating:
            average_rating = float_or_none(average_rating.replace(',', '.'))

        categories = self._html_search_regex(
            r'(?s)Catégories\s*:\s*<b>(.+?)</b>', webpage, 'categories', fatal=False)
        if categories:
            categories = [category.strip() for category in categories.split(',')]

        subtitles = {'fr': [{
            'url': subtitle,
        } for subtitle in settings.get('main', {}).get('vtt_tracks', {}).values()]}

        return {
            'id': video_id,
            'display_id': display_id,
            'formats': formats,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'upload_date': upload_date,
            'duration': duration,
            'view_count': view_count,
            'average_rating': average_rating,
            'categories': categories,
            'age_limit': 18,
            'subtitles': subtitles,
        }


code:
from .common import InfoExtractor
from ..utils import (
    str_to_int,
)

class PornoXOIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?pornoxo\.com/videos/(?P<id>\d+)/(?P<display_id>[^/]+)\.html'
    _TEST = {
        'url': 'http://www.pornoxo.com/videos/7564/striptease-from-sexy-secretary.html',
        'md5': '582f28ecbaa9e6e24cb90f50f524ce87',
        'info_dict': {
            'id': '7564',
            'ext': 'flv',
            'title': 'Striptease From Sexy Secretary!',
            'display_id': 'striptease-from-sexy-secretary',
            'description': 'md5:0ee35252b685b3883f4a1d38332f9980',
            'categories': list,  # NSFW
            'thumbnail': r're:https?://.*\.jpg$',
            'age_limit': 18,
        }
    }

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

        webpage = self._download_webpage(url, video_id)
        video_data = self._extract_jwplayer_data(webpage, video_id, require_title=False)

        title = self._html_search_regex(
            r'<title>([^<]+)\s*-\s*PornoXO', webpage, 'title')

        view_count = str_to_int(self._html_search_regex(
            r'[vV]iews:\s*([0-9,]+)', webpage, 'view count', fatal=False))

        categories_str = self._html_search_regex(
            r'<meta name="description" content=".*featuring\s*([^"]+)"',
            webpage, 'categories', fatal=False)
        categories = (
            None if categories_str is None
            else categories_str.split(','))

        video_data.update({
            'id': video_id,
            'title': title,
            'display_id': display_id,
            'description': self._html_search_meta('description', webpage),
            'categories': categories,
            'view_count': view_count,
            'age_limit': 18,
        })

        return video_data


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

class PremiershipRugbyIE(InfoExtractor):
    _VALID_URL = r'https?://(?:\w+\.)premiershiprugby\.(?:com)/watch/(?P<id>[\w-]+)'
    _TESTS = [{
        'url': 'https://www.premiershiprugby.com/watch/full-match-harlequins-v-newcastle-falcons',
        'info_dict': {
            'id': '0_mbkb7ldt',
            'title': 'Full Match: Harlequins v Newcastle Falcons',
            'ext': 'mp4',
            'thumbnail': 'https://open.http.mp.streamamg.com/p/3000914/sp/300091400/thumbnail/entry_id/0_mbkb7ldt//width/960/height/540/type/1/quality/75',
            'duration': 6093.0,
            'tags': ['video'],
            'categories': ['Full Match', 'Harlequins', 'Newcastle Falcons', 'gallaher premiership'],
        }
    }]

    def _real_extract(self, url):
        display_id = self._match_id(url)
        json_data = self._download_json(
            f'https://article-cms-api.incrowdsports.com/v2/articles/slug/{display_id}',
            display_id, query={'clientId': 'PRL'})['data']['article']

        formats, subs = self._extract_m3u8_formats_and_subtitles(
            json_data['heroMedia']['content']['videoLink'], display_id)

        return {
            'id': json_data['heroMedia']['content']['sourceSystemId'],
            'display_id': display_id,
            'title': traverse_obj(json_data, ('heroMedia', 'title')),
            'formats': formats,
            'subtitles': subs,
            'thumbnail': traverse_obj(json_data, ('heroMedia', 'content', 'videoThumbnail')),
            'duration': int_or_none(traverse_obj(json_data, ('heroMedia', 'content', 'metadata', 'msDuration')), scale=1000),
            'tags': json_data.get('tags'),
            'categories': traverse_obj(json_data, ('categories', ..., 'text')),
        }


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

class PressTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?presstv\.ir/[^/]+/(?P<y>\d+)/(?P<m>\d+)/(?P<d>\d+)/(?P<id>\d+)/(?P<display_id>[^/]+)?'

    _TEST = {
        'url': 'http://www.presstv.ir/Detail/2016/04/09/459911/Australian-sewerage-treatment-facility-/',
        'md5': '5d7e3195a447cb13e9267e931d8dd5a5',
        'info_dict': {
            'id': '459911',
            'display_id': 'Australian-sewerage-treatment-facility-',
            'ext': 'mp4',
            'title': 'Organic mattresses used to clean waste water',
            'upload_date': '20160409',
            'thumbnail': r're:^https?://.*\.jpg',
            'description': 'md5:20002e654bbafb6908395a5c0cfcd125'
        }
    }

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

        webpage = self._download_webpage(url, display_id)

        # extract video URL from webpage
        video_url = self._hidden_inputs(webpage)['inpPlayback']

        # build list of available formats
        # specified in http://www.presstv.ir/Scripts/playback.js
        base_url = 'http://192.99.219.222:82/presstv'
        _formats = [
            (180, '_low200.mp4'),
            (360, '_low400.mp4'),
            (720, '_low800.mp4'),
            (1080, '.mp4')
        ]

        formats = [{
            'url': base_url + video_url[:-4] + extension,
            'format_id': '%dp' % height,
            'height': height,
        } for height, extension in _formats]

        # extract video metadata
        title = remove_start(
            self._html_search_meta('title', webpage, fatal=True), 'PressTV-')

        thumbnail = self._og_search_thumbnail(webpage)
        description = self._og_search_description(webpage)

        upload_date = '%04d%02d%02d' % (
            int(mobj.group('y')),
            int(mobj.group('m')),
            int(mobj.group('d')),
        )

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


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

class ProjectVeritasIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?projectveritas\.com/(?P<type>news|video)/(?P<id>[^/?#]+)'
    _TESTS = [{
        'url': 'https://www.projectveritas.com/news/exclusive-inside-the-new-york-and-new-jersey-hospitals-battling-coronavirus/',
        'info_dict': {
            'id': '51910aab-365a-5cf1-88f2-8eb1ca5fd3c6',
            'ext': 'mp4',
            'title': 'Exclusive: Inside The New York and New Jersey Hospitals Battling Coronavirus',
            'upload_date': '20200327',
            'thumbnail': 'md5:6076477fe50b03eb8708be9415e18e1c',
        }
    }, {
        'url': 'https://www.projectveritas.com/video/ilhan-omar-connected-ballot-harvester-in-cash-for-ballots-scheme-car-is-full/',
        'info_dict': {
            'id': 'c5aab304-a56b-54b1-9f0b-03b77bc5f2f6',
            'ext': 'mp4',
            'title': 'Ilhan Omar connected Ballot Harvester in cash-for-ballots scheme: "Car is full" of absentee ballots',
            'upload_date': '20200927',
            'thumbnail': 'md5:194b8edf0e2ba64f25500ff4378369a4',
        }
    }]

    def _real_extract(self, url):
        id, type = self._match_valid_url(url).group('id', 'type')
        api_url = f'https://www.projectveritas.com/page-data/{type}/{id}/page-data.json'
        data_json = self._download_json(api_url, id)['result']['data']
        main_data = traverse_obj(data_json, 'video', 'post')
        video_id = main_data['id']
        thumbnail = traverse_obj(main_data, ('image', 'ogImage', 'src'))
        mux_asset = traverse_obj(main_data,
                                 'muxAsset', ('body', 'json', 'content', ..., 'data', 'target', 'fields', 'muxAsset'),
                                 get_all=False, expected_type=dict)
        if not mux_asset:
            raise ExtractorError('No video on the provided url.', expected=True)
        playback_id = traverse_obj(mux_asset, 'playbackId', ('en-US', 'playbackId'))
        formats = self._extract_m3u8_formats(f'https://stream.mux.com/{playback_id}.m3u8', video_id)
        self._sort_formats(formats)
        return {
            'id': video_id,
            'title': main_data['title'],
            'upload_date': unified_strdate(main_data.get('date')),
            'thumbnail': thumbnail.replace('//', ''),
            'formats': formats,
        }