code:
import re

from .common import InfoExtractor
from ..utils import (
    parse_resolution,
    str_to_int,
    unified_strdate,
    urlencode_postdata,
    urljoin,
)

class RadioJavanIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?radiojavan\.com/videos/video/(?P<id>[^/]+)/?'
    _TEST = {
        'url': 'http://www.radiojavan.com/videos/video/chaartaar-ashoobam',
        'md5': 'e85208ffa3ca8b83534fca9fe19af95b',
        'info_dict': {
            'id': 'chaartaar-ashoobam',
            'ext': 'mp4',
            'title': 'Chaartaar - Ashoobam',
            'thumbnail': r're:^https?://.*\.jpe?g$',
            'upload_date': '20150215',
            'view_count': int,
            'like_count': int,
            'dislike_count': int,
        }
    }

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

        download_host = self._download_json(
            'https://www.radiojavan.com/videos/video_host', video_id,
            data=urlencode_postdata({'id': video_id}),
            headers={
                'Content-Type': 'application/x-www-form-urlencoded',
                'Referer': url,
            }).get('host', 'https://host1.rjmusicmedia.com')

        webpage = self._download_webpage(url, video_id)

        formats = []
        for format_id, _, video_path in re.findall(
                r'RJ\.video(?P<format_id>\d+[pPkK])\s*=\s*(["\'])(?P<url>(?:(?!\2).)+)\2',
                webpage):
            f = parse_resolution(format_id)
            f.update({
                'url': urljoin(download_host, video_path),
                'format_id': format_id,
            })
            formats.append(f)
        self._sort_formats(formats)

        title = self._og_search_title(webpage)
        thumbnail = self._og_search_thumbnail(webpage)

        upload_date = unified_strdate(self._search_regex(
            r'class="date_added">Date added: ([^<]+)<',
            webpage, 'upload date', fatal=False))

        view_count = str_to_int(self._search_regex(
            r'class="views">Plays: ([\d,]+)',
            webpage, 'view count', fatal=False))
        like_count = str_to_int(self._search_regex(
            r'class="rating">([\d,]+) likes',
            webpage, 'like count', fatal=False))
        dislike_count = str_to_int(self._search_regex(
            r'class="rating">([\d,]+) dislikes',
            webpage, 'dislike count', fatal=False))

        return {
            'id': video_id,
            'title': title,
            'thumbnail': thumbnail,
            'upload_date': upload_date,
            'view_count': view_count,
            'like_count': like_count,
            'dislike_count': dislike_count,
            'formats': formats,
        }


code:
from .common import InfoExtractor
from ..utils import (
    clean_html,
    traverse_obj,
    unescapeHTML,
)

import itertools
from urllib.parse import urlencode

class RadioKapitalBaseIE(InfoExtractor):
    def _call_api(self, resource, video_id, note='Downloading JSON metadata', qs={}):
        return self._download_json(
            f'https://www.radiokapital.pl/wp-json/kapital/v1/{resource}?{urlencode(qs)}',
            video_id, note=note)

    def _parse_episode(self, data):
        release = '%s%s%s' % (data['published'][6:11], data['published'][3:6], data['published'][:3])
        return {
            '_type': 'url_transparent',
            'url': data['mixcloud_url'],
            'ie_key': 'Mixcloud',
            'title': unescapeHTML(data['title']),
            'description': clean_html(data.get('content')),
            'tags': traverse_obj(data, ('tags', ..., 'name')),
            'release_date': release,
            'series': traverse_obj(data, ('show', 'title')),
        }

class RadioKapitalIE(RadioKapitalBaseIE):
    IE_NAME = 'radiokapital'
    _VALID_URL = r'https?://(?:www\.)?radiokapital\.pl/shows/[a-z\d-]+/(?P<id>[a-z\d-]+)'

    _TESTS = [{
        'url': 'https://radiokapital.pl/shows/tutaj-sa-smoki/5-its-okay-to-be-immaterial',
        'info_dict': {
            'id': 'radiokapital_radio-kapitał-tutaj-są-smoki-5-its-okay-to-be-immaterial-2021-05-20',
            'ext': 'm4a',
            'title': '#5: It’s okay to\xa0be\xa0immaterial',
            'description': 'md5:2499da5fbfb0e88333b7d37ec8e9e4c4',
            'uploader': 'Radio Kapitał',
            'uploader_id': 'radiokapital',
            'timestamp': 1621640164,
            'upload_date': '20210521',
        },
    }]

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

        episode = self._call_api('episodes/%s' % video_id, video_id)
        return self._parse_episode(episode)

class RadioKapitalShowIE(RadioKapitalBaseIE):
    IE_NAME = 'radiokapital:show'
    _VALID_URL = r'https?://(?:www\.)?radiokapital\.pl/shows/(?P<id>[a-z\d-]+)/?(?:$|[?#])'

    _TESTS = [{
        'url': 'https://radiokapital.pl/shows/wesz',
        'info_dict': {
            'id': '100',
            'title': 'WĘSZ',
            'description': 'md5:3a557a1e0f31af612b0dcc85b1e0ca5c',
        },
        'playlist_mincount': 17,
    }]

    def _get_episode_list(self, series_id, page_no):
        return self._call_api(
            'episodes', series_id,
            f'Downloading episode list page #{page_no}', qs={
                'show': series_id,
                'page': page_no,
            })

    def _entries(self, series_id):
        for page_no in itertools.count(1):
            episode_list = self._get_episode_list(series_id, page_no)
            yield from (self._parse_episode(ep) for ep in episode_list['items'])
            if episode_list['next'] is None:
                break

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

        show = self._call_api(f'shows/{series_id}', series_id, 'Downloading show metadata')
        entries = self._entries(series_id)
        return {
            '_type': 'playlist',
            'entries': entries,
            'id': str(show['id']),
            'title': show.get('title'),
            'description': clean_html(show.get('content')),
        }

repo_name: Lesmiscore/ytdl


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

class RadioZetPodcastIE(InfoExtractor):
    _VALID_URL = r'https?://player\.radiozet\.pl\/Podcasty/.*?/(?P<id>.+)'
    _TEST = {
        'url': 'https://player.radiozet.pl/Podcasty/Nie-Ma-Za-Co/O-przedmiotach-szkolnych-ktore-przydaja-sie-w-zyciu',
        'md5': 'e03665c316b4fbc5f6a8f232948bbba3',
        'info_dict': {
            'id': '42154',
            'display_id': 'O-przedmiotach-szkolnych-ktore-przydaja-sie-w-zyciu',
            'title': 'O przedmiotach szkolnych, które przydają się w życiu',
            'description': 'md5:fa72bed49da334b09e5b2f79851f185c',
            'release_timestamp': 1592985480,
            'ext': 'mp3',
            'thumbnail': r're:^https?://.*\.png$',
            'duration': 83,
            'series': 'Nie Ma Za Co',
            'creator': 'Katarzyna Pakosińska',
        }
    }

    def _call_api(self, podcast_id, display_id):
        return self._download_json(
            f'https://player.radiozet.pl/api/podcasts/getPodcast/(node)/{podcast_id}/(station)/radiozet',
            display_id)

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        podcast_id = self._html_search_regex(r'<div.*?\sid="player".*?\sdata-id=[\'"]([^\'"]+)[\'"]',
                                             webpage, 'podcast id')
        data = self._call_api(podcast_id, display_id)['data'][0]

        return {
            'id': podcast_id,
            'display_id': display_id,
            'title': strip_or_none(data.get('title')),
            'description': strip_or_none(traverse_obj(data, ('program', 'desc'))),
            'release_timestamp': data.get('published_date'),
            'url': traverse_obj(data, ('player', 'stream')),
            'thumbnail': traverse_obj(data, ('program', 'image', 'original')),
            'duration': traverse_obj(data, ('player', 'duration')),
            'series': strip_or_none(traverse_obj(data, ('program', 'title'))),
            'creator': strip_or_none(traverse_obj(data, ('presenter', 0, 'title'))),
        }


code:
import json

from ..utils import (
    ExtractorError,
    format_field,
    traverse_obj,
    try_get,
    unified_timestamp
)
from .common import InfoExtractor

class RadLiveIE(InfoExtractor):
    IE_NAME = 'radlive'
    _VALID_URL = r'https?://(?:www\.)?rad\.live/content/(?P<content_type>feature|episode)/(?P<id>[a-f0-9-]+)'
    _TESTS = [{
        'url': 'https://rad.live/content/feature/dc5acfbc-761b-4bec-9564-df999905116a',
        'md5': '6219d5d31d52de87d21c9cf5b7cb27ff',
        'info_dict': {
            'id': 'dc5acfbc-761b-4bec-9564-df999905116a',
            'ext': 'mp4',
            'title': 'Deathpact - Digital Mirage 2 [Full Set]',
            'language': 'en',
            'thumbnail': 'https://static.12core.net/cb65ae077a079c68380e38f387fbc438.png',
            'description': '',
            'release_timestamp': 1600185600.0,
            'channel': 'Proximity',
            'channel_id': '9ce6dd01-70a4-4d59-afb6-d01f807cd009',
            'channel_url': 'https://rad.live/content/channel/9ce6dd01-70a4-4d59-afb6-d01f807cd009',
        }
    }, {
        'url': 'https://rad.live/content/episode/bbcf66ec-0d02-4ca0-8dc0-4213eb2429bf',
        'md5': '40b2175f347592125d93e9a344080125',
        'info_dict': {
            'id': 'bbcf66ec-0d02-4ca0-8dc0-4213eb2429bf',
            'ext': 'mp4',
            'title': 'E01: Bad Jokes 1',
            'language': 'en',
            'thumbnail': 'https://lsp.littlstar.com/channels/WHISTLE/BAD_JOKES/SEASON_1/BAD_JOKES_101/poster.jpg',
            'description': 'Bad Jokes - Champions, Adam Pally, Super Troopers, Team Edge and 2Hype',
            'release_timestamp': None,
            'channel': None,
            'channel_id': None,
            'channel_url': None,
            'episode': 'E01: Bad Jokes 1',
            'episode_number': 1,
            'episode_id': '336',
        },
    }]

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

        webpage = self._download_webpage(url, video_id)

        content_info = json.loads(self._search_regex(
            r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
            webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
        video_info = content_info[content_type]

        if not video_info:
            raise ExtractorError('Unable to extract video info, make sure the URL is valid')

        formats = self._extract_m3u8_formats(video_info['assets']['videos'][0]['url'], video_id)
        self._sort_formats(formats)

        data = video_info.get('structured_data', {})

        release_date = unified_timestamp(traverse_obj(data, ('releasedEvent', 'startDate')))
        channel = next(iter(content_info.get('channels', [])), {})
        channel_id = channel.get('lrn', '').split(':')[-1] or None

        result = {
            'id': video_id,
            'title': video_info['title'],
            'formats': formats,
            'language': traverse_obj(data, ('potentialAction', 'target', 'inLanguage')),
            'thumbnail': traverse_obj(data, ('image', 'contentUrl')),
            'description': data.get('description'),
            'release_timestamp': release_date,
            'channel': channel.get('name'),
            'channel_id': channel_id,
            'channel_url': format_field(channel_id, None, 'https://rad.live/content/channel/%s'),

        }
        if content_type == 'episode':
            result.update({
                # TODO: Get season number when downloading single episode
                'episode': video_info.get('title'),
                'episode_number': video_info.get('number'),
                'episode_id': video_info.get('id'),
            })

        return result

class RadLiveSeasonIE(RadLiveIE):
    IE_NAME = 'radlive:season'
    _VALID_URL = r'https?://(?:www\.)?rad\.live/content/season/(?P<id>[a-f0-9-]+)'
    _TESTS = [{
        'url': 'https://rad.live/content/season/08a290f7-c9ef-4e22-9105-c255995a2e75',
        'md5': '40b2175f347592125d93e9a344080125',
        'info_dict': {
            'id': '08a290f7-c9ef-4e22-9105-c255995a2e75',
            'title': 'Bad Jokes - Season 1',
        },
        'playlist_mincount': 5,
    }]

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

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

        content_info = json.loads(self._search_regex(
            r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
            webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
        video_info = content_info['season']

        entries = [{
            '_type': 'url_transparent',
            'id': episode['structured_data']['url'].split('/')[-1],
            'url': episode['structured_data']['url'],
            'series': try_get(content_info, lambda x: x['series']['title']),
            'season': video_info['title'],
            'season_number': video_info.get('number'),
            'season_id': video_info.get('id'),
            'ie_key': RadLiveIE.ie_key(),
        } for episode in video_info['episodes']]

        return self.playlist_result(entries, season_id, video_info.get('title'))

class RadLiveChannelIE(RadLiveIE):
    IE_NAME = 'radlive:channel'
    _VALID_URL = r'https?://(?:www\.)?rad\.live/content/channel/(?P<id>[a-f0-9-]+)'
    _TESTS = [{
        'url': 'https://rad.live/content/channel/5c4d8df4-6fa0-413c-81e3-873479b49274',
        'md5': '625156a08b7f2b0b849f234e664457ac',
        'info_dict': {
            'id': '5c4d8df4-6fa0-413c-81e3-873479b49274',
            'title': 'Whistle Sports',
        },
        'playlist_mincount': 7,
    }]

    _QUERY = '''
query WebChannelListing ($lrn: ID!) {
  channel (id:$lrn) {
    name
    features {
      structured_data
    }
  }
}'''

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

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

        graphql = self._download_json(
            'https://content.mhq.12core.net/graphql', channel_id,
            headers={'Content-Type': 'application/json'},
            data=json.dumps({
                'query': self._QUERY,
                'variables': {'lrn': f'lrn:12core:media:content:channel:{channel_id}'}
            }).encode('utf-8'))

        data = traverse_obj(graphql, ('data', 'channel'))
        if not data:
            raise ExtractorError('Unable to extract video info, make sure the URL is valid')

        entries = [{
            '_type': 'url_transparent',
            'url': feature['structured_data']['url'],
            'ie_key': RadLiveIE.ie_key(),
        } for feature in data['features']]

        return self.playlist_result(entries, channel_id, data.get('name'))


code:
import re

from .common import InfoExtractor
from ..compat import (
    compat_str,
    compat_urlparse,
)
from ..utils import (
    clean_html,
    determine_ext,
    ExtractorError,
    filter_dict,
    find_xpath_attr,
    fix_xml_ampersands,
    GeoRestrictedError,
    HEADRequest,
    int_or_none,
    join_nonempty,
    parse_duration,
    remove_start,
    strip_or_none,
    traverse_obj,
    try_get,
    unified_strdate,
    unified_timestamp,
    update_url_query,
    urljoin,
    xpath_text,
)

class RaiBaseIE(InfoExtractor):
    _UUID_RE = r'[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}'
    _GEO_COUNTRIES = ['IT']
    _GEO_BYPASS = False

    def _extract_relinker_info(self, relinker_url, video_id, audio_only=False):
        if not re.match(r'https?://', relinker_url):
            return {'formats': [{'url': relinker_url}]}

        formats = []
        geoprotection = None
        is_live = None
        duration = None

        for platform in ('mon', 'flash', 'native'):
            relinker = self._download_xml(
                relinker_url, video_id,
                note=f'Downloading XML metadata for platform {platform}',
                transform_source=fix_xml_ampersands,
                query={'output': 45, 'pl': platform},
                headers=self.geo_verification_headers())

            if xpath_text(relinker, './license_url', default='{}') != '{}':
                self.report_drm(video_id)

            if not geoprotection:
                geoprotection = xpath_text(
                    relinker, './geoprotection', default=None) == 'Y'

            if not is_live:
                is_live = xpath_text(
                    relinker, './is_live', default=None) == 'Y'
            if not duration:
                duration = parse_duration(xpath_text(
                    relinker, './duration', default=None))

            url_elem = find_xpath_attr(relinker, './url', 'type', 'content')
            if url_elem is None:
                continue

            media_url = url_elem.text

            # This does not imply geo restriction (e.g.
            # http://www.raisport.rai.it/dl/raiSport/media/rassegna-stampa-04a9f4bd-b563-40cf-82a6-aad3529cb4a9.html)
            if '/video_no_available.mp4' in media_url:
                continue

            ext = determine_ext(media_url)
            if (ext == 'm3u8' and platform != 'mon') or (ext == 'f4m' and platform != 'flash'):
                continue

            if ext == 'mp3':
                formats.append({
                    'url': media_url,
                    'vcodec': 'none',
                    'acodec': 'mp3',
                    'format_id': 'http-mp3',
                })
                break
            elif ext == 'm3u8' or 'format=m3u8' in media_url or platform == 'mon':
                formats.extend(self._extract_m3u8_formats(
                    media_url, video_id, 'mp4', 'm3u8_native',
                    m3u8_id='hls', fatal=False))
            elif ext == 'f4m' or platform == 'flash':
                manifest_url = update_url_query(
                    media_url.replace('manifest#live_hds.f4m', 'manifest.f4m'),
                    {'hdcore': '3.7.0', 'plugin': 'aasp-3.7.0.39.44'})
                formats.extend(self._extract_f4m_formats(
                    manifest_url, video_id, f4m_id='hds', fatal=False))
            else:
                bitrate = int_or_none(xpath_text(relinker, 'bitrate'))
                formats.append({
                    'url': media_url,
                    'tbr': bitrate if bitrate > 0 else None,
                    'format_id': f'http-{bitrate if bitrate > 0 else "http"}',
                })

        if not formats and geoprotection is True:
            self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)

        if not audio_only:
            formats.extend(self._create_http_urls(relinker_url, formats))

        return filter_dict({
            'is_live': is_live,
            'duration': duration,
            'formats': formats,
        })

    def _create_http_urls(self, relinker_url, fmts):
        _RELINKER_REG = r'https?://(?P<host>[^/]+?)/(?:i/)?(?P<extra>[^/]+?)/(?P<path>.+?)/(?P<id>\w+)(?:_(?P<quality>[\d\,]+))?(?:\.mp4|/playlist\.m3u8).+?'
        _MP4_TMPL = '%s&overrideUserAgentRule=mp4-%s'
        _QUALITY = {
            # tbr: w, h
            '250': [352, 198],
            '400': [512, 288],
            '700': [512, 288],
            '800': [700, 394],
            '1200': [736, 414],
            '1800': [1024, 576],
            '2400': [1280, 720],
            '3200': [1440, 810],
            '3600': [1440, 810],
            '5000': [1920, 1080],
            '10000': [1920, 1080],
        }

        def test_url(url):
            resp = self._request_webpage(
                HEADRequest(url), None, headers={'User-Agent': 'Rai'},
                fatal=False, errnote=False, note=False)

            if resp is False:
                return False

            if resp.code == 200:
                return False if resp.url == url else resp.url
            return None

        # filter out audio-only formats
        fmts = [f for f in fmts if not f.get('vcodec') == 'none']

        def get_format_info(tbr):
            import math
            br = int_or_none(tbr)
            if len(fmts) == 1 and not br:
                br = fmts[0].get('tbr')
            if br and br > 300:
                tbr = compat_str(math.floor(br / 100) * 100)
            else:
                tbr = '250'

            # try extracting info from available m3u8 formats
            format_copy = None
            for f in fmts:
                if f.get('tbr'):
                    br_limit = math.floor(br / 100)
                    if br_limit - 1 <= math.floor(f['tbr'] / 100) <= br_limit + 1:
                        format_copy = f.copy()
            return {
                'width': format_copy.get('width'),
                'height': format_copy.get('height'),
                'tbr': format_copy.get('tbr'),
                'vcodec': format_copy.get('vcodec'),
                'acodec': format_copy.get('acodec'),
                'fps': format_copy.get('fps'),
                'format_id': f'https-{tbr}',
            } if format_copy else {
                'width': _QUALITY[tbr][0],
                'height': _QUALITY[tbr][1],
                'format_id': f'https-{tbr}',
                'tbr': int(tbr),
            }

        loc = test_url(_MP4_TMPL % (relinker_url, '*'))
        if not isinstance(loc, compat_str):
            return []

        mobj = re.match(
            _RELINKER_REG,
            test_url(relinker_url) or '')
        if not mobj:
            return []

        available_qualities = mobj.group('quality').split(',') if mobj.group('quality') else ['*']
        available_qualities = [i for i in available_qualities if i]

        formats = []
        for q in available_qualities:
            fmt = {
                'url': _MP4_TMPL % (relinker_url, q),
                'protocol': 'https',
                'ext': 'mp4',
                **get_format_info(q)
            }
            formats.append(fmt)
        return formats

    @staticmethod
    def _extract_subtitles(url, video_data):
        STL_EXT = 'stl'
        SRT_EXT = 'srt'
        subtitles = {}
        subtitles_array = video_data.get('subtitlesArray') or []
        for k in ('subtitles', 'subtitlesUrl'):
            subtitles_array.append({'url': video_data.get(k)})
        for subtitle in subtitles_array:
            sub_url = subtitle.get('url')
            if sub_url and isinstance(sub_url, compat_str):
                sub_lang = subtitle.get('language') or 'it'
                sub_url = urljoin(url, sub_url)
                sub_ext = determine_ext(sub_url, SRT_EXT)
                subtitles.setdefault(sub_lang, []).append({
                    'ext': sub_ext,
                    'url': sub_url,
                })
                if STL_EXT == sub_ext:
                    subtitles[sub_lang].append({
                        'ext': SRT_EXT,
                        'url': sub_url[:-len(STL_EXT)] + SRT_EXT,
                    })
        return subtitles

class RaiPlayIE(RaiBaseIE):
    _VALID_URL = rf'(?P<base>https?://(?:www\.)?raiplay\.it/.+?-(?P<id>{RaiBaseIE._UUID_RE}))\.(?:html|json)'
    _TESTS = [{
        'url': 'http://www.raiplay.it/video/2014/04/Report-del-07042014-cb27157f-9dd0-4aee-b788-b1f67643a391.html',
        'md5': '8970abf8caf8aef4696e7b1f2adfc696',
        'info_dict': {
            'id': 'cb27157f-9dd0-4aee-b788-b1f67643a391',
            'ext': 'mp4',
            'title': 'Report del 07/04/2014',
            'alt_title': 'St 2013/14 - Report - Espresso nel caffè - 07/04/2014',
            'description': 'md5:d730c168a58f4bb35600fc2f881ec04e',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'Rai Gulp',
            'duration': 6160,
            'series': 'Report',
            'season': '2013/14',
            'subtitles': {
                'it': 'count:4',
            },
            'release_year': 2022,
            'episode': 'Espresso nel caffè - 07/04/2014',
            'timestamp': 1396919880,
            'upload_date': '20140408',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # 1080p direct mp4 url
        'url': 'https://www.raiplay.it/video/2021/11/Blanca-S1E1-Senza-occhi-b1255a4a-8e72-4a2f-b9f3-fc1308e00736.html',
        'md5': 'aeda7243115380b2dd5e881fd42d949a',
        'info_dict': {
            'id': 'b1255a4a-8e72-4a2f-b9f3-fc1308e00736',
            'ext': 'mp4',
            'title': 'Blanca - S1E1 - Senza occhi',
            'alt_title': 'St 1 Ep 1 - Blanca - Senza occhi',
            'description': 'md5:75f95d5c030ec8bac263b1212322e28c',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'Rai 1',
            'duration': 6493,
            'series': 'Blanca',
            'season': 'Season 1',
            'episode_number': 1,
            'release_year': 2021,
            'season_number': 1,
            'episode': 'Senza occhi',
            'timestamp': 1637318940,
            'upload_date': '20211119',
        },
    }, {
        'url': 'http://www.raiplay.it/video/2016/11/gazebotraindesi-efebe701-969c-4593-92f3-285f0d1ce750.html?',
        'only_matching': True,
    }, {
        # subtitles at 'subtitlesArray' key (see #27698)
        'url': 'https://www.raiplay.it/video/2020/12/Report---04-01-2021-2e90f1de-8eee-4de4-ac0e-78d21db5b600.html',
        'only_matching': True,
    }, {
        # DRM protected
        'url': 'https://www.raiplay.it/video/2021/06/Lo-straordinario-mondo-di-Zoey-S2E1-Lo-straordinario-ritorno-di-Zoey-3ba992de-2332-41ad-9214-73e32ab209f4.html',
        'only_matching': True,
    }]

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

        media = self._download_json(
            base + '.json', video_id, 'Downloading video JSON')

        if not self.get_param('allow_unplayable_formats'):
            if try_get(
                    media,
                    (lambda x: x['rights_management']['rights']['drm'],
                     lambda x: x['program_info']['rights_management']['rights']['drm']),
                    dict):
                self.report_drm(video_id)

        title = media['name']
        video = media['video']

        relinker_info = self._extract_relinker_info(video['content_url'], video_id)
        self._sort_formats(relinker_info['formats'])

        thumbnails = []
        for _, value in media.get('images', {}).items():
            if value:
                thumbnails.append({
                    'url': urljoin(url, value),
                })

        date_published = media.get('date_published')
        time_published = media.get('time_published')
        if date_published and time_published:
            date_published += ' ' + time_published

        subtitles = self._extract_subtitles(url, video)

        program_info = media.get('program_info') or {}
        season = media.get('season')

        alt_title = join_nonempty(media.get('subtitle'), media.get('toptitle'), delim=' - ')

        return {
            'id': remove_start(media.get('id'), 'ContentItem-') or video_id,
            'display_id': video_id,
            'title': title,
            'alt_title': strip_or_none(alt_title or None),
            'description': media.get('description'),
            'uploader': strip_or_none(media.get('channel') or None),
            'creator': strip_or_none(media.get('editor') or None),
            'duration': parse_duration(video.get('duration')),
            'timestamp': unified_timestamp(date_published),
            'thumbnails': thumbnails,
            'series': program_info.get('name'),
            'season_number': int_or_none(season),
            'season': season if (season and not season.isdigit()) else None,
            'episode': media.get('episode_title'),
            'episode_number': int_or_none(media.get('episode')),
            'subtitles': subtitles,
            'release_year': int_or_none(traverse_obj(media, ('track_info', 'edit_year'))),
            **relinker_info
        }

class RaiPlayLiveIE(RaiPlayIE):
    _VALID_URL = r'(?P<base>https?://(?:www\.)?raiplay\.it/dirette/(?P<id>[^/?#&]+))'
    _TESTS = [{
        'url': 'http://www.raiplay.it/dirette/rainews24',
        'info_dict': {
            'id': 'd784ad40-e0ae-4a69-aa76-37519d238a9c',
            'display_id': 'rainews24',
            'ext': 'mp4',
            'title': 're:^Diretta di Rai News 24 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'description': 'md5:4d00bcf6dc98b27c6ec480de329d1497',
            'uploader': 'Rai News 24',
            'creator': 'Rai News 24',
            'is_live': True,
            'live_status': 'is_live',
            'upload_date': '20090502',
            'timestamp': 1241276220,
        },
        'params': {
            'skip_download': True,
        },
    }]

class RaiPlayPlaylistIE(InfoExtractor):
    _VALID_URL = r'(?P<base>https?://(?:www\.)?raiplay\.it/programmi/(?P<id>[^/?#&]+))(?:/(?P<extra_id>[^?#&]+))?'
    _TESTS = [{
        'url': 'https://www.raiplay.it/programmi/nondirloalmiocapo/',
        'info_dict': {
            'id': 'nondirloalmiocapo',
            'title': 'Non dirlo al mio capo',
            'description': 'md5:98ab6b98f7f44c2843fd7d6f045f153b',
        },
        'playlist_mincount': 12,
    }, {
        'url': 'https://www.raiplay.it/programmi/nondirloalmiocapo/episodi/stagione-2/',
        'info_dict': {
            'id': 'nondirloalmiocapo',
            'title': 'Non dirlo al mio capo - Stagione 2',
            'description': 'md5:98ab6b98f7f44c2843fd7d6f045f153b',
        },
        'playlist_mincount': 12,
    }]

    def _real_extract(self, url):
        base, playlist_id, extra_id = self._match_valid_url(url).groups()

        program = self._download_json(
            base + '.json', playlist_id, 'Downloading program JSON')

        if extra_id:
            extra_id = extra_id.upper().rstrip('/')

        playlist_title = program.get('name')
        entries = []
        for b in (program.get('blocks') or []):
            for s in (b.get('sets') or []):
                if extra_id:
                    if extra_id != join_nonempty(
                            b.get('name'), s.get('name'), delim='/').replace(' ', '-').upper():
                        continue
                    playlist_title = join_nonempty(playlist_title, s.get('name'), delim=' - ')

                s_id = s.get('id')
                if not s_id:
                    continue
                medias = self._download_json(
                    f'{base}/{s_id}.json', s_id,
                    'Downloading content set JSON', fatal=False)
                if not medias:
                    continue
                for m in (medias.get('items') or []):
                    path_id = m.get('path_id')
                    if not path_id:
                        continue
                    video_url = urljoin(url, path_id)
                    entries.append(self.url_result(
                        video_url, ie=RaiPlayIE.ie_key(),
                        video_id=RaiPlayIE._match_id(video_url)))

        return self.playlist_result(
            entries, playlist_id, playlist_title,
            try_get(program, lambda x: x['program_info']['description']))

class RaiPlaySoundIE(RaiBaseIE):
    _VALID_URL = rf'(?P<base>https?://(?:www\.)?raiplaysound\.it/.+?-(?P<id>{RaiBaseIE._UUID_RE}))\.(?:html|json)'
    _TESTS = [{
        'url': 'https://www.raiplaysound.it/audio/2021/12/IL-RUGGITO-DEL-CONIGLIO-1ebae2a7-7cdb-42bb-842e-fe0d193e9707.html',
        'md5': '8970abf8caf8aef4696e7b1f2adfc696',
        'info_dict': {
            'id': '1ebae2a7-7cdb-42bb-842e-fe0d193e9707',
            'ext': 'mp3',
            'title': 'Il Ruggito del Coniglio del 10/12/2021',
            'alt_title': 'md5:0e6476cd57858bb0f3fcc835d305b455',
            'description': 'md5:2a17d2107e59a4a8faa0e18334139ee2',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'rai radio 2',
            'duration': 5685,
            'series': 'Il Ruggito del Coniglio',
            'episode': 'Il Ruggito del Coniglio del 10/12/2021',
            'creator': 'rai radio 2',
            'timestamp': 1638346620,
            'upload_date': '20211201',
        },
        'params': {
            'skip_download': True,
        },
    }]

    def _real_extract(self, url):
        base, audio_id = self._match_valid_url(url).group('base', 'id')
        media = self._download_json(f'{base}.json', audio_id, 'Downloading audio JSON')
        uid = try_get(media, lambda x: remove_start(remove_start(x['uniquename'], 'ContentItem-'), 'Page-'))

        info = {}
        formats = []
        relinkers = set(traverse_obj(media, (('downloadable_audio', 'audio', ('live', 'cards', 0, 'audio')), 'url')))
        for r in relinkers:
            info = self._extract_relinker_info(r, audio_id, True)
            formats.extend(info.get('formats'))

        date_published = try_get(media, (lambda x: f'{x["create_date"]} {x.get("create_time") or ""}',
                                         lambda x: x['live']['create_date']))

        podcast_info = traverse_obj(media, 'podcast_info', ('live', 'cards', 0)) or {}
        thumbnails = [{
            'url': urljoin(url, thumb_url),
        } for thumb_url in (podcast_info.get('images') or {}).values() if thumb_url]

        return {
            **info,
            'id': uid or audio_id,
            'display_id': audio_id,
            'title': traverse_obj(media, 'title', 'episode_title'),
            'alt_title': traverse_obj(media, ('track_info', 'media_name'), expected_type=strip_or_none),
            'description': media.get('description'),
            'uploader': traverse_obj(media, ('track_info', 'channel'), expected_type=strip_or_none),
            'creator': traverse_obj(media, ('track_info', 'editor'), expected_type=strip_or_none),
            'timestamp': unified_timestamp(date_published),
            'thumbnails': thumbnails,
            'series': podcast_info.get('title'),
            'season_number': int_or_none(media.get('season')),
            'episode': media.get('episode_title'),
            'episode_number': int_or_none(media.get('episode')),
            'formats': formats,
        }

class RaiPlaySoundLiveIE(RaiPlaySoundIE):
    _VALID_URL = r'(?P<base>https?://(?:www\.)?raiplaysound\.it/(?P<id>[^/?#&]+)$)'
    _TESTS = [{
        'url': 'https://www.raiplaysound.it/radio2',
        'info_dict': {
            'id': 'b00a50e6-f404-4af6-8f8c-ff3b9af73a44',
            'display_id': 'radio2',
            'ext': 'mp4',
            'title': r're:Rai Radio 2 \d+-\d+-\d+ \d+:\d+',
            'thumbnail': r're:https://www.raiplaysound.it/dl/img/.+?png',
            'uploader': 'rai radio 2',
            'series': 'Rai Radio 2',
            'creator': 'raiplaysound',
            'is_live': True,
            'live_status': 'is_live',
        },
        'params': {
            'skip_download': 'live',
        },
    }]

class RaiPlaySoundPlaylistIE(InfoExtractor):
    _VALID_URL = r'(?P<base>https?://(?:www\.)?raiplaysound\.it/(?:programmi|playlist|audiolibri)/(?P<id>[^/?#&]+))(?:/(?P<extra_id>[^?#&]+))?'
    _TESTS = [{
        'url': 'https://www.raiplaysound.it/programmi/ilruggitodelconiglio',
        'info_dict': {
            'id': 'ilruggitodelconiglio',
            'title': 'Il Ruggito del Coniglio',
            'description': 'md5:1bbaf631245a7ab1ec4d9fbb3c7aa8f3',
        },
        'playlist_mincount': 65,
    }, {
        'url': 'https://www.raiplaysound.it/programmi/ilruggitodelconiglio/puntate/prima-stagione-1995',
        'info_dict': {
            'id': 'ilruggitodelconiglio_puntate_prima-stagione-1995',
            'title': 'Prima Stagione 1995',
        },
        'playlist_count': 1,
    }]

    def _real_extract(self, url):
        base, playlist_id, extra_id = self._match_valid_url(url).group('base', 'id', 'extra_id')
        url = f'{base}.json'
        program = self._download_json(url, playlist_id, 'Downloading program JSON')

        if extra_id:
            extra_id = extra_id.rstrip('/')
            playlist_id += '_' + extra_id.replace('/', '_')
            path = next(c['path_id'] for c in program.get('filters') or [] if extra_id in c.get('weblink'))
            program = self._download_json(
                urljoin('https://www.raiplaysound.it', path), playlist_id, 'Downloading program secondary JSON')

        entries = [
            self.url_result(urljoin(base, c['path_id']), ie=RaiPlaySoundIE.ie_key())
            for c in traverse_obj(program, 'cards', ('block', 'cards')) or []
            if c.get('path_id')]

        return self.playlist_result(entries, playlist_id, program.get('title'),
                                    traverse_obj(program, ('podcast_info', 'description')))

class RaiIE(RaiBaseIE):
    _VALID_URL = rf'https?://[^/]+\.(?:rai\.(?:it|tv))/.+?-(?P<id>{RaiBaseIE._UUID_RE})(?:-.+?)?\.html'
    _TESTS = [{
        # var uniquename = "ContentItem-..."
        # data-id="ContentItem-..."
        'url': 'https://www.raisport.rai.it/dl/raiSport/media/rassegna-stampa-04a9f4bd-b563-40cf-82a6-aad3529cb4a9.html',
        'info_dict': {
            'id': '04a9f4bd-b563-40cf-82a6-aad3529cb4a9',
            'ext': 'mp4',
            'title': 'TG PRIMO TEMPO',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 1758,
            'upload_date': '20140612',
        },
        'skip': 'This content is available only in Italy',
    }, {
        # with ContentItem in og:url
        'url': 'https://www.rai.it/dl/RaiTV/programmi/media/ContentItem-efb17665-691c-45d5-a60c-5301333cbb0c.html',
        'md5': '06345bd97c932f19ffb129973d07a020',
        'info_dict': {
            'id': 'efb17665-691c-45d5-a60c-5301333cbb0c',
            'ext': 'mp4',
            'title': 'TG1 ore 20:00 del 03/11/2016',
            'description': 'TG1 edizione integrale ore 20:00 del giorno 03/11/2016',
            'thumbnail': r're:^https?://.*\.jpg$',
            'duration': 2214,
            'upload_date': '20161103'
        }
    }, {
        # Direct MMS URL
        'url': 'http://www.rai.it/dl/RaiTV/programmi/media/ContentItem-b63a4089-ac28-48cf-bca5-9f5b5bc46df5.html',
        'only_matching': True,
    }]

    def _extract_from_content_id(self, content_id, url):
        media = self._download_json(
            f'https://www.rai.tv/dl/RaiTV/programmi/media/ContentItem-{content_id}.html?json',
            content_id, 'Downloading video JSON')

        title = media['name'].strip()

        media_type = media['type']
        if 'Audio' in media_type:
            relinker_info = {
                'formats': [{
                    'format_id': media.get('formatoAudio'),
                    'url': media['audioUrl'],
                    'ext': media.get('formatoAudio'),
                }]
            }
        elif 'Video' in media_type:
            relinker_info = self._extract_relinker_info(media['mediaUri'], content_id)
        else:
            raise ExtractorError('not a media file')

        self._sort_formats(relinker_info['formats'])

        thumbnails = []
        for image_type in ('image', 'image_medium', 'image_300'):
            thumbnail_url = media.get(image_type)
            if thumbnail_url:
                thumbnails.append({
                    'url': compat_urlparse.urljoin(url, thumbnail_url),
                })

        subtitles = self._extract_subtitles(url, media)

        return {
            'id': content_id,
            'title': title,
            'description': strip_or_none(media.get('desc') or None),
            'thumbnails': thumbnails,
            'uploader': strip_or_none(media.get('author') or None),
            'upload_date': unified_strdate(media.get('date')),
            'duration': parse_duration(media.get('length')),
            'subtitles': subtitles,
            **relinker_info
        }

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

        webpage = self._download_webpage(url, video_id)

        content_item_id = None

        content_item_url = self._html_search_meta(
            ('og:url', 'og:video', 'og:video:secure_url', 'twitter:url',
             'twitter:player', 'jsonlink'), webpage, default=None)
        if content_item_url:
            content_item_id = self._search_regex(
                rf'ContentItem-({self._UUID_RE})', content_item_url,
                'content item id', default=None)

        if not content_item_id:
            content_item_id = self._search_regex(
                rf'''(?x)
                    (?:
                        (?:initEdizione|drawMediaRaiTV)\(|
                        <(?:[^>]+\bdata-id|var\s+uniquename)=|
                        <iframe[^>]+\bsrc=
                    )
                    (["\'])
                    (?:(?!\1).)*\bContentItem-(?P<id>{self._UUID_RE})
                ''',
                webpage, 'content item id', default=None, group='id')

        content_item_ids = set()
        if content_item_id:
            content_item_ids.add(content_item_id)
        if video_id not in content_item_ids:
            content_item_ids.add(video_id)

        for content_item_id in content_item_ids:
            try:
                return self._extract_from_content_id(content_item_id, url)
            except GeoRestrictedError:
                raise
            except ExtractorError:
                pass

        relinker_url = self._proto_relative_url(self._search_regex(
            r'''(?x)
                (?:
                    var\s+videoURL|
                    mediaInfo\.mediaUri
                )\s*=\s*
                ([\'"])
                (?P<url>
                    (?:https?:)?
                    //mediapolis(?:vod)?\.rai\.it/relinker/relinkerServlet\.htm\?
                    (?:(?!\1).)*\bcont=(?:(?!\1).)+)\1
            ''',
            webpage, 'relinker URL', group='url'))

        relinker_info = self._extract_relinker_info(
            urljoin(url, relinker_url), video_id)
        self._sort_formats(relinker_info['formats'])

        title = self._search_regex(
            r'var\s+videoTitolo\s*=\s*([\'"])(?P<title>[^\'"]+)\1',
            webpage, 'title', group='title',
            default=None) or self._og_search_title(webpage)

        return {
            'id': video_id,
            'title': title,
            **relinker_info
        }

class RaiNewsIE(RaiIE):
    _VALID_URL = rf'https?://(www\.)?rainews\.it/(?!articoli)[^?#]+-(?P<id>{RaiBaseIE._UUID_RE})(?:-[^/?#]+)?\.html'
    _EMBED_REGEX = [rf'<iframe[^>]+data-src="(?P<url>/iframe/[^?#]+?{RaiBaseIE._UUID_RE}\.html)']
    _TESTS = [{
        # new rainews player (#3911)
        'url': 'https://www.rainews.it/rubriche/24mm/video/2022/05/24mm-del-29052022-12cf645d-1ffd-4220-b27c-07c226dbdecf.html',
        'info_dict': {
            'id': '12cf645d-1ffd-4220-b27c-07c226dbdecf',
            'ext': 'mp4',
            'title': 'Puntata del 29/05/2022',
            'duration': 1589,
            'upload_date': '20220529',
            'uploader': 'rainews',
        }
    }, {
        # old content with fallback method to extract media urls
        'url': 'https://www.rainews.it/dl/rainews/media/Weekend-al-cinema-da-Hollywood-arriva-il-thriller-di-Tate-Taylor-La-ragazza-del-treno-1632c009-c843-4836-bb65-80c33084a64b.html',
        'info_dict': {
            'id': '1632c009-c843-4836-bb65-80c33084a64b',
            'ext': 'mp4',
            'title': 'Weekend al cinema, da Hollywood arriva il thriller di Tate Taylor "La ragazza del treno"',
            'description': 'I film in uscita questa settimana.',
            'thumbnail': r're:^https?://.*\.png$',
            'duration': 833,
            'upload_date': '20161103'
        },
        'expected_warnings': ['unable to extract player_data'],
    }, {
        # iframe + drm
        'url': 'https://www.rainews.it/iframe/video/2022/07/euro2022-europei-calcio-femminile-italia-belgio-gol-0-1-video-4de06a69-de75-4e32-a657-02f0885f8118.html',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, video_id)

        player_data = self._search_json(
            r'<rainews-player\s*data=\'', webpage, 'player_data', video_id,
            transform_source=clean_html, fatal=False)
        track_info = player_data.get('track_info')
        relinker_url = traverse_obj(player_data, 'mediapolis', 'content_url')

        if not relinker_url:
            # fallback on old implementation for some old content
            try:
                return self._extract_from_content_id(video_id, url)
            except GeoRestrictedError:
                raise
            except ExtractorError as e:
                raise ExtractorError('Relinker URL not found', cause=e)

        relinker_info = self._extract_relinker_info(urljoin(url, relinker_url), video_id)

        self._sort_formats(relinker_info['formats'])

        return {
            'id': video_id,
            'title': track_info.get('title') or self._og_search_title(webpage),
            'upload_date': unified_strdate(track_info.get('date')),
            'uploader': strip_or_none(track_info.get('editor') or None),
            **relinker_info
        }

class RaiSudtirolIE(RaiBaseIE):
    _VALID_URL = r'https?://raisudtirol\.rai\.it/.+?media=(?P<id>[TP]tv\d+)'
    _TESTS = [{
        'url': 'https://raisudtirol.rai.it/la/index.php?media=Ptv1619729460',
        'info_dict': {
            'id': 'Ptv1619729460',
            'ext': 'mp4',
            'title': 'Euro: trasmisciun d\'economia - 29-04-2021 20:51',
            'series': 'Euro: trasmisciun d\'economia',
            'upload_date': '20210429',
            'thumbnail': r're:https://raisudtirol\.rai\.it/img/.+?\.jpg',
            'uploader': 'raisudtirol',
        }
    }]

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

        video_date = self._html_search_regex(r'<span class="med_data">(.+?)</span>', webpage, 'video_date', fatal=False)
        video_title = self._html_search_regex(r'<span class="med_title">(.+?)</span>', webpage, 'video_title', fatal=False)
        video_url = self._html_search_regex(r'sources:\s*\[\{file:\s*"(.+?)"\}\]', webpage, 'video_url')
        video_thumb = self._html_search_regex(r'image: \'(.+?)\'', webpage, 'video_thumb', fatal=False)

        return {
            'id': video_id,
            'title': join_nonempty(video_title, video_date, delim=' - '),
            'series': video_title,
            'upload_date': unified_strdate(video_date),
            'thumbnail': urljoin('https://raisudtirol.rai.it/', video_thumb),
            'uploader': 'raisudtirol',
            'formats': [{
                'format_id': 'https-mp4',
                'url': self._proto_relative_url(video_url),
                'width': 1024,
                'height': 576,
                'fps': 25,
                'vcodec': 'h264',
                'acodec': 'aac',
            }],
        }


code:
import re

from .common import InfoExtractor
from .vimeo import VimeoIE
from ..compat import compat_str
from ..utils import (
    ExtractorError,
    int_or_none,
    merge_dicts,
    try_get,
    unescapeHTML,
    unified_timestamp,
    urljoin,
)

class RayWenderlichIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            videos\.raywenderlich\.com/courses|
                            (?:www\.)?raywenderlich\.com
                        )/
                        (?P<course_id>[^/]+)/lessons/(?P<id>\d+)
                    '''

    _TESTS = [{
        'url': 'https://www.raywenderlich.com/3530-testing-in-ios/lessons/1',
        'info_dict': {
            'id': '248377018',
            'ext': 'mp4',
            'title': 'Introduction',
            'description': 'md5:804d031b3efa9fcb49777d512d74f722',
            'timestamp': 1513906277,
            'upload_date': '20171222',
            'duration': 133,
            'uploader': 'Ray Wenderlich',
            'uploader_id': 'user3304672',
        },
        'params': {
            'noplaylist': True,
            'skip_download': True,
        },
        'add_ie': [VimeoIE.ie_key()],
        'expected_warnings': ['HTTP Error 403: Forbidden'],
    }, {
        'url': 'https://videos.raywenderlich.com/courses/105-testing-in-ios/lessons/1',
        'only_matching': True,
    }]

    @staticmethod
    def _extract_video_id(data, lesson_id):
        if not data:
            return
        groups = try_get(data, lambda x: x['groups'], list) or []
        if not groups:
            return
        for group in groups:
            if not isinstance(group, dict):
                continue
            contents = try_get(data, lambda x: x['contents'], list) or []
            for content in contents:
                if not isinstance(content, dict):
                    continue
                ordinal = int_or_none(content.get('ordinal'))
                if ordinal != lesson_id:
                    continue
                video_id = content.get('identifier')
                if video_id:
                    return compat_str(video_id)

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        course_id, lesson_id = mobj.group('course_id', 'id')
        display_id = '%s/%s' % (course_id, lesson_id)

        webpage = self._download_webpage(url, display_id)

        thumbnail = self._og_search_thumbnail(
            webpage, default=None) or self._html_search_meta(
            'twitter:image', webpage, 'thumbnail')

        if '>Subscribe to unlock' in webpage:
            raise ExtractorError(
                'This content is only available for subscribers',
                expected=True)

        info = {
            'thumbnail': thumbnail,
        }

        vimeo_id = self._search_regex(
            r'data-vimeo-id=["\'](\d+)', webpage, 'vimeo id', default=None)

        if not vimeo_id:
            data = self._parse_json(
                self._search_regex(
                    r'data-collection=(["\'])(?P<data>{.+?})\1', webpage,
                    'data collection', default='{}', group='data'),
                display_id, transform_source=unescapeHTML, fatal=False)
            video_id = self._extract_video_id(
                data, lesson_id) or self._search_regex(
                r'/videos/(\d+)/', thumbnail, 'video id')
            headers = {
                'Referer': url,
                'X-Requested-With': 'XMLHttpRequest',
            }
            csrf_token = self._html_search_meta(
                'csrf-token', webpage, 'csrf token', default=None)
            if csrf_token:
                headers['X-CSRF-Token'] = csrf_token
            video = self._download_json(
                'https://videos.raywenderlich.com/api/v1/videos/%s.json'
                % video_id, display_id, headers=headers)['video']
            vimeo_id = video['clips'][0]['provider_id']
            info.update({
                '_type': 'url_transparent',
                'title': video.get('name'),
                'description': video.get('description') or video.get(
                    'meta_description'),
                'duration': int_or_none(video.get('duration')),
                'timestamp': unified_timestamp(video.get('created_at')),
            })

        return merge_dicts(info, self.url_result(
            VimeoIE._smuggle_referrer(
                'https://player.vimeo.com/video/%s' % vimeo_id, url),
            ie=VimeoIE.ie_key(), video_id=vimeo_id))

class RayWenderlichCourseIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:
                            videos\.raywenderlich\.com/courses|
                            (?:www\.)?raywenderlich\.com
                        )/
                        (?P<id>[^/]+)
                    '''

    _TEST = {
        'url': 'https://www.raywenderlich.com/3530-testing-in-ios',
        'info_dict': {
            'title': 'Testing in iOS',
            'id': '3530-testing-in-ios',
        },
        'params': {
            'noplaylist': False,
        },
        'playlist_count': 29,
    }

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

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

        webpage = self._download_webpage(url, course_id)

        entries = []
        lesson_urls = set()
        for lesson_url in re.findall(
                r'<a[^>]+\bhref=["\'](/%s/lessons/\d+)' % course_id, webpage):
            if lesson_url in lesson_urls:
                continue
            lesson_urls.add(lesson_url)
            entries.append(self.url_result(
                urljoin(url, lesson_url), ie=RayWenderlichIE.ie_key()))

        title = self._og_search_title(
            webpage, default=None) or self._html_search_meta(
            'twitter:title', webpage, 'title', default=None)

        return self.playlist_result(entries, course_id, title)


code:
from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
    clean_html,
    int_or_none,
    unified_timestamp,
    update_url_query,
)

class RBMARadioIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?(?:rbmaradio|redbullradio)\.com/shows/(?P<show_id>[^/]+)/episodes/(?P<id>[^/?#&]+)'
    _TEST = {
        'url': 'https://www.rbmaradio.com/shows/main-stage/episodes/ford-lopatin-live-at-primavera-sound-2011',
        'md5': '6bc6f9bcb18994b4c983bc3bf4384d95',
        'info_dict': {
            'id': 'ford-lopatin-live-at-primavera-sound-2011',
            'ext': 'mp3',
            'title': 'Main Stage - Ford & Lopatin at Primavera Sound',
            'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
            'thumbnail': r're:^https?://.*\.jpg',
            'duration': 2452,
            'timestamp': 1307103164,
            'upload_date': '20110603',
        },
    }

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        show_id = mobj.group('show_id')
        episode_id = mobj.group('id')

        webpage = self._download_webpage(url, episode_id)

        episode = self._parse_json(
            self._search_regex(
                r'__INITIAL_STATE__\s*=\s*({.+?})\s*</script>',
                webpage, 'json data'),
            episode_id)['episodes'][show_id][episode_id]

        title = episode['title']

        show_title = episode.get('showTitle')
        if show_title:
            title = '%s - %s' % (show_title, title)

        formats = [{
            'url': update_url_query(episode['audioURL'], query={'cbr': abr}),
            'format_id': compat_str(abr),
            'abr': abr,
            'vcodec': 'none',
        } for abr in (96, 128, 192, 256)]
        self._check_formats(formats, episode_id)

        description = clean_html(episode.get('longTeaser'))
        thumbnail = self._proto_relative_url(episode.get('imageURL', {}).get('landscape'))
        duration = int_or_none(episode.get('duration'))
        timestamp = unified_timestamp(episode.get('publishedAt'))

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


code:
import re

from .common import InfoExtractor
from ..utils import (
    clean_html,
    ExtractorError,
    js_to_json,
    base_url,
    url_basename,
    urljoin,
)

class RCSBaseIE(InfoExtractor):
    # based on VideoPlayerLoader.prototype.getVideoSrc
    # and VideoPlayerLoader.prototype.transformSrc from
    # https://js2.corriereobjects.it/includes2013/LIBS/js/corriere_video.sjs
    _ALL_REPLACE = {
        'media2vam.corriere.it.edgesuite.net':
            'media2vam-corriere-it.akamaized.net',
        'media.youreporter.it.edgesuite.net':
            'media-youreporter-it.akamaized.net',
        'corrierepmd.corriere.it.edgesuite.net':
            'corrierepmd-corriere-it.akamaized.net',
        'media2vam-corriere-it.akamaized.net/fcs.quotidiani/vr/videos/':
            'video.corriere.it/vr360/videos/',
        '.net//': '.net/',
    }
    _MP4_REPLACE = {
        'media2vam.corbologna.corriere.it.edgesuite.net':
            'media2vam-bologna-corriere-it.akamaized.net',
        'media2vam.corfiorentino.corriere.it.edgesuite.net':
            'media2vam-fiorentino-corriere-it.akamaized.net',
        'media2vam.cormezzogiorno.corriere.it.edgesuite.net':
            'media2vam-mezzogiorno-corriere-it.akamaized.net',
        'media2vam.corveneto.corriere.it.edgesuite.net':
            'media2vam-veneto-corriere-it.akamaized.net',
        'media2.oggi.it.edgesuite.net':
            'media2-oggi-it.akamaized.net',
        'media2.quimamme.it.edgesuite.net':
            'media2-quimamme-it.akamaized.net',
        'media2.amica.it.edgesuite.net':
            'media2-amica-it.akamaized.net',
        'media2.living.corriere.it.edgesuite.net':
            'media2-living-corriere-it.akamaized.net',
        'media2.style.corriere.it.edgesuite.net':
            'media2-style-corriere-it.akamaized.net',
        'media2.iodonna.it.edgesuite.net':
            'media2-iodonna-it.akamaized.net',
        'media2.leitv.it.edgesuite.net':
            'media2-leitv-it.akamaized.net',
    }
    _MIGRATION_MAP = {
        'videoamica-vh.akamaihd': 'amica',
        'media2-amica-it.akamaized': 'amica',
        'corrierevam-vh.akamaihd': 'corriere',
        'media2vam-corriere-it.akamaized': 'corriere',
        'cormezzogiorno-vh.akamaihd': 'corrieredelmezzogiorno',
        'media2vam-mezzogiorno-corriere-it.akamaized': 'corrieredelmezzogiorno',
        'corveneto-vh.akamaihd': 'corrieredelveneto',
        'media2vam-veneto-corriere-it.akamaized': 'corrieredelveneto',
        'corbologna-vh.akamaihd': 'corrieredibologna',
        'media2vam-bologna-corriere-it.akamaized': 'corrieredibologna',
        'corfiorentino-vh.akamaihd': 'corrierefiorentino',
        'media2vam-fiorentino-corriere-it.akamaized': 'corrierefiorentino',
        'corinnovazione-vh.akamaihd': 'corriereinnovazione',
        'media2-gazzanet-gazzetta-it.akamaized': 'gazzanet',
        'videogazzanet-vh.akamaihd': 'gazzanet',
        'videogazzaworld-vh.akamaihd': 'gazzaworld',
        'gazzettavam-vh.akamaihd': 'gazzetta',
        'media2vam-gazzetta-it.akamaized': 'gazzetta',
        'videoiodonna-vh.akamaihd': 'iodonna',
        'media2-leitv-it.akamaized': 'leitv',
        'videoleitv-vh.akamaihd': 'leitv',
        'videoliving-vh.akamaihd': 'living',
        'media2-living-corriere-it.akamaized': 'living',
        'media2-oggi-it.akamaized': 'oggi',
        'videooggi-vh.akamaihd': 'oggi',
        'media2-quimamme-it.akamaized': 'quimamme',
        'quimamme-vh.akamaihd': 'quimamme',
        'videorunning-vh.akamaihd': 'running',
        'media2-style-corriere-it.akamaized': 'style',
        'style-vh.akamaihd': 'style',
        'videostyle-vh.akamaihd': 'style',
        'media2-stylepiccoli-it.akamaized': 'stylepiccoli',
        'stylepiccoli-vh.akamaihd': 'stylepiccoli',
        'doveviaggi-vh.akamaihd': 'viaggi',
        'media2-doveviaggi-it.akamaized': 'viaggi',
        'media2-vivimilano-corriere-it.akamaized': 'vivimilano',
        'vivimilano-vh.akamaihd': 'vivimilano',
        'media2-youreporter-it.akamaized': 'youreporter'
    }
    _MIGRATION_MEDIA = {
        'advrcs-vh.akamaihd': '',
        'corriere-f.akamaihd': '',
        'corrierepmd-corriere-it.akamaized': '',
        'corrprotetto-vh.akamaihd': '',
        'gazzetta-f.akamaihd': '',
        'gazzettapmd-gazzetta-it.akamaized': '',
        'gazzprotetto-vh.akamaihd': '',
        'periodici-f.akamaihd': '',
        'periodicisecure-vh.akamaihd': '',
        'videocoracademy-vh.akamaihd': ''
    }

    def _get_video_src(self, video):
        mediaFiles = video.get('mediaProfile').get('mediaFile')
        src = {}
        # audio
        if video.get('mediaType') == 'AUDIO':
            for aud in mediaFiles:
                # todo: check
                src['mp3'] = aud.get('value')
        # video
        else:
            for vid in mediaFiles:
                if vid.get('mimeType') == 'application/vnd.apple.mpegurl':
                    src['m3u8'] = vid.get('value')
                if vid.get('mimeType') == 'video/mp4':
                    src['mp4'] = vid.get('value')

        # replace host
        for t in src:
            for s, r in self._ALL_REPLACE.items():
                src[t] = src[t].replace(s, r)
            for s, r in self._MP4_REPLACE.items():
                src[t] = src[t].replace(s, r)

        # switch cdn
        if 'mp4' in src and 'm3u8' in src:
            if ('-lh.akamaihd' not in src.get('m3u8')
                    and 'akamai' in src.get('mp4')):
                if 'm3u8' in src:
                    matches = re.search(r'(?:https*:)?\/\/(?P<host>.*)\.net\/i(?P<path>.*)$', src.get('m3u8'))
                    src['m3u8'] = 'https://vod.rcsobjects.it/hls/%s%s' % (
                        self._MIGRATION_MAP[matches.group('host')],
                        matches.group('path').replace(
                            '///', '/').replace(
                            '//', '/').replace(
                            '.csmil', '.urlset'
                        )
                    )
                if 'mp4' in src:
                    matches = re.search(r'(?:https*:)?\/\/(?P<host>.*)\.net\/i(?P<path>.*)$', src.get('mp4'))
                    if matches:
                        if matches.group('host') in self._MIGRATION_MEDIA:
                            vh_stream = 'https://media2.corriereobjects.it'
                            if src.get('mp4').find('fcs.quotidiani_!'):
                                vh_stream = 'https://media2-it.corriereobjects.it'
                            src['mp4'] = '%s%s' % (
                                vh_stream,
                                matches.group('path').replace(
                                    '///', '/').replace(
                                    '//', '/').replace(
                                    '/fcs.quotidiani/mediacenter', '').replace(
                                    '/fcs.quotidiani_!/mediacenter', '').replace(
                                    'corriere/content/mediacenter/', '').replace(
                                    'gazzetta/content/mediacenter/', '')
                            )
                        else:
                            src['mp4'] = 'https://vod.rcsobjects.it/%s%s' % (
                                self._MIGRATION_MAP[matches.group('host')],
                                matches.group('path').replace('///', '/').replace('//', '/')
                            )

        if 'mp3' in src:
            src['mp3'] = src.get('mp3').replace(
                'media2vam-corriere-it.akamaized.net',
                'vod.rcsobjects.it/corriere')
        if 'mp4' in src:
            if src.get('mp4').find('fcs.quotidiani_!'):
                src['mp4'] = src.get('mp4').replace('vod.rcsobjects', 'vod-it.rcsobjects')
        if 'm3u8' in src:
            if src.get('m3u8').find('fcs.quotidiani_!'):
                src['m3u8'] = src.get('m3u8').replace('vod.rcsobjects', 'vod-it.rcsobjects')

        if 'geoblocking' in video.get('mediaProfile'):
            if 'm3u8' in src:
                src['m3u8'] = src.get('m3u8').replace('vod.rcsobjects', 'vod-it.rcsobjects')
            if 'mp4' in src:
                src['mp4'] = src.get('mp4').replace('vod.rcsobjects', 'vod-it.rcsobjects')
        if 'm3u8' in src:
            if src.get('m3u8').find('csmil') and src.get('m3u8').find('vod'):
                src['m3u8'] = src.get('m3u8').replace('.csmil', '.urlset')

        return src

    def _create_formats(self, urls, video_id):
        formats = []
        formats = self._extract_m3u8_formats(
            urls.get('m3u8'), video_id, 'mp4', entry_protocol='m3u8_native',
            m3u8_id='hls', fatal=False)

        if urls.get('mp4'):
            formats.append({
                'format_id': 'http-mp4',
                'url': urls['mp4']
            })
        self._sort_formats(formats)
        return formats

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

        if 'cdn' not in mobj.groupdict():
            raise ExtractorError('CDN not found in url: %s' % url)

        # for leitv/youreporter/viaggi don't use the embed page
        if ((mobj.group('cdn') not in ['leitv.it', 'youreporter.it'])
                and (mobj.group('vid') == 'video')):
            url = 'https://video.%s/video-embed/%s' % (mobj.group('cdn'), video_id)

        page = self._download_webpage(url, video_id)

        video_data = None
        # look for json video data url
        json = self._search_regex(
            r'''(?x)url\s*=\s*(["'])
            (?P<url>
                (?:https?:)?//video\.rcs\.it
                /fragment-includes/video-includes/.+?\.json
            )\1;''',
            page, video_id, group='url', default=None)
        if json:
            if json.startswith('//'):
                json = 'https:%s' % json
            video_data = self._download_json(json, video_id)

        # if json url not found, look for json video data directly in the page
        else:
            # RCS normal pages and most of the embeds
            json = self._search_regex(
                r'[\s;]video\s*=\s*({[\s\S]+?})(?:;|,playlist=)',
                page, video_id, default=None)
            if not json and 'video-embed' in url:
                page = self._download_webpage(url.replace('video-embed', 'video-json'), video_id)
                json = self._search_regex(
                    r'##start-video##({[\s\S]+?})##end-video##',
                    page, video_id, default=None)
            if not json:
                # if no video data found try search for iframes
                emb = RCSEmbedsIE._extract_url(page)
                if emb:
                    return {
                        '_type': 'url_transparent',
                        'url': emb,
                        'ie_key': RCSEmbedsIE.ie_key()
                    }
            if json:
                video_data = self._parse_json(
                    json, video_id, transform_source=js_to_json)

        if not video_data:
            raise ExtractorError('Video data not found in the page')

        formats = self._create_formats(
            self._get_video_src(video_data), video_id)

        description = (video_data.get('description')
                       or clean_html(video_data.get('htmlDescription'))
                       or self._html_search_meta('description', page))
        uploader = video_data.get('provider') or mobj.group('cdn')

        return {
            'id': video_id,
            'title': video_data.get('title'),
            'description': description,
            'uploader': uploader,
            'formats': formats
        }

class RCSEmbedsIE(RCSBaseIE):
    _VALID_URL = r'''(?x)
                    https?://(?P<vid>video)\.
                    (?P<cdn>
                    (?:
                        rcs|
                        (?:corriere\w+\.)?corriere|
                        (?:gazzanet\.)?gazzetta
                    )\.it)
                    /video-embed/(?P<id>[^/=&\?]+?)(?:$|\?)'''
    _EMBED_REGEX = [r'''(?x)
            (?:
                data-frame-src=|
                <iframe[^\n]+src=
            )
            (["'])
                (?P<url>(?:https?:)?//video\.
                    (?:
                        rcs|
                        (?:corriere\w+\.)?corriere|
                        (?:gazzanet\.)?gazzetta
                    )
                \.it/video-embed/.+?)
            \1''']
    _TESTS = [{
        'url': 'https://video.rcs.it/video-embed/iodonna-0001585037',
        'md5': '623ecc8ffe7299b2d0c1046d8331a9df',
        'info_dict': {
            'id': 'iodonna-0001585037',
            'ext': 'mp4',
            'title': 'Sky Arte racconta Madonna nella serie "Artist to icon"',
            'description': 'md5:65b09633df9ffee57f48b39e34c9e067',
            'uploader': 'rcs.it',
        }
    }, {
        # redownload the page changing 'video-embed' in 'video-json'
        'url': 'https://video.gazzanet.gazzetta.it/video-embed/gazzanet-mo05-0000260789',
        'md5': 'a043e3fecbe4d9ed7fc5d888652a5440',
        'info_dict': {
            'id': 'gazzanet-mo05-0000260789',
            'ext': 'mp4',
            'title': 'Valentino Rossi e papà Graziano si divertono col drifting',
            'description': 'md5:a8bf90d6adafd9815f70fc74c0fc370a',
            'uploader': 'rcd',
        }
    }, {
        'url': 'https://video.corriere.it/video-embed/b727632a-f9d0-11ea-91b0-38d50a849abb?player',
        'match_only': True
    }, {
        'url': 'https://video.gazzetta.it/video-embed/49612410-00ca-11eb-bcd8-30d4253e0140',
        'match_only': True
    }]

    @staticmethod
    def _sanitize_urls(urls):
        # add protocol if missing
        for i, e in enumerate(urls):
            if e.startswith('//'):
                urls[i] = 'https:%s' % e
        # clean iframes urls
        for i, e in enumerate(urls):
            urls[i] = urljoin(base_url(e), url_basename(e))
        return urls

    @classmethod
    def _extract_embed_urls(cls, url, webpage):
        return cls._sanitize_urls(tuple(super()._extract_embed_urls(url, webpage)))

class RCSIE(RCSBaseIE):
    _VALID_URL = r'''(?x)https?://(?P<vid>video|viaggi)\.
                    (?P<cdn>
                    (?:
                        corrieredelmezzogiorno\.
                        |corrieredelveneto\.
                        |corrieredibologna\.
                        |corrierefiorentino\.
                    )?corriere\.it
                    |(?:gazzanet\.)?gazzetta\.it)
                    /(?!video-embed/).+?/(?P<id>[^/\?]+)(?=\?|/$|$)'''
    _TESTS = [{
        'url': 'https://video.corriere.it/sport/formula-1/vettel-guida-ferrari-sf90-mugello-suo-fianco-c-elecrerc-bendato-video-esilarante/b727632a-f9d0-11ea-91b0-38d50a849abb',
        'md5': '0f4ededc202b0f00b6e509d831e2dcda',
        'info_dict': {
            'id': 'b727632a-f9d0-11ea-91b0-38d50a849abb',
            'ext': 'mp4',
            'title': 'Vettel guida la Ferrari SF90 al Mugello e al suo fianco c\'è Leclerc (bendato): il video è esilarante',
            'description': 'md5:93b51c9161ac8a64fb2f997b054d0152',
            'uploader': 'Corriere Tv',
        }
    }, {
        # video data inside iframe
        'url': 'https://viaggi.corriere.it/video/norvegia-il-nuovo-ponte-spettacolare-sopra-la-cascata-di-voringsfossen/',
        'md5': 'da378e4918d2afbf7d61c35abb948d4c',
        'info_dict': {
            'id': '5b7cd134-e2c1-11ea-89b3-b56dd0df2aa2',
            'ext': 'mp4',
            'title': 'La nuova spettacolare attrazione in Norvegia: il ponte sopra Vøringsfossen',
            'description': 'md5:18b35a291f6746c0c8dacd16e5f5f4f8',
            'uploader': 'DOVE Viaggi',
        }
    }, {
        'url': 'https://video.gazzetta.it/video-motogp-catalogna-cadute-dovizioso-vale-rossi/49612410-00ca-11eb-bcd8-30d4253e0140?vclk=Videobar',
        'md5': 'eedc1b5defd18e67383afef51ff7bdf9',
        'info_dict': {
            'id': '49612410-00ca-11eb-bcd8-30d4253e0140',
            'ext': 'mp4',
            'title': 'Dovizioso, il contatto con Zarco e la caduta. E anche Vale finisce a terra',
            'description': 'md5:8c6e905dc3b9413218beca11ebd69778',
            'uploader': 'AMorici',
        }
    }, {
        'url': 'https://video.corriere.it/video-360/metro-copenaghen-tutta-italiana/a248a7f0-e2db-11e9-9830-af2de6b1f945',
        'match_only': True
    }]

class RCSVariousIE(RCSBaseIE):
    _VALID_URL = r'''(?x)https?://www\.
                    (?P<cdn>
                        leitv\.it|
                        youreporter\.it
                    )/(?:[^/]+/)?(?P<id>[^/]+?)(?:$|\?|/)'''
    _TESTS = [{
        'url': 'https://www.leitv.it/benessere/mal-di-testa-come-combatterlo-ed-evitarne-la-comparsa/',
        'md5': '92b4e63667b8f95acb0a04da25ae28a1',
        'info_dict': {
            'id': 'mal-di-testa-come-combatterlo-ed-evitarne-la-comparsa',
            'ext': 'mp4',
            'title': 'Cervicalgia e mal di testa, il video con i suggerimenti dell\'esperto',
            'description': 'md5:ae21418f34cee0b8d02a487f55bcabb5',
            'uploader': 'leitv.it',
        }
    }, {
        'url': 'https://www.youreporter.it/fiume-sesia-3-ottobre-2020/',
        'md5': '8dccd436b47a830bab5b4a88232f391a',
        'info_dict': {
            'id': 'fiume-sesia-3-ottobre-2020',
            'ext': 'mp4',
            'title': 'Fiume Sesia 3 ottobre 2020',
            'description': 'md5:0070eef1cc884d13c970a4125063de55',
            'uploader': 'youreporter.it',
        }
    }]


code:
import json
import random
import time

from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    dict_get,
    ExtractorError,
    strip_or_none,
    traverse_obj,
    try_get
)

class RCTIPlusBaseIE(InfoExtractor):
    def _real_initialize(self):
        self._AUTH_KEY = self._download_json(
            'https://api.rctiplus.com/api/v1/visitor?platform=web',  # platform can be web, mweb, android, ios
            None, 'Fetching authorization key')['data']['access_token']

    def _call_api(self, url, video_id, note=None):
        json = self._download_json(
            url, video_id, note=note, headers={'Authorization': self._AUTH_KEY})
        if json.get('status', {}).get('code', 0) != 0:
            raise ExtractorError(f'{self.IE_NAME} said: {json["status"]["message_client"]}', cause=json)
        return json.get('data'), json.get('meta')

class RCTIPlusIE(RCTIPlusBaseIE):
    _VALID_URL = r'https://www\.rctiplus\.com/(?:programs/\d+?/.*?/)?(?P<type>episode|clip|extra|live-event|missed-event)/(?P<id>\d+)/(?P<display_id>[^/?#&]+)'
    _TESTS = [{
        'url': 'https://www.rctiplus.com/programs/1259/kiko-untuk-lola/episode/22124/untuk-lola',
        'md5': '56ed45affad45fa18d5592a1bc199997',
        'info_dict': {
            'id': 'v_e22124',
            'title': 'Untuk Lola',
            'display_id': 'untuk-lola',
            'description': 'md5:2b809075c0b1e071e228ad6d13e41deb',
            'ext': 'mp4',
            'duration': 1400,
            'timestamp': 1615978800,
            'upload_date': '20210317',
            'series': 'Kiko : Untuk Lola',
            'season_number': 1,
            'episode_number': 1,
            'channel': 'RCTI',
        },
        'params': {
            'fixup': 'never',
        },
    }, {  # Clip; Series title doesn't appear on metadata JSON
        'url': 'https://www.rctiplus.com/programs/316/cahaya-terindah/clip/3921/make-a-wish',
        'md5': 'd179b2ff356f0e91a53bcc6a4d8504f0',
        'info_dict': {
            'id': 'v_c3921',
            'title': 'Make A Wish',
            'display_id': 'make-a-wish',
            'description': 'Make A Wish',
            'ext': 'mp4',
            'duration': 288,
            'timestamp': 1571652600,
            'upload_date': '20191021',
            'series': 'Cahaya Terindah',
            'channel': 'RCTI',
        },
        'params': {
            'fixup': 'never',
        },
    }, {  # Extra
        'url': 'https://www.rctiplus.com/programs/616/inews-malam/extra/9438/diungkapkan-melalui-surat-terbuka-ceo-ruangguru-belva-devara-mundur-dari-staf-khusus-presiden',
        'md5': 'c48106afdbce609749f5e0c007d9278a',
        'info_dict': {
            'id': 'v_ex9438',
            'title': 'md5:2ede828c0f8bde249e0912be150314ca',
            'display_id': 'md5:62b8d4e9ff096db527a1ad797e8a9933',
            'description': 'md5:2ede828c0f8bde249e0912be150314ca',
            'ext': 'mp4',
            'duration': 93,
            'timestamp': 1587561540,
            'upload_date': '20200422',
            'series': 'iNews Malam',
            'channel': 'INews',
        },
    }, {  # Missed event/replay
        'url': 'https://www.rctiplus.com/missed-event/2507/mou-signing-ceremony-27-juli-2021-1400-wib',
        'md5': '649c5f27250faed1452ca8b91e06922d',
        'info_dict': {
            'id': 'v_pe2507',
            'title': 'MOU Signing Ceremony | 27 Juli 2021 | 14.00 WIB',
            'display_id': 'mou-signing-ceremony-27-juli-2021-1400-wib',
            'ext': 'mp4',
            'timestamp': 1627142400,
            'upload_date': '20210724',
            'was_live': True,
            'release_timestamp': 1627369200,
        },
        'params': {
            'fixup': 'never',
        },
    }, {  # Live event; Cloudfront CDN
        'url': 'https://www.rctiplus.com/live-event/2530/dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
        'info_dict': {
            'id': 'v_le2530',
            'title': 'Dai Muda : Charging Imun dengan Iman | 4 Agustus 2021 | 16.00 WIB',
            'display_id': 'dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
            'ext': 'mp4',
            'timestamp': 1627898400,
            'upload_date': '20210802',
            'release_timestamp': 1628067600,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'This live event has ended.',
    }, {  # TV; live_at is null
        'url': 'https://www.rctiplus.com/live-event/1/rcti',
        'info_dict': {
            'id': 'v_lt1',
            'title': 'RCTI',
            'display_id': 'rcti',
            'ext': 'mp4',
            'timestamp': 1546344000,
            'upload_date': '20190101',
            'is_live': True,
        },
        'params': {
            'skip_download': True,
        },
    }]
    _CONVIVA_JSON_TEMPLATE = {
        't': 'CwsSessionHb',
        'cid': 'ff84ae928c3b33064b76dec08f12500465e59a6f',
        'clid': '0',
        'sid': 0,
        'seq': 0,
        'caps': 0,
        'sf': 7,
        'sdk': True,
    }

    def _real_extract(self, url):
        match = self._match_valid_url(url).groupdict()
        video_type, video_id, display_id = match['type'], match['id'], match['display_id']

        url_api_version = 'v2' if video_type == 'missed-event' else 'v1'
        appier_id = '23984824_' + str(random.randint(0, 10000000000))  # Based on the webpage's uuidRandom generator
        video_json = self._call_api(
            f'https://api.rctiplus.com/api/{url_api_version}/{video_type}/{video_id}/url?appierid={appier_id}', display_id, 'Downloading video URL JSON')[0]
        video_url = video_json['url']

        is_upcoming = try_get(video_json, lambda x: x['current_date'] < x['live_at'])
        if is_upcoming is None:
            is_upcoming = try_get(video_json, lambda x: x['current_date'] < x['start_date'])
        if is_upcoming:
            self.raise_no_formats(
                'This event will start at %s.' % video_json['live_label'] if video_json.get('live_label') else 'This event has not started yet.', expected=True)
        if 'akamaized' in video_url:
            # For some videos hosted on Akamai's CDN (possibly AES-encrypted ones?), a session needs to at least be made via Conviva's API
            conviva_json_data = {
                **self._CONVIVA_JSON_TEMPLATE,
                'url': video_url,
                'sst': int(time.time())
            }
            conviva_json_res = self._download_json(
                'https://ff84ae928c3b33064b76dec08f12500465e59a6f.cws.conviva.com/0/wsg', display_id,
                'Creating Conviva session', 'Failed to create Conviva session',
                fatal=False, data=json.dumps(conviva_json_data).encode('utf-8'))
            if conviva_json_res and conviva_json_res.get('err') != 'ok':
                self.report_warning('Conviva said: %s' % str(conviva_json_res.get('err')))

        video_meta, meta_paths = self._call_api(
            'https://api.rctiplus.com/api/v1/%s/%s' % (video_type, video_id), display_id, 'Downloading video metadata')

        thumbnails, image_path = [], meta_paths.get('image_path', 'https://rstatic.akamaized.net/media/')
        if video_meta.get('portrait_image'):
            thumbnails.append({
                'id': 'portrait_image',
                'url': '%s%d%s' % (image_path, 2000, video_meta['portrait_image'])  # 2000px seems to be the highest resolution that can be given
            })
        if video_meta.get('landscape_image'):
            thumbnails.append({
                'id': 'landscape_image',
                'url': '%s%d%s' % (image_path, 2000, video_meta['landscape_image'])
            })
        try:
            formats = self._extract_m3u8_formats(video_url, display_id, 'mp4', headers={'Referer': 'https://www.rctiplus.com/'})
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
                self.raise_geo_restricted(countries=['ID'], metadata_available=True)
            else:
                raise e
        for f in formats:
            if 'akamaized' in f['url'] or 'cloudfront' in f['url']:
                f.setdefault('http_headers', {})['Referer'] = 'https://www.rctiplus.com/'  # Referer header is required for akamai/cloudfront CDNs

        self._sort_formats(formats)

        return {
            'id': video_meta.get('product_id') or video_json.get('product_id'),
            'title': dict_get(video_meta, ('title', 'name')) or dict_get(video_json, ('content_name', 'assets_name')),
            'display_id': display_id,
            'description': video_meta.get('summary'),
            'timestamp': video_meta.get('release_date') or video_json.get('start_date'),
            'duration': video_meta.get('duration'),
            'categories': [video_meta['genre']] if video_meta.get('genre') else None,
            'average_rating': video_meta.get('star_rating'),
            'series': video_meta.get('program_title') or video_json.get('program_title'),
            'season_number': video_meta.get('season'),
            'episode_number': video_meta.get('episode'),
            'channel': video_json.get('tv_name'),
            'channel_id': video_json.get('tv_id'),
            'formats': formats,
            'thumbnails': thumbnails,
            'is_live': video_type == 'live-event' and not is_upcoming,
            'was_live': video_type == 'missed-event',
            'live_status': 'is_upcoming' if is_upcoming else None,
            'release_timestamp': video_json.get('live_at'),
        }

class RCTIPlusSeriesIE(RCTIPlusBaseIE):
    _VALID_URL = r'https://www\.rctiplus\.com/programs/(?P<id>\d+)/(?P<display_id>[^/?#&]+)(?:/(?P<type>episodes|extras|clips))?'
    _TESTS = [{
        'url': 'https://www.rctiplus.com/programs/829/putri-untuk-pangeran',
        'playlist_mincount': 1019,
        'info_dict': {
            'id': '829',
            'title': 'Putri Untuk Pangeran',
            'description': 'md5:aca7b54d05bd95a67d4f4613cc1d622d',
            'age_limit': 2,
            'cast': ['Verrel Bramasta', 'Ranty Maria', 'Riza Syah', 'Ivan Fadilla', 'Nicole Parham', 'Dll', 'Aviv Elham'],
            'display_id': 'putri-untuk-pangeran',
            'tag': 'count:18',
        },
    }, {  # No episodes
        'url': 'https://www.rctiplus.com/programs/615/inews-pagi',
        'playlist_mincount': 388,
        'info_dict': {
            'id': '615',
            'title': 'iNews Pagi',
            'description': 'md5:f18ee3d4643cfb41c358e5a9b693ee04',
            'age_limit': 2,
            'tag': 'count:11',
            'display_id': 'inews-pagi',
        }
    }]
    _AGE_RATINGS = {  # Based off https://id.wikipedia.org/wiki/Sistem_rating_konten_televisi with additional ratings
        'S-SU': 2,
        'SU': 2,
        'P': 2,
        'A': 7,
        'R': 13,
        'R-R/1': 17,  # Labelled as 17+ despite being R
        'D': 18,
    }

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

    def _entries(self, url, display_id=None, note='Downloading entries JSON', metadata={}):
        total_pages = 0
        try:
            total_pages = self._call_api(
                '%s&length=20&page=0' % url,
                display_id, note)[1]['pagination']['total_page']
        except ExtractorError as e:
            if 'not found' in str(e):
                return []
            raise e
        if total_pages <= 0:
            return []

        for page_num in range(1, total_pages + 1):
            episode_list = self._call_api(
                '%s&length=20&page=%s' % (url, page_num),
                display_id, '%s page %s' % (note, page_num))[0] or []

            for video_json in episode_list:
                yield {
                    '_type': 'url',
                    'url': video_json['share_link'],
                    'ie_key': RCTIPlusIE.ie_key(),
                    'id': video_json.get('product_id'),
                    'title': video_json.get('title'),
                    'display_id': video_json.get('title_code').replace('_', '-'),
                    'description': video_json.get('summary'),
                    'timestamp': video_json.get('release_date'),
                    'duration': video_json.get('duration'),
                    'season_number': video_json.get('season'),
                    'episode_number': video_json.get('episode'),
                    **metadata
                }

    def _series_entries(self, series_id, display_id=None, video_type=None, metadata={}):
        if not video_type or video_type in 'episodes':
            try:
                seasons_list = self._call_api(
                    f'https://api.rctiplus.com/api/v1/program/{series_id}/season',
                    display_id, 'Downloading seasons list JSON')[0]
            except ExtractorError as e:
                if 'not found' not in str(e):
                    raise
                seasons_list = []
            for season in seasons_list:
                yield from self._entries(
                    f'https://api.rctiplus.com/api/v2/program/{series_id}/episode?season={season["season"]}',
                    display_id, f'Downloading season {season["season"]} episode entries', metadata)
        if not video_type or video_type in 'extras':
            yield from self._entries(
                f'https://api.rctiplus.com/api/v2/program/{series_id}/extra?content_id=0',
                display_id, 'Downloading extra entries', metadata)
        if not video_type or video_type in 'clips':
            yield from self._entries(
                f'https://api.rctiplus.com/api/v2/program/{series_id}/clip?content_id=0',
                display_id, 'Downloading clip entries', metadata)

    def _real_extract(self, url):
        series_id, display_id, video_type = self._match_valid_url(url).group('id', 'display_id', 'type')
        if video_type:
            self.report_warning(
                f'Only {video_type} will be downloaded. '
                f'To download everything from the series, remove "/{video_type}" from the URL')

        series_meta, meta_paths = self._call_api(
            f'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id, 'Downloading series metadata')
        metadata = {
            'age_limit': try_get(series_meta, lambda x: self._AGE_RATINGS[x['age_restriction'][0]['code']]),
            'cast': traverse_obj(series_meta, (('starring', 'creator', 'writer'), ..., 'name'),
                                 expected_type=lambda x: strip_or_none(x) or None),
            'tag': traverse_obj(series_meta, ('tag', ..., 'name'),
                                expected_type=lambda x: strip_or_none(x) or None),
        }
        return self.playlist_result(
            self._series_entries(series_id, display_id, video_type, metadata), series_id,
            series_meta.get('title'), series_meta.get('summary'), display_id=display_id, **metadata)

class RCTIPlusTVIE(RCTIPlusBaseIE):
    _VALID_URL = r'https://www\.rctiplus\.com/((tv/(?P<tvname>\w+))|(?P<eventname>live-event|missed-event))'
    _TESTS = [{
        'url': 'https://www.rctiplus.com/tv/rcti',
        'info_dict': {
            'id': 'v_lt1',
            'title': 'RCTI',
            'ext': 'mp4',
            'timestamp': 1546344000,
            'upload_date': '20190101',
        },
        'params': {
            'skip_download': True,
        }
    }, {
        # Returned video will always change
        'url': 'https://www.rctiplus.com/live-event',
        'only_matching': True,
    }, {
        # Returned video will also always change
        'url': 'https://www.rctiplus.com/missed-event',
        'only_matching': True,
    }]

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

    def _real_extract(self, url):
        match = self._match_valid_url(url).groupdict()
        tv_id = match.get('tvname') or match.get('eventname')
        webpage = self._download_webpage(url, tv_id)
        video_type, video_id = self._search_regex(
            r'url\s*:\s*["\']https://api\.rctiplus\.com/api/v./(?P<type>[^/]+)/(?P<id>\d+)/url',
            webpage, 'video link', group=('type', 'id'))
        return self.url_result(f'https://www.rctiplus.com/{video_type}/{video_id}/{tv_id}', 'RCTIPlus')


code:
from .common import InfoExtractor
from ..utils import (
    parse_duration,
    parse_iso8601,
    js_to_json,
)
from ..compat import compat_str

class RDSIE(InfoExtractor):
    IE_DESC = 'RDS.ca'
    _VALID_URL = r'https?://(?:www\.)?rds\.ca/vid(?:[eé]|%C3%A9)os/(?:[^/]+/)*(?P<id>[^/]+)-\d+\.\d+'

    _TESTS = [{
        # has two 9c9media ContentPackages, the web player selects the first ContentPackage
        'url': 'https://www.rds.ca/videos/Hockey/NationalHockeyLeague/teams/9/forum-du-5-a-7-jesperi-kotkaniemi-de-retour-de-finlande-3.1377606',
        'info_dict': {
            'id': '2083309',
            'display_id': 'forum-du-5-a-7-jesperi-kotkaniemi-de-retour-de-finlande',
            'ext': 'flv',
            'title': 'Forum du 5 à 7 : Kotkaniemi de retour de Finlande',
            'description': 'md5:83fa38ecc4a79b19e433433254077f25',
            'timestamp': 1606129030,
            'upload_date': '20201123',
            'duration': 773.039,
        }
    }, {
        'url': 'http://www.rds.ca/vid%C3%A9os/un-voyage-positif-3.877934',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, display_id)

        item = self._parse_json(self._search_regex(r'(?s)itemToPush\s*=\s*({.+?});', webpage, 'item'), display_id, js_to_json)
        video_id = compat_str(item['id'])
        title = item.get('title') or self._og_search_title(webpage) or self._html_search_meta(
            'title', webpage, 'title', fatal=True)
        description = self._og_search_description(webpage) or self._html_search_meta(
            'description', webpage, 'description')
        thumbnail = item.get('urlImageBig') or self._og_search_thumbnail(webpage) or self._search_regex(
            [r'<link[^>]+itemprop="thumbnailUrl"[^>]+href="([^"]+)"',
             r'<span[^>]+itemprop="thumbnailUrl"[^>]+content="([^"]+)"'],
            webpage, 'thumbnail', fatal=False)
        timestamp = parse_iso8601(self._search_regex(
            r'<span[^>]+itemprop="uploadDate"[^>]+content="([^"]+)"',
            webpage, 'upload date', fatal=False))
        duration = parse_duration(self._search_regex(
            r'<span[^>]+itemprop="duration"[^>]+content="([^"]+)"',
            webpage, 'duration', fatal=False))
        age_limit = self._family_friendly_search(webpage)

        return {
            '_type': 'url_transparent',
            'id': video_id,
            'display_id': display_id,
            'url': '9c9media:rds_web:%s' % video_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'timestamp': timestamp,
            'duration': duration,
            'age_limit': age_limit,
            'ie_key': 'NineCNineMedia',
        }