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

class PicartoIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www.)?picarto\.tv/(?P<id>[a-zA-Z0-9]+)'
    _TEST = {
        'url': 'https://picarto.tv/Setz',
        'info_dict': {
            'id': 'Setz',
            'ext': 'mp4',
            'title': 're:^Setz [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'timestamp': int,
            'is_live': True
        },
        'skip': 'Stream is offline',
    }

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

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

        data = self._download_json(
            'https://ptvintern.picarto.tv/ptvapi', channel_id, query={
                'query': '''{
  channel(name: "%s") {
    adult
    id
    online
    stream_name
    title
  }
  getLoadBalancerUrl(channel_name: "%s") {
    url
  }
}''' % (channel_id, channel_id),
            })['data']
        metadata = data['channel']

        if metadata.get('online') == 0:
            raise ExtractorError('Stream is offline', expected=True)
        title = metadata['title']

        cdn_data = self._download_json(
            data['getLoadBalancerUrl']['url'] + '/stream/json_' + metadata['stream_name'] + '.js',
            channel_id, 'Downloading load balancing info')

        formats = []
        for source in (cdn_data.get('source') or []):
            source_url = source.get('url')
            if not source_url:
                continue
            source_type = source.get('type')
            if source_type == 'html5/application/vnd.apple.mpegurl':
                formats.extend(self._extract_m3u8_formats(
                    source_url, channel_id, 'mp4', m3u8_id='hls', fatal=False))
            elif source_type == 'html5/video/mp4':
                formats.append({
                    'url': source_url,
                })
        self._sort_formats(formats)

        mature = metadata.get('adult')
        if mature is None:
            age_limit = None
        else:
            age_limit = 18 if mature is True else 0

        return {
            'id': channel_id,
            'title': title.strip(),
            'is_live': True,
            'channel': channel_id,
            'channel_id': metadata.get('id'),
            'channel_url': 'https://picarto.tv/%s' % channel_id,
            'age_limit': age_limit,
            'formats': formats,
        }

class PicartoVodIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www.)?picarto\.tv/videopopout/(?P<id>[^/?#&]+)'
    _TESTS = [{
        'url': 'https://picarto.tv/videopopout/ArtofZod_2017.12.12.00.13.23.flv',
        'md5': '3ab45ba4352c52ee841a28fb73f2d9ca',
        'info_dict': {
            'id': 'ArtofZod_2017.12.12.00.13.23.flv',
            'ext': 'mp4',
            'title': 'ArtofZod_2017.12.12.00.13.23.flv',
            'thumbnail': r're:^https?://.*\.jpg'
        },
    }, {
        'url': 'https://picarto.tv/videopopout/Plague',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, video_id)

        vod_info = self._parse_json(
            self._search_regex(
                r'(?s)#vod-player["\']\s*,\s*(\{.+?\})\s*\)', webpage,
                'vod player'),
            video_id, transform_source=js_to_json)

        formats = self._extract_m3u8_formats(
            vod_info['vod'], video_id, 'mp4', entry_protocol='m3u8_native',
            m3u8_id='hls')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': video_id,
            'thumbnail': vod_info.get('vodThumb'),
            'formats': formats,
        }


code:
import re

from .common import InfoExtractor
from ..utils import (
    dict_get,
    ExtractorError,
    int_or_none,
    join_nonempty,
    parse_iso8601,
    try_get,
    unescapeHTML,
)

class PikselIE(InfoExtractor):
    _VALID_URL = r'''(?x)https?://
        (?:
            (?:
                player\.
                    (?:
                        olympusattelecom|
                        vibebyvista
                    )|
                (?:api|player)\.multicastmedia|
                (?:api-ovp|player)\.piksel
            )\.com|
            (?:
                mz-edge\.stream\.co|
                movie-s\.nhk\.or
            )\.jp|
            vidego\.baltimorecity\.gov
        )/v/(?:refid/(?P<refid>[^/]+)/prefid/)?(?P<id>[\w-]+)'''
    _EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?:)?//player\.piksel\.com/v/[a-z0-9]+)']
    _TESTS = [
        {
            'url': 'http://player.piksel.com/v/ums2867l',
            'md5': '34e34c8d89dc2559976a6079db531e85',
            'info_dict': {
                'id': 'ums2867l',
                'ext': 'mp4',
                'title': 'GX-005 with Caption',
                'timestamp': 1481335659,
                'upload_date': '20161210'
            }
        },
        {
            # Original source: http://www.uscourts.gov/cameras-courts/state-washington-vs-donald-j-trump-et-al
            'url': 'https://player.piksel.com/v/v80kqp41',
            'md5': '753ddcd8cc8e4fa2dda4b7be0e77744d',
            'info_dict': {
                'id': 'v80kqp41',
                'ext': 'mp4',
                'title': 'WAW- State of Washington vs. Donald J. Trump, et al',
                'description': 'State of Washington vs. Donald J. Trump, et al, Case Number 17-CV-00141-JLR, TRO Hearing, Civil Rights Case, 02/3/2017, 1:00 PM (PST), Seattle Federal Courthouse, Seattle, WA, Judge James L. Robart presiding.',
                'timestamp': 1486171129,
                'upload_date': '20170204'
            }
        },
        {
            # https://www3.nhk.or.jp/nhkworld/en/ondemand/video/2019240/
            'url': 'http://player.piksel.com/v/refid/nhkworld/prefid/nw_vod_v_en_2019_240_20190823233000_02_1566873477',
            'only_matching': True,
        }
    ]

    def _call_api(self, app_token, resource, display_id, query, fatal=True):
        response = (self._download_json(
            'http://player.piksel.com/ws/ws_%s/api/%s/mode/json/apiv/5' % (resource, app_token),
            display_id, query=query, fatal=fatal) or {}).get('response')
        failure = try_get(response, lambda x: x['failure']['reason'])
        if failure:
            if fatal:
                raise ExtractorError(failure, expected=True)
            self.report_warning(failure)
        return response

    def _real_extract(self, url):
        ref_id, display_id = self._match_valid_url(url).groups()
        webpage = self._download_webpage(url, display_id)
        app_token = self._search_regex([
            r'clientAPI\s*:\s*"([^"]+)"',
            r'data-de-api-key\s*=\s*"([^"]+)"'
        ], webpage, 'app token')
        query = {'refid': ref_id, 'prefid': display_id} if ref_id else {'v': display_id}
        program = self._call_api(
            app_token, 'program', display_id, query)['WsProgramResponse']['program']
        video_id = program['uuid']
        video_data = program['asset']
        title = video_data['title']
        asset_type = dict_get(video_data, ['assetType', 'asset_type'])

        formats = []

        def process_asset_file(asset_file):
            if not asset_file:
                return
            # TODO: extract rtmp formats
            http_url = asset_file.get('http_url')
            if not http_url:
                return
            tbr = None
            vbr = int_or_none(asset_file.get('videoBitrate'), 1024)
            abr = int_or_none(asset_file.get('audioBitrate'), 1024)
            if asset_type == 'video':
                tbr = vbr + abr
            elif asset_type == 'audio':
                tbr = abr

            formats.append({
                'format_id': join_nonempty('http', tbr),
                'url': unescapeHTML(http_url),
                'vbr': vbr,
                'abr': abr,
                'width': int_or_none(asset_file.get('videoWidth')),
                'height': int_or_none(asset_file.get('videoHeight')),
                'filesize': int_or_none(asset_file.get('filesize')),
                'tbr': tbr,
            })

        def process_asset_files(asset_files):
            for asset_file in (asset_files or []):
                process_asset_file(asset_file)

        process_asset_files(video_data.get('assetFiles'))
        process_asset_file(video_data.get('referenceFile'))
        if not formats:
            asset_id = video_data.get('assetid') or program.get('assetid')
            if asset_id:
                process_asset_files(try_get(self._call_api(
                    app_token, 'asset_file', display_id, {
                        'assetid': asset_id,
                    }, False), lambda x: x['WsAssetFileResponse']['AssetFiles']))

        m3u8_url = dict_get(video_data, [
            'm3u8iPadURL',
            'ipadM3u8Url',
            'm3u8AndroidURL',
            'm3u8iPhoneURL',
            'iphoneM3u8Url'])
        if m3u8_url:
            formats.extend(self._extract_m3u8_formats(
                m3u8_url, video_id, 'mp4', 'm3u8_native',
                m3u8_id='hls', fatal=False))

        smil_url = dict_get(video_data, ['httpSmil', 'hdSmil', 'rtmpSmil'])
        if smil_url:
            transform_source = None
            if ref_id == 'nhkworld':
                # TODO: figure out if this is something to be fixed in urljoin,
                # _parse_smil_formats or keep it here
                transform_source = lambda x: x.replace('src="/', 'src="').replace('/media"', '/media/"')
            formats.extend(self._extract_smil_formats(
                re.sub(r'/od/[^/]+/', '/od/http/', smil_url), video_id,
                transform_source=transform_source, fatal=False))

        self._sort_formats(formats, ('tbr', ))  # Incomplete resolution information

        subtitles = {}
        for caption in video_data.get('captions', []):
            caption_url = caption.get('url')
            if caption_url:
                subtitles.setdefault(caption.get('locale', 'en'), []).append({
                    'url': caption_url})

        return {
            'id': video_id,
            'title': title,
            'description': video_data.get('description'),
            'thumbnail': video_data.get('thumbnailUrl'),
            'timestamp': parse_iso8601(video_data.get('dateadd')),
            'formats': formats,
            'subtitles': subtitles,
        }

repo_name: Lesmiscore/ytdl


code:
import re

from .common import InfoExtractor
from ..utils import (
    int_or_none,
    remove_end,
    remove_start,
    str_to_int,
    unified_strdate,
)

class PinkbikeIE(InfoExtractor):
    _VALID_URL = r'https?://(?:(?:www\.)?pinkbike\.com/video/|es\.pinkbike\.org/i/kvid/kvid-y5\.swf\?id=)(?P<id>[0-9]+)'
    _TESTS = [{
        'url': 'http://www.pinkbike.com/video/402811/',
        'md5': '4814b8ca7651034cd87e3361d5c2155a',
        'info_dict': {
            'id': '402811',
            'ext': 'mp4',
            'title': 'Brandon Semenuk - RAW 100',
            'description': 'Official release: www.redbull.ca/rupertwalker',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 100,
            'upload_date': '20150406',
            'uploader': 'revelco',
            'location': 'Victoria, British Columbia, Canada',
            'view_count': int,
            'comment_count': int,
        }
    }, {
        'url': 'http://es.pinkbike.org/i/kvid/kvid-y5.swf?id=406629',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(
            'http://www.pinkbike.com/video/%s' % video_id, video_id)

        formats = []
        for _, format_id, src in re.findall(
                r'data-quality=((?:\\)?["\'])(.+?)\1[^>]+src=\1(.+?)\1', webpage):
            height = int_or_none(self._search_regex(
                r'^(\d+)[pP]$', format_id, 'height', default=None))
            formats.append({
                'url': src,
                'format_id': format_id,
                'height': height,
            })
        self._sort_formats(formats)

        title = remove_end(self._og_search_title(webpage), ' Video - Pinkbike')
        description = self._html_search_regex(
            r'(?s)id="media-description"[^>]*>(.+?)<',
            webpage, 'description', default=None) or remove_start(
            self._og_search_description(webpage), title + '. ')
        thumbnail = self._og_search_thumbnail(webpage)
        duration = int_or_none(self._html_search_meta(
            'video:duration', webpage, 'duration'))

        uploader = self._search_regex(
            r']+\brel=["\']author[^>]+>([^<]+)', webpage,
            'uploader', fatal=False)
        upload_date = unified_strdate(self._search_regex(
            r'class="fullTime"[^>]+title="([^"]+)"',
            webpage, 'upload date', fatal=False))

        location = self._html_search_regex(
            r'(?s)
Location
\s*
(.+?)<',
            webpage, 'location', fatal=False)

        def extract_count(webpage, label):
            return str_to_int(self._search_regex(
                r']+class="stat-num"[^>]*>([\d,.]+)\s*]+class="stat-label"[^>]*>%s' % label,
                webpage, label, fatal=False))

        view_count = extract_count(webpage, 'Views')
        comment_count = extract_count(webpage, 'Comments')

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


code:
import json

from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    determine_ext,
    float_or_none,
    int_or_none,
    try_get,
    unified_timestamp,
    url_or_none,
)

class PinterestBaseIE(InfoExtractor):
    _VALID_URL_BASE = r'https?://(?:[^/]+\.)?pinterest\.(?:com|fr|de|ch|jp|cl|ca|it|co\.uk|nz|ru|com\.au|at|pt|co\.kr|es|com\.mx|dk|ph|th|com\.uy|co|nl|info|kr|ie|vn|com\.vn|ec|mx|in|pe|co\.at|hu|co\.in|co\.nz|id|com\.ec|com\.py|tw|be|uk|com\.bo|com\.pe)'

    def _call_api(self, resource, video_id, options):
        return self._download_json(
            'https://www.pinterest.com/resource/%sResource/get/' % resource,
            video_id, 'Download %s JSON metadata' % resource, query={
                'data': json.dumps({'options': options})
            })['resource_response']

    def _extract_video(self, data, extract_formats=True):
        video_id = data['id']

        title = (data.get('title') or data.get('grid_title') or video_id).strip()

        urls = []
        formats = []
        duration = None
        if extract_formats:
            for format_id, format_dict in data['videos']['video_list'].items():
                if not isinstance(format_dict, dict):
                    continue
                format_url = url_or_none(format_dict.get('url'))
                if not format_url or format_url in urls:
                    continue
                urls.append(format_url)
                duration = float_or_none(format_dict.get('duration'), scale=1000)
                ext = determine_ext(format_url)
                if 'hls' in format_id.lower() or ext == 'm3u8':
                    formats.extend(self._extract_m3u8_formats(
                        format_url, video_id, 'mp4', entry_protocol='m3u8_native',
                        m3u8_id=format_id, fatal=False))
                else:
                    formats.append({
                        'url': format_url,
                        'format_id': format_id,
                        'width': int_or_none(format_dict.get('width')),
                        'height': int_or_none(format_dict.get('height')),
                        'duration': duration,
                    })
            self._sort_formats(formats)

        description = data.get('description') or data.get('description_html') or data.get('seo_description')
        timestamp = unified_timestamp(data.get('created_at'))

        def _u(field):
            return try_get(data, lambda x: x['closeup_attribution'][field], compat_str)

        uploader = _u('full_name')
        uploader_id = _u('id')

        repost_count = int_or_none(data.get('repin_count'))
        comment_count = int_or_none(data.get('comment_count'))
        categories = try_get(data, lambda x: x['pin_join']['visual_annotation'], list)
        tags = data.get('hashtags')

        thumbnails = []
        images = data.get('images')
        if isinstance(images, dict):
            for thumbnail_id, thumbnail in images.items():
                if not isinstance(thumbnail, dict):
                    continue
                thumbnail_url = url_or_none(thumbnail.get('url'))
                if not thumbnail_url:
                    continue
                thumbnails.append({
                    'url': thumbnail_url,
                    'width': int_or_none(thumbnail.get('width')),
                    'height': int_or_none(thumbnail.get('height')),
                })

        return {
            'id': video_id,
            'title': title,
            'description': description,
            'duration': duration,
            'timestamp': timestamp,
            'thumbnails': thumbnails,
            'uploader': uploader,
            'uploader_id': uploader_id,
            'repost_count': repost_count,
            'comment_count': comment_count,
            'categories': categories,
            'tags': tags,
            'formats': formats,
            'extractor_key': PinterestIE.ie_key(),
        }

class PinterestIE(PinterestBaseIE):
    _VALID_URL = r'%s/pin/(?P<id>\d+)' % PinterestBaseIE._VALID_URL_BASE
    _TESTS = [{
        'url': 'https://www.pinterest.com/pin/664281013778109217/',
        'md5': '6550c2af85d6d9f3fe3b88954d1577fc',
        'info_dict': {
            'id': '664281013778109217',
            'ext': 'mp4',
            'title': 'Origami',
            'description': 'md5:b9d90ddf7848e897882de9e73344f7dd',
            'duration': 57.7,
            'timestamp': 1593073622,
            'upload_date': '20200625',
            'uploader': 'Love origami -I am Dafei',
            'uploader_id': '586523688879454212',
            'repost_count': 50,
            'comment_count': 0,
            'categories': list,
            'tags': list,
        },
    }, {
        'url': 'https://co.pinterest.com/pin/824721750502199491/',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        data = self._call_api(
            'Pin', video_id, {
                'field_set_key': 'unauth_react_main_pin',
                'id': video_id,
            })['data']
        return self._extract_video(data)

class PinterestCollectionIE(PinterestBaseIE):
    _VALID_URL = r'%s/(?P<username>[^/]+)/(?P<id>[^/?#&]+)' % PinterestBaseIE._VALID_URL_BASE
    _TESTS = [{
        'url': 'https://www.pinterest.ca/mashal0407/cool-diys/',
        'info_dict': {
            'id': '585890301462791043',
            'title': 'cool diys',
        },
        'playlist_count': 8,
    }, {
        'url': 'https://www.pinterest.ca/fudohub/videos/',
        'info_dict': {
            'id': '682858430939307450',
            'title': 'VIDEOS',
        },
        'playlist_mincount': 365,
        'skip': 'Test with extract_formats=False',
    }]

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

    def _real_extract(self, url):
        username, slug = self._match_valid_url(url).groups()
        board = self._call_api(
            'Board', slug, {
                'slug': slug,
                'username': username
            })['data']
        board_id = board['id']
        options = {
            'board_id': board_id,
            'page_size': 250,
        }
        bookmark = None
        entries = []
        while True:
            if bookmark:
                options['bookmarks'] = [bookmark]
            board_feed = self._call_api('BoardFeed', board_id, options)
            for item in (board_feed.get('data') or []):
                if not isinstance(item, dict) or item.get('type') != 'pin':
                    continue
                video_id = item.get('id')
                if video_id:
                    # Some pins may not be available anonymously via pin URL
                    # video = self._extract_video(item, extract_formats=False)
                    # video.update({
                    #     '_type': 'url_transparent',
                    #     'url': 'https://www.pinterest.com/pin/%s/' % video_id,
                    # })
                    # entries.append(video)
                    entries.append(self._extract_video(item))
            bookmark = board_feed.get('bookmark')
            if not bookmark:
                break
        return self.playlist_result(
            entries, playlist_id=board_id, playlist_title=board.get('name'))


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

class PixivSketchBaseIE(InfoExtractor):
    def _call_api(self, video_id, path, referer, note='Downloading JSON metadata'):
        response = self._download_json(f'https://sketch.pixiv.net/api/{path}', video_id, note=note, headers={
            'Referer': referer,
            'X-Requested-With': referer,
        })
        errors = traverse_obj(response, ('errors', ..., 'message'))
        if errors:
            raise ExtractorError(' '.join(f'{e}.' for e in errors))
        return response.get('data') or {}

class PixivSketchIE(PixivSketchBaseIE):
    IE_NAME = 'pixiv:sketch'
    _VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<uploader_id>[a-zA-Z0-9_-]+)/lives/(?P<id>\d+)/?'
    _TESTS = [{
        'url': 'https://sketch.pixiv.net/@nuhutya/lives/3654620468641830507',
        'info_dict': {
            'id': '7370666691623196569',
            'title': 'まにあえクリスマス！',
            'uploader': 'ぬふちゃ',
            'uploader_id': 'nuhutya',
            'channel_id': '9844815',
            'age_limit': 0,
            'timestamp': 1640351536,
        },
        'skip': True,
    }, {
        # these two (age_limit > 0) requires you to login on website, but it's actually not required for download
        'url': 'https://sketch.pixiv.net/@namahyou/lives/4393103321546851377',
        'info_dict': {
            'id': '4907995960957946943',
            'title': 'クリスマスなんて知らん🖕',
            'uploader': 'すゃもり',
            'uploader_id': 'suya2mori2',
            'channel_id': '31169300',
            'age_limit': 15,
            'timestamp': 1640347640,
        },
        'skip': True,
    }, {
        'url': 'https://sketch.pixiv.net/@8aki/lives/3553803162487249670',
        'info_dict': {
            'id': '1593420639479156945',
            'title': 'おまけ本作業(リョナ有)',
            'uploader': 'おぶい / Obui',
            'uploader_id': 'oving',
            'channel_id': '17606',
            'age_limit': 18,
            'timestamp': 1640330263,
        },
        'skip': True,
    }]

    def _real_extract(self, url):
        video_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
        data = self._call_api(video_id, f'lives/{video_id}.json', url)

        if not traverse_obj(data, 'is_broadcasting'):
            raise ExtractorError(f'This live is offline. Use https://sketch.pixiv.net/@{uploader_id} for ongoing live.', expected=True)

        m3u8_url = traverse_obj(data, ('owner', 'hls_movie', 'url'))
        formats = self._extract_m3u8_formats(
            m3u8_url, video_id, ext='mp4',
            entry_protocol='m3u8_native', m3u8_id='hls')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': data.get('name'),
            'formats': formats,
            'uploader': traverse_obj(data, ('user', 'name'), ('owner', 'user', 'name')),
            'uploader_id': traverse_obj(data, ('user', 'unique_name'), ('owner', 'user', 'unique_name')),
            'channel_id': str(traverse_obj(data, ('user', 'pixiv_user_id'), ('owner', 'user', 'pixiv_user_id'))),
            'age_limit': 18 if data.get('is_r18') else 15 if data.get('is_r15') else 0,
            'timestamp': unified_timestamp(data.get('created_at')),
            'is_live': True
        }

class PixivSketchUserIE(PixivSketchBaseIE):
    IE_NAME = 'pixiv:sketch:user'
    _VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<id>[a-zA-Z0-9_-]+)/?'
    _TESTS = [{
        'url': 'https://sketch.pixiv.net/@nuhutya',
        'only_matching': True,
    }, {
        'url': 'https://sketch.pixiv.net/@namahyou',
        'only_matching': True,
    }, {
        'url': 'https://sketch.pixiv.net/@8aki',
        'only_matching': True,
    }]

    @classmethod
    def suitable(cls, url):
        return super(PixivSketchUserIE, cls).suitable(url) and not PixivSketchIE.suitable(url)

    def _real_extract(self, url):
        user_id = self._match_id(url)
        data = self._call_api(user_id, f'lives/users/@{user_id}.json', url)

        if not traverse_obj(data, 'is_broadcasting'):
            try:
                self._call_api(user_id, 'users/current.json', url, 'Investigating reason for request failure')
            except ExtractorError as ex:
                if ex.cause and ex.cause.code == 401:
                    self.raise_login_required(f'Please log in, or use direct link like https://sketch.pixiv.net/@{user_id}/1234567890', method='cookies')
            raise ExtractorError('This user is offline', expected=True)

        return self.url_result(f'https://sketch.pixiv.net/@{user_id}/lives/{data["id"]}')


code:
from .common import InfoExtractor
from ..utils import (
    determine_ext,
    ExtractorError,
    int_or_none,
    parse_qs,
    xpath_text,
    qualities,
)

class PladformIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            (?:
                                out\.pladform\.ru/player|
                                static\.pladform\.ru/player\.swf
                            )
                            \?.*\bvideoid=|
                            video\.pladform\.ru/catalog/video/videoid/
                        )
                        (?P<id>\d+)
                    '''
    _EMBED_REGEX = [r']+src=(["\'])(?P<url>(?:https?:)?//out\.pladform\.ru/player\?.+?)\1']
    _TESTS = [{
        'url': 'http://out.pladform.ru/player?pl=18079&type=html5&videoid=100231282',
        'info_dict': {
            'id': '6216d548e755edae6e8280667d774791',
            'ext': 'mp4',
            'timestamp': 1406117012,
            'title': 'Гарик Мартиросян и Гарик Харламов - Кастинг на концерт ко Дню милиции',
            'age_limit': 0,
            'upload_date': '20140723',
            'thumbnail': str,
            'view_count': int,
            'description': str,
            'category': list,
            'uploader_id': '12082',
            'uploader': 'Comedy Club',
            'duration': 367,
        },
        'expected_warnings': ['HTTP Error 404: Not Found']
    }, {
        'url': 'https://out.pladform.ru/player?pl=64471&videoid=3777899&vk_puid15=0&vk_puid34=0',
        'md5': '53362fac3a27352da20fa2803cc5cd6f',
        'info_dict': {
            'id': '3777899',
            'ext': 'mp4',
            'title': 'СТУДИЯ СОЮЗ • Шоу Студия Союз, 24 выпуск (01.02.2018) Нурлан Сабуров и Слава Комиссаренко',
            'description': 'md5:05140e8bf1b7e2d46e7ba140be57fd95',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 3190,
        },
    }, {
        'url': 'http://static.pladform.ru/player.swf?pl=21469&videoid=100183293&vkcid=0',
        'only_matching': True,
    }, {
        'url': 'http://video.pladform.ru/catalog/video/videoid/100183293/vkcid/0',
        'only_matching': True,
    }]

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

        qs = parse_qs(url)
        pl = qs.get('pl', ['1'])[0]

        video = self._download_xml(
            'http://out.pladform.ru/getVideo', video_id, query={
                'pl': pl,
                'videoid': video_id,
            }, fatal=False)

        def fail(text):
            raise ExtractorError(
                '%s returned error: %s' % (self.IE_NAME, text),
                expected=True)

        if not video:
            targetUrl = self._request_webpage(url, video_id, note='Resolving final URL').geturl()
            if targetUrl == url:
                raise ExtractorError('Can\'t parse page')
            return self.url_result(targetUrl)

        if video.tag == 'error':
            fail(video.text)

        quality = qualities(('ld', 'sd', 'hd'))

        formats = []
        for src in video.findall('./src'):
            if src is None:
                continue
            format_url = src.text
            if not format_url:
                continue
            if src.get('type') == 'hls' or determine_ext(format_url) == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    format_url, video_id, 'mp4', entry_protocol='m3u8_native',
                    m3u8_id='hls', fatal=False))
            else:
                formats.append({
                    'url': src.text,
                    'format_id': src.get('quality'),
                    'quality': quality(src.get('quality')),
                })

        if not formats:
            error = xpath_text(video, './cap', 'error', default=None)
            if error:
                fail(error)

        self._sort_formats(formats)

        webpage = self._download_webpage(
            'http://video.pladform.ru/catalog/video/videoid/%s' % video_id,
            video_id)

        title = self._og_search_title(webpage, fatal=False) or xpath_text(
            video, './/title', 'title', fatal=True)
        description = self._search_regex(
            r'
\s*
([^<]+)
', webpage, 'description', fatal=False)
        thumbnail = self._og_search_thumbnail(webpage) or xpath_text(
            video, './/cover', 'cover')

        duration = int_or_none(xpath_text(video, './/time', 'duration'))
        age_limit = int_or_none(xpath_text(video, './/age18', 'age limit'))

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


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

class PlanetMarathiIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?planetmarathi\.com/titles/(?P<id>[^/#&?$]+)'
    _TESTS = [{
        'url': 'https://www.planetmarathi.com/titles/ek-unad-divas',
        'playlist_mincount': 2,
        'info_dict': {
            'id': 'ek-unad-divas',
        },
        'playlist': [{
            'info_dict': {
                'id': 'ASSETS-MOVIE-ASSET-01_ek-unad-divas',
                'ext': 'mp4',
                'title': 'ek unad divas',
                'alt_title': 'चित्रपट',
                'description': 'md5:41c7ed6b041c2fea9820a3f3125bd881',
                'season_number': None,
                'episode_number': 1,
                'duration': 5539,
                'upload_date': '20210829',
            },
        }]  # Trailer skipped
    }, {
        'url': 'https://www.planetmarathi.com/titles/baap-beep-baap-season-1',
        'playlist_mincount': 10,
        'info_dict': {
            'id': 'baap-beep-baap-season-1',
        },
        'playlist': [{
            'info_dict': {
                'id': 'ASSETS-CHARACTER-PROFILE-SEASON-01-ASSET-01_baap-beep-baap-season-1',
                'ext': 'mp4',
                'title': 'Manohar Kanhere',
                'alt_title': 'मनोहर कान्हेरे',
                'description': 'md5:285ed45d5c0ab5522cac9a043354ebc6',
                'season_number': 1,
                'episode_number': 1,
                'duration': 29,
                'upload_date': '20210829',
            },
        }]  # Trailers, Episodes, other Character profiles skipped
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        entries = []
        json_data = self._download_json(f'https://www.planetmarathi.com/api/v1/titles/{id}/assets', id)['assets']
        for asset in json_data:
            asset_title = asset['mediaAssetName']['en']
            if asset_title == 'Movie':
                asset_title = id.replace('-', ' ')
            asset_id = f'{asset["sk"]}_{id}'.replace('#', '-')
            formats, subtitles = self._extract_m3u8_formats_and_subtitles(asset['mediaAssetURL'], asset_id)
            self._sort_formats(formats)
            entries.append({
                'id': asset_id,
                'title': asset_title,
                'alt_title': try_get(asset, lambda x: x['mediaAssetName']['mr']),
                'description': try_get(asset, lambda x: x['mediaAssetDescription']['en']),
                'season_number': asset.get('mediaAssetSeason'),
                'episode_number': asset.get('mediaAssetIndexForAssetType'),
                'duration': asset.get('mediaAssetDurationInSeconds'),
                'upload_date': unified_strdate(asset.get('created')),
                'formats': formats,
                'subtitles': subtitles,
            })
        return self.playlist_result(entries, playlist_id=id)


code:
from .common import InfoExtractor
from ..compat import (
    compat_b64decode,
    compat_str,
)
from ..utils import (
    clean_html,
    ExtractorError,
    int_or_none,
    str_or_none,
    try_get,
    url_or_none,
    urlencode_postdata,
    urljoin,
)

class PlatziBaseIE(InfoExtractor):
    _LOGIN_URL = 'https://platzi.com/login/'
    _NETRC_MACHINE = 'platzi'

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

        login_form = self._hidden_inputs(login_page)

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

        urlh = self._request_webpage(
            self._LOGIN_URL, None, 'Logging in',
            data=urlencode_postdata(login_form),
            headers={'Referer': self._LOGIN_URL})

        # login succeeded
        if 'platzi.com/login' not in urlh.geturl():
            return

        login_error = self._webpage_read_content(
            urlh, self._LOGIN_URL, None, 'Downloading login error page')

        login = self._parse_json(
            self._search_regex(
                r'login\s*=\s*({.+?})(?:\s*;|\s*</script)', login_error, 'login'),
            None)

        for kind in ('error', 'password', 'nonFields'):
            error = str_or_none(login.get('%sError' % kind))
            if error:
                raise ExtractorError(
                    'Unable to login: %s' % error, expected=True)
        raise ExtractorError('Unable to log in')

class PlatziIE(PlatziBaseIE):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            platzi\.com/clases|           # es version
                            courses\.platzi\.com/classes  # en version
                        )/[^/]+/(?P<id>\d+)-[^/?\#&]+
                    '''

    _TESTS = [{
        'url': 'https://platzi.com/clases/1311-next-js/12074-creando-nuestra-primera-pagina/',
        'md5': '8f56448241005b561c10f11a595b37e3',
        'info_dict': {
            'id': '12074',
            'ext': 'mp4',
            'title': 'Creando nuestra primera página',
            'description': 'md5:4c866e45034fc76412fbf6e60ae008bc',
            'duration': 420,
        },
        'skip': 'Requires platzi account credentials',
    }, {
        'url': 'https://courses.platzi.com/classes/1367-communication-codestream/13430-background/',
        'info_dict': {
            'id': '13430',
            'ext': 'mp4',
            'title': 'Background',
            'description': 'md5:49c83c09404b15e6e71defaf87f6b305',
            'duration': 360,
        },
        'skip': 'Requires platzi account credentials',
        'params': {
            'skip_download': True,
        },
    }]

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

        webpage = self._download_webpage(url, lecture_id)

        data = self._parse_json(
            self._search_regex(
                # client_data may contain "};" so that we have to try more
                # strict regex first
                (r'client_data\s*=\s*({.+?})\s*;\s*\n',
                 r'client_data\s*=\s*({.+?})\s*;'),
                webpage, 'client data'),
            lecture_id)

        material = data['initialState']['material']
        desc = material['description']
        title = desc['title']

        formats = []
        for server_id, server in material['videos'].items():
            if not isinstance(server, dict):
                continue
            for format_id in ('hls', 'dash'):
                format_url = url_or_none(server.get(format_id))
                if not format_url:
                    continue
                if format_id == 'hls':
                    formats.extend(self._extract_m3u8_formats(
                        format_url, lecture_id, 'mp4',
                        entry_protocol='m3u8_native', m3u8_id=format_id,
                        note='Downloading %s m3u8 information' % server_id,
                        fatal=False))
                elif format_id == 'dash':
                    formats.extend(self._extract_mpd_formats(
                        format_url, lecture_id, mpd_id=format_id,
                        note='Downloading %s MPD manifest' % server_id,
                        fatal=False))
        self._sort_formats(formats)

        content = str_or_none(desc.get('content'))
        description = (clean_html(compat_b64decode(content).decode('utf-8'))
                       if content else None)
        duration = int_or_none(material.get('duration'), invscale=60)

        return {
            'id': lecture_id,
            'title': title,
            'description': description,
            'duration': duration,
            'formats': formats,
        }

class PlatziCourseIE(PlatziBaseIE):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            platzi\.com/clases|           # es version
                            courses\.platzi\.com/classes  # en version
                        )/(?P<id>[^/?\#&]+)
                    '''
    _TESTS = [{
        'url': 'https://platzi.com/clases/next-js/',
        'info_dict': {
            'id': '1311',
            'title': 'Curso de Next.js',
        },
        'playlist_count': 22,
    }, {
        'url': 'https://courses.platzi.com/classes/communication-codestream/',
        'info_dict': {
            'id': '1367',
            'title': 'Codestream Course',
        },
        'playlist_count': 14,
    }]

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

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

        webpage = self._download_webpage(url, course_name)

        props = self._parse_json(
            self._search_regex(r'data\s*=\s*({.+?})\s*;', webpage, 'data'),
            course_name)['initialProps']

        entries = []
        for chapter_num, chapter in enumerate(props['concepts'], 1):
            if not isinstance(chapter, dict):
                continue
            materials = chapter.get('materials')
            if not materials or not isinstance(materials, list):
                continue
            chapter_title = chapter.get('title')
            chapter_id = str_or_none(chapter.get('id'))
            for material in materials:
                if not isinstance(material, dict):
                    continue
                if material.get('material_type') != 'video':
                    continue
                video_url = urljoin(url, material.get('url'))
                if not video_url:
                    continue
                entries.append({
                    '_type': 'url_transparent',
                    'url': video_url,
                    'title': str_or_none(material.get('name')),
                    'id': str_or_none(material.get('id')),
                    'ie_key': PlatziIE.ie_key(),
                    'chapter': chapter_title,
                    'chapter_number': chapter_num,
                    'chapter_id': chapter_id,
                })

        course_id = compat_str(try_get(props, lambda x: x['course']['id']))
        course_title = try_get(props, lambda x: x['course']['name'], compat_str)

        return self.playlist_result(entries, course_id, course_title)


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

class PlayFMIE(InfoExtractor):
    IE_NAME = 'play.fm'
    _VALID_URL = r'https?://(?:www\.)?play\.fm/(?P<slug>(?:[^/]+/)+(?P<id>[^/]+))/?(?:$|[?#])'

    _TEST = {
        'url': 'https://www.play.fm/dan-drastic/sven-tasnadi-leipzig-electronic-music-batofar-paris-fr-2014-07-12',
        'md5': 'c505f8307825a245d0c7ad1850001f22',
        'info_dict': {
            'id': '71276',
            'ext': 'mp3',
            'title': 'Sven Tasnadi - LEIPZIG ELECTRONIC MUSIC @ Batofar (Paris,FR) - 2014-07-12',
            'description': '',
            'duration': 5627,
            'timestamp': 1406033781,
            'upload_date': '20140722',
            'uploader': 'Dan Drastic',
            'uploader_id': '71170',
            'view_count': int,
            'comment_count': int,
        },
    }

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

        recordings = self._download_json(
            'http://v2api.play.fm/recordings/slug/%s' % slug, video_id)

        error = recordings.get('error')
        if isinstance(error, dict):
            raise ExtractorError(
                '%s returned error: %s' % (self.IE_NAME, error.get('message')),
                expected=True)

        audio_url = recordings['audio']
        video_id = compat_str(recordings.get('id') or video_id)
        title = recordings['title']
        description = recordings.get('description')
        duration = int_or_none(recordings.get('recordingDuration'))
        timestamp = parse_iso8601(recordings.get('created_at'))
        uploader = recordings.get('page', {}).get('title')
        uploader_id = compat_str(recordings.get('page', {}).get('id'))
        view_count = int_or_none(recordings.get('playCount'))
        comment_count = int_or_none(recordings.get('commentCount'))
        categories = [tag['name'] for tag in recordings.get('tags', []) if tag.get('name')]

        return {
            'id': video_id,
            'url': audio_url,
            'title': title,
            'description': description,
            'duration': duration,
            'timestamp': timestamp,
            'uploader': uploader,
            'uploader_id': uploader_id,
            'view_count': view_count,
            'comment_count': comment_count,
            'categories': categories,
        }


code:
import json

from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    clean_html,
    ExtractorError,
    int_or_none,
    PUTRequest,
)

class PlayPlusTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?playplus\.(?:com|tv)/VOD/(?P<project_id>[0-9]+)/(?P<id>[0-9a-f]{32})'
    _TEST = {
        'url': 'https://www.playplus.tv/VOD/7572/db8d274a5163424e967f35a30ddafb8e',
        'md5': 'd078cb89d7ab6b9df37ce23c647aef72',
        'info_dict': {
            'id': 'db8d274a5163424e967f35a30ddafb8e',
            'ext': 'mp4',
            'title': 'Capítulo 179 - Final',
            'description': 'md5:01085d62d8033a1e34121d3c3cabc838',
            'timestamp': 1529992740,
            'upload_date': '20180626',
        },
        'skip': 'Requires account credential',
    }
    _NETRC_MACHINE = 'playplustv'
    _GEO_COUNTRIES = ['BR']
    _token = None
    _profile_id = None

    def _call_api(self, resource, video_id=None, query=None):
        return self._download_json('https://api.playplus.tv/api/media/v2/get' + resource, video_id, headers={
            'Authorization': 'Bearer ' + self._token,
        }, query=query)

    def _perform_login(self, username, password):
        req = PUTRequest(
            'https://api.playplus.tv/api/web/login', json.dumps({
                'email': username,
                'password': password,
            }).encode(), {
                'Content-Type': 'application/json; charset=utf-8',
            })

        try:
            self._token = self._download_json(req, None)['token']
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
                raise ExtractorError(self._parse_json(
                    e.cause.read(), None)['errorMessage'], expected=True)
            raise

        self._profile = self._call_api('Profiles')['list'][0]['_id']

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

    def _real_extract(self, url):
        project_id, media_id = self._match_valid_url(url).groups()
        media = self._call_api(
            'Media', media_id, {
                'profileId': self._profile,
                'projectId': project_id,
                'mediaId': media_id,
            })['obj']
        title = media['title']

        formats = []
        for f in media.get('files', []):
            f_url = f.get('url')
            if not f_url:
                continue
            file_info = f.get('fileInfo') or {}
            formats.append({
                'url': f_url,
                'width': int_or_none(file_info.get('width')),
                'height': int_or_none(file_info.get('height')),
            })
        self._sort_formats(formats)

        thumbnails = []
        for thumb in media.get('thumbs', []):
            thumb_url = thumb.get('url')
            if not thumb_url:
                continue
            thumbnails.append({
                'url': thumb_url,
                'width': int_or_none(thumb.get('width')),
                'height': int_or_none(thumb.get('height')),
            })

        return {
            'id': media_id,
            'title': title,
            'formats': formats,
            'thumbnails': thumbnails,
            'description': clean_html(media.get('description')) or media.get('shortDescription'),
            'timestamp': int_or_none(media.get('publishDate'), 1000),
            'view_count': int_or_none(media.get('numberOfViews')),
            'comment_count': int_or_none(media.get('numberOfComments')),
            'tags': media.get('tags'),
        }


code:
import re

from .common import InfoExtractor
from ..utils import int_or_none

class PlaysTVIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?plays\.tv/(?:video|embeds)/(?P<id>[0-9a-f]{18})'
    _TESTS = [{
        'url': 'https://plays.tv/video/56af17f56c95335490/when-you-outplay-the-azir-wall',
        'md5': 'dfeac1198506652b5257a62762cec7bc',
        'info_dict': {
            'id': '56af17f56c95335490',
            'ext': 'mp4',
            'title': 'Bjergsen - When you outplay the Azir wall',
            'description': 'Posted by Bjergsen',
        }
    }, {
        'url': 'https://plays.tv/embeds/56af17f56c95335490',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(
            'https://plays.tv/video/%s' % video_id, video_id)

        info = self._search_json_ld(webpage, video_id,)

        mpd_url, sources = re.search(
            r'(?s)<video[^>]+data-mpd="([^"]+)"[^>]*>(.+?)</video>',
            webpage).groups()
        formats = self._extract_mpd_formats(
            self._proto_relative_url(mpd_url), video_id, mpd_id='DASH')
        for format_id, height, format_url in re.findall(r'<source\s+res="((\d+)h?)"\s+src="([^"]+)"', sources):
            formats.append({
                'url': self._proto_relative_url(format_url),
                'format_id': 'http-' + format_id,
                'height': int_or_none(height),
            })
        self._sort_formats(formats)

        info.update({
            'id': video_id,
            'description': self._og_search_description(webpage),
            'thumbnail': info.get('thumbnail') or self._og_search_thumbnail(webpage),
            'formats': formats,
        })

        return info


code:
from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    smuggle_url,
    try_get,
)

class PlayStuffIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?play\.stuff\.co\.nz/details/(?P<id>[^/?#&]+)'
    _TESTS = [{
        'url': 'https://play.stuff.co.nz/details/608778ac1de1c4001a3fa09a',
        'md5': 'c82d3669e5247c64bc382577843e5bd0',
        'info_dict': {
            'id': '6250584958001',
            'ext': 'mp4',
            'title': 'Episode 1: Rotorua/Mt Maunganui/Tauranga',
            'description': 'md5:c154bafb9f0dd02d01fd4100fb1c1913',
            'uploader_id': '6005208634001',
            'timestamp': 1619491027,
            'upload_date': '20210427',
        },
        'add_ie': ['BrightcoveNew'],
    }, {
        # geo restricted, bypassable
        'url': 'https://play.stuff.co.nz/details/_6155660351001',
        'only_matching': True,
    }]
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_default/index.html?videoId=%s'

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

        webpage = self._download_webpage(url, video_id)

        state = self._parse_json(
            self._search_regex(
                r'__INITIAL_STATE__\s*=\s*({.+?})\s*;', webpage, 'state'),
            video_id)

        account_id = try_get(
            state, lambda x: x['configurations']['accountId'],
            compat_str) or '6005208634001'
        player_id = try_get(
            state, lambda x: x['configurations']['playerId'],
            compat_str) or 'default'

        entries = []
        for item_id, video in state['items'].items():
            if not isinstance(video, dict):
                continue
            asset_id = try_get(
                video, lambda x: x['content']['attributes']['assetId'],
                compat_str)
            if not asset_id:
                continue
            entries.append(self.url_result(
                smuggle_url(
                    self.BRIGHTCOVE_URL_TEMPLATE % (account_id, player_id, asset_id),
                    {'geo_countries': ['NZ']}),
                'BrightcoveNew', video_id))

        return self.playlist_result(entries, video_id)


code:
import json

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

class PlaySuisseIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?playsuisse\.ch/watch/(?P<id>[0-9]+)'
    _TESTS = [
        {
            'url': 'https://www.playsuisse.ch/watch/763211/0',
            'md5': '82df2a470b2dfa60c2d33772a8a60cf8',
            'info_dict': {
                'id': '763211',
                'ext': 'mp4',
                'title': 'Knochen',
                'description': 'md5:8ea7a8076ba000cd9e8bc132fd0afdd8',
                'duration': 3344,
                'series': 'Wilder',
                'season': 'Season 1',
                'season_number': 1,
                'episode': 'Knochen',
                'episode_number': 1,
                'thumbnail': 'md5:9260abe0c0ec9b69914d0a10d54c5878'
            }
        },
        {
            'url': 'https://www.playsuisse.ch/watch/808675/0',
            'md5': '818b94c1d2d7c4beef953f12cb8f3e75',
            'info_dict': {
                'id': '808675',
                'ext': 'mp4',
                'title': 'Der Läufer',
                'description': 'md5:9f61265c7e6dcc3e046137a792b275fd',
                'duration': 5280,
                'episode': 'Der Läufer',
                'thumbnail': 'md5:44af7d65ee02bbba4576b131868bb783'
            }
        },
        {
            'url': 'https://www.playsuisse.ch/watch/817193/0',
            'md5': '1d6c066f92cd7fffd8b28a53526d6b59',
            'info_dict': {
                'id': '817193',
                'ext': 'mp4',
                'title': 'Die Einweihungsparty',
                'description': 'md5:91ebf04d3a42cb3ab70666acf750a930',
                'duration': 1380,
                'series': 'Nr. 47',
                'season': 'Season 1',
                'season_number': 1,
                'episode': 'Die Einweihungsparty',
                'episode_number': 1,
                'thumbnail': 'md5:637585fb106e3a4bcd991958924c7e44'
            }
        }
    ]

    _GRAPHQL_QUERY = '''
        query AssetWatch($assetId: ID!) {
            assetV2(id: $assetId) {
                ...Asset
                episodes {
                    ...Asset
                }
            }
        }
        fragment Asset on AssetV2 {
            id
            name
            description
            duration
            episodeNumber
            seasonNumber
            seriesName
            medias {
                type
                url
            }
            thumbnail16x9 {
                ...ImageDetails
            }
            thumbnail2x3 {
                ...ImageDetails
            }
            thumbnail16x9WithTitle {
                ...ImageDetails
            }
            thumbnail2x3WithTitle {
                ...ImageDetails
            }
        }
        fragment ImageDetails on AssetImage {
            id
            url
        }'''

    def _get_media_data(self, media_id):
        # NOTE In the web app, the "locale" header is used to switch between languages,
        # However this doesn't seem to take effect when passing the header here.
        response = self._download_json(
            'https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql',
            media_id, data=json.dumps({
                'operationName': 'AssetWatch',
                'query': self._GRAPHQL_QUERY,
                'variables': {'assetId': media_id}
            }).encode('utf-8'),
            headers={'Content-Type': 'application/json', 'locale': 'de'})

        return response['data']['assetV2']

    def _real_extract(self, url):
        media_id = self._match_id(url)
        media_data = self._get_media_data(media_id)
        info = self._extract_single(media_data)
        if media_data.get('episodes'):
            info.update({
                '_type': 'playlist',
                'entries': map(self._extract_single, media_data['episodes']),
            })
        return info

    def _extract_single(self, media_data):
        thumbnails = traverse_obj(media_data, lambda k, _: k.startswith('thumbnail'))

        formats, subtitles = [], {}
        for media in traverse_obj(media_data, 'medias', default=[]):
            if not media.get('url') or media.get('type') != 'HLS':
                continue
            f, subs = self._extract_m3u8_formats_and_subtitles(
                media['url'], media_data['id'], 'mp4', m3u8_id='HLS', fatal=False)
            formats.extend(f)
            self._merge_subtitles(subs, target=subtitles)

        return {
            'id': media_data['id'],
            'title': media_data.get('name'),
            'description': media_data.get('description'),
            'thumbnails': thumbnails,
            'duration': int_or_none(media_data.get('duration')),
            'formats': formats,
            'subtitles': subtitles,
            'series': media_data.get('seriesName'),
            'season_number': int_or_none(media_data.get('seasonNumber')),
            'episode': media_data.get('name'),
            'episode_number': int_or_none(media_data.get('episodeNumber')),
        }


code:
from .common import InfoExtractor
from ..compat import (
    compat_urlparse,
    compat_urllib_parse_urlencode,
)
from ..utils import (
    ExtractorError,
    int_or_none,
    parse_iso8601,
    qualities,
)

class PlaytvakIE(InfoExtractor):
    IE_DESC = 'Playtvak.cz, iDNES.cz and Lidovky.cz'
    _VALID_URL = r'https?://(?:.+?\.)?(?:playtvak|idnes|lidovky|metro)\.cz/.*\?(?:c|idvideo)=(?P<id>[^&]+)'
    _TESTS = [{
        'url': 'http://www.playtvak.cz/vyzente-vosy-a-srsne-ze-zahrady-dn5-/hodinovy-manzel.aspx?c=A150730_150323_hodinovy-manzel_kuko',
        'md5': '4525ae312c324b4be2f4603cc78ceb4a',
        'info_dict': {
            'id': 'A150730_150323_hodinovy-manzel_kuko',
            'ext': 'mp4',
            'title': 'Vyžeňte vosy a sršně ze zahrady',
            'description': 'md5:4436e61b7df227a093778efb7e373571',
            'thumbnail': r're:(?i)^https?://.*\.(?:jpg|png)$',
            'duration': 279,
            'timestamp': 1438732860,
            'upload_date': '20150805',
            'is_live': False,
        }
    }, {  # live video test
        'url': 'http://slowtv.playtvak.cz/planespotting-0pr-/planespotting.aspx?c=A150624_164934_planespotting_cat',
        'info_dict': {
            'id': 'A150624_164934_planespotting_cat',
            'ext': 'flv',
            'title': 're:^Planespotting [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'description': 'Sledujte provoz na ranveji Letiště Václava Havla v Praze',
            'is_live': True,
        },
        'params': {
            'skip_download': True,  # requires rtmpdump
        },
    }, {  # another live stream, this one without Misc.videoFLV
        'url': 'https://slowtv.playtvak.cz/zive-sledujte-vlaky-v-primem-prenosu-dwi-/hlavni-nadrazi.aspx?c=A151218_145728_hlavni-nadrazi_plap',
        'info_dict': {
            'id': 'A151218_145728_hlavni-nadrazi_plap',
            'ext': 'flv',
            'title': 're:^Hlavní nádraží [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'is_live': True,
        },
        'params': {
            'skip_download': True,  # requires rtmpdump
        },
    }, {  # idnes.cz
        'url': 'http://zpravy.idnes.cz/pes-zavreny-v-aute-rozbijeni-okynek-v-aute-fj5-/domaci.aspx?c=A150809_104116_domaci_pku',
        'md5': '819832ba33cd7016e58a6658577fe289',
        'info_dict': {
            'id': 'A150809_104116_domaci_pku',
            'ext': 'mp4',
            'title': 'Zavřeli jsme mraženou pizzu do auta. Upekla se',
            'description': 'md5:01e73f02329e2e5760bd5eed4d42e3c2',
            'thumbnail': r're:(?i)^https?://.*\.(?:jpg|png)$',
            'duration': 39,
            'timestamp': 1438969140,
            'upload_date': '20150807',
            'is_live': False,
        }
    }, {  # lidovky.cz
        'url': 'http://www.lidovky.cz/dalsi-demonstrace-v-praze-o-migraci-duq-/video.aspx?c=A150808_214044_ln-video_ELE',
        'md5': 'c7209ac4ba9d234d4ad5bab7485bcee8',
        'info_dict': {
            'id': 'A150808_214044_ln-video_ELE',
            'ext': 'mp4',
            'title': 'Táhni! Demonstrace proti imigrantům budila emoce',
            'description': 'md5:97c81d589a9491fbfa323c9fa3cca72c',
            'thumbnail': r're:(?i)^https?://.*\.(?:jpg|png)$',
            'timestamp': 1439052180,
            'upload_date': '20150808',
            'is_live': False,
        }
    }, {  # metro.cz
        'url': 'http://www.metro.cz/video-pod-billboardem-se-na-vltavske-roztocil-kolotoc-deti-vozil-jen-par-hodin-1hx-/metro-extra.aspx?c=A141111_173251_metro-extra_row',
        'md5': '84fc1deedcac37b7d4a6ccae7c716668',
        'info_dict': {
            'id': 'A141111_173251_metro-extra_row',
            'ext': 'mp4',
            'title': 'Recesisté udělali z billboardu kolotoč',
            'description': 'md5:7369926049588c3989a66c9c1a043c4c',
            'thumbnail': r're:(?i)^https?://.*\.(?:jpg|png)$',
            'timestamp': 1415725500,
            'upload_date': '20141111',
            'is_live': False,
        }
    }, {
        'url': 'http://www.playtvak.cz/embed.aspx?idvideo=V150729_141549_play-porad_kuko',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, video_id)

        info_url = self._html_search_regex(
            r'Misc\.video(?:FLV)?\(\s*{\s*data\s*:\s*"([^"]+)"', webpage, 'info url')

        parsed_url = compat_urlparse.urlparse(info_url)

        qs = compat_urlparse.parse_qs(parsed_url.query)
        qs.update({
            'reklama': ['0'],
            'type': ['js'],
        })

        info_url = compat_urlparse.urlunparse(
            parsed_url._replace(query=compat_urllib_parse_urlencode(qs, True)))

        json_info = self._download_json(
            info_url, video_id,
            transform_source=lambda s: s[s.index('{'):s.rindex('}') + 1])

        item = None
        for i in json_info['items']:
            if i.get('type') == 'video' or i.get('type') == 'stream':
                item = i
                break
        if not item:
            raise ExtractorError('No suitable stream found')

        quality = qualities(('low', 'middle', 'high'))

        formats = []
        for fmt in item['video']:
            video_url = fmt.get('file')
            if not video_url:
                continue

            format_ = fmt['format']
            format_id = '%s_%s' % (format_, fmt['quality'])
            preference = None

            if format_ in ('mp4', 'webm'):
                ext = format_
            elif format_ == 'rtmp':
                ext = 'flv'
            elif format_ == 'apple':
                ext = 'mp4'
                # Some streams have mp3 audio which does not play
                # well with ffmpeg filter aac_adtstoasc
                preference = -10
            elif format_ == 'adobe':  # f4m manifest fails with 404 in 80% of requests
                continue
            else:  # Other formats not supported yet
                continue

            formats.append({
                'url': video_url,
                'ext': ext,
                'format_id': format_id,
                'quality': quality(fmt.get('quality')),
                'preference': preference,
            })
        self._sort_formats(formats)

        title = item['title']
        is_live = item['type'] == 'stream'
        description = self._og_search_description(webpage, default=None) or self._html_search_meta(
            'description', webpage, 'description', default=None)
        timestamp = None
        duration = None
        if not is_live:
            duration = int_or_none(item.get('length'))
            timestamp = item.get('published')
            if timestamp:
                timestamp = parse_iso8601(timestamp[:-5])

        return {
            'id': video_id,
            'title': title,
            'description': description,
            'thumbnail': item.get('image'),
            'duration': duration,
            'timestamp': timestamp,
            'is_live': is_live,
            'formats': formats,
        }


code:
import re
import urllib.parse

from .common import InfoExtractor
from ..compat import compat_urllib_parse_unquote
from ..utils import ExtractorError, clean_html

class PlayvidIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?playvid\.com/watch(\?v=|/)(?P<id>.+?)(?:#|$)'
    _TESTS = [{
        'url': 'http://www.playvid.com/watch/RnmBNgtrrJu',
        'md5': 'ffa2f6b2119af359f544388d8c01eb6c',
        'info_dict': {
            'id': 'RnmBNgtrrJu',
            'ext': 'mp4',
            'title': 'md5:9256d01c6317e3f703848b5906880dc8',
            'duration': 82,
            'age_limit': 18,
        },
        'skip': 'Video removed due to ToS',
    }, {
        'url': 'http://www.playvid.com/watch/hwb0GpNkzgH',
        'md5': '39d49df503ad7b8f23a4432cbf046477',
        'info_dict': {
            'id': 'hwb0GpNkzgH',
            'ext': 'mp4',
            'title': 'Ellen Euro Cutie Blond Takes a Sexy Survey Get Facial in The Park',
            'age_limit': 18,
            'thumbnail': r're:^https?://.*\.jpg$',
        },
    }]

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

        m_error = re.search(
            r'
\s*
\s*
(?P<msg>.+?)
\s*
', webpage)
        if m_error:
            raise ExtractorError(clean_html(m_error.group('msg')), expected=True)

        video_title = None
        duration = None
        video_thumbnail = None
        formats = []

        # most of the information is stored in the flashvars
        flashvars = self._html_search_regex(
            r'flashvars="(.+?)"', webpage, 'flashvars')

        infos = compat_urllib_parse_unquote(flashvars).split(r'&')
        for info in infos:
            videovars_match = re.match(r'^video_vars\[(.+?)\]=(.+?)$', info)
            if videovars_match:
                key = videovars_match.group(1)
                val = videovars_match.group(2)

                if key == 'title':
                    video_title = urllib.parse.unquote_plus(val)
                if key == 'duration':
                    try:
                        duration = int(val)
                    except ValueError:
                        pass
                if key == 'big_thumb':
                    video_thumbnail = val

                videourl_match = re.match(
                    r'^video_urls\]\[(?P<resolution>[0-9]+)p', key)
                if videourl_match:
                    height = int(videourl_match.group('resolution'))
                    formats.append({
                        'height': height,
                        'url': val,
                    })
        self._sort_formats(formats)

        # Extract title - should be in the flashvars; if not, look elsewhere
        if video_title is None:
            video_title = self._html_extract_title(webpage)

        return {
            'id': video_id,
            'formats': formats,
            'title': video_title,
            'thumbnail': video_thumbnail,
            'duration': duration,
            'description': None,
            'age_limit': 18
        }


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

class PlaywireIE(InfoExtractor):
    _VALID_URL = r'https?://(?:config|cdn)\.playwire\.com(?:/v2)?/(?P<publisher_id>\d+)/(?:videos/v2|embed|config)/(?P<id>\d+)'
    _EMBED_REGEX = [r'<script[^>]+data-config=(["\'])(?P<url>(?:https?:)?//config\.playwire\.com/.+?)\1']

    _TESTS = [{
        'url': 'http://config.playwire.com/14907/videos/v2/3353705/player.json',
        'md5': 'e6398701e3595888125729eaa2329ed9',
        'info_dict': {
            'id': '3353705',
            'ext': 'mp4',
            'title': 'S04_RM_UCL_Rus',
            'thumbnail': r're:^https?://.*\.png$',
            'duration': 145.94,
        },
    }, {
        # m3u8 in f4m
        'url': 'http://config.playwire.com/21772/videos/v2/4840492/zeus.json',
        'info_dict': {
            'id': '4840492',
            'ext': 'mp4',
            'title': 'ITV EL SHOW FULL',
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        },
    }, {
        # Multiple resolutions while bitrates missing
        'url': 'http://cdn.playwire.com/11625/embed/85228.html',
        'only_matching': True,
    }, {
        'url': 'http://config.playwire.com/12421/videos/v2/3389892/zeus.json',
        'only_matching': True,
    }, {
        'url': 'http://cdn.playwire.com/v2/12342/config/1532636.json',
        'only_matching': True,
    }]

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

        player = self._download_json(
            'http://config.playwire.com/%s/videos/v2/%s/zeus.json' % (publisher_id, video_id),
            video_id)

        title = player['settings']['title']
        duration = float_or_none(player.get('duration'), 1000)

        content = player['content']
        thumbnail = content.get('poster')
        src = content['media']['f4m']

        formats = self._extract_f4m_formats(src, video_id, m3u8_id='hls')
        for a_format in formats:
            if not dict_get(a_format, ['tbr', 'width', 'height']):
                a_format['quality'] = 1 if '-hd.' in a_format['url'] else 0
        self._sort_formats(formats)

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


code:
import collections
import json
import os
import random
import re

from .common import InfoExtractor
from ..compat import (
    compat_str,
    compat_urlparse,
)
from ..utils import (
    dict_get,
    ExtractorError,
    float_or_none,
    int_or_none,
    parse_duration,
    parse_qs,
    qualities,
    srt_subtitles_timecode,
    try_get,
    update_url_query,
    urlencode_postdata,
)

class PluralsightBaseIE(InfoExtractor):
    _API_BASE = 'https://app.pluralsight.com'

    _GRAPHQL_EP = '%s/player/api/graphql' % _API_BASE
    _GRAPHQL_HEADERS = {
        'Content-Type': 'application/json;charset=UTF-8',
    }
    _GRAPHQL_COURSE_TMPL = '''
query BootstrapPlayer {
  rpc {
    bootstrapPlayer {
      profile {
        firstName
        lastName
        email
        username
        userHandle
        authed
        isAuthed
        plan
      }
      course(courseId: "%s") {
        name
        title
        courseHasCaptions
        translationLanguages {
          code
          name
        }
        supportsWideScreenVideoFormats
        timestamp
        modules {
          name
          title
          duration
          formattedDuration
          author
          authorized
          clips {
            authorized
            clipId
            duration
            formattedDuration
            id
            index
            moduleIndex
            moduleTitle
            name
            title
            watched
          }
        }
      }
    }
  }
}'''

    def _download_course(self, course_id, url, display_id):
        try:
            return self._download_course_rpc(course_id, url, display_id)
        except ExtractorError:
            # Old API fallback
            return self._download_json(
                'https://app.pluralsight.com/player/user/api/v1/player/payload',
                display_id, data=urlencode_postdata({'courseId': course_id}),
                headers={'Referer': url})

    def _download_course_rpc(self, course_id, url, display_id):
        response = self._download_json(
            self._GRAPHQL_EP, display_id, data=json.dumps({
                'query': self._GRAPHQL_COURSE_TMPL % course_id,
                'variables': {}
            }).encode('utf-8'), headers=self._GRAPHQL_HEADERS)

        course = try_get(
            response, lambda x: x['data']['rpc']['bootstrapPlayer']['course'],
            dict)
        if course:
            return course

        raise ExtractorError(
            '%s said: %s' % (self.IE_NAME, response['error']['message']),
            expected=True)

class PluralsightIE(PluralsightBaseIE):
    IE_NAME = 'pluralsight'
    _VALID_URL = r'https?://(?:(?:www|app)\.)?pluralsight\.com/(?:training/)?player\?'
    _LOGIN_URL = 'https://app.pluralsight.com/id/'

    _NETRC_MACHINE = 'pluralsight'

    _TESTS = [{
        'url': 'http://www.pluralsight.com/training/player?author=mike-mckeown&name=hosting-sql-server-windows-azure-iaas-m7-mgmt&mode=live&clip=3&course=hosting-sql-server-windows-azure-iaas',
        'md5': '4d458cf5cf4c593788672419a8dd4cf8',
        'info_dict': {
            'id': 'hosting-sql-server-windows-azure-iaas-m7-mgmt-04',
            'ext': 'mp4',
            'title': 'Demo Monitoring',
            'duration': 338,
        },
        'skip': 'Requires pluralsight account credentials',
    }, {
        'url': 'https://app.pluralsight.com/training/player?course=angularjs-get-started&author=scott-allen&name=angularjs-get-started-m1-introduction&clip=0&mode=live',
        'only_matching': True,
    }, {
        # available without pluralsight account
        'url': 'http://app.pluralsight.com/training/player?author=scott-allen&name=angularjs-get-started-m1-introduction&mode=live&clip=0&course=angularjs-get-started',
        'only_matching': True,
    }, {
        'url': 'https://app.pluralsight.com/player?course=ccna-intro-networking&author=ross-bagurdes&name=ccna-intro-networking-m06&clip=0',
        'only_matching': True,
    }]

    GRAPHQL_VIEWCLIP_TMPL = '''
query viewClip {
  viewClip(input: {
    author: "%(author)s",
    clipIndex: %(clipIndex)d,
    courseName: "%(courseName)s",
    includeCaptions: %(includeCaptions)s,
    locale: "%(locale)s",
    mediaType: "%(mediaType)s",
    moduleName: "%(moduleName)s",
    quality: "%(quality)s"
  }) {
    urls {
      url
      cdn
      rank
      source
    },
    status
  }
}'''

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

        login_form = self._hidden_inputs(login_page)

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

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

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

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

        error = self._search_regex(
            r'<span[^>]+class="field-validation-error"[^>]*>([^<]+)</span>',
            response, 'error message', default=None)
        if error:
            raise ExtractorError('Unable to login: %s' % error, expected=True)

        if all(not re.search(p, response) for p in (
                r'__INITIAL_STATE__', r'["\']currentUser["\']',
                # new layout?
                r'>\s*Sign out\s*<')):
            BLOCKED = 'Your account has been blocked due to suspicious activity'
            if BLOCKED in response:
                raise ExtractorError(
                    'Unable to login: %s' % BLOCKED, expected=True)
            MUST_AGREE = 'To continue using Pluralsight, you must agree to'
            if any(p in response for p in (MUST_AGREE, '>Disagree<', '>Agree<')):
                raise ExtractorError(
                    'Unable to login: %s some documents. Go to pluralsight.com, '
                    'log in and agree with what Pluralsight requires.'
                    % MUST_AGREE, expected=True)

            raise ExtractorError('Unable to log in')

    def _get_subtitles(self, author, clip_idx, clip_id, lang, name, duration, video_id):
        captions = None
        if clip_id:
            captions = self._download_json(
                '%s/transcript/api/v1/caption/json/%s/%s'
                % (self._API_BASE, clip_id, lang), video_id,
                'Downloading captions JSON', 'Unable to download captions JSON',
                fatal=False)
        if not captions:
            captions_post = {
                'a': author,
                'cn': int(clip_idx),
                'lc': lang,
                'm': name,
            }
            captions = self._download_json(
                '%s/player/retrieve-captions' % self._API_BASE, video_id,
                'Downloading captions JSON', 'Unable to download captions JSON',
                fatal=False, data=json.dumps(captions_post).encode('utf-8'),
                headers={'Content-Type': 'application/json;charset=utf-8'})
        if captions:
            return {
                lang: [{
                    'ext': 'json',
                    'data': json.dumps(captions),
                }, {
                    'ext': 'srt',
                    'data': self._convert_subtitles(duration, captions),
                }]
            }

    @staticmethod
    def _convert_subtitles(duration, subs):
        srt = ''
        TIME_OFFSET_KEYS = ('displayTimeOffset', 'DisplayTimeOffset')
        TEXT_KEYS = ('text', 'Text')
        for num, current in enumerate(subs):
            current = subs[num]
            start, text = (
                float_or_none(dict_get(current, TIME_OFFSET_KEYS, skip_false_values=False)),
                dict_get(current, TEXT_KEYS))
            if start is None or text is None:
                continue
            end = duration if num == len(subs) - 1 else float_or_none(
                dict_get(subs[num + 1], TIME_OFFSET_KEYS, skip_false_values=False))
            if end is None:
                continue
            srt += os.linesep.join(
                (
                    '%d' % num,
                    '%s --> %s' % (
                        srt_subtitles_timecode(start),
                        srt_subtitles_timecode(end)),
                    text,
                    os.linesep,
                ))
        return srt

    def _real_extract(self, url):
        qs = parse_qs(url)

        author = qs.get('author', [None])[0]
        name = qs.get('name', [None])[0]
        clip_idx = qs.get('clip', [None])[0]
        course_name = qs.get('course', [None])[0]

        if any(not f for f in (author, name, clip_idx, course_name,)):
            raise ExtractorError('Invalid URL', expected=True)

        display_id = '%s-%s' % (name, clip_idx)

        course = self._download_course(course_name, url, display_id)

        collection = course['modules']

        clip = None

        for module_ in collection:
            if name in (module_.get('moduleName'), module_.get('name')):
                for clip_ in module_.get('clips', []):
                    clip_index = clip_.get('clipIndex')
                    if clip_index is None:
                        clip_index = clip_.get('index')
                    if clip_index is None:
                        continue
                    if compat_str(clip_index) == clip_idx:
                        clip = clip_
                        break

        if not clip:
            raise ExtractorError('Unable to resolve clip')

        title = clip['title']
        clip_id = clip.get('clipName') or clip.get('name') or clip['clipId']

        QUALITIES = {
            'low': {'width': 640, 'height': 480},
            'medium': {'width': 848, 'height': 640},
            'high': {'width': 1024, 'height': 768},
            'high-widescreen': {'width': 1280, 'height': 720},
        }

        QUALITIES_PREFERENCE = ('low', 'medium', 'high', 'high-widescreen',)
        quality_key = qualities(QUALITIES_PREFERENCE)

        AllowedQuality = collections.namedtuple('AllowedQuality', ['ext', 'qualities'])

        ALLOWED_QUALITIES = (
            AllowedQuality('webm', ['high', ]),
            AllowedQuality('mp4', ['low', 'medium', 'high', ]),
        )

        # Some courses also offer widescreen resolution for high quality (see
        # https://github.com/ytdl-org/youtube-dl/issues/7766)
        widescreen = course.get('supportsWideScreenVideoFormats') is True
        best_quality = 'high-widescreen' if widescreen else 'high'
        if widescreen:
            for allowed_quality in ALLOWED_QUALITIES:
                allowed_quality.qualities.append(best_quality)

        # In order to minimize the number of calls to ViewClip API and reduce
        # the probability of being throttled or banned by Pluralsight we will request
        # only single format until formats listing was explicitly requested.
        if self.get_param('listformats', False):
            allowed_qualities = ALLOWED_QUALITIES
        else:
            def guess_allowed_qualities():
                req_format = self.get_param('format') or 'best'
                req_format_split = req_format.split('-', 1)
                if len(req_format_split) > 1:
                    req_ext, req_quality = req_format_split
                    req_quality = '-'.join(req_quality.split('-')[:2])
                    for allowed_quality in ALLOWED_QUALITIES:
                        if req_ext == allowed_quality.ext and req_quality in allowed_quality.qualities:
                            return (AllowedQuality(req_ext, (req_quality, )), )
                req_ext = 'webm' if self.get_param('prefer_free_formats') else 'mp4'
                return (AllowedQuality(req_ext, (best_quality, )), )
            allowed_qualities = guess_allowed_qualities()

        formats = []
        for ext, qualities_ in allowed_qualities:
            for quality in qualities_:
                f = QUALITIES[quality].copy()
                clip_post = {
                    'author': author,
                    'includeCaptions': 'false',
                    'clipIndex': int(clip_idx),
                    'courseName': course_name,
                    'locale': 'en',
                    'moduleName': name,
                    'mediaType': ext,
                    'quality': '%dx%d' % (f['width'], f['height']),
                }
                format_id = '%s-%s' % (ext, quality)

                try:
                    viewclip = self._download_json(
                        self._GRAPHQL_EP, display_id,
                        'Downloading %s viewclip graphql' % format_id,
                        data=json.dumps({
                            'query': self.GRAPHQL_VIEWCLIP_TMPL % clip_post,
                            'variables': {}
                        }).encode('utf-8'),
                        headers=self._GRAPHQL_HEADERS)['data']['viewClip']
                except ExtractorError:
                    # Still works but most likely will go soon
                    viewclip = self._download_json(
                        '%s/video/clips/viewclip' % self._API_BASE, display_id,
                        'Downloading %s viewclip JSON' % format_id, fatal=False,
                        data=json.dumps(clip_post).encode('utf-8'),
                        headers={'Content-Type': 'application/json;charset=utf-8'})

                # Pluralsight tracks multiple sequential calls to ViewClip API and start
                # to return 429 HTTP errors after some time (see
                # https://github.com/ytdl-org/youtube-dl/pull/6989). Moreover it may even lead
                # to account ban (see https://github.com/ytdl-org/youtube-dl/issues/6842).
                # To somewhat reduce the probability of these consequences
                # we will sleep random amount of time before each call to ViewClip.
                self._sleep(
                    random.randint(5, 10), display_id,
                    '%(video_id)s: Waiting for %(timeout)s seconds to avoid throttling')

                if not viewclip:
                    continue

                clip_urls = viewclip.get('urls')
                if not isinstance(clip_urls, list):
                    continue

                for clip_url_data in clip_urls:
                    clip_url = clip_url_data.get('url')
                    if not clip_url:
                        continue
                    cdn = clip_url_data.get('cdn')
                    clip_f = f.copy()
                    clip_f.update({
                        'url': clip_url,
                        'ext': ext,
                        'format_id': '%s-%s' % (format_id, cdn) if cdn else format_id,
                        'quality': quality_key(quality),
                        'source_preference': int_or_none(clip_url_data.get('rank')),
                    })
                    formats.append(clip_f)

        self._sort_formats(formats)

        duration = int_or_none(
            clip.get('duration')) or parse_duration(clip.get('formattedDuration'))

        # TODO: other languages?
        subtitles = self.extract_subtitles(
            author, clip_idx, clip.get('clipId'), 'en', name, duration, display_id)

        return {
            'id': clip_id,
            'title': title,
            'duration': duration,
            'creator': author,
            'formats': formats,
            'subtitles': subtitles,
        }

class PluralsightCourseIE(PluralsightBaseIE):
    IE_NAME = 'pluralsight:course'
    _VALID_URL = r'https?://(?:(?:www|app)\.)?pluralsight\.com/(?:library/)?courses/(?P<id>[^/]+)'
    _TESTS = [{
        # Free course from Pluralsight Starter Subscription for Microsoft TechNet
        # https://offers.pluralsight.com/technet?loc=zTS3z&prod=zOTprodz&tech=zOttechz&prog=zOTprogz&type=zSOz&media=zOTmediaz&country=zUSz
        'url': 'http://www.pluralsight.com/courses/hosting-sql-server-windows-azure-iaas',
        'info_dict': {
            'id': 'hosting-sql-server-windows-azure-iaas',
            'title': 'Hosting SQL Server in Microsoft Azure IaaS Fundamentals',
            'description': 'md5:61b37e60f21c4b2f91dc621a977d0986',
        },
        'playlist_count': 31,
    }, {
        # available without pluralsight account
        'url': 'https://www.pluralsight.com/courses/angularjs-get-started',
        'only_matching': True,
    }, {
        'url': 'https://app.pluralsight.com/library/courses/understanding-microsoft-azure-amazon-aws/table-of-contents',
        'only_matching': True,
    }]

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

        # TODO: PSM cookie

        course = self._download_course(course_id, url, course_id)

        title = course['title']
        course_name = course['name']
        course_data = course['modules']
        description = course.get('description') or course.get('shortDescription')

        entries = []
        for num, module in enumerate(course_data, 1):
            author = module.get('author')
            module_name = module.get('name')
            if not author or not module_name:
                continue
            for clip in module.get('clips', []):
                clip_index = int_or_none(clip.get('index'))
                if clip_index is None:
                    continue
                clip_url = update_url_query(
                    '%s/player' % self._API_BASE, query={
                        'mode': 'live',
                        'course': course_name,
                        'author': author,
                        'name': module_name,
                        'clip': clip_index,
                    })
                entries.append({
                    '_type': 'url_transparent',
                    'url': clip_url,
                    'ie_key': PluralsightIE.ie_key(),
                    'chapter': module.get('title'),
                    'chapter_number': num,
                    'chapter_id': module.get('moduleRef'),
                })

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


code:
import re
import uuid

from .common import InfoExtractor
from ..compat import (
    compat_str,
    compat_urlparse,
)
from ..utils import (
    ExtractorError,
    float_or_none,
    int_or_none,
    try_get,
    url_or_none,
)

class PlutoTVIE(InfoExtractor):
    _VALID_URL = r'''(?x)
        https?://(?:www\.)?pluto\.tv(?:/[^/]+)?/on-demand
        /(?P<video_type>movies|series)
        /(?P<series_or_movie_slug>[^/]+)
        (?:
            (?:/seasons?/(?P<season_no>\d+))?
            (?:/episode/(?P<episode_slug>[^/]+))?
        )?
        /?(?:$|[#?])'''

    _INFO_URL = 'https://service-vod.clusters.pluto.tv/v3/vod/slugs/'
    _INFO_QUERY_PARAMS = {
        'appName': 'web',
        'appVersion': 'na',
        'clientID': compat_str(uuid.uuid1()),
        'clientModelNumber': 'na',
        'serverSideAds': 'false',
        'deviceMake': 'unknown',
        'deviceModel': 'web',
        'deviceType': 'web',
        'deviceVersion': 'unknown',
        'sid': compat_str(uuid.uuid1()),
    }
    _TESTS = [
        {
            'url': 'https://pluto.tv/on-demand/series/i-love-money/season/2/episode/its-in-the-cards-2009-2-3',
            'md5': 'ebcdd8ed89aaace9df37924f722fd9bd',
            'info_dict': {
                'id': '5de6c598e9379ae4912df0a8',
                'ext': 'mp4',
                'title': 'It\'s In The Cards',
                'episode': 'It\'s In The Cards',
                'description': 'The teams face off against each other in a 3-on-2 soccer showdown.  Strategy comes into play, though, as each team gets to select their opposing teams’ two defenders.',
                'series': 'I Love Money',
                'season_number': 2,
                'episode_number': 3,
                'duration': 3600,
            }
        }, {
            'url': 'https://pluto.tv/on-demand/series/i-love-money/season/1/',
            'playlist_count': 11,
            'info_dict': {
                'id': '5de6c582e9379ae4912dedbd',
                'title': 'I Love Money - Season 1',
            }
        }, {
            'url': 'https://pluto.tv/on-demand/series/i-love-money/',
            'playlist_count': 26,
            'info_dict': {
                'id': '5de6c582e9379ae4912dedbd',
                'title': 'I Love Money',
            }
        }, {
            'url': 'https://pluto.tv/on-demand/movies/arrival-2015-1-1',
            'md5': '3cead001d317a018bf856a896dee1762',
            'info_dict': {
                'id': '5e83ac701fa6a9001bb9df24',
                'ext': 'mp4',
                'title': 'Arrival',
                'description': 'When mysterious spacecraft touch down across the globe, an elite team - led by expert translator Louise Banks (Academy Award® nominee Amy Adams) – races against time to decipher their intent.',
                'duration': 9000,
            }
        }, {
            'url': 'https://pluto.tv/en/on-demand/series/manhunters-fugitive-task-force/seasons/1/episode/third-times-the-charm-1-1',
            'only_matching': True,
        }, {
            'url': 'https://pluto.tv/it/on-demand/series/csi-vegas/episode/legacy-2021-1-1',
            'only_matching': True,
        }
    ]

    def _to_ad_free_formats(self, video_id, formats, subtitles):
        ad_free_formats, ad_free_subtitles, m3u8_urls = [], {}, set()
        for fmt in formats:
            res = self._download_webpage(
                fmt.get('url'), video_id, note='Downloading m3u8 playlist',
                fatal=False)
            if not res:
                continue
            first_segment_url = re.search(
                r'^(https?://.*/)0\-(end|[0-9]+)/[^/]+\.ts$', res,
                re.MULTILINE)
            if first_segment_url:
                m3u8_urls.add(
                    compat_urlparse.urljoin(first_segment_url.group(1), '0-end/master.m3u8'))
                continue
            first_segment_url = re.search(
                r'^(https?://.*/).+\-0+\.ts$', res,
                re.MULTILINE)
            if first_segment_url:
                m3u8_urls.add(
                    compat_urlparse.urljoin(first_segment_url.group(1), 'master.m3u8'))
                continue

        for m3u8_url in m3u8_urls:
            fmts, subs = self._extract_m3u8_formats_and_subtitles(
                m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
            ad_free_formats.extend(fmts)
            ad_free_subtitles = self._merge_subtitles(ad_free_subtitles, subs)
        if ad_free_formats:
            formats, subtitles = ad_free_formats, ad_free_subtitles
        else:
            self.report_warning('Unable to find ad-free formats')
        return formats, subtitles

    def _get_video_info(self, video_json, slug, series_name=None):
        video_id = video_json.get('_id', slug)
        formats, subtitles = [], {}
        for video_url in try_get(video_json, lambda x: x['stitched']['urls'], list) or []:
            if video_url.get('type') != 'hls':
                continue
            url = url_or_none(video_url.get('url'))

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

        formats, subtitles = self._to_ad_free_formats(video_id, formats, subtitles)
        self._sort_formats(formats)

        info = {
            'id': video_id,
            'formats': formats,
            'subtitles': subtitles,
            'title': video_json.get('name'),
            'description': video_json.get('description'),
            'duration': float_or_none(video_json.get('duration'), scale=1000),
        }
        if series_name:
            info.update({
                'series': series_name,
                'episode': video_json.get('name'),
                'season_number': int_or_none(video_json.get('season')),
                'episode_number': int_or_none(video_json.get('number')),
            })
        return info

    def _real_extract(self, url):
        mobj = self._match_valid_url(url).groupdict()
        info_slug = mobj['series_or_movie_slug']
        video_json = self._download_json(self._INFO_URL + info_slug, info_slug, query=self._INFO_QUERY_PARAMS)

        if mobj['video_type'] == 'series':
            series_name = video_json.get('name', info_slug)
            season_number, episode_slug = mobj.get('season_number'), mobj.get('episode_slug')

            videos = []
            for season in video_json['seasons']:
                if season_number is not None and season_number != int_or_none(season.get('number')):
                    continue
                for episode in season['episodes']:
                    if episode_slug is not None and episode_slug != episode.get('slug'):
                        continue
                    videos.append(self._get_video_info(episode, episode_slug, series_name))
            if not videos:
                raise ExtractorError('Failed to find any videos to extract')
            if episode_slug is not None and len(videos) == 1:
                return videos[0]
            playlist_title = series_name
            if season_number is not None:
                playlist_title += ' - Season %d' % season_number
            return self.playlist_result(videos,
                                        playlist_id=video_json.get('_id', info_slug),
                                        playlist_title=playlist_title)
        return self._get_video_info(video_json, info_slug)


code:
import functools
import json

from .common import InfoExtractor
from ..utils import (
    OnDemandPagedList,
    float_or_none,
    str_or_none,
    str_to_int,
    traverse_obj,
    unified_timestamp,
)

class PodchaserIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?podchaser\.com/podcasts/[\w-]+-(?P<podcast_id>\d+)(?:/episodes/[\w-]+-(?P<id>\d+))?'
    _PAGE_SIZE = 100
    _TESTS = [{
        'url': 'https://www.podchaser.com/podcasts/cum-town-36924/episodes/ep-285-freeze-me-off-104365585',
        'info_dict': {
            'id': '104365585',
            'title': 'Ep. 285 – freeze me off',
            'description': 'cam ahn',
            'thumbnail': r're:^https?://.*\.jpg$',
            'ext': 'mp3',
            'categories': ['Comedy'],
            'tags': ['comedy', 'dark humor'],
            'series': 'Cum Town',
            'duration': 3708,
            'timestamp': 1636531259,
            'upload_date': '20211110',
            'rating': 4.0
        }
    }, {
        'url': 'https://www.podchaser.com/podcasts/the-bone-zone-28853',
        'info_dict': {
            'id': '28853',
            'title': 'The Bone Zone',
            'description': 'Podcast by The Bone Zone',
        },
        'playlist_count': 275
    }, {
        'url': 'https://www.podchaser.com/podcasts/sean-carrolls-mindscape-scienc-699349/episodes',
        'info_dict': {
            'id': '699349',
            'title': 'Sean Carroll\'s Mindscape: Science, Society, Philosophy, Culture, Arts, and Ideas',
            'description': 'md5:2cbd8f4749891a84dc8235342e0b5ff1'
        },
        'playlist_mincount': 225
    }]

    @staticmethod
    def _parse_episode(episode, podcast):
        return {
            'id': str(episode.get('id')),
            'title': episode.get('title'),
            'description': episode.get('description'),
            'url': episode.get('audio_url'),
            'thumbnail': episode.get('image_url'),
            'duration': str_to_int(episode.get('length')),
            'timestamp': unified_timestamp(episode.get('air_date')),
            'rating': float_or_none(episode.get('rating')),
            'categories': list(set(traverse_obj(podcast, (('summary', None), 'categories', ..., 'text')))),
            'tags': traverse_obj(podcast, ('tags', ..., 'text')),
            'series': podcast.get('title'),
        }

    def _call_api(self, path, *args, **kwargs):
        return self._download_json(f'https://api.podchaser.com/{path}', *args, **kwargs)

    def _fetch_page(self, podcast_id, podcast, page):
        json_response = self._call_api(
            'list/episode', podcast_id,
            headers={'Content-Type': 'application/json;charset=utf-8'},
            data=json.dumps({
                'start': page * self._PAGE_SIZE,
                'count': self._PAGE_SIZE,
                'sort_order': 'SORT_ORDER_RECENT',
                'filters': {
                    'podcast_id': podcast_id
                },
                'options': {}
            }).encode())

        for episode in json_response['entities']:
            yield self._parse_episode(episode, podcast)

    def _real_extract(self, url):
        podcast_id, episode_id = self._match_valid_url(url).group('podcast_id', 'id')
        podcast = self._call_api(f'podcasts/{podcast_id}', episode_id or podcast_id)
        if not episode_id:
            return self.playlist_result(
                OnDemandPagedList(functools.partial(self._fetch_page, podcast_id, podcast), self._PAGE_SIZE),
                str_or_none(podcast.get('id')), podcast.get('title'), podcast.get('description'))

        episode = self._call_api(f'episodes/{episode_id}', episode_id)
        return self._parse_episode(episode, podcast)