code:
import re

from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    float_or_none,
    parse_iso8601,
    str_or_none,
    try_get,
    unescapeHTML,
    url_or_none,
    ExtractorError,
)

class RteBaseIE(InfoExtractor):
    def _real_extract(self, url):
        item_id = self._match_id(url)

        info_dict = {}
        formats = []

        ENDPOINTS = (
            'https://feeds.rasset.ie/rteavgen/player/playlist?type=iptv&format=json&showId=',
            'http://www.rte.ie/rteavgen/getplaylist/?type=web&format=json&id=',
        )

        for num, ep_url in enumerate(ENDPOINTS, start=1):
            try:
                data = self._download_json(ep_url + item_id, item_id)
            except ExtractorError as ee:
                if num < len(ENDPOINTS) or formats:
                    continue
                if isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404:
                    error_info = self._parse_json(ee.cause.read().decode(), item_id, fatal=False)
                    if error_info:
                        raise ExtractorError(
                            '%s said: %s' % (self.IE_NAME, error_info['message']),
                            expected=True)
                raise

            # NB the string values in the JSON are stored using XML escaping(!)
            show = try_get(data, lambda x: x['shows'][0], dict)
            if not show:
                continue

            if not info_dict:
                title = unescapeHTML(show['title'])
                description = unescapeHTML(show.get('description'))
                thumbnail = show.get('thumbnail')
                duration = float_or_none(show.get('duration'), 1000)
                timestamp = parse_iso8601(show.get('published'))
                info_dict = {
                    'id': item_id,
                    'title': title,
                    'description': description,
                    'thumbnail': thumbnail,
                    'timestamp': timestamp,
                    'duration': duration,
                }

            mg = try_get(show, lambda x: x['media:group'][0], dict)
            if not mg:
                continue

            if mg.get('url'):
                m = re.match(r'(?P<url>rtmpe?://[^/]+)/(?P<app>.+)/(?P<playpath>mp4:.*)', mg['url'])
                if m:
                    m = m.groupdict()
                    formats.append({
                        'url': m['url'] + '/' + m['app'],
                        'app': m['app'],
                        'play_path': m['playpath'],
                        'player_url': url,
                        'ext': 'flv',
                        'format_id': 'rtmp',
                    })

            if mg.get('hls_server') and mg.get('hls_url'):
                formats.extend(self._extract_m3u8_formats(
                    mg['hls_server'] + mg['hls_url'], item_id, 'mp4',
                    entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))

            if mg.get('hds_server') and mg.get('hds_url'):
                formats.extend(self._extract_f4m_formats(
                    mg['hds_server'] + mg['hds_url'], item_id,
                    f4m_id='hds', fatal=False))

            mg_rte_server = str_or_none(mg.get('rte:server'))
            mg_url = str_or_none(mg.get('url'))
            if mg_rte_server and mg_url:
                hds_url = url_or_none(mg_rte_server + mg_url)
                if hds_url:
                    formats.extend(self._extract_f4m_formats(
                        hds_url, item_id, f4m_id='hds', fatal=False))

        self._sort_formats(formats)

        info_dict['formats'] = formats
        return info_dict

class RteIE(RteBaseIE):
    IE_NAME = 'rte'
    IE_DESC = 'Raidió Teilifís Éireann TV'
    _VALID_URL = r'https?://(?:www\.)?rte\.ie/player/[^/]{2,3}/show/[^/]+/(?P<id>[0-9]+)'
    _TEST = {
        'url': 'http://www.rte.ie/player/ie/show/iwitness-862/10478715/',
        'md5': '4a76eb3396d98f697e6e8110563d2604',
        'info_dict': {
            'id': '10478715',
            'ext': 'mp4',
            'title': 'iWitness',
            'thumbnail': r're:^https?://.*\.jpg$',
            'description': 'The spirit of Ireland, one voice and one minute at a time.',
            'duration': 60.046,
            'upload_date': '20151012',
            'timestamp': 1444694160,
        },
    }

class RteRadioIE(RteBaseIE):
    IE_NAME = 'rte:radio'
    IE_DESC = 'Raidió Teilifís Éireann radio'
    # Radioplayer URLs have two distinct specifier formats,
    # the old format #!rii=<channel_id>:<id>:<playable_item_id>:<date>:
    # the new format #!rii=b<channel_id>_<id>_<playable_item_id>_<date>_
    # where the IDs are int/empty, the date is DD-MM-YYYY, and the specifier may be truncated.
    # An <id> uniquely defines an individual recording, and is the only part we require.
    _VALID_URL = r'https?://(?:www\.)?rte\.ie/radio/utils/radioplayer/rteradioweb\.html#!rii=(?:b?[0-9]*)(?:%3A|:|%5F|_)(?P<id>[0-9]+)'

    _TESTS = [{
        # Old-style player URL; HLS and RTMPE formats
        'url': 'http://www.rte.ie/radio/utils/radioplayer/rteradioweb.html#!rii=16:10507902:2414:27-12-2015:',
        'md5': 'c79ccb2c195998440065456b69760411',
        'info_dict': {
            'id': '10507902',
            'ext': 'mp4',
            'title': 'Gloria',
            'thumbnail': r're:^https?://.*\.jpg$',
            'description': 'md5:9ce124a7fb41559ec68f06387cabddf0',
            'timestamp': 1451203200,
            'upload_date': '20151227',
            'duration': 7230.0,
        },
    }, {
        # New-style player URL; RTMPE formats only
        'url': 'http://rte.ie/radio/utils/radioplayer/rteradioweb.html#!rii=b16_3250678_8861_06-04-2012_',
        'info_dict': {
            'id': '3250678',
            'ext': 'flv',
            'title': 'The Lyric Concert with Paul Herriott',
            'thumbnail': r're:^https?://.*\.jpg$',
            'description': '',
            'timestamp': 1333742400,
            'upload_date': '20120406',
            'duration': 7199.016,
        },
        'params': {
            # rtmp download
            'skip_download': True,
        },
    }]


code:
import re

from .common import InfoExtractor
from ..aes import aes_cbc_decrypt_bytes, unpad_pkcs7
from ..compat import (
    compat_b64decode,
    compat_str,
)
from ..utils import (
    ExtractorError,
    int_or_none,
    strip_or_none,
)

class RTL2IE(InfoExtractor):
    IE_NAME = 'rtl2'
    _VALID_URL = r'https?://(?:www\.)?rtl2\.de/sendung/[^/]+/(?:video/(?P<vico_id>\d+)[^/]+/(?P<vivi_id>\d+)-|folge/)(?P<id>[^/?#]+)'
    _TESTS = [{
        'url': 'http://www.rtl2.de/sendung/grip-das-motormagazin/folge/folge-203-0',
        'info_dict': {
            'id': 'folge-203-0',
            'ext': 'f4v',
            'title': 'GRIP sucht den Sommerkönig',
            'description': 'md5:e3adbb940fd3c6e76fa341b8748b562f'
        },
        'params': {
            # rtmp download
            'skip_download': True,
        },
        'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
    }, {
        'url': 'http://www.rtl2.de/sendung/koeln-50667/video/5512-anna/21040-anna-erwischt-alex/',
        'info_dict': {
            'id': 'anna-erwischt-alex',
            'ext': 'mp4',
            'title': 'Anna erwischt Alex!',
            'description': 'Anna nimmt ihrem Vater nicht ab, dass er nicht spielt. Und tatsächlich erwischt sie ihn auf frischer Tat.'
        },
        'params': {
            # rtmp download
            'skip_download': True,
        },
        'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
    }]

    def _real_extract(self, url):
        vico_id, vivi_id, display_id = self._match_valid_url(url).groups()
        if not vico_id:
            webpage = self._download_webpage(url, display_id)

            mobj = re.search(
                r'data-collection="(?P<vico_id>\d+)"[^>]+data-video="(?P<vivi_id>\d+)"',
                webpage)
            if mobj:
                vico_id = mobj.group('vico_id')
                vivi_id = mobj.group('vivi_id')
            else:
                vico_id = self._html_search_regex(
                    r'vico_id\s*:\s*([0-9]+)', webpage, 'vico_id')
                vivi_id = self._html_search_regex(
                    r'vivi_id\s*:\s*([0-9]+)', webpage, 'vivi_id')

        info = self._download_json(
            'https://service.rtl2.de/api-player-vipo/video.php',
            display_id, query={
                'vico_id': vico_id,
                'vivi_id': vivi_id,
            })
        video_info = info['video']
        title = video_info['titel']

        formats = []

        rtmp_url = video_info.get('streamurl')
        if rtmp_url:
            rtmp_url = rtmp_url.replace('\\', '')
            stream_url = 'mp4:' + self._html_search_regex(r'/ondemand/(.+)', rtmp_url, 'stream URL')
            rtmp_conn = ['S:connect', 'O:1', 'NS:pageUrl:' + url, 'NB:fpad:0', 'NN:videoFunction:1', 'O:0']

            formats.append({
                'format_id': 'rtmp',
                'url': rtmp_url,
                'play_path': stream_url,
                'player_url': 'https://www.rtl2.de/sites/default/modules/rtl2/jwplayer/jwplayer-7.6.0/jwplayer.flash.swf',
                'page_url': url,
                'flash_version': 'LNX 11,2,202,429',
                'rtmp_conn': rtmp_conn,
                'no_resume': True,
                'quality': 1,
            })

        m3u8_url = video_info.get('streamurl_hls')
        if m3u8_url:
            formats.extend(self._extract_akamai_formats(m3u8_url, display_id))

        self._sort_formats(formats)

        return {
            'id': display_id,
            'title': title,
            'thumbnail': video_info.get('image'),
            'description': video_info.get('beschreibung'),
            'duration': int_or_none(video_info.get('duration')),
            'formats': formats,
        }

class RTL2YouBaseIE(InfoExtractor):
    _BACKWERK_BASE_URL = 'https://p-you-backwerk.rtl2apps.de/'

class RTL2YouIE(RTL2YouBaseIE):
    IE_NAME = 'rtl2:you'
    _VALID_URL = r'http?://you\.rtl2\.de/(?:video/\d+/|youplayer/index\.html\?.*?\bvid=)(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://you.rtl2.de/video/3002/15740/MJUNIK%20%E2%80%93%20Home%20of%20YOU/307-hirn-wo-bist-du',
        'info_dict': {
            'id': '15740',
            'ext': 'mp4',
            'title': 'MJUNIK – Home of YOU - #307 Hirn, wo bist du?!',
            'description': 'md5:ddaa95c61b372b12b66e115b2772fe01',
            'age_limit': 12,
        },
    }, {
        'url': 'http://you.rtl2.de/youplayer/index.html?vid=15712',
        'only_matching': True,
    }]
    _AES_KEY = b'\xe9W\xe4.<*\xb8\x1a\xd2\xb6\x92\xf3C\xd3\xefL\x1b\x03*\xbbbH\xc0\x03\xffo\xc2\xf2(\xaa\xaa!'
    _GEO_COUNTRIES = ['DE']

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

        stream_data = self._download_json(
            self._BACKWERK_BASE_URL + 'stream/video/' + video_id, video_id)

        data, iv = compat_b64decode(stream_data['streamUrl']).decode().split(':')
        stream_url = unpad_pkcs7(aes_cbc_decrypt_bytes(
            compat_b64decode(data), self._AES_KEY, compat_b64decode(iv)))
        if b'rtl2_you_video_not_found' in stream_url:
            raise ExtractorError('video not found', expected=True)

        formats = self._extract_m3u8_formats(stream_url.decode(), video_id, 'mp4', 'm3u8_native')
        self._sort_formats(formats)

        video_data = self._download_json(
            self._BACKWERK_BASE_URL + 'video/' + video_id, video_id)

        series = video_data.get('formatTitle')
        title = episode = video_data.get('title') or series
        if series and series != title:
            title = '%s - %s' % (series, title)

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'description': strip_or_none(video_data.get('description')),
            'thumbnail': video_data.get('image'),
            'duration': int_or_none(stream_data.get('duration') or video_data.get('duration'), 1000),
            'series': series,
            'episode': episode,
            'age_limit': int_or_none(video_data.get('minimumAge')),
        }

class RTL2YouSeriesIE(RTL2YouBaseIE):
    IE_NAME = 'rtl2:you:series'
    _VALID_URL = r'http?://you\.rtl2\.de/videos/(?P<id>\d+)'
    _TEST = {
        'url': 'http://you.rtl2.de/videos/115/dragon-ball',
        'info_dict': {
            'id': '115',
        },
        'playlist_mincount': 5,
    }

    def _real_extract(self, url):
        series_id = self._match_id(url)
        stream_data = self._download_json(
            self._BACKWERK_BASE_URL + 'videos',
            series_id, query={
                'formatId': series_id,
                'limit': 1000000000,
            })

        entries = []
        for video in stream_data.get('videos', []):
            video_id = compat_str(video['videoId'])
            if not video_id:
                continue
            entries.append(self.url_result(
                'http://you.rtl2.de/video/%s/%s' % (series_id, video_id),
                'RTL2You', video_id))
        return self.playlist_result(entries, series_id)

repo_name: Lesmiscore/ytdl


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

class RtlNlIE(InfoExtractor):
    IE_NAME = 'rtl.nl'
    IE_DESC = 'rtl.nl and rtlxl.nl'
    _EMBED_REGEX = [r'<iframe[^>]+?\bsrc=(?P<q1>[\'"])(?P<url>(?:https?:)?//(?:(?:www|static)\.)?rtl\.nl/(?:system/videoplayer/[^"]+(?:video_)?)?embed[^"]+)(?P=q1)']
    _VALID_URL = r'''(?x)
        https?://(?:(?:www|static)\.)?
        (?:
            rtlxl\.nl/(?:[^\#]*\#!|programma)/[^/]+/|
            rtl\.nl/(?:(?:system/videoplayer/(?:[^/]+/)+(?:video_)?embed\.html|embed)\b.+?\buuid=|video/)|
            embed\.rtl\.nl/\#uuid=
        )
        (?P<id>[0-9a-f-]+)'''

    _TESTS = [{
        # new URL schema
        'url': 'https://www.rtlxl.nl/programma/rtl-nieuws/0bd1384d-d970-3086-98bb-5c104e10c26f',
        'md5': '490428f1187b60d714f34e1f2e3af0b6',
        'info_dict': {
            'id': '0bd1384d-d970-3086-98bb-5c104e10c26f',
            'ext': 'mp4',
            'title': 'RTL Nieuws',
            'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
            'timestamp': 1593293400,
            'upload_date': '20200627',
            'duration': 661.08,
        },
    }, {
        # old URL schema
        'url': 'http://www.rtlxl.nl/#!/rtl-nieuws-132237/82b1aad1-4a14-3d7b-b554-b0aed1b2c416',
        'md5': '473d1946c1fdd050b2c0161a4b13c373',
        'info_dict': {
            'id': '82b1aad1-4a14-3d7b-b554-b0aed1b2c416',
            'ext': 'mp4',
            'title': 'RTL Nieuws',
            'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
            'timestamp': 1461951000,
            'upload_date': '20160429',
            'duration': 1167.96,
        },
        'skip': '404',
    }, {
        # best format available a3t
        'url': 'http://www.rtl.nl/system/videoplayer/derden/rtlnieuws/video_embed.html#uuid=84ae5571-ac25-4225-ae0c-ef8d9efb2aed/autoplay=false',
        'md5': 'dea7474214af1271d91ef332fb8be7ea',
        'info_dict': {
            'id': '84ae5571-ac25-4225-ae0c-ef8d9efb2aed',
            'ext': 'mp4',
            'timestamp': 1424039400,
            'title': 'RTL Nieuws - Nieuwe beelden Kopenhagen: chaos direct na aanslag',
            'thumbnail': r're:^https?://screenshots\.rtl\.nl/(?:[^/]+/)*sz=[0-9]+x[0-9]+/uuid=84ae5571-ac25-4225-ae0c-ef8d9efb2aed$',
            'upload_date': '20150215',
            'description': 'Er zijn nieuwe beelden vrijgegeven die vlak na de aanslag in Kopenhagen zijn gemaakt. Op de video is goed te zien hoe omstanders zich bekommeren om één van de slachtoffers, terwijl de eerste agenten ter plaatse komen.',
        }
    }, {
        # empty synopsis and missing episodes (see https://github.com/ytdl-org/youtube-dl/issues/6275)
        # best format available nettv
        'url': 'http://www.rtl.nl/system/videoplayer/derden/rtlnieuws/video_embed.html#uuid=f536aac0-1dc3-4314-920e-3bd1c5b3811a/autoplay=false',
        'info_dict': {
            'id': 'f536aac0-1dc3-4314-920e-3bd1c5b3811a',
            'ext': 'mp4',
            'title': 'RTL Nieuws - Meer beelden van overval juwelier',
            'thumbnail': r're:^https?://screenshots\.rtl\.nl/(?:[^/]+/)*sz=[0-9]+x[0-9]+/uuid=f536aac0-1dc3-4314-920e-3bd1c5b3811a$',
            'timestamp': 1437233400,
            'upload_date': '20150718',
            'duration': 30.474,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # encrypted m3u8 streams, georestricted
        'url': 'http://www.rtlxl.nl/#!/afl-2-257632/52a74543-c504-4cde-8aa8-ec66fe8d68a7',
        'only_matching': True,
    }, {
        'url': 'http://www.rtl.nl/system/videoplayer/derden/embed.html#!/uuid=bb0353b0-d6a4-1dad-90e9-18fe75b8d1f0',
        'only_matching': True,
    }, {
        'url': 'http://rtlxl.nl/?_ga=1.204735956.572365465.1466978370#!/rtl-nieuws-132237/3c487912-023b-49ac-903e-2c5d79f8410f',
        'only_matching': True,
    }, {
        'url': 'https://www.rtl.nl/video/c603c9c2-601d-4b5e-8175-64f1e942dc7d/',
        'only_matching': True,
    }, {
        'url': 'https://static.rtl.nl/embed/?uuid=1a2970fc-5c0b-43ff-9fdc-927e39e6d1bc&autoplay=false&publicatiepunt=rtlnieuwsnl',
        'only_matching': True,
    }, {
        # new embed URL schema
        'url': 'https://embed.rtl.nl/#uuid=84ae5571-ac25-4225-ae0c-ef8d9efb2aed/autoplay=false',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        uuid = self._match_id(url)
        info = self._download_json(
            'http://www.rtl.nl/system/s4m/vfd/version=2/uuid=%s/fmt=adaptive/' % uuid,
            uuid)

        material = info['material'][0]
        title = info['abstracts'][0]['name']
        subtitle = material.get('title')
        if subtitle:
            title += ' - %s' % subtitle
        description = material.get('synopsis')

        meta = info.get('meta', {})

        videopath = material['videopath']
        m3u8_url = meta.get('videohost', 'http://manifest.us.rtl.nl') + videopath

        formats = self._extract_m3u8_formats(
            m3u8_url, uuid, 'mp4', m3u8_id='hls', fatal=False)
        self._sort_formats(formats)

        thumbnails = []

        for p in ('poster_base_url', '"thumb_base_url"'):
            if not meta.get(p):
                continue

            thumbnails.append({
                'url': self._proto_relative_url(meta[p] + uuid),
                'width': int_or_none(self._search_regex(
                    r'/sz=([0-9]+)', meta[p], 'thumbnail width', fatal=False)),
                'height': int_or_none(self._search_regex(
                    r'/sz=[0-9]+x([0-9]+)',
                    meta[p], 'thumbnail height', fatal=False))
            })

        return {
            'id': uuid,
            'title': title,
            'formats': formats,
            'timestamp': material['original_date'],
            'description': description,
            'duration': parse_duration(material.get('duration')),
            'thumbnails': thumbnails,
        }

class RTLLuBaseIE(InfoExtractor):
    _MEDIA_REGEX = {
        'video': r'<rtl-player\s[^>]*\bhls\s*=\s*"([^"]+)',
        'audio': r'<rtl-audioplayer\s[^>]*\bsrc\s*=\s*"([^"]+)',
        'thumbnail': r'<rtl-player\s[^>]*\bposter\s*=\s*"([^"]+)',
    }

    def get_media_url(self, webpage, video_id, media_type):
        return self._search_regex(self._MEDIA_REGEX[media_type], webpage, f'{media_type} url', default=None)

    def get_formats_and_subtitles(self, webpage, video_id):
        video_url, audio_url = self.get_media_url(webpage, video_id, 'video'), self.get_media_url(webpage, video_id, 'audio')

        formats, subtitles = [], {}
        if video_url is not None:
            formats, subtitles = self._extract_m3u8_formats_and_subtitles(video_url, video_id)
        if audio_url is not None:
            formats.append({'url': audio_url, 'ext': 'mp3', 'vcodec': 'none'})

        return formats, subtitles

    def _real_extract(self, url):
        video_id = self._match_id(url)
        is_live = video_id in ('live', 'live-2', 'lauschteren')

        # TODO: extract comment from https://www.rtl.lu/comments?status=1&order=desc&context=news|article|<video_id>
        # we can context from <rtl-comments context=<context> in webpage
        webpage = self._download_webpage(url, video_id)

        formats, subtitles = self.get_formats_and_subtitles(webpage, video_id)
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': self._og_search_title(webpage),
            'description': self._og_search_description(webpage, default=None),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnail': self.get_media_url(webpage, video_id, 'thumbnail') or self._og_search_thumbnail(webpage, default=None),
            'is_live': is_live,
        }

class RTLLuTeleVODIE(RTLLuBaseIE):
    IE_NAME = 'rtl.lu:tele-vod'
    _VALID_URL = r'https?://(?:www\.)?rtl\.lu/(tele/(?P<slug>[\w-]+)/v/|video/)(?P<id>\d+)(\.html)?'
    _TESTS = [{
        'url': 'https://www.rtl.lu/tele/de-journal-vun-der-tele/v/3266757.html',
        'info_dict': {
            'id': '3266757',
            'title': 'Informatiounsversammlung Héichwaasser',
            'ext': 'mp4',
            'thumbnail': 'https://replay-assets.rtl.lu/2021/11/16/d3647fc4-470d-11ec-adc2-3a00abd6e90f_00008.jpg',
            'description': 'md5:b1db974408cc858c9fd241812e4a2a14',
        }
    }, {
        'url': 'https://www.rtl.lu/video/3295215',
        'info_dict': {
            'id': '3295215',
            'title': 'Kulturassisen iwwer d\'Bestandsopnam vum Lëtzebuerger Konscht',
            'ext': 'mp4',
            'thumbnail': 'https://replay-assets.rtl.lu/2022/06/28/0000_3295215_0000.jpg',
            'description': 'md5:85bcd4e0490aa6ec969d9bf16927437b',
        }
    }]

class RTLLuArticleIE(RTLLuBaseIE):
    IE_NAME = 'rtl.lu:article'
    _VALID_URL = r'https?://(?:(www|5minutes|today)\.)rtl\.lu/(?:[\w-]+)/(?:[\w-]+)/a/(?P<id>\d+)\.html'
    _TESTS = [{
        # Audio-only
        'url': 'https://www.rtl.lu/sport/news/a/1934360.html',
        'info_dict': {
            'id': '1934360',
            'ext': 'mp3',
            'thumbnail': 'https://static.rtl.lu/rtl2008.lu/nt/p/2022/06/28/19/e4b37d66ddf00bab4c45617b91a5bb9b.jpeg',
            'description': 'md5:5eab4a2a911c1fff7efc1682a38f9ef7',
            'title': 'md5:40aa85f135578fbd549d3c9370321f99',
        }
    }, {
        # 5minutes
        'url': 'https://5minutes.rtl.lu/espace-frontaliers/frontaliers-en-questions/a/1853173.html',
        'info_dict': {
            'id': '1853173',
            'ext': 'mp4',
            'description': 'md5:ac031da0740e997a5cf4633173634fee',
            'title': 'md5:87e17722ed21af0f24be3243f4ec0c46',
            'thumbnail': 'https://replay-assets.rtl.lu/2022/01/26/screenshot_20220126104933_3274749_12b249833469b0d6e4440a1dec83cdfa.jpg',
        }
    }, {
        # today.lu
        'url': 'https://today.rtl.lu/entertainment/news/a/1936203.html',
        'info_dict': {
            'id': '1936203',
            'ext': 'mp4',
            'title': 'Once Upon A Time...zu Lëtzebuerg: The Three Witches\' Tower',
            'description': 'The witchy theme continues in the latest episode of Once Upon A Time...',
            'thumbnail': 'https://replay-assets.rtl.lu/2022/07/02/screenshot_20220702122859_3290019_412dc5185951b7f6545a4039c8be9235.jpg',
        }
    }]

class RTLLuLiveIE(RTLLuBaseIE):
    _VALID_URL = r'https?://www\.rtl\.lu/(?:tele|radio)/(?P<id>live(?:-\d+)?|lauschteren)'
    _TESTS = [{
        # Tele:live
        'url': 'https://www.rtl.lu/tele/live',
        'info_dict': {
            'id': 'live',
            'ext': 'mp4',
            'live_status': 'is_live',
            'title': r're:RTL - Télé LIVE \d{4}-\d{2}-\d{2} \d{2}:\d{2}',
            'thumbnail': 'https://static.rtl.lu/livestream/channel1.jpg',
        }
    }, {
        # Tele:live-2
        'url': 'https://www.rtl.lu/tele/live-2',
        'info_dict': {
            'id': 'live-2',
            'ext': 'mp4',
            'live_status': 'is_live',
            'title': r're:RTL - Télé LIVE \d{4}-\d{2}-\d{2} \d{2}:\d{2}',
            'thumbnail': 'https://static.rtl.lu/livestream/channel2.jpg',
        }
    }, {
        # Radio:lauschteren
        'url': 'https://www.rtl.lu/radio/lauschteren',
        'info_dict': {
            'id': 'lauschteren',
            'ext': 'mp4',
            'live_status': 'is_live',
            'title': r're:RTL - Radio LIVE \d{4}-\d{2}-\d{2} \d{2}:\d{2}',
            'thumbnail': 'https://static.rtl.lu/livestream/rtlradiowebtv.jpg',
        }
    }]

class RTLLuRadioIE(RTLLuBaseIE):
    _VALID_URL = r'https?://www\.rtl\.lu/radio/(?:[\w-]+)/s/(?P<id>\d+)(\.html)?'
    _TESTS = [{
        'url': 'https://www.rtl.lu/radio/5-vir-12/s/4033058.html',
        'info_dict': {
            'id': '4033058',
            'ext': 'mp3',
            'description': 'md5:f855a4f3e3235393ae47ed1db5d934b9',
            'title': '5 vir 12 - Stau um Stau',
            'thumbnail': 'https://static.rtl.lu/rtlg//2022/06/24/c9c19e5694a14be46a3647a3760e1f62.jpg',
        }
    }]


code:
import re

from .common import InfoExtractor
from ..utils import js_to_json

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

    _TESTS = [{
        'url': 'https://www.rt.com/sport/546301-djokovic-arrives-belgrade-crowds/',
        'playlist_mincount': 2,
        'info_dict': {
            'id': '546301',
            'title': 'Crowds gather to greet deported Djokovic as he returns to Serbia (VIDEO)',
            'description': 'md5:1d5bfe1a988d81fd74227cfdf93d314d',
            'thumbnail': 'https://cdni.rt.com/files/2022.01/article/61e587a085f540102c3386c1.png'
        },
    }, {
        'url': 'https://www.rt.com/shows/in-question/535980-plot-to-assassinate-julian-assange/',
        'playlist_mincount': 1,
        'info_dict': {
            'id': '535980',
            'title': 'The plot to assassinate Julian Assange',
            'description': 'md5:55279ce5e4441dc1d16e2e4a730152cd',
            'thumbnail': 'https://cdni.rt.com/files/2021.09/article/615226f42030274e8879b53d.png'
        },
        'playlist': [{
            'info_dict': {
                'id': '6152271d85f5400464496162',
                'ext': 'mp4',
                'title': '6152271d85f5400464496162',
            },
        }]
    }]

    def _entries(self, webpage):
        video_urls = set(re.findall(r'https://cdnv\.rt\.com/.*[a-f0-9]+\.mp4', webpage))
        for v_url in video_urls:
            v_id = re.search(r'([a-f0-9]+)\.mp4', v_url).group(1)
            if v_id:
                yield {
                    'id': v_id,
                    'title': v_id,
                    'url': v_url,
                }

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

        return {
            '_type': 'playlist',
            'id': id,
            'entries': self._entries(webpage),
            'title': self._og_search_title(webpage),
            'description': self._og_search_description(webpage),
            'thumbnail': self._og_search_thumbnail(webpage),
        }

class RTDocumentryIE(InfoExtractor):
    _VALID_URL = r'https?://rtd\.rt\.com/(?:(?:series|shows)/[^/]+|films)/(?P<id>[^/?$&#]+)'

    _TESTS = [{
        'url': 'https://rtd.rt.com/films/escobars-hitman/',
        'info_dict': {
            'id': 'escobars-hitman',
            'ext': 'mp4',
            'title': "Escobar's Hitman. Former drug-gang killer, now loved and loathed in Colombia",
            'description': 'md5:647c76984b7cb9a8b52a567e87448d88',
            'thumbnail': 'https://cdni.rt.com/rtd-files/films/escobars-hitman/escobars-hitman_11.jpg',
            'average_rating': 8.53,
            'duration': 3134.0
        },
        'params': {'skip_download': True}
    }, {
        'url': 'https://rtd.rt.com/shows/the-kalashnikova-show-military-secrets-anna-knishenko/iskander-tactical-system-natos-headache/',
        'info_dict': {
            'id': 'iskander-tactical-system-natos-headache',
            'ext': 'mp4',
            'title': "Iskander tactical system. NATO's headache | The Kalashnikova Show. Episode 10",
            'description': 'md5:da7c24a0aa67bc2bb88c86658508ca87',
            'thumbnail': 'md5:89de8ce38c710b7c501ff02d47e2aa89',
            'average_rating': 9.27,
            'duration': 274.0,
            'timestamp': 1605726000,
            'view_count': int,
            'upload_date': '20201118'
        },
        'params': {'skip_download': True}
    }, {
        'url': 'https://rtd.rt.com/series/i-am-hacked-trailer/introduction-to-safe-digital-life-ep2/',
        'info_dict': {
            'id': 'introduction-to-safe-digital-life-ep2',
            'ext': 'mp4',
            'title': 'How to Keep your Money away from Hackers | I am Hacked. Episode 2',
            'description': 'md5:c46fa9a5af86c0008c45a3940a8cce87',
            'thumbnail': 'md5:a5e81b9bf5aed8f5e23d9c053601b825',
            'average_rating': 10.0,
            'duration': 1524.0,
            'timestamp': 1636977600,
            'view_count': int,
            'upload_date': '20211115'
        },
        'params': {'skip_download': True}
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        ld_json = self._search_json_ld(webpage, None, fatal=False)
        if not ld_json:
            self.raise_no_formats('No video/audio found at the provided url.', expected=True)
        media_json = self._parse_json(
            self._search_regex(r'(?s)\'Med\'\s*:\s*\[\s*({.+})\s*\]\s*};', webpage, 'media info'),
            id, transform_source=js_to_json)
        if 'title' not in ld_json and 'title' in media_json:
            ld_json['title'] = media_json['title']
        formats = [{'url': src['file']} for src in media_json.get('sources') or [] if src.get('file')]

        return {
            'id': id,
            'thumbnail': media_json.get('image'),
            'formats': formats,
            **ld_json
        }

class RTDocumentryPlaylistIE(InfoExtractor):
    _VALID_URL = r'https?://rtd\.rt\.com/(?:series|shows)/(?P<id>[^/]+)/$'

    _TESTS = [{
        'url': 'https://rtd.rt.com/series/i-am-hacked-trailer/',
        'playlist_mincount': 6,
        'info_dict': {
            'id': 'i-am-hacked-trailer',
        },
    }, {
        'url': 'https://rtd.rt.com/shows/the-kalashnikova-show-military-secrets-anna-knishenko/',
        'playlist_mincount': 34,
        'info_dict': {
            'id': 'the-kalashnikova-show-military-secrets-anna-knishenko',
        },
    }]

    def _entries(self, webpage, id):
        video_urls = set(re.findall(r'list-2__link\s*"\s*href="([^"]+)"', webpage))
        for v_url in video_urls:
            if id not in v_url:
                continue
            yield self.url_result(
                'https://rtd.rt.com%s' % v_url,
                ie=RTDocumentryIE.ie_key())

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

        return {
            '_type': 'playlist',
            'id': id,
            'entries': self._entries(webpage, id),
        }

class RuptlyIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?ruptly\.tv/[a-z]{2}/videos/(?P<id>\d+-\d+)'

    _TESTS = [{
        'url': 'https://www.ruptly.tv/en/videos/20220112-020-Japan-Double-trouble-Tokyo-zoo-presents-adorable-panda-twins',
        'info_dict': {
            'id': '20220112-020',
            'ext': 'mp4',
            'title': 'Japan: Double trouble! Tokyo zoo presents adorable panda twins | Video Ruptly',
            'description': 'md5:85a8da5fdb31486f0562daf4360ce75a',
            'thumbnail': 'https://storage.ruptly.tv/thumbnails/20220112-020/i6JQKnTNpYuqaXsR/i6JQKnTNpYuqaXsR.jpg'
        },
        'params': {'skip_download': True}
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        m3u8_url = self._search_regex(r'preview_url"\s?:\s?"(https?://storage\.ruptly\.tv/video_projects/.+\.m3u8)"', webpage, 'm3u8 url', fatal=False)
        if not m3u8_url:
            self.raise_no_formats('No video/audio found at the provided url.', expected=True)
        formats, subs = self._extract_m3u8_formats_and_subtitles(m3u8_url, id, ext='mp4')
        return {
            'id': id,
            'formats': formats,
            'subtitles': subs,
            'title': self._og_search_title(webpage),
            'description': self._og_search_description(webpage),
            'thumbnail': self._og_search_thumbnail(webpage),
        }


code:
from .common import InfoExtractor
from ..utils import js_to_json
import re
import json
import urllib.parse
import base64

class RTPIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?rtp\.pt/play/p(?P<program_id>[0-9]+)/(?P<id>[^/?#]+)/?'
    _TESTS = [{
        'url': 'http://www.rtp.pt/play/p405/e174042/paixoes-cruzadas',
        'md5': 'e736ce0c665e459ddb818546220b4ef8',
        'info_dict': {
            'id': 'e174042',
            'ext': 'mp3',
            'title': 'Paixões Cruzadas',
            'description': 'As paixões musicais de António Cartaxo e António Macedo',
            'thumbnail': r're:^https?://.*\.jpg',
        },
    }, {
        'url': 'http://www.rtp.pt/play/p831/a-quimica-das-coisas',
        'only_matching': True,
    }]

    _RX_OBFUSCATION = re.compile(r'''(?xs)
        atob\s*\(\s*decodeURIComponent\s*\(\s*
            (\[[0-9A-Za-z%,'"]*\])
        \s*\.\s*join\(\s*(?:""|'')\s*\)\s*\)\s*\)
    ''')

    def __unobfuscate(self, data, *, video_id):
        if data.startswith('{'):
            data = self._RX_OBFUSCATION.sub(
                lambda m: json.dumps(
                    base64.b64decode(urllib.parse.unquote(
                        ''.join(self._parse_json(m.group(1), video_id))
                    )).decode('iso-8859-1')),
                data)
        return js_to_json(data)

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

        webpage = self._download_webpage(url, video_id)
        title = self._html_search_meta(
            'twitter:title', webpage, display_name='title', fatal=True)

        f, config = self._search_regex(
            r'''(?sx)
                var\s+f\s*=\s*(?P<f>".*?"|{[^;]+?});\s*
                var\s+player1\s+=\s+new\s+RTPPlayer\s*\((?P<config>{(?:(?!\*/).)+?})\);(?!\s*\*/)
            ''', webpage,
            'player config', group=('f', 'config'))

        f = self._parse_json(
            f, video_id,
            lambda data: self.__unobfuscate(data, video_id=video_id))
        config = self._parse_json(
            config, video_id,
            lambda data: self.__unobfuscate(data, video_id=video_id))

        formats = []
        if isinstance(f, dict):
            f_hls = f.get('hls')
            if f_hls is not None:
                formats.extend(self._extract_m3u8_formats(
                    f_hls, video_id, 'mp4', 'm3u8_native', m3u8_id='hls'))

            f_dash = f.get('dash')
            if f_dash is not None:
                formats.extend(self._extract_mpd_formats(f_dash, video_id, mpd_id='dash'))
        else:
            formats.append({
                'format_id': 'f',
                'url': f,
                'vcodec': 'none' if config.get('mediaType') == 'audio' else None,
            })

        subtitles = {}

        vtt = config.get('vtt')
        if vtt is not None:
            for lcode, lname, url in vtt:
                subtitles.setdefault(lcode, []).append({
                    'name': lname,
                    'url': url,
                })

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'description': self._html_search_meta(['description', 'twitter:description'], webpage),
            'thumbnail': config.get('poster') or self._og_search_thumbnail(webpage),
            'subtitles': subtitles,
        }


code:
from .common import InfoExtractor

class RTRFMIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?rtrfm\.com\.au/(?:shows|show-episode)/(?P<id>[^/?\#&]+)'
    _TESTS = [
        {
            'url': 'https://rtrfm.com.au/shows/breakfast/',
            'md5': '46168394d3a5ce237cf47e85d0745413',
            'info_dict': {
                'id': 'breakfast-2021-11-16',
                'ext': 'mp3',
                'series': 'Breakfast with Taylah',
                'title': r're:^Breakfast with Taylah \d{4}-\d{2}-\d{2}$',
                'description': 'md5:0979c3ab1febfbec3f1ccb743633c611',
            },
            'skip': 'ID and md5 changes daily',
        },
        {
            'url': 'https://rtrfm.com.au/show-episode/breakfast-2021-11-11/',
            'md5': '396bedf1e40f96c62b30d4999202a790',
            'info_dict': {
                'id': 'breakfast-2021-11-11',
                'ext': 'mp3',
                'series': 'Breakfast with Taylah',
                'title': 'Breakfast with Taylah 2021-11-11',
                'description': 'md5:0979c3ab1febfbec3f1ccb743633c611',
            },
        },
        {
            'url': 'https://rtrfm.com.au/show-episode/breakfast-2020-06-01/',
            'md5': '594027f513ec36a24b15d65007a24dff',
            'info_dict': {
                'id': 'breakfast-2020-06-01',
                'ext': 'mp3',
                'series': 'Breakfast with Taylah',
                'title': 'Breakfast with Taylah 2020-06-01',
                'description': r're:^Breakfast with Taylah ',
            },
            'skip': 'This audio has expired',
        },
    ]

    def _real_extract(self, url):
        display_id = self._match_id(url)
        webpage = self._download_webpage(url, display_id)
        show, date, title = self._search_regex(
            r'''\.playShow(?:From)?\(['"](?P<show>[^'"]+)['"],\s*['"](?P<date>[0-9]{4}-[0-9]{2}-[0-9]{2})['"],\s*['"](?P<title>[^'"]+)['"]''',
            webpage, 'details', group=('show', 'date', 'title'))
        url = self._download_json(
            'https://restreams.rtrfm.com.au/rzz',
            show, 'Downloading MP3 URL', query={'n': show, 'd': date})['u']
        # This is the only indicator of an error until trying to download the URL and
        # downloads of mp4 URLs always fail (403 for current episodes, 404 for missing).
        if '.mp4' in url:
            url = None
            self.raise_no_formats('Expired or no episode on this date', expected=True)
        return {
            'id': '%s-%s' % (show, date),
            'title': '%s %s' % (title, date),
            'series': title,
            'url': url,
            'release_date': date,
            'description': self._og_search_description(webpage),
        }


code:
import re

from .srgssr import SRGSSRIE
from ..compat import compat_str
from ..utils import (
    determine_ext,
    int_or_none,
    parse_duration,
    parse_iso8601,
    unescapeHTML,
    urljoin,
)

class RTSIE(SRGSSRIE):
    IE_DESC = 'RTS.ch'
    _VALID_URL = r'rts:(?P<rts_id>\d+)|https?://(?:.+?\.)?rts\.ch/(?:[^/]+/){2,}(?P<id>[0-9]+)-(?P<display_id>.+?)\.html'

    _TESTS = [
        {
            'url': 'http://www.rts.ch/archives/tv/divers/3449373-les-enfants-terribles.html',
            'md5': '753b877968ad8afaeddccc374d4256a5',
            'info_dict': {
                'id': '3449373',
                'display_id': 'les-enfants-terribles',
                'ext': 'mp4',
                'duration': 1488,
                'title': 'Les Enfants Terribles',
                'description': 'France Pommier et sa soeur Luce Feral, les deux filles de ce groupe de 5.',
                'uploader': 'Divers',
                'upload_date': '19680921',
                'timestamp': -40280400,
                'thumbnail': r're:^https?://.*\.image',
                'view_count': int,
            },
            'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
        },
        {
            'url': 'http://www.rts.ch/emissions/passe-moi-les-jumelles/5624067-entre-ciel-et-mer.html',
            'info_dict': {
                'id': '5624065',
                'title': 'Passe-moi les jumelles',
            },
            'playlist_mincount': 4,
        },
        {
            'url': 'http://www.rts.ch/video/sport/hockey/5745975-1-2-kloten-fribourg-5-2-second-but-pour-gotteron-par-kwiatowski.html',
            'info_dict': {
                'id': '5745975',
                'display_id': '1-2-kloten-fribourg-5-2-second-but-pour-gotteron-par-kwiatowski',
                'ext': 'mp4',
                'duration': 48,
                'title': '1/2, Kloten - Fribourg (5-2): second but pour Gottéron par Kwiatowski',
                'description': 'Hockey - Playoff',
                'uploader': 'Hockey',
                'upload_date': '20140403',
                'timestamp': 1396556882,
                'thumbnail': r're:^https?://.*\.image',
                'view_count': int,
            },
            'params': {
                # m3u8 download
                'skip_download': True,
            },
            'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
            'skip': 'Blocked outside Switzerland',
        },
        {
            'url': 'http://www.rts.ch/video/info/journal-continu/5745356-londres-cachee-par-un-epais-smog.html',
            'md5': '9bb06503773c07ce83d3cbd793cebb91',
            'info_dict': {
                'id': '5745356',
                'display_id': 'londres-cachee-par-un-epais-smog',
                'ext': 'mp4',
                'duration': 33,
                'title': 'Londres cachée par un épais smog',
                'description': 'Un important voile de smog recouvre Londres depuis mercredi, provoqué par la pollution et du sable du Sahara.',
                'uploader': 'L\'actu en vidéo',
                'upload_date': '20140403',
                'timestamp': 1396537322,
                'thumbnail': r're:^https?://.*\.image',
                'view_count': int,
            },
            'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
        },
        {
            'url': 'http://www.rts.ch/audio/couleur3/programmes/la-belle-video-de-stephane-laurenceau/5706148-urban-hippie-de-damien-krisl-03-04-2014.html',
            'md5': 'dd8ef6a22dff163d063e2a52bc8adcae',
            'info_dict': {
                'id': '5706148',
                'display_id': 'urban-hippie-de-damien-krisl-03-04-2014',
                'ext': 'mp3',
                'duration': 123,
                'title': '"Urban Hippie", de Damien Krisl',
                'description': 'Des Hippies super glam.',
                'upload_date': '20140403',
                'timestamp': 1396551600,
            },
        },
        {
            # article with videos on rhs
            'url': 'http://www.rts.ch/sport/hockey/6693917-hockey-davos-decroche-son-31e-titre-de-champion-de-suisse.html',
            'info_dict': {
                'id': '6693917',
                'title': 'Hockey: Davos décroche son 31e titre de champion de Suisse',
            },
            'playlist_mincount': 5,
        },
        {
            'url': 'http://pages.rts.ch/emissions/passe-moi-les-jumelles/5624065-entre-ciel-et-mer.html',
            'only_matching': True,
        }
    ]

    def _real_extract(self, url):
        m = self._match_valid_url(url)
        media_id = m.group('rts_id') or m.group('id')
        display_id = m.group('display_id') or media_id

        def download_json(internal_id):
            return self._download_json(
                'http://www.rts.ch/a/%s.html?f=json/article' % internal_id,
                display_id)

        all_info = download_json(media_id)

        # media_id extracted out of URL is not always a real id
        if 'video' not in all_info and 'audio' not in all_info:
            entries = []

            for item in all_info.get('items', []):
                item_url = item.get('url')
                if not item_url:
                    continue
                entries.append(self.url_result(item_url, 'RTS'))

            if not entries:
                page, urlh = self._download_webpage_handle(url, display_id)
                if re.match(self._VALID_URL, urlh.geturl()).group('id') != media_id:
                    return self.url_result(urlh.geturl(), 'RTS')

                # article with videos on rhs
                videos = re.findall(
                    r'<article[^>]+class="content-item"[^>]*>\s*<a[^>]+data-video-urn="urn:([^"]+)"',
                    page)
                if not videos:
                    videos = re.findall(
                        r'(?s)<iframe[^>]+class="srg-player"[^>]+src="[^"]+urn:([^"]+)"',
                        page)
                if videos:
                    entries = [self.url_result('srgssr:%s' % video_urn, 'SRGSSR') for video_urn in videos]

            if entries:
                return self.playlist_result(entries, media_id, all_info.get('title'))

            internal_id = self._html_search_regex(
                r'<(?:video|audio) data-id="([0-9]+)"', page,
                'internal video id')
            all_info = download_json(internal_id)

        media_type = 'video' if 'video' in all_info else 'audio'

        # check for errors
        self._get_media_data('rts', media_type, media_id)

        info = all_info['video']['JSONinfo'] if 'video' in all_info else all_info['audio']

        title = info['title']

        def extract_bitrate(url):
            return int_or_none(self._search_regex(
                r'-([0-9]+)k\.', url, 'bitrate', default=None))

        formats = []
        streams = info.get('streams', {})
        for format_id, format_url in streams.items():
            if format_id == 'hds_sd' and 'hds' in streams:
                continue
            if format_id == 'hls_sd' and 'hls' in streams:
                continue
            ext = determine_ext(format_url)
            if ext in ('m3u8', 'f4m'):
                format_url = self._get_tokenized_src(format_url, media_id, format_id)
                if ext == 'f4m':
                    formats.extend(self._extract_f4m_formats(
                        format_url + ('?' if '?' not in format_url else '&') + 'hdcore=3.4.0',
                        media_id, f4m_id=format_id, fatal=False))
                else:
                    formats.extend(self._extract_m3u8_formats(
                        format_url, media_id, 'mp4', 'm3u8_native', m3u8_id=format_id, fatal=False))
            else:
                formats.append({
                    'format_id': format_id,
                    'url': format_url,
                    'tbr': extract_bitrate(format_url),
                })

        download_base = 'http://rtsww%s-d.rts.ch/' % ('-a' if media_type == 'audio' else '')
        for media in info.get('media', []):
            media_url = media.get('url')
            if not media_url or re.match(r'https?://', media_url):
                continue
            rate = media.get('rate')
            ext = media.get('ext') or determine_ext(media_url, 'mp4')
            format_id = ext
            if rate:
                format_id += '-%dk' % rate
            formats.append({
                'format_id': format_id,
                'url': urljoin(download_base, media_url),
                'tbr': rate or extract_bitrate(media_url),
            })

        self._check_formats(formats, media_id)
        self._sort_formats(formats)

        duration = info.get('duration') or info.get('cutout') or info.get('cutduration')
        if isinstance(duration, compat_str):
            duration = parse_duration(duration)

        return {
            'id': media_id,
            'display_id': display_id,
            'formats': formats,
            'title': title,
            'description': info.get('intro'),
            'duration': duration,
            'view_count': int_or_none(info.get('plays')),
            'uploader': info.get('programName'),
            'timestamp': parse_iso8601(info.get('broadcast_date')),
            'thumbnail': unescapeHTML(info.get('preview_image_url')),
        }


code:
import base64
import io
import struct

from .common import InfoExtractor
from ..compat import compat_b64decode
from ..utils import (
    ExtractorError,
    determine_ext,
    float_or_none,
    qualities,
    remove_end,
    remove_start,
    try_get,
)

class RTVEALaCartaIE(InfoExtractor):
    IE_NAME = 'rtve.es:alacarta'
    IE_DESC = 'RTVE a la carta'
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(m/)?(alacarta/videos|filmoteca)/[^/]+/[^/]+/(?P<id>\d+)'

    _TESTS = [{
        'url': 'http://www.rtve.es/alacarta/videos/balonmano/o-swiss-cup-masculina-final-espana-suecia/2491869/',
        'md5': '1d49b7e1ca7a7502c56a4bf1b60f1b43',
        'info_dict': {
            'id': '2491869',
            'ext': 'mp4',
            'title': 'Balonmano - Swiss Cup masculina. Final: España-Suecia',
            'duration': 5024.566,
            'series': 'Balonmano',
        },
        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
    }, {
        'note': 'Live stream',
        'url': 'http://www.rtve.es/alacarta/videos/television/24h-live/1694255/',
        'info_dict': {
            'id': '1694255',
            'ext': 'mp4',
            'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'is_live': True,
        },
        'params': {
            'skip_download': 'live stream',
        },
    }, {
        'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
        'info_dict': {
            'id': '4236788',
            'ext': 'mp4',
            'title': 'Servir y proteger - Capítulo 104',
            'duration': 3222.0,
        },
        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
    }, {
        'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
        'only_matching': True,
    }, {
        'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
        'only_matching': True,
    }]

    def _real_initialize(self):
        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode('utf-8')).decode('utf-8')
        self._manager = self._download_json(
            'http://www.rtve.es/odin/loki/' + user_agent_b64,
            None, 'Fetching manager info')['manager']

    @staticmethod
    def _decrypt_url(png):
        encrypted_data = io.BytesIO(compat_b64decode(png)[8:])
        while True:
            length = struct.unpack('!I', encrypted_data.read(4))[0]
            chunk_type = encrypted_data.read(4)
            if chunk_type == b'IEND':
                break
            data = encrypted_data.read(length)
            if chunk_type == b'tEXt':
                alphabet_data, text = data.split(b'\0')
                quality, url_data = text.split(b'%%')
                alphabet = []
                e = 0
                d = 0
                for l in alphabet_data.decode('iso-8859-1'):
                    if d == 0:
                        alphabet.append(l)
                        d = e = (e + 1) % 4
                    else:
                        d -= 1
                url = ''
                f = 0
                e = 3
                b = 1
                for letter in url_data.decode('iso-8859-1'):
                    if f == 0:
                        l = int(letter) * 10
                        f = 1
                    else:
                        if e == 0:
                            l += int(letter)
                            url += alphabet[l]
                            e = (b + 3) % 4
                            f = 0
                            b += 1
                        else:
                            e -= 1

                yield quality.decode(), url
            encrypted_data.read(4)  # CRC

    def _extract_png_formats(self, video_id):
        png = self._download_webpage(
            'http://www.rtve.es/ztnr/movil/thumbnail/%s/videos/%s.png' % (self._manager, video_id),
            video_id, 'Downloading url information', query={'q': 'v2'})
        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
        formats = []
        for quality, video_url in self._decrypt_url(png):
            ext = determine_ext(video_url)
            if ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    video_url, video_id, 'mp4', 'm3u8_native',
                    m3u8_id='hls', fatal=False))
            elif ext == 'mpd':
                formats.extend(self._extract_mpd_formats(
                    video_url, video_id, 'dash', fatal=False))
            else:
                formats.append({
                    'format_id': quality,
                    'quality': q(quality),
                    'url': video_url,
                })
        self._sort_formats(formats)
        return formats

    def _real_extract(self, url):
        video_id = self._match_id(url)
        info = self._download_json(
            'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json' % video_id,
            video_id)['page']['items'][0]
        if info['state'] == 'DESPU':
            raise ExtractorError('The video is no longer available', expected=True)
        title = info['title'].strip()
        formats = self._extract_png_formats(video_id)

        subtitles = None
        sbt_file = info.get('sbtFile')
        if sbt_file:
            subtitles = self.extract_subtitles(video_id, sbt_file)

        is_live = info.get('live') is True

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'thumbnail': info.get('image'),
            'subtitles': subtitles,
            'duration': float_or_none(info.get('duration'), 1000),
            'is_live': is_live,
            'series': info.get('programTitle'),
        }

    def _get_subtitles(self, video_id, sub_file):
        subs = self._download_json(
            sub_file + '.json', video_id,
            'Downloading subtitles info')['page']['items']
        return dict(
            (s['lang'], [{'ext': 'vtt', 'url': s['src']}])
            for s in subs)

class RTVEAudioIE(RTVEALaCartaIE):
    IE_NAME = 'rtve.es:audio'
    IE_DESC = 'RTVE audio'
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(alacarta|play)/audios/[^/]+/[^/]+/(?P<id>[0-9]+)'

    _TESTS = [{
        'url': 'https://www.rtve.es/alacarta/audios/a-hombros-de-gigantes/palabra-ingeniero-codigos-informaticos-27-04-21/5889192/',
        'md5': 'ae06d27bff945c4e87a50f89f6ce48ce',
        'info_dict': {
            'id': '5889192',
            'ext': 'mp3',
            'title': 'Códigos informáticos',
            'thumbnail': r're:https?://.+/1598856591583.jpg',
            'duration': 349.440,
            'series': 'A hombros de gigantes',
        },
    }, {
        'url': 'https://www.rtve.es/play/audios/en-radio-3/ignatius-farray/5791165/',
        'md5': '072855ab89a9450e0ba314c717fa5ebc',
        'info_dict': {
            'id': '5791165',
            'ext': 'mp3',
            'title': 'Ignatius Farray',
            'thumbnail': r're:https?://.+/1613243011863.jpg',
            'duration': 3559.559,
            'series': 'En Radio 3'
        },
    }, {
        'url': 'https://www.rtve.es/play/audios/frankenstein-o-el-moderno-prometeo/capitulo-26-ultimo-muerte-victor-juan-jose-plans-mary-shelley/6082623/',
        'md5': '0eadab248cc8dd193fa5765712e84d5c',
        'info_dict': {
            'id': '6082623',
            'ext': 'mp3',
            'title': 'Capítulo 26 y último: La muerte de Victor',
            'thumbnail': r're:https?://.+/1632147445707.jpg',
            'duration': 3174.086,
            'series': 'Frankenstein o el moderno Prometeo'
        },
    }]

    def _extract_png_formats(self, audio_id):
        """
        This function retrieves media related png thumbnail which obfuscate
        valuable information about the media. This information is decrypted
        via base class _decrypt_url function providing media quality and
        media url
        """
        png = self._download_webpage(
            'http://www.rtve.es/ztnr/movil/thumbnail/%s/audios/%s.png' %
            (self._manager, audio_id),
            audio_id, 'Downloading url information', query={'q': 'v2'})
        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
        formats = []
        for quality, audio_url in self._decrypt_url(png):
            ext = determine_ext(audio_url)
            if ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    audio_url, audio_id, 'mp4', 'm3u8_native',
                    m3u8_id='hls', fatal=False))
            elif ext == 'mpd':
                formats.extend(self._extract_mpd_formats(
                    audio_url, audio_id, 'dash', fatal=False))
            else:
                formats.append({
                    'format_id': quality,
                    'quality': q(quality),
                    'url': audio_url,
                })
        self._sort_formats(formats)
        return formats

    def _real_extract(self, url):
        audio_id = self._match_id(url)
        info = self._download_json(
            'https://www.rtve.es/api/audios/%s.json' % audio_id,
            audio_id)['page']['items'][0]

        return {
            'id': audio_id,
            'title': info['title'].strip(),
            'thumbnail': info.get('thumbnail'),
            'duration': float_or_none(info.get('duration'), 1000),
            'series': try_get(info, lambda x: x['programInfo']['title']),
            'formats': self._extract_png_formats(audio_id),
        }

class RTVEInfantilIE(RTVEALaCartaIE):
    IE_NAME = 'rtve.es:infantil'
    IE_DESC = 'RTVE infantil'
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/infantil/serie/[^/]+/video/[^/]+/(?P<id>[0-9]+)/'

    _TESTS = [{
        'url': 'http://www.rtve.es/infantil/serie/cleo/video/maneras-vivir/3040283/',
        'md5': '5747454717aedf9f9fdf212d1bcfc48d',
        'info_dict': {
            'id': '3040283',
            'ext': 'mp4',
            'title': 'Maneras de vivir',
            'thumbnail': r're:https?://.+/1426182947956\.JPG',
            'duration': 357.958,
        },
        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
    }]

class RTVELiveIE(RTVEALaCartaIE):
    IE_NAME = 'rtve.es:live'
    IE_DESC = 'RTVE.es live streams'
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/directo/(?P<id>[a-zA-Z0-9-]+)'

    _TESTS = [{
        'url': 'http://www.rtve.es/directo/la-1/',
        'info_dict': {
            'id': 'la-1',
            'ext': 'mp4',
            'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
        },
        'params': {
            'skip_download': 'live stream',
        }
    }]

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

        webpage = self._download_webpage(url, video_id)
        title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
        title = remove_start(title, 'Estoy viendo ')

        vidplayer_id = self._search_regex(
            (r'playerId=player([0-9]+)',
             r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
             r'data-id=["\'](\d+)'),
            webpage, 'internal video ID')

        return {
            'id': video_id,
            'title': title,
            'formats': self._extract_png_formats(vidplayer_id),
            'is_live': True,
        }

class RTVETelevisionIE(InfoExtractor):
    IE_NAME = 'rtve.es:television'
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/television/[^/]+/[^/]+/(?P<id>\d+).shtml'

    _TEST = {
        'url': 'http://www.rtve.es/television/20160628/revolucion-del-movil/1364141.shtml',
        'info_dict': {
            'id': '3069778',
            'ext': 'mp4',
            'title': 'Documentos TV - La revolución del móvil',
            'duration': 3496.948,
        },
        'params': {
            'skip_download': True,
        },
    }

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

        alacarta_url = self._search_regex(
            r'data-location="alacarta_videos"[^<]+url":"(http://www\.rtve\.es/alacarta.+?)&',
            webpage, 'alacarta url', default=None)
        if alacarta_url is None:
            raise ExtractorError(
                'The webpage doesn\'t contain any video', expected=True)

        return self.url_result(alacarta_url, ie=RTVEALaCartaIE.ie_key())


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

class RTVNHIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?rtvnh\.nl/video/(?P<id>[0-9]+)'
    _TEST = {
        'url': 'http://www.rtvnh.nl/video/131946',
        'md5': 'cdbec9f44550763c8afc96050fa747dc',
        'info_dict': {
            'id': '131946',
            'ext': 'mp4',
            'title': 'Grote zoektocht in zee bij Zandvoort naar vermiste vrouw',
            'thumbnail': r're:^https?:.*\.jpg$'
        }
    }

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

        meta = self._parse_json(self._download_webpage(
            'http://www.rtvnh.nl/video/json?m=' + video_id, video_id), video_id)

        status = meta.get('status')
        if status != 200:
            raise ExtractorError(
                '%s returned error code %d' % (self.IE_NAME, status), expected=True)

        formats = []
        rtmp_formats = self._extract_smil_formats(
            'http://www.rtvnh.nl/video/smil?m=' + video_id, video_id)
        formats.extend(rtmp_formats)

        for rtmp_format in rtmp_formats:
            rtmp_url = '%s/%s' % (rtmp_format['url'], rtmp_format['play_path'])
            rtsp_format = rtmp_format.copy()
            del rtsp_format['play_path']
            del rtsp_format['ext']
            rtsp_format.update({
                'format_id': rtmp_format['format_id'].replace('rtmp', 'rtsp'),
                'url': rtmp_url.replace('rtmp://', 'rtsp://'),
                'protocol': 'rtsp',
            })
            formats.append(rtsp_format)
            http_base_url = rtmp_url.replace('rtmp://', 'http://')
            formats.extend(self._extract_m3u8_formats(
                http_base_url + '/playlist.m3u8', video_id, 'mp4',
                'm3u8_native', m3u8_id='hls', fatal=False))
            formats.extend(self._extract_f4m_formats(
                http_base_url + '/manifest.f4m',
                video_id, f4m_id='hds', fatal=False))
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': meta['title'].strip(),
            'thumbnail': meta.get('image'),
            'formats': formats
        }


code:
import re

from .common import InfoExtractor

from ..utils import (
    parse_duration,
    traverse_obj,
    unified_timestamp,
)

class RTVSIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?rtvs\.sk/(?:radio|televizia)/archiv(?:/\d+)?/(?P<id>\d+)/?(?:[#?]|$)'
    _TESTS = [{
        # radio archive
        'url': 'http://www.rtvs.sk/radio/archiv/11224/414872',
        'md5': '134d5d6debdeddf8a5d761cbc9edacb8',
        'info_dict': {
            'id': '414872',
            'ext': 'mp3',
            'title': 'Ostrov pokladov 1 časť.mp3',
            'duration': 2854,
            'thumbnail': 'https://www.rtvs.sk/media/a501/image/file/2/0000/b1R8.rtvs.jpg',
            'display_id': '135331',
        }
    }, {
        # tv archive
        'url': 'http://www.rtvs.sk/televizia/archiv/8249/63118',
        'info_dict': {
            'id': '63118',
            'ext': 'mp4',
            'title': 'Amaro Džives - Náš deň',
            'description': 'Galavečer pri príležitosti Medzinárodného dňa Rómov.',
            'thumbnail': 'https://www.rtvs.sk/media/a501/image/file/2/0031/L7Qm.amaro_dzives_png.jpg',
            'timestamp': 1428555900,
            'upload_date': '20150409',
            'duration': 4986,
        }
    }, {
        # tv archive
        'url': 'https://www.rtvs.sk/televizia/archiv/18083?utm_source=web&utm_medium=rozcestnik&utm_campaign=Robin',
        'info_dict': {
            'id': '18083',
            'ext': 'mp4',
            'title': 'Robin',
            'description': 'md5:2f70505a7b8364491003d65ff7a0940a',
            'timestamp': 1636652760,
            'display_id': '307655',
            'duration': 831,
            'upload_date': '20211111',
            'thumbnail': 'https://www.rtvs.sk/media/a501/image/file/2/0916/robin.jpg',
        }
    }]

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

        webpage = self._download_webpage(url, video_id)
        iframe_id = self._search_regex(
            r'<iframe[^>]+id\s*=\s*"player_[^_]+_([0-9]+)"', webpage, 'Iframe ID')
        iframe_url = self._search_regex(
            fr'<iframe[^>]+id\s*=\s*"player_[^_]+_{re.escape(iframe_id)}"[^>]+src\s*=\s*"([^"]+)"', webpage, 'Iframe URL')

        webpage = self._download_webpage(iframe_url, video_id, 'Downloading iframe')
        json_url = self._search_regex(r'var\s+url\s*=\s*"([^"]+)"\s*\+\s*ruurl', webpage, 'json URL')
        data = self._download_json(f'https:{json_url}b=mozilla&p=win&v=97&f=0&d=1', video_id)

        if data.get('clip'):
            data['playlist'] = [data['clip']]

        if traverse_obj(data, ('playlist', 0, 'sources', 0, 'type')) == 'audio/mp3':
            formats = [{'url': traverse_obj(data, ('playlist', 0, 'sources', 0, 'src'))}]
        else:
            formats = self._extract_m3u8_formats(traverse_obj(data, ('playlist', 0, 'sources', 0, 'src')), video_id)
        self._sort_formats(formats)

        return {
            'id': video_id,
            'display_id': iframe_id,
            'title': traverse_obj(data, ('playlist', 0, 'title')),
            'description': traverse_obj(data, ('playlist', 0, 'description')),
            'duration': parse_duration(traverse_obj(data, ('playlist', 0, 'length'))),
            'thumbnail': traverse_obj(data, ('playlist', 0, 'image')),
            'timestamp': unified_timestamp(traverse_obj(data, ('playlist', 0, 'datetime_create'))),
            'formats': formats
        }


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

class RTVSLOIE(InfoExtractor):
    IE_NAME = 'rtvslo.si'
    _VALID_URL = r'''(?x)
        https?://(?:
            (?:365|4d)\.rtvslo.si/arhiv/[^/?#&;]+|
            (?:www\.)?rtvslo\.si/rtv365/arhiv
        )/(?P<id>\d+)'''
    _GEO_COUNTRIES = ['SI']

    _API_BASE = 'https://api.rtvslo.si/ava/{}/{}?client_id=82013fb3a531d5414f478747c1aca622'
    SUB_LANGS_MAP = {'Slovenski': 'sl'}

    _TESTS = [
        {
            'url': 'https://www.rtvslo.si/rtv365/arhiv/174842550?s=tv',
            'info_dict': {
                'id': '174842550',
                'ext': 'flv',
                'release_timestamp': 1643140032,
                'upload_date': '20220125',
                'series': 'Dnevnik',
                'thumbnail': 'https://img.rtvcdn.si/_up/ava/ava_misc/show_logos/92/dnevnik_3_wide2.jpg',
                'description': 'md5:76a18692757aeb8f0f51221106277dd2',
                'timestamp': 1643137046,
                'title': 'Dnevnik',
                'series_id': '92',
                'release_date': '20220125',
                'duration': 1789,
            },
        }, {
            'url': 'https://365.rtvslo.si/arhiv/utrip/174843754',
            'info_dict': {
                'id': '174843754',
                'ext': 'mp4',
                'series_id': '94',
                'release_date': '20220129',
                'timestamp': 1643484455,
                'title': 'Utrip',
                'duration': 813,
                'thumbnail': 'https://img.rtvcdn.si/_up/ava/ava_misc/show_logos/94/utrip_1_wide2.jpg',
                'description': 'md5:77f2892630c7b17bb7a5bb84319020c9',
                'release_timestamp': 1643485825,
                'upload_date': '20220129',
                'series': 'Utrip',
            },
        }, {
            'url': 'https://365.rtvslo.si/arhiv/il-giornale-della-sera/174844609',
            'info_dict': {
                'id': '174844609',
                'ext': 'mp3',
                'series_id': '106615841',
                'title': 'Il giornale della sera',
                'duration': 1328,
                'series': 'Il giornale della sera',
                'timestamp': 1643743800,
                'release_timestamp': 1643745424,
                'thumbnail': 'https://img.rtvcdn.si/_up/ava/ava_misc/show_logos/il-giornale-della-sera_wide2.jpg',
                'upload_date': '20220201',
                'tbr': 128000,
                'release_date': '20220201',
            },

        }, {
            'url': 'https://4d.rtvslo.si/arhiv/dnevnik/174842550',
            'only_matching': True
        }
    ]

    def _real_extract(self, url):
        v_id = self._match_id(url)
        meta = self._download_json(self._API_BASE.format('getRecordingDrm', v_id), v_id)['response']

        thumbs = [{'id': k, 'url': v, 'http_headers': {'Accept': 'image/jpeg'}}
                  for k, v in (meta.get('images') or {}).items()]

        subs = {}
        for s in traverse_obj(meta, 'subs', 'subtitles', default=[]):
            lang = self.SUB_LANGS_MAP.get(s.get('language'), s.get('language') or 'und')
            subs.setdefault(lang, []).append({
                'url': s.get('file'),
                'ext': traverse_obj(s, 'format', expected_type=str.lower),
            })

        jwt = meta.get('jwt')
        if not jwt:
            raise ExtractorError('Site did not provide an authentication token, cannot proceed.')

        media = self._download_json(self._API_BASE.format('getMedia', v_id), v_id, query={'jwt': jwt})['response']

        formats = []
        adaptive_url = traverse_obj(media, ('addaptiveMedia', 'hls_sec'), expected_type=url_or_none)
        if adaptive_url:
            formats = self._extract_wowza_formats(adaptive_url, v_id, skip_protocols=['smil'])

        adaptive_url = traverse_obj(media, ('addaptiveMedia_sl', 'hls_sec'), expected_type=url_or_none)
        if adaptive_url:
            for f in self._extract_wowza_formats(adaptive_url, v_id, skip_protocols=['smil']):
                formats.append({
                    **f,
                    'format_id': 'sign-' + f['format_id'],
                    'format_note': 'Sign language interpretation', 'preference': -10,
                    'language': (
                        'slv' if f.get('language') == 'eng' and f.get('acodec') != 'none'
                        else f.get('language'))
                })

        formats.extend(
            {
                'url': f['streams'][strm],
                'ext': traverse_obj(f, 'mediaType', expected_type=str.lower),
                'width': f.get('width'),
                'height': f.get('height'),
                'tbr': f.get('bitrate'),
                'filesize': f.get('filesize'),
            }
            for strm in ('http', 'https')
            for f in media.get('mediaFiles') or []
            if traverse_obj(f, ('streams', strm))
        )

        if any('intermission.mp4' in x['url'] for x in formats):
            self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
        if any('dummy_720p.mp4' in x.get('manifest_url', '') for x in formats) and meta.get('stub') == 'error':
            raise ExtractorError(f'{self.IE_NAME} said: Clip not available', expected=True)

        self._sort_formats(formats)
        return {
            'id': v_id,
            'webpage_url': ''.join(traverse_obj(meta, ('canonical', ('domain', 'path')))),
            'title': meta.get('title'),
            'formats': formats,
            'subtitles': subs,
            'thumbnails': thumbs,
            'description': meta.get('description'),
            'timestamp': unified_timestamp(traverse_obj(meta, 'broadcastDate', ('broadcastDates', 0))),
            'release_timestamp': unified_timestamp(meta.get('recordingDate')),
            'duration': meta.get('duration') or parse_duration(meta.get('length')),
            'tags': meta.get('genre'),
            'series': meta.get('showName'),
            'series_id': meta.get('showId'),
        }


code:
from .common import InfoExtractor

class RUHDIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?ruhd\.ru/play\.php\?vid=(?P<id>\d+)'
    _TEST = {
        'url': 'http://www.ruhd.ru/play.php?vid=207',
        'md5': 'd1a9ec4edf8598e3fbd92bb16072ba83',
        'info_dict': {
            'id': '207',
            'ext': 'divx',
            'title': 'КОТ бааааам',
            'description': 'классный кот)',
            'thumbnail': r're:^http://.*\.jpg$',
        }
    }

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

        video_url = self._html_search_regex(
            r'<param name="src" value="([^"]+)"', webpage, 'video url')
        title = self._html_search_regex(
            r'<title>([^<]+)   RUHD\.ru - Видео Высокого качества №1 в России!</title>',
            webpage, 'title')
        description = self._html_search_regex(
            r'(?s)
(.+?)',
            webpage, 'description', fatal=False)
        thumbnail = self._html_search_regex(
            r'<param name="previewImage" value="([^"]+)"',
            webpage, 'thumbnail', fatal=False)
        if thumbnail:
            thumbnail = 'http://www.ruhd.ru' + thumbnail

        return {
            'id': video_id,
            'url': video_url,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
        }


code:
import re

from ..utils import parse_duration
from .common import InfoExtractor

class Rule34VideoIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?rule34video\.com/videos/(?P<id>\d+)'
    _TESTS = [
        {
            'url': 'https://rule34video.com/videos/3065157/shot-it-mmd-hmv/',
            'md5': 'ffccac2c23799dabbd192621ae4d04f3',
            'info_dict': {
                'id': '3065157',
                'ext': 'mp4',
                'title': 'Shot It-(mmd hmv)',
                'thumbnail': 'https://rule34video.com/contents/videos_screenshots/3065000/3065157/preview.jpg',
                'duration': 347.0,
                'age_limit': 18
            }
        },
        {
            'url': 'https://rule34video.com/videos/3065296/lara-in-trouble-ep-7-wildeerstudio/',
            'md5': '6bb5169f9f6b38cd70882bf2e64f6b86',
            'info_dict': {
                'id': '3065296',
                'ext': 'mp4',
                'title': 'Lara in Trouble Ep. 7 [WildeerStudio]',
                'thumbnail': 'https://rule34video.com/contents/videos_screenshots/3065000/3065296/preview.jpg',
                'duration': 938.0,
                'age_limit': 18
            }
        },
    ]

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

        formats = []

        for mobj in re.finditer(r']+href="(?P<video_url>[^"]+download=true[^"]+)".*>(?P<ext>[^\s]+) (?P<quality>[^<]+)p', webpage):
            url, ext, quality = mobj.groups()
            formats.append({
                'url': url,
                'ext': ext.lower(),
                'quality': quality,
            })

        title = self._html_extract_title(webpage)
        thumbnail = self._html_search_regex(r'preview_url:\s+\'([^\']+)\'', webpage, 'thumbnail', default=None)
        duration = self._html_search_regex(r'"icon-clock">\s+((?:\d+:?)+)', webpage, 'duration', default=None)

        self._sort_formats(formats)

        return {
            'id': video_id,
            'formats': formats,
            'title': title,
            'thumbnail': thumbnail,
            'duration': parse_duration(duration),
            'age_limit': 18
        }


code:
import itertools
import re

from .common import InfoExtractor
from ..compat import compat_str, compat_HTTPError
from ..utils import (
    determine_ext,
    int_or_none,
    parse_iso8601,
    try_get,
    unescapeHTML,
    ExtractorError,
)

class RumbleEmbedIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?rumble\.com/embed/(?:[0-9a-z]+\.)?(?P<id>[0-9a-z]+)'
    _EMBED_REGEX = [fr'(?:<(?:script|iframe)[^>]+\bsrc=|["\']embedUrl["\']\s*:\s*)["\'](?P<url>{_VALID_URL})']
    _TESTS = [{
        'url': 'https://rumble.com/embed/v5pv5f',
        'md5': '36a18a049856720189f30977ccbb2c34',
        'info_dict': {
            'id': 'v5pv5f',
            'ext': 'mp4',
            'title': 'WMAR 2 News Latest Headlines | October 20, 6pm',
            'timestamp': 1571611968,
            'upload_date': '20191020',
            'channel_url': 'https://rumble.com/c/WMAR',
            'channel': 'WMAR',
            'thumbnail': 'https://sp.rmbl.ws/s8/1/5/M/z/1/5Mz1a.OvCc-small-WMAR-2-News-Latest-Headline.jpg',
            'duration': 234,
            'uploader': 'WMAR',
        }
    }, {
        'url': 'https://rumble.com/embed/vslb7v',
        'md5': '7418035de1a30a178b8af34dc2b6a52b',
        'info_dict': {
            'id': 'vslb7v',
            'ext': 'mp4',
            'title': 'Defense Sec. says US Commitment to NATO Defense \'Ironclad\'',
            'timestamp': 1645142135,
            'upload_date': '20220217',
            'channel_url': 'https://rumble.com/c/CyberTechNews',
            'channel': 'CTNews',
            'thumbnail': 'https://sp.rmbl.ws/s8/6/7/i/9/h/7i9hd.OvCc.jpg',
            'duration': 901,
            'uploader': 'CTNews',
        }
    }, {
        'url': 'https://rumble.com/embed/ufe9n.v5pv5f',
        'only_matching': True,
    }]

    @classmethod
    def _extract_embed_urls(cls, url, webpage):
        embeds = tuple(super()._extract_embed_urls(url, webpage))
        if embeds:
            return embeds
        return [f'https://rumble.com/embed/{mobj.group("id")}' for mobj in re.finditer(
            r'<script>\s*Rumble\(\s*"play"\s*,\s*{\s*[\'"]video[\'"]\s*:\s*[\'"](?P<id>[0-9a-z]+)[\'"]', webpage)]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        video = self._download_json(
            'https://rumble.com/embedJS/', video_id,
            query={'request': 'video', 'v': video_id})
        title = unescapeHTML(video['title'])

        formats = []
        for height, ua in (video.get('ua') or {}).items():
            for i in range(2):
                f_url = try_get(ua, lambda x: x[i], compat_str)
                if f_url:
                    ext = determine_ext(f_url)
                    f = {
                        'ext': ext,
                        'format_id': '%s-%sp' % (ext, height),
                        'height': int_or_none(height),
                        'url': f_url,
                    }
                    bitrate = try_get(ua, lambda x: x[i + 2]['bitrate'])
                    if bitrate:
                        f['tbr'] = int_or_none(bitrate)
                    formats.append(f)
        self._sort_formats(formats)

        subtitles = {
            lang: [{
                'url': sub_info['path'],
                'name': sub_info.get('language') or '',
            }] for lang, sub_info in (video.get('cc') or {}).items() if sub_info.get('path')
        }

        author = video.get('author') or {}

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'subtitles': subtitles,
            'thumbnail': video.get('i'),
            'timestamp': parse_iso8601(video.get('pubDate')),
            'channel': author.get('name'),
            'channel_url': author.get('url'),
            'duration': int_or_none(video.get('duration')),
            'uploader': author.get('name'),
        }

class RumbleChannelIE(InfoExtractor):
    _VALID_URL = r'(?P<url>https?://(?:www\.)?rumble\.com/(?:c|user)/(?P<id>[^&?#$/]+))'

    _TESTS = [{
        'url': 'https://rumble.com/c/Styxhexenhammer666',
        'playlist_mincount': 1160,
        'info_dict': {
            'id': 'Styxhexenhammer666',
        },
    }, {
        'url': 'https://rumble.com/user/goldenpoodleharleyeuna',
        'playlist_count': 4,
        'info_dict': {
            'id': 'goldenpoodleharleyeuna',
        },
    }]

    def entries(self, url, playlist_id):
        for page in itertools.count(1):
            try:
                webpage = self._download_webpage(f'{url}?page={page}', playlist_id, note='Downloading page %d' % page)
            except ExtractorError as e:
                if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404:
                    break
                raise
            for video_url in re.findall(r'class=video-item--a\s?href=([^>]+\.html)', webpage):
                yield self.url_result('https://rumble.com' + video_url)

    def _real_extract(self, url):
        url, playlist_id = self._match_valid_url(url).groups()
        return self.playlist_result(self.entries(url, playlist_id), playlist_id=playlist_id)


code:
import itertools

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

class RutubeBaseIE(InfoExtractor):
    def _download_api_info(self, video_id, query=None):
        if not query:
            query = {}
        query['format'] = 'json'
        return self._download_json(
            'http://rutube.ru/api/video/%s/' % video_id,
            video_id, 'Downloading video JSON',
            'Unable to download video JSON', query=query)

    @staticmethod
    def _extract_info(video, video_id=None, require_title=True):
        title = video['title'] if require_title else video.get('title')

        age_limit = video.get('is_adult')
        if age_limit is not None:
            age_limit = 18 if age_limit is True else 0

        uploader_id = try_get(video, lambda x: x['author']['id'])
        category = try_get(video, lambda x: x['category']['name'])

        return {
            'id': video.get('id') or video_id if video_id else video['id'],
            'title': title,
            'description': video.get('description'),
            'thumbnail': video.get('thumbnail_url'),
            'duration': int_or_none(video.get('duration')),
            'uploader': try_get(video, lambda x: x['author']['name']),
            'uploader_id': compat_str(uploader_id) if uploader_id else None,
            'timestamp': unified_timestamp(video.get('created_ts')),
            'category': [category] if category else None,
            'age_limit': age_limit,
            'view_count': int_or_none(video.get('hits')),
            'comment_count': int_or_none(video.get('comments_count')),
            'is_live': bool_or_none(video.get('is_livestream')),
        }

    def _download_and_extract_info(self, video_id, query=None):
        return self._extract_info(
            self._download_api_info(video_id, query=query), video_id)

    def _download_api_options(self, video_id, query=None):
        if not query:
            query = {}
        query['format'] = 'json'
        return self._download_json(
            'http://rutube.ru/api/play/options/%s/' % video_id,
            video_id, 'Downloading options JSON',
            'Unable to download options JSON',
            headers=self.geo_verification_headers(), query=query)

    def _extract_formats(self, options, video_id):
        formats = []
        for format_id, format_url in options['video_balancer'].items():
            ext = determine_ext(format_url)
            if ext == 'm3u8':
                formats.extend(self._extract_m3u8_formats(
                    format_url, video_id, 'mp4', m3u8_id=format_id, fatal=False))
            elif ext == 'f4m':
                formats.extend(self._extract_f4m_formats(
                    format_url, video_id, f4m_id=format_id, fatal=False))
            else:
                formats.append({
                    'url': format_url,
                    'format_id': format_id,
                })
        self._sort_formats(formats)
        return formats

    def _download_and_extract_formats(self, video_id, query=None):
        return self._extract_formats(
            self._download_api_options(video_id, query=query), video_id)

class RutubeIE(RutubeBaseIE):
    IE_NAME = 'rutube'
    IE_DESC = 'Rutube videos'
    _VALID_URL = r'https?://rutube\.ru/(?:video|(?:play/)?embed)/(?P<id>[\da-z]{32})'
    _EMBED_REGEX = [r']+?src=(["\'])(?P<url>(?:https?:)?//rutube\.ru/embed/[\da-z]{32}.*?)\1']

    _TESTS = [{
        'url': 'http://rutube.ru/video/3eac3b4561676c17df9132a9a1e62e3e/',
        'md5': '1d24f180fac7a02f3900712e5a5764d6',
        'info_dict': {
            'id': '3eac3b4561676c17df9132a9a1e62e3e',
            'ext': 'mp4',
            'title': 'Раненный кенгуру забежал в аптеку',
            'description': 'http://www.ntdtv.ru ',
            'duration': 81,
            'uploader': 'NTDRussian',
            'uploader_id': '29790',
            'timestamp': 1381943602,
            'upload_date': '20131016',
            'age_limit': 0,
        },
    }, {
        'url': 'http://rutube.ru/play/embed/a10e53b86e8f349080f718582ce4c661',
        'only_matching': True,
    }, {
        'url': 'http://rutube.ru/embed/a10e53b86e8f349080f718582ce4c661',
        'only_matching': True,
    }, {
        'url': 'http://rutube.ru/video/3eac3b4561676c17df9132a9a1e62e3e/?pl_id=4252',
        'only_matching': True,
    }, {
        'url': 'https://rutube.ru/video/10b3a03fc01d5bbcc632a2f3514e8aab/?pl_type=source',
        'only_matching': True,
    }]

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

    def _real_extract(self, url):
        video_id = self._match_id(url)
        info = self._download_and_extract_info(video_id)
        info['formats'] = self._download_and_extract_formats(video_id)
        return info

class RutubeEmbedIE(RutubeBaseIE):
    IE_NAME = 'rutube:embed'
    IE_DESC = 'Rutube embedded videos'
    _VALID_URL = r'https?://rutube\.ru/(?:video|play)/embed/(?P<id>[0-9]+)'

    _TESTS = [{
        'url': 'http://rutube.ru/video/embed/6722881?vk_puid37=&vk_puid38=',
        'info_dict': {
            'id': 'a10e53b86e8f349080f718582ce4c661',
            'ext': 'mp4',
            'timestamp': 1387830582,
            'upload_date': '20131223',
            'uploader_id': '297833',
            'description': 'Видео группы ★http://vk.com/foxkidsreset★ музей Fox Kids и Jetix

 восстановлено и сделано в шикоформате subziro89 http://vk.com/subziro89',
            'uploader': 'subziro89 ILya',
            'title': 'Мистический городок Эйри в Индиан 5 серия озвучка subziro89',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://rutube.ru/play/embed/8083783',
        'only_matching': True,
    }, {
        # private video
        'url': 'https://rutube.ru/play/embed/10631925?p=IbAigKqWd1do4mjaM5XLIQ',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        embed_id = self._match_id(url)
        # Query may contain private videos token and should be passed to API
        # requests (see #19163)
        query = parse_qs(url)
        options = self._download_api_options(embed_id, query)
        video_id = options['effective_video']
        formats = self._extract_formats(options, video_id)
        info = self._download_and_extract_info(video_id, query)
        info.update({
            'extractor_key': 'Rutube',
            'formats': formats,
        })
        return info

class RutubePlaylistBaseIE(RutubeBaseIE):
    def _next_page_url(self, page_num, playlist_id, *args, **kwargs):
        return self._PAGE_TEMPLATE % (playlist_id, page_num)

    def _entries(self, playlist_id, *args, **kwargs):
        next_page_url = None
        for pagenum in itertools.count(1):
            page = self._download_json(
                next_page_url or self._next_page_url(
                    pagenum, playlist_id, *args, **kwargs),
                playlist_id, 'Downloading page %s' % pagenum)

            results = page.get('results')
            if not results or not isinstance(results, list):
                break

            for result in results:
                video_url = url_or_none(result.get('video_url'))
                if not video_url:
                    continue
                entry = self._extract_info(result, require_title=False)
                entry.update({
                    '_type': 'url',
                    'url': video_url,
                    'ie_key': RutubeIE.ie_key(),
                })
                yield entry

            next_page_url = page.get('next')
            if not next_page_url or not page.get('has_next'):
                break

    def _extract_playlist(self, playlist_id, *args, **kwargs):
        return self.playlist_result(
            self._entries(playlist_id, *args, **kwargs),
            playlist_id, kwargs.get('playlist_name'))

    def _real_extract(self, url):
        return self._extract_playlist(self._match_id(url))

class RutubeTagsIE(RutubePlaylistBaseIE):
    IE_NAME = 'rutube:tags'
    IE_DESC = 'Rutube tags'
    _VALID_URL = r'https?://rutube\.ru/tags/video/(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://rutube.ru/tags/video/1800/',
        'info_dict': {
            'id': '1800',
        },
        'playlist_mincount': 68,
    }]

    _PAGE_TEMPLATE = 'http://rutube.ru/api/tags/video/%s/?page=%s&format=json'

class RutubeMovieIE(RutubePlaylistBaseIE):
    IE_NAME = 'rutube:movie'
    IE_DESC = 'Rutube movies'
    _VALID_URL = r'https?://rutube\.ru/metainfo/tv/(?P<id>\d+)'
    _TESTS = []

    _MOVIE_TEMPLATE = 'http://rutube.ru/api/metainfo/tv/%s/?format=json'
    _PAGE_TEMPLATE = 'http://rutube.ru/api/metainfo/tv/%s/video?page=%s&format=json'

    def _real_extract(self, url):
        movie_id = self._match_id(url)
        movie = self._download_json(
            self._MOVIE_TEMPLATE % movie_id, movie_id,
            'Downloading movie JSON')
        return self._extract_playlist(
            movie_id, playlist_name=movie.get('name'))

class RutubePersonIE(RutubePlaylistBaseIE):
    IE_NAME = 'rutube:person'
    IE_DESC = 'Rutube person videos'
    _VALID_URL = r'https?://rutube\.ru/video/person/(?P<id>\d+)'
    _TESTS = [{
        'url': 'http://rutube.ru/video/person/313878/',
        'info_dict': {
            'id': '313878',
        },
        'playlist_mincount': 37,
    }]

    _PAGE_TEMPLATE = 'http://rutube.ru/api/video/person/%s/?page=%s&format=json'

class RutubePlaylistIE(RutubePlaylistBaseIE):
    IE_NAME = 'rutube:playlist'
    IE_DESC = 'Rutube playlists'
    _VALID_URL = r'https?://rutube\.ru/(?:video|(?:play/)?embed)/[\da-z]{32}/\?.*?\bpl_id=(?P<id>\d+)'
    _TESTS = [{
        'url': 'https://rutube.ru/video/cecd58ed7d531fc0f3d795d51cee9026/?pl_id=3097&pl_type=tag',
        'info_dict': {
            'id': '3097',
        },
        'playlist_count': 27,
    }, {
        'url': 'https://rutube.ru/video/10b3a03fc01d5bbcc632a2f3514e8aab/?pl_id=4252&pl_type=source',
        'only_matching': True,
    }]

    _PAGE_TEMPLATE = 'http://rutube.ru/api/playlist/%s/%s/?page=%s&format=json'

    @classmethod
    def suitable(cls, url):
        from ..utils import int_or_none, parse_qs

        if not super(RutubePlaylistIE, cls).suitable(url):
            return False
        params = parse_qs(url)
        return params.get('pl_type', [None])[0] and int_or_none(params.get('pl_id', [None])[0])

    def _next_page_url(self, page_num, playlist_id, item_kind):
        return self._PAGE_TEMPLATE % (item_kind, playlist_id, page_num)

    def _real_extract(self, url):
        qs = parse_qs(url)
        playlist_kind = qs['pl_type'][0]
        playlist_id = qs['pl_id'][0]
        return self._extract_playlist(playlist_id, item_kind=playlist_kind)

class RutubeChannelIE(RutubePlaylistBaseIE):
    IE_NAME = 'rutube:channel'
    IE_DESC = 'Rutube channel'
    _VALID_URL = r'https?://rutube\.ru/channel/(?P<id>\d+)/videos'
    _TESTS = [{
        'url': 'https://rutube.ru/channel/639184/videos/',
        'info_dict': {
            'id': '639184',
        },
        'playlist_mincount': 133,
    }]

    _PAGE_TEMPLATE = 'http://rutube.ru/api/video/person/%s/?page=%s&format=json'