code:
import json
import re

from .common import InfoExtractor
from .generic import GenericIE
from ..utils import (
    determine_ext,
    ExtractorError,
    int_or_none,
    parse_duration,
    qualities,
    str_or_none,
    try_get,
    unified_strdate,
    unified_timestamp,
    update_url_query,
    url_or_none,
    xpath_text,
)
from ..compat import compat_etree_fromstring

class ARDMediathekBaseIE(InfoExtractor):
    _GEO_COUNTRIES = ['DE']

    def _extract_media_info(self, media_info_url, webpage, video_id):
        media_info = self._download_json(
            media_info_url, video_id, 'Downloading media JSON')
        return self._parse_media_info(media_info, video_id, '"fsk"' in webpage)

    def _parse_media_info(self, media_info, video_id, fsk):
        formats = self._extract_formats(media_info, video_id)

        if not formats:
            if fsk:
                self.raise_no_formats(
                    'This video is only available after 20:00', expected=True)
            elif media_info.get('_geoblocked'):
                self.raise_geo_restricted(
                    'This video is not available due to geoblocking',
                    countries=self._GEO_COUNTRIES, metadata_available=True)

        self._sort_formats(formats)

        subtitles = {}
        subtitle_url = media_info.get('_subtitleUrl')
        if subtitle_url:
            subtitles['de'] = [{
                'ext': 'ttml',
                'url': subtitle_url,
            }]

        return {
            'id': video_id,
            'duration': int_or_none(media_info.get('_duration')),
            'thumbnail': media_info.get('_previewImage'),
            'is_live': media_info.get('_isLive') is True,
            'formats': formats,
            'subtitles': subtitles,
        }

    def _ARD_extract_episode_info(self, title):
        """Try to extract season/episode data from the title."""
        res = {}
        if not title:
            return res

        for pattern in [
            # Pattern for title like "Homo sapiens (S06/E07) - Originalversion"
            # from: https://www.ardmediathek.de/one/sendung/doctor-who/Y3JpZDovL3dkci5kZS9vbmUvZG9jdG9yIHdobw
            r'.*(?P<ep_info> \(S(?P<season_number>\d+)/E(?P<episode_number>\d+)\)).*',
            # E.g.: title="Fritjof aus Norwegen (2) (AD)"
            # from: https://www.ardmediathek.de/ard/sammlung/der-krieg-und-ich/68cMkqJdllm639Skj4c7sS/
            r'.*(?P<ep_info> \((?:Folge |Teil )?(?P<episode_number>\d+)(?:/\d+)?\)).*',
            r'.*(?P<ep_info>Folge (?P<episode_number>\d+)(?:\:| -|) )\"(?P<episode>.+)\".*',
            # E.g.: title="Folge 25/42: Symmetrie"
            # from: https://www.ardmediathek.de/ard/video/grips-mathe/folge-25-42-symmetrie/ard-alpha/Y3JpZDovL2JyLmRlL3ZpZGVvLzMyYzI0ZjczLWQ1N2MtNDAxNC05ZmZhLTFjYzRkZDA5NDU5OQ/
            # E.g.: title="Folge 1063 - Vertrauen"
            # from: https://www.ardmediathek.de/ard/sendung/die-fallers/Y3JpZDovL3N3ci5kZS8yMzAyMDQ4/
            r'.*(?P<ep_info>Folge (?P<episode_number>\d+)(?:/\d+)?(?:\:| -|) ).*',
        ]:
            m = re.match(pattern, title)
            if m:
                groupdict = m.groupdict()
                res['season_number'] = int_or_none(groupdict.get('season_number'))
                res['episode_number'] = int_or_none(groupdict.get('episode_number'))
                res['episode'] = str_or_none(groupdict.get('episode'))
                # Build the episode title by removing numeric episode information:
                if groupdict.get('ep_info') and not res['episode']:
                    res['episode'] = str_or_none(
                        title.replace(groupdict.get('ep_info'), ''))
                if res['episode']:
                    res['episode'] = res['episode'].strip()
                break

        # As a fallback use the whole title as the episode name:
        if not res.get('episode'):
            res['episode'] = title.strip()
        return res

    def _extract_formats(self, media_info, video_id):
        type_ = media_info.get('_type')
        media_array = media_info.get('_mediaArray', [])
        formats = []
        for num, media in enumerate(media_array):
            for stream in media.get('_mediaStreamArray', []):
                stream_urls = stream.get('_stream')
                if not stream_urls:
                    continue
                if not isinstance(stream_urls, list):
                    stream_urls = [stream_urls]
                quality = stream.get('_quality')
                server = stream.get('_server')
                for stream_url in stream_urls:
                    if not url_or_none(stream_url):
                        continue
                    ext = determine_ext(stream_url)
                    if quality != 'auto' and ext in ('f4m', 'm3u8'):
                        continue
                    if ext == 'f4m':
                        formats.extend(self._extract_f4m_formats(
                            update_url_query(stream_url, {
                                'hdcore': '3.1.1',
                                'plugin': 'aasp-3.1.1.69.124'
                            }), video_id, f4m_id='hds', fatal=False))
                    elif ext == 'm3u8':
                        formats.extend(self._extract_m3u8_formats(
                            stream_url, video_id, 'mp4', 'm3u8_native',
                            m3u8_id='hls', fatal=False))
                    else:
                        if server and server.startswith('rtmp'):
                            f = {
                                'url': server,
                                'play_path': stream_url,
                                'format_id': 'a%s-rtmp-%s' % (num, quality),
                            }
                        else:
                            f = {
                                'url': stream_url,
                                'format_id': 'a%s-%s-%s' % (num, ext, quality)
                            }
                        m = re.search(
                            r'_(?P<width>\d+)x(?P<height>\d+)\.mp4$',
                            stream_url)
                        if m:
                            f.update({
                                'width': int(m.group('width')),
                                'height': int(m.group('height')),
                            })
                        if type_ == 'audio':
                            f['vcodec'] = 'none'
                        formats.append(f)
        return formats

class ARDMediathekIE(ARDMediathekBaseIE):
    IE_NAME = 'ARD:mediathek'
    _VALID_URL = r'^https?://(?:(?:(?:www|classic)\.)?ardmediathek\.de|mediathek\.(?:daserste|rbb-online)\.de|one\.ard\.de)/(?:.*/)(?P<video_id>[0-9]+|[^0-9][^/\?]+)[^/\?]*(?:\?.*)?'

    _TESTS = [{
        # available till 26.07.2022
        'url': 'http://www.ardmediathek.de/tv/S%C3%9CDLICHT/Was-ist-die-Kunst-der-Zukunft-liebe-Ann/BR-Fernsehen/Video?bcastId=34633636&documentId=44726822',
        'info_dict': {
            'id': '44726822',
            'ext': 'mp4',
            'title': 'Was ist die Kunst der Zukunft, liebe Anna McCarthy?',
            'description': 'md5:4ada28b3e3b5df01647310e41f3a62f5',
            'duration': 1740,
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        }
    }, {
        'url': 'https://one.ard.de/tv/Mord-mit-Aussicht/Mord-mit-Aussicht-6-39-T%C3%B6dliche-Nach/ONE/Video?bcastId=46384294&documentId=55586872',
        'only_matching': True,
    }, {
        # audio
        'url': 'http://www.ardmediathek.de/tv/WDR-H%C3%B6rspiel-Speicher/Tod-eines-Fu%C3%9Fballers/WDR-3/Audio-Podcast?documentId=28488308&bcastId=23074086',
        'only_matching': True,
    }, {
        'url': 'http://mediathek.daserste.de/sendungen_a-z/328454_anne-will/22429276_vertrauen-ist-gut-spionieren-ist-besser-geht',
        'only_matching': True,
    }, {
        # audio
        'url': 'http://mediathek.rbb-online.de/radio/Hörspiel/Vor-dem-Fest/kulturradio/Audio?documentId=30796318&topRessort=radio&bcastId=9839158',
        'only_matching': True,
    }, {
        'url': 'https://classic.ardmediathek.de/tv/Panda-Gorilla-Co/Panda-Gorilla-Co-Folge-274/Das-Erste/Video?bcastId=16355486&documentId=58234698',
        'only_matching': True,
    }]

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

    def _real_extract(self, url):
        # determine video id from url
        m = self._match_valid_url(url)

        document_id = None

        numid = re.search(r'documentId=([0-9]+)', url)
        if numid:
            document_id = video_id = numid.group(1)
        else:
            video_id = m.group('video_id')

        webpage = self._download_webpage(url, video_id)

        ERRORS = (
            ('>Leider liegt eine Störung vor.', 'Video %s is unavailable'),
            ('>Der gewünschte Beitrag ist nicht mehr verfügbar.<',
             'Video %s is no longer available'),
        )

        for pattern, message in ERRORS:
            if pattern in webpage:
                raise ExtractorError(message % video_id, expected=True)

        if re.search(r'[\?&]rss($|[=&])', url):
            doc = compat_etree_fromstring(webpage.encode('utf-8'))
            if doc.tag == 'rss':
                return GenericIE()._extract_rss(url, video_id, doc)

        title = self._og_search_title(webpage, default=None) or self._html_search_regex(
            [r'
(.*?)
',
             r'<meta name="dcterms\.title" content="(.*?)"/>',
             r'
(.*?)
',
             r'<title[^>]*>(.*?)</title>'],
            webpage, 'title')
        description = self._og_search_description(webpage, default=None) or self._html_search_meta(
            'dcterms.abstract', webpage, 'description', default=None)
        if description is None:
            description = self._html_search_meta(
                'description', webpage, 'meta description', default=None)
        if description is None:
            description = self._html_search_regex(
                r'
(.+?)
',
                webpage, 'teaser text', default=None)

        # Thumbnail is sometimes not present.
        # It is in the mobile version, but that seems to use a different URL
        # structure altogether.
        thumbnail = self._og_search_thumbnail(webpage, default=None)

        media_streams = re.findall(r'''(?x)
            mediaCollection\.addMediaStream\([0-9]+,\s*[0-9]+,\s*"[^"]*",\s*
            "([^"]+)"''', webpage)

        if media_streams:
            QUALITIES = qualities(['lo', 'hi', 'hq'])
            formats = []
            for furl in set(media_streams):
                if furl.endswith('.f4m'):
                    fid = 'f4m'
                else:
                    fid_m = re.match(r'.*\.([^.]+)\.[^.]+$', furl)
                    fid = fid_m.group(1) if fid_m else None
                formats.append({
                    'quality': QUALITIES(fid),
                    'format_id': fid,
                    'url': furl,
                })
            self._sort_formats(formats)
            info = {
                'formats': formats,
            }
        else:  # request JSON file
            if not document_id:
                video_id = self._search_regex(
                    (r'/play/(?:config|media|sola)/(\d+)', r'contentId["\']\s*:\s*(\d+)'),
                    webpage, 'media id', default=None)
            info = self._extract_media_info(
                'http://www.ardmediathek.de/play/media/%s' % video_id,
                webpage, video_id)

        info.update({
            'id': video_id,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
        })
        info.update(self._ARD_extract_episode_info(info['title']))

        return info

class ARDIE(InfoExtractor):
    _VALID_URL = r'(?P<mainurl>https?://(?:www\.)?daserste\.de/(?:[^/?#&]+/)+(?P<id>[^/?#&]+))\.html'
    _TESTS = [{
        # available till 7.01.2022
        'url': 'https://www.daserste.de/information/talk/maischberger/videos/maischberger-die-woche-video100.html',
        'md5': '867d8aa39eeaf6d76407c5ad1bb0d4c1',
        'info_dict': {
            'id': 'maischberger-die-woche-video100',
            'display_id': 'maischberger-die-woche-video100',
            'ext': 'mp4',
            'duration': 3687.0,
            'title': 'maischberger. die woche vom 7. Januar 2021',
            'upload_date': '20210107',
            'thumbnail': r're:^https?://.*\.jpg$',
        },
    }, {
        'url': 'https://www.daserste.de/information/politik-weltgeschehen/morgenmagazin/videosextern/dominik-kahun-aus-der-nhl-direkt-zur-weltmeisterschaft-100.html',
        'only_matching': True,
    }, {
        'url': 'https://www.daserste.de/information/nachrichten-wetter/tagesthemen/videosextern/tagesthemen-17736.html',
        'only_matching': True,
    }, {
        'url': 'https://www.daserste.de/unterhaltung/serie/in-aller-freundschaft-die-jungen-aerzte/videos/diversity-tag-sanam-afrashteh100.html',
        'only_matching': True,
    }, {
        'url': 'http://www.daserste.de/information/reportage-dokumentation/dokus/videos/die-story-im-ersten-mission-unter-falscher-flagge-100.html',
        'only_matching': True,
    }, {
        'url': 'https://www.daserste.de/unterhaltung/serie/in-aller-freundschaft-die-jungen-aerzte/Drehpause-100.html',
        'only_matching': True,
    }, {
        'url': 'https://www.daserste.de/unterhaltung/film/filmmittwoch-im-ersten/videos/making-ofwendezeit-video-100.html',
        'only_matching': True,
    }]

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

        player_url = mobj.group('mainurl') + '~playerXml.xml'
        doc = self._download_xml(player_url, display_id)
        video_node = doc.find('./video')
        upload_date = unified_strdate(xpath_text(
            video_node, './broadcastDate'))
        thumbnail = xpath_text(video_node, './/teaserImage//variant/url')

        formats = []
        for a in video_node.findall('.//asset'):
            file_name = xpath_text(a, './fileName', default=None)
            if not file_name:
                continue
            format_type = a.attrib.get('type')
            format_url = url_or_none(file_name)
            if format_url:
                ext = determine_ext(file_name)
                if ext == 'm3u8':
                    formats.extend(self._extract_m3u8_formats(
                        format_url, display_id, 'mp4', entry_protocol='m3u8_native',
                        m3u8_id=format_type or 'hls', fatal=False))
                    continue
                elif ext == 'f4m':
                    formats.extend(self._extract_f4m_formats(
                        update_url_query(format_url, {'hdcore': '3.7.0'}),
                        display_id, f4m_id=format_type or 'hds', fatal=False))
                    continue
            f = {
                'format_id': format_type,
                'width': int_or_none(xpath_text(a, './frameWidth')),
                'height': int_or_none(xpath_text(a, './frameHeight')),
                'vbr': int_or_none(xpath_text(a, './bitrateVideo')),
                'abr': int_or_none(xpath_text(a, './bitrateAudio')),
                'vcodec': xpath_text(a, './codecVideo'),
                'tbr': int_or_none(xpath_text(a, './totalBitrate')),
            }
            server_prefix = xpath_text(a, './serverPrefix', default=None)
            if server_prefix:
                f.update({
                    'url': server_prefix,
                    'playpath': file_name,
                })
            else:
                if not format_url:
                    continue
                f['url'] = format_url
            formats.append(f)
        self._sort_formats(formats)

        _SUB_FORMATS = (
            ('./dataTimedText', 'ttml'),
            ('./dataTimedTextNoOffset', 'ttml'),
            ('./dataTimedTextVtt', 'vtt'),
        )

        subtitles = {}
        for subsel, subext in _SUB_FORMATS:
            for node in video_node.findall(subsel):
                subtitles.setdefault('de', []).append({
                    'url': node.attrib['url'],
                    'ext': subext,
                })

        return {
            'id': xpath_text(video_node, './videoId', default=display_id),
            'formats': formats,
            'subtitles': subtitles,
            'display_id': display_id,
            'title': video_node.find('./title').text,
            'duration': parse_duration(video_node.find('./duration').text),
            'upload_date': upload_date,
            'thumbnail': thumbnail,
        }

class ARDBetaMediathekIE(ARDMediathekBaseIE):
    _VALID_URL = r'''(?x)https://
        (?:(?:beta|www)\.)?ardmediathek\.de/
        (?:(?P<client>[^/]+)/)?
        (?:player|live|video|(?P<playlist>sendung|sammlung))/
        (?:(?P<display_id>(?(playlist)[^?#]+?|[^?#]+))/)?
        (?P<id>(?(playlist)|Y3JpZDovL)[a-zA-Z0-9]+)
        (?(playlist)/(?P<season>\d+)?/?(?:[?#]|$))'''

    _TESTS = [{
        'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
        'md5': 'a1dc75a39c61601b980648f7c9f9f71d',
        'info_dict': {
            'display_id': 'die-robuste-roswita',
            'id': '78566716',
            'title': 'Die robuste Roswita',
            'description': r're:^Der Mord.*totgeglaubte Ehefrau Roswita',
            'duration': 5316,
            'thumbnail': 'https://img.ardmediathek.de/standard/00/78/56/67/84/575672121/16x9/960?mandant=ard',
            'timestamp': 1596658200,
            'upload_date': '20200805',
            'ext': 'mp4',
        },
        'skip': 'Error',
    }, {
        'url': 'https://www.ardmediathek.de/video/tagesschau-oder-tagesschau-20-00-uhr/das-erste/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll',
        'md5': 'f1837e563323b8a642a8ddeff0131f51',
        'info_dict': {
            'id': '10049223',
            'ext': 'mp4',
            'title': 'tagesschau, 20:00 Uhr',
            'timestamp': 1636398000,
            'description': 'md5:39578c7b96c9fe50afdf5674ad985e6b',
            'upload_date': '20211108',
        },
    }, {
        'url': 'https://www.ardmediathek.de/sendung/beforeigners/beforeigners/staffel-1/Y3JpZDovL2Rhc2Vyc3RlLmRlL2JlZm9yZWlnbmVycw/1',
        'playlist_count': 6,
        'info_dict': {
            'id': 'Y3JpZDovL2Rhc2Vyc3RlLmRlL2JlZm9yZWlnbmVycw',
            'title': 'beforeigners/beforeigners/staffel-1',
        },
    }, {
        'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
        'only_matching': True,
    }, {
        'url': 'https://ardmediathek.de/ard/video/saartalk/saartalk-gesellschaftsgift-haltung-gegen-hass/sr-fernsehen/Y3JpZDovL3NyLW9ubGluZS5kZS9TVF84MTY4MA/',
        'only_matching': True,
    }, {
        'url': 'https://www.ardmediathek.de/ard/video/trailer/private-eyes-s01-e01/one/Y3JpZDovL3dkci5kZS9CZWl0cmFnLTE1MTgwYzczLWNiMTEtNGNkMS1iMjUyLTg5MGYzOWQxZmQ1YQ/',
        'only_matching': True,
    }, {
        'url': 'https://www.ardmediathek.de/ard/player/Y3JpZDovL3N3ci5kZS9hZXgvbzEwNzE5MTU/',
        'only_matching': True,
    }, {
        'url': 'https://www.ardmediathek.de/swr/live/Y3JpZDovL3N3ci5kZS8xMzQ4MTA0Mg',
        'only_matching': True,
    }, {
        # playlist of type 'sendung'
        'url': 'https://www.ardmediathek.de/ard/sendung/doctor-who/Y3JpZDovL3dkci5kZS9vbmUvZG9jdG9yIHdobw/',
        'only_matching': True,
    }, {
        # playlist of type 'sammlung'
        'url': 'https://www.ardmediathek.de/ard/sammlung/team-muenster/5JpTzLSbWUAK8184IOvEir/',
        'only_matching': True,
    }, {
        'url': 'https://www.ardmediathek.de/video/coronavirus-update-ndr-info/astrazeneca-kurz-lockdown-und-pims-syndrom-81/ndr/Y3JpZDovL25kci5kZS84NzE0M2FjNi0wMWEwLTQ5ODEtOTE5NS1mOGZhNzdhOTFmOTI/',
        'only_matching': True,
    }, {
        'url': 'https://www.ardmediathek.de/ard/player/Y3JpZDovL3dkci5kZS9CZWl0cmFnLWQ2NDJjYWEzLTMwZWYtNGI4NS1iMTI2LTU1N2UxYTcxOGIzOQ/tatort-duo-koeln-leipzig-ihr-kinderlein-kommet',
        'only_matching': True,
    }]

    def _ARD_load_playlist_snipped(self, playlist_id, display_id, client, mode, pageNumber):
        """ Query the ARD server for playlist information
        and returns the data in "raw" format """
        if mode == 'sendung':
            graphQL = json.dumps({
                'query': '''{
                    showPage(
                        client: "%s"
                        showId: "%s"
                        pageNumber: %d
                    ) {
                        pagination {
                            pageSize
                            totalElements
                        }
                        teasers {        # Array
                            mediumTitle
                            links { target { id href title } }
                            type
                        }
                    }}''' % (client, playlist_id, pageNumber),
            }).encode()
        else:  # mode == 'sammlung'
            graphQL = json.dumps({
                'query': '''{
                    morePage(
                        client: "%s"
                        compilationId: "%s"
                        pageNumber: %d
                    ) {
                        widget {
                            pagination {
                                pageSize
                                totalElements
                            }
                            teasers {        # Array
                                mediumTitle
                                links { target { id href title } }
                                type
                            }
                        }
                    }}''' % (client, playlist_id, pageNumber),
            }).encode()
        # Ressources for ARD graphQL debugging:
        # https://api-test.ardmediathek.de/public-gateway
        show_page = self._download_json(
            'https://api.ardmediathek.de/public-gateway',
            '[Playlist] %s' % display_id,
            data=graphQL,
            headers={'Content-Type': 'application/json'})['data']
        # align the structure of the returned data:
        if mode == 'sendung':
            show_page = show_page['showPage']
        else:  # mode == 'sammlung'
            show_page = show_page['morePage']['widget']
        return show_page

    def _ARD_extract_playlist(self, url, playlist_id, display_id, client, mode):
        """ Collects all playlist entries and returns them as info dict.
        Supports playlists of mode 'sendung' and 'sammlung', and also nested
        playlists. """
        entries = []
        pageNumber = 0
        while True:  # iterate by pageNumber
            show_page = self._ARD_load_playlist_snipped(
                playlist_id, display_id, client, mode, pageNumber)
            for teaser in show_page['teasers']:  # process playlist items
                if '/compilation/' in teaser['links']['target']['href']:
                    # alternativ cond.: teaser['type'] == "compilation"
                    # => This is an nested compilation, e.g. like:
                    # https://www.ardmediathek.de/ard/sammlung/die-kirche-bleibt-im-dorf/5eOHzt8XB2sqeFXbIoJlg2/
                    link_mode = 'sammlung'
                else:
                    link_mode = 'video'

                item_url = 'https://www.ardmediathek.de/%s/%s/%s/%s/%s' % (
                    client, link_mode, display_id,
                    # perform HTLM quoting of episode title similar to ARD:
                    re.sub('^-|-$', '',  # remove '-' from begin/end
                           re.sub('[^a-zA-Z0-9]+', '-',  # replace special chars by -
                                  teaser['links']['target']['title'].lower()
                                  .replace('ä', 'ae').replace('ö', 'oe')
                                  .replace('ü', 'ue').replace('ß', 'ss'))),
                    teaser['links']['target']['id'])
                entries.append(self.url_result(
                    item_url,
                    ie=ARDBetaMediathekIE.ie_key()))

            if (show_page['pagination']['pageSize'] * (pageNumber + 1)
               >= show_page['pagination']['totalElements']):
                # we've processed enough pages to get all playlist entries
                break
            pageNumber = pageNumber + 1

        return self.playlist_result(entries, playlist_id, playlist_title=display_id)

    def _real_extract(self, url):
        video_id, display_id, playlist_type, client, season_number = self._match_valid_url(url).group(
            'id', 'display_id', 'playlist', 'client', 'season')
        display_id, client = display_id or video_id, client or 'ard'

        if playlist_type:
            # TODO: Extract only specified season
            return self._ARD_extract_playlist(url, video_id, display_id, client, playlist_type)

        player_page = self._download_json(
            'https://api.ardmediathek.de/public-gateway',
            display_id, data=json.dumps({
                'query': '''{
  playerPage(client:"%s", clipId: "%s") {
    blockedByFsk
    broadcastedOn
    maturityContentRating
    mediaCollection {
      _duration
      _geoblocked
      _isLive
      _mediaArray {
        _mediaStreamArray {
          _quality
          _server
          _stream
        }
      }
      _previewImage
      _subtitleUrl
      _type
    }
    show {
      title
    }
    synopsis
    title
    tracking {
      atiCustomVars {
        contentId
      }
    }
  }
}''' % (client, video_id),
            }).encode(), headers={
                'Content-Type': 'application/json'
            })['data']['playerPage']
        title = player_page['title']
        content_id = str_or_none(try_get(
            player_page, lambda x: x['tracking']['atiCustomVars']['contentId']))
        media_collection = player_page.get('mediaCollection') or {}
        if not media_collection and content_id:
            media_collection = self._download_json(
                'https://www.ardmediathek.de/play/media/' + content_id,
                content_id, fatal=False) or {}
        info = self._parse_media_info(
            media_collection, content_id or video_id,
            player_page.get('blockedByFsk'))
        age_limit = None
        description = player_page.get('synopsis')
        maturity_content_rating = player_page.get('maturityContentRating')
        if maturity_content_rating:
            age_limit = int_or_none(maturity_content_rating.lstrip('FSK'))
        if not age_limit and description:
            age_limit = int_or_none(self._search_regex(
                r'\(FSK\s*(\d+)\)\s*$', description, 'age limit', default=None))
        info.update({
            'age_limit': age_limit,
            'display_id': display_id,
            'title': title,
            'description': description,
            'timestamp': unified_timestamp(player_page.get('broadcastedOn')),
            'series': try_get(player_page, lambda x: x['show']['title']),
        })
        info.update(self._ARD_extract_episode_info(info['title']))
        return info


code:
from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    float_or_none,
    int_or_none,
    parse_iso8601,
    parse_qs,
    try_get,
)

class ArkenaIE(InfoExtractor):
    _VALID_URL = r'''(?x)
                        https?://
                            (?:
                                video\.(?:arkena|qbrick)\.com/play2/embed/player\?|
                                play\.arkena\.com/(?:config|embed)/avp/v\d/player/media/(?P<id>[^/]+)/[^/]+/(?P<account_id>\d+)
                            )
                        '''
    # See https://support.arkena.com/display/PLAY/Ways+to+embed+your+video
    _EMBED_REGEX = [r'<iframe[^>]+src=(["\'])(?P<url>(?:https?:)?//play\.arkena\.com/embed/avp/.+?)\1']
    _TESTS = [{
        'url': 'https://video.qbrick.com/play2/embed/player?accountId=1034090&mediaId=d8ab4607-00090107-aab86310',
        'md5': '97f117754e5f3c020f5f26da4a44ebaf',
        'info_dict': {
            'id': 'd8ab4607-00090107-aab86310',
            'ext': 'mp4',
            'title': 'EM_HT20_117_roslund_v2.mp4',
            'timestamp': 1608285912,
            'upload_date': '20201218',
            'duration': 1429.162667,
            'subtitles': {
                'sv': 'count:3',
            },
        },
    }, {
        'url': 'https://play.arkena.com/embed/avp/v2/player/media/b41dda37-d8e7-4d3f-b1b5-9a9db578bdfe/1/129411',
        'only_matching': True,
    }, {
        'url': 'https://play.arkena.com/config/avp/v2/player/media/b41dda37-d8e7-4d3f-b1b5-9a9db578bdfe/1/129411/?callbackMethod=jQuery1111023664739129262213_1469227693893',
        'only_matching': True,
    }, {
        'url': 'http://play.arkena.com/config/avp/v1/player/media/327336/darkmatter/131064/?callbackMethod=jQuery1111002221189684892677_1469227595972',
        'only_matching': True,
    }, {
        'url': 'http://play.arkena.com/embed/avp/v1/player/media/327336/darkmatter/131064/',
        'only_matching': True,
    }, {
        'url': 'http://video.arkena.com/play2/embed/player?accountId=472718&mediaId=35763b3b-00090078-bf604299&pageStyling=styled',
        'only_matching': True,
    }]

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

        # Handle http://video.arkena.com/play2/embed/player URL
        if not video_id:
            qs = parse_qs(url)
            video_id = qs.get('mediaId', [None])[0]
            account_id = qs.get('accountId', [None])[0]
            if not video_id or not account_id:
                raise ExtractorError('Invalid URL', expected=True)

        media = self._download_json(
            'https://video.qbrick.com/api/v1/public/accounts/%s/medias/%s' % (account_id, video_id),
            video_id, query={
                # https://video.qbrick.com/docs/api/examples/library-api.html
                'fields': 'asset/resources/*/renditions/*(height,id,language,links/*(href,mimeType),type,size,videos/*(audios/*(codec,sampleRate),bitrate,codec,duration,height,width),width),created,metadata/*(title,description),tags',
            })
        metadata = media.get('metadata') or {}
        title = metadata['title']

        duration = None
        formats = []
        thumbnails = []
        subtitles = {}
        for resource in media['asset']['resources']:
            for rendition in (resource.get('renditions') or []):
                rendition_type = rendition.get('type')
                for i, link in enumerate(rendition.get('links') or []):
                    href = link.get('href')
                    if not href:
                        continue
                    if rendition_type == 'image':
                        thumbnails.append({
                            'filesize': int_or_none(rendition.get('size')),
                            'height': int_or_none(rendition.get('height')),
                            'id': rendition.get('id'),
                            'url': href,
                            'width': int_or_none(rendition.get('width')),
                        })
                    elif rendition_type == 'subtitle':
                        subtitles.setdefault(rendition.get('language') or 'en', []).append({
                            'url': href,
                        })
                    elif rendition_type == 'video':
                        f = {
                            'filesize': int_or_none(rendition.get('size')),
                            'format_id': rendition.get('id'),
                            'url': href,
                        }
                        video = try_get(rendition, lambda x: x['videos'][i], dict)
                        if video:
                            if not duration:
                                duration = float_or_none(video.get('duration'))
                            f.update({
                                'height': int_or_none(video.get('height')),
                                'tbr': int_or_none(video.get('bitrate'), 1000),
                                'vcodec': video.get('codec'),
                                'width': int_or_none(video.get('width')),
                            })
                            audio = try_get(video, lambda x: x['audios'][0], dict)
                            if audio:
                                f.update({
                                    'acodec': audio.get('codec'),
                                    'asr': int_or_none(audio.get('sampleRate')),
                                })
                        formats.append(f)
                    elif rendition_type == 'index':
                        mime_type = link.get('mimeType')
                        if mime_type == 'application/smil+xml':
                            formats.extend(self._extract_smil_formats(
                                href, video_id, fatal=False))
                        elif mime_type == 'application/x-mpegURL':
                            formats.extend(self._extract_m3u8_formats(
                                href, video_id, 'mp4', 'm3u8_native',
                                m3u8_id='hls', fatal=False))
                        elif mime_type == 'application/hds+xml':
                            formats.extend(self._extract_f4m_formats(
                                href, video_id, f4m_id='hds', fatal=False))
                        elif mime_type == 'application/dash+xml':
                            formats.extend(self._extract_f4m_formats(
                                href, video_id, f4m_id='hds', fatal=False))
                        elif mime_type == 'application/vnd.ms-sstr+xml':
                            formats.extend(self._extract_ism_formats(
                                href, video_id, ism_id='mss', fatal=False))
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'description': metadata.get('description'),
            'timestamp': parse_iso8601(media.get('created')),
            'thumbnails': thumbnails,
            'subtitles': subtitles,
            'duration': duration,
            'tags': media.get('tags'),
            'formats': formats,
        }

repo_name: Lesmiscore/ytdl


code:
from .common import InfoExtractor
from ..compat import (
    compat_parse_qs,
    compat_urllib_parse_urlparse,
)
from ..utils import (
    format_field,
    float_or_none,
    int_or_none,
    parse_iso8601,
    remove_start,
)

class ArnesIE(InfoExtractor):
    IE_NAME = 'video.arnes.si'
    IE_DESC = 'Arnes Video'
    _VALID_URL = r'https?://video\.arnes\.si/(?:[a-z]{2}/)?(?:watch|embed|api/(?:asset|public/video))/(?P<id>[0-9a-zA-Z]{12})'
    _TESTS = [{
        'url': 'https://video.arnes.si/watch/a1qrWTOQfVoU?t=10',
        'md5': '4d0f4d0a03571b33e1efac25fd4a065d',
        'info_dict': {
            'id': 'a1qrWTOQfVoU',
            'ext': 'mp4',
            'title': 'Linearna neodvisnost, definicija',
            'description': 'Linearna neodvisnost, definicija',
            'license': 'PRIVATE',
            'creator': 'Polona Oblak',
            'timestamp': 1585063725,
            'upload_date': '20200324',
            'channel': 'Polona Oblak',
            'channel_id': 'q6pc04hw24cj',
            'channel_url': 'https://video.arnes.si/?channel=q6pc04hw24cj',
            'duration': 596.75,
            'view_count': int,
            'tags': ['linearna_algebra'],
            'start_time': 10,
        }
    }, {
        'url': 'https://video.arnes.si/api/asset/s1YjnV7hadlC/play.mp4',
        'only_matching': True,
    }, {
        'url': 'https://video.arnes.si/embed/s1YjnV7hadlC',
        'only_matching': True,
    }, {
        'url': 'https://video.arnes.si/en/watch/s1YjnV7hadlC',
        'only_matching': True,
    }, {
        'url': 'https://video.arnes.si/embed/s1YjnV7hadlC?t=123&hideRelated=1',
        'only_matching': True,
    }, {
        'url': 'https://video.arnes.si/api/public/video/s1YjnV7hadlC',
        'only_matching': True,
    }]
    _BASE_URL = 'https://video.arnes.si'

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

        video = self._download_json(
            self._BASE_URL + '/api/public/video/' + video_id, video_id)['data']
        title = video['title']

        formats = []
        for media in (video.get('media') or []):
            media_url = media.get('url')
            if not media_url:
                continue
            formats.append({
                'url': self._BASE_URL + media_url,
                'format_id': remove_start(media.get('format'), 'FORMAT_'),
                'format_note': media.get('formatTranslation'),
                'width': int_or_none(media.get('width')),
                'height': int_or_none(media.get('height')),
            })
        self._sort_formats(formats)

        channel = video.get('channel') or {}
        channel_id = channel.get('url')
        thumbnail = video.get('thumbnailUrl')

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'thumbnail': self._BASE_URL + thumbnail,
            'description': video.get('description'),
            'license': video.get('license'),
            'creator': video.get('author'),
            'timestamp': parse_iso8601(video.get('creationTime')),
            'channel': channel.get('name'),
            'channel_id': channel_id,
            'channel_url': format_field(channel_id, None, f'{self._BASE_URL}/?channel=%s'),
            'duration': float_or_none(video.get('duration'), 1000),
            'view_count': int_or_none(video.get('views')),
            'tags': video.get('hashtags'),
            'start_time': int_or_none(compat_parse_qs(
                compat_urllib_parse_urlparse(url).query).get('t', [None])[0]),
        }


code:
import re

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    GeoRestrictedError,
    int_or_none,
    parse_iso8601,
    parse_qs,
    strip_or_none,
    traverse_obj,
    url_or_none,
)

class ArteTVBaseIE(InfoExtractor):
    _ARTE_LANGUAGES = 'fr|de|en|es|it|pl'
    _API_BASE = 'https://api.arte.tv/api/player/v2'

class ArteTVIE(ArteTVBaseIE):
    _VALID_URL = r'''(?x)
                    (?:https?://
                        (?:
                            (?:www\.)?arte\.tv/(?P<lang>%(langs)s)/videos|
                            api\.arte\.tv/api/player/v\d+/config/(?P<lang_2>%(langs)s)
                        )
                    |arte://program)
                        /(?P<id>\d{6}-\d{3}-[AF]|LIVE)
                    ''' % {'langs': ArteTVBaseIE._ARTE_LANGUAGES}
    _TESTS = [{
        'url': 'https://www.arte.tv/en/videos/088501-000-A/mexico-stealing-petrol-to-survive/',
        'only_matching': True,
    }, {
        'url': 'https://www.arte.tv/pl/videos/100103-000-A/usa-dyskryminacja-na-porodowce/',
        'info_dict': {
            'id': '100103-000-A',
            'title': 'USA: Dyskryminacja na porodówce',
            'description': 'md5:242017b7cce59ffae340a54baefcafb1',
            'alt_title': 'ARTE Reportage',
            'upload_date': '20201103',
            'duration': 554,
            'thumbnail': r're:https://api-cdn\.arte\.tv/.+940x530',
            'timestamp': 1604417980,
            'ext': 'mp4',
        },
        'params': {'skip_download': 'm3u8'}
    }, {
        'note': 'No alt_title',
        'url': 'https://www.arte.tv/fr/videos/110371-000-A/la-chaleur-supplice-des-arbres-de-rue/',
        'info_dict': {
            'id': '110371-000-A',
            'ext': 'mp4',
            'upload_date': '20220718',
            'duration': 154,
            'timestamp': 1658162460,
            'description': 'md5:5890f36fe7dccfadb8b7c0891de54786',
            'title': 'La chaleur, supplice des arbres de rue',
            'thumbnail': 'https://api-cdn.arte.tv/img/v2/image/CPE2sQDtD8GLQgt8DuYHLf/940x530',
        },
        'params': {'skip_download': 'm3u8'}
    }, {
        'url': 'https://api.arte.tv/api/player/v2/config/de/100605-013-A',
        'only_matching': True,
    }, {
        'url': 'https://api.arte.tv/api/player/v2/config/de/LIVE',
        'only_matching': True,
    }]

    _GEO_BYPASS = True

    _LANG_MAP = {  # ISO639 -> French abbreviations
        'fr': 'F',
        'de': 'A',
        'en': 'E[ANG]',
        'es': 'E[ESP]',
        'it': 'E[ITA]',
        'pl': 'E[POL]',
        # XXX: probably means mixed; <https://www.arte.tv/en/videos/107710-029-A/dispatches-from-ukraine-local-journalists-report/>
        # uses this code for audio that happens to be in Ukrainian, but the manifest uses the ISO code 'mul' (mixed)
        'mul': 'EU',
    }

    _VERSION_CODE_RE = re.compile(r'''(?x)
        V
        (?P<original_voice>O?)
        (?P<vlang>[FA]|E\[[A-Z]+\]|EU)?
        (?P<audio_desc>AUD|)
        (?:
            (?P<has_sub>-ST)
            (?P<sdh_sub>M?)
            (?P<sub_lang>[FA]|E\[[A-Z]+\]|EU)
        )?
    ''')

    # all obtained by exhaustive testing
    _COUNTRIES_MAP = {
        'DE_FR': (
            'BL', 'DE', 'FR', 'GF', 'GP', 'MF', 'MQ', 'NC',
            'PF', 'PM', 'RE', 'WF', 'YT',
        ),
        # with both of the below 'BE' sometimes works, sometimes doesn't
        'EUR_DE_FR': (
            'AT', 'BL', 'CH', 'DE', 'FR', 'GF', 'GP', 'LI',
            'MC', 'MF', 'MQ', 'NC', 'PF', 'PM', 'RE', 'WF',
            'YT',
        ),
        'SAT': (
            'AD', 'AT', 'AX', 'BG', 'BL', 'CH', 'CY', 'CZ',
            'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GF',
            'GR', 'HR', 'HU', 'IE', 'IS', 'IT', 'KN', 'LI',
            'LT', 'LU', 'LV', 'MC', 'MF', 'MQ', 'MT', 'NC',
            'NL', 'NO', 'PF', 'PL', 'PM', 'PT', 'RE', 'RO',
            'SE', 'SI', 'SK', 'SM', 'VA', 'WF', 'YT',
        ),
    }

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        video_id = mobj.group('id')
        lang = mobj.group('lang') or mobj.group('lang_2')
        langauge_code = self._LANG_MAP.get(lang)

        config = self._download_json(f'{self._API_BASE}/config/{lang}/{video_id}', video_id)

        geoblocking = traverse_obj(config, ('data', 'attributes', 'restriction', 'geoblocking')) or {}
        if geoblocking.get('restrictedArea'):
            raise GeoRestrictedError(f'Video restricted to {geoblocking["code"]!r}',
                                     countries=self._COUNTRIES_MAP.get(geoblocking['code'], ('DE', 'FR')))

        if not traverse_obj(config, ('data', 'attributes', 'rights')):
            # Eg: https://www.arte.tv/de/videos/097407-215-A/28-minuten
            # Eg: https://www.arte.tv/es/videos/104351-002-A/serviteur-du-peuple-1-23
            raise ExtractorError(
                'Video is not available in this language edition of Arte or broadcast rights expired', expected=True)

        formats, subtitles = [], {}
        for stream in config['data']['attributes']['streams']:
            # official player contains code like `e.get("versions")[0].eStat.ml5`
            stream_version = stream['versions'][0]
            stream_version_code = stream_version['eStat']['ml5']

            lang_pref = -1
            m = self._VERSION_CODE_RE.match(stream_version_code)
            if m:
                lang_pref = int(''.join('01'[x] for x in (
                    m.group('vlang') == langauge_code,      # we prefer voice in the requested language
                    not m.group('audio_desc'),              # and not the audio description version
                    bool(m.group('original_voice')),        # but if voice is not in the requested language, at least choose the original voice
                    m.group('sub_lang') == langauge_code,   # if subtitles are present, we prefer them in the requested language
                    not m.group('has_sub'),                 # but we prefer no subtitles otherwise
                    not m.group('sdh_sub'),                 # and we prefer not the hard-of-hearing subtitles if there are subtitles
                )))

            if stream['protocol'].startswith('HLS'):
                fmts, subs = self._extract_m3u8_formats_and_subtitles(
                    stream['url'], video_id=video_id, ext='mp4', m3u8_id=stream_version_code, fatal=False)
                for fmt in fmts:
                    fmt.update({
                        'format_note': f'{stream_version.get("label", "unknown")} [{stream_version.get("shortLabel", "?")}]',
                        'language_preference': lang_pref,
                    })
                formats.extend(fmts)
                self._merge_subtitles(subs, target=subtitles)

            elif stream['protocol'] in ('HTTPS', 'RTMP'):
                formats.append({
                    'format_id': f'{stream["protocol"]}-{stream_version_code}',
                    'url': stream['url'],
                    'format_note': f'{stream_version.get("label", "unknown")} [{stream_version.get("shortLabel", "?")}]',
                    'language_preference': lang_pref,
                    # 'ext': 'mp4',  # XXX: may or may not be necessary, at least for HTTPS
                })

            else:
                self.report_warning(f'Skipping stream with unknown protocol {stream["protocol"]}')

            # TODO: chapters from stream['segments']?
            # The JS also looks for chapters in config['data']['attributes']['chapters'],
            # but I am yet to find a video having those

        self._sort_formats(formats)

        metadata = config['data']['attributes']['metadata']

        return {
            'id': metadata['providerId'],
            'webpage_url': traverse_obj(metadata, ('link', 'url')),
            'title': traverse_obj(metadata, 'subtitle', 'title'),
            'alt_title': metadata.get('subtitle') and metadata.get('title'),
            'description': metadata.get('description'),
            'duration': traverse_obj(metadata, ('duration', 'seconds')),
            'language': metadata.get('language'),
            'timestamp': traverse_obj(config, ('data', 'attributes', 'rights', 'begin'), expected_type=parse_iso8601),
            'is_live': config['data']['attributes'].get('live', False),
            'formats': formats,
            'subtitles': subtitles,
            'thumbnails': [
                {'url': image['url'], 'id': image.get('caption')}
                for image in metadata.get('images') or [] if url_or_none(image.get('url'))
            ],
        }

class ArteTVEmbedIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?arte\.tv/player/v\d+/index\.php\?.*?\bjson_url=.+'
    _EMBED_REGEX = [r'<(?:iframe|script)[^>]+src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?arte\.tv/player/v\d+/index\.php\?.*?\bjson_url=.+?)\1']
    _TESTS = [{
        'url': 'https://www.arte.tv/player/v5/index.php?json_url=https%3A%2F%2Fapi.arte.tv%2Fapi%2Fplayer%2Fv2%2Fconfig%2Fde%2F100605-013-A&lang=de&autoplay=true&mute=0100605-013-A',
        'info_dict': {
            'id': '100605-013-A',
            'ext': 'mp4',
            'title': 'United we Stream November Lockdown Edition #13',
            'description': 'md5:be40b667f45189632b78c1425c7c2ce1',
            'upload_date': '20201116',
        },
        'skip': 'No video available'
    }, {
        'url': 'https://www.arte.tv/player/v3/index.php?json_url=https://api.arte.tv/api/player/v2/config/de/100605-013-A',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        qs = parse_qs(url)
        json_url = qs['json_url'][0]
        video_id = ArteTVIE._match_id(json_url)
        return self.url_result(
            json_url, ie=ArteTVIE.ie_key(), video_id=video_id)

class ArteTVPlaylistIE(ArteTVBaseIE):
    _VALID_URL = r'https?://(?:www\.)?arte\.tv/(?P<lang>%s)/videos/(?P<id>RC-\d{6})' % ArteTVBaseIE._ARTE_LANGUAGES
    _TESTS = [{
        'url': 'https://www.arte.tv/en/videos/RC-016954/earn-a-living/',
        'only_matching': True,
    }, {
        'url': 'https://www.arte.tv/pl/videos/RC-014123/arte-reportage/',
        'playlist_mincount': 100,
        'info_dict': {
            'description': 'md5:84e7bf1feda248bc325ebfac818c476e',
            'id': 'RC-014123',
            'title': 'ARTE Reportage - najlepsze reportaże',
        },
    }]

    def _real_extract(self, url):
        lang, playlist_id = self._match_valid_url(url).group('lang', 'id')
        playlist = self._download_json(
            f'{self._API_BASE}/playlist/{lang}/{playlist_id}', playlist_id)['data']['attributes']

        entries = [{
            '_type': 'url_transparent',
            'url': video['config']['url'],
            'ie_key': ArteTVIE.ie_key(),
            'id': video.get('providerId'),
            'title': video.get('title'),
            'alt_title': video.get('subtitle'),
            'thumbnail': url_or_none(traverse_obj(video, ('mainImage', 'url'))),
            'duration': int_or_none(traverse_obj(video, ('duration', 'seconds'))),
        } for video in traverse_obj(playlist, ('items', lambda _, v: v['config']['url']))]

        return self.playlist_result(entries, playlist_id,
                                    traverse_obj(playlist, ('metadata', 'title')),
                                    traverse_obj(playlist, ('metadata', 'description')))

class ArteTVCategoryIE(ArteTVBaseIE):
    _VALID_URL = r'https?://(?:www\.)?arte\.tv/(?P<lang>%s)/videos/(?P<id>[\w-]+(?:/[\w-]+)*)/?\s*$' % ArteTVBaseIE._ARTE_LANGUAGES
    _TESTS = [{
        'url': 'https://www.arte.tv/en/videos/politics-and-society/',
        'info_dict': {
            'id': 'politics-and-society',
            'title': 'Politics and society',
            'description': 'Investigative documentary series, geopolitical analysis, and international commentary',
        },
        'playlist_mincount': 13,
    }]

    @classmethod
    def suitable(cls, url):
        return (
            not any(ie.suitable(url) for ie in (ArteTVIE, ArteTVPlaylistIE, ))
            and super().suitable(url))

    def _real_extract(self, url):
        lang, playlist_id = self._match_valid_url(url).groups()
        webpage = self._download_webpage(url, playlist_id)

        items = []
        for video in re.finditer(
                r'<a\b[^>]*?href\s*=\s*(?P<q>"|\'|\b)(?P<url>https?://www\.arte\.tv/%s/videos/[\w/-]+)(?P=q)' % lang,
                webpage):
            video = video.group('url')
            if video == url:
                continue
            if any(ie.suitable(video) for ie in (ArteTVIE, ArteTVPlaylistIE, )):
                items.append(video)

        title = (self._og_search_title(webpage, default=None)
                 or self._html_search_regex(r'<title\b[^>]*>([^<]+)</title>', default=None))
        title = strip_or_none(title.rsplit('|', 1)[0]) or self._generic_title(url)

        return self.playlist_from_matches(items, playlist_id=playlist_id, playlist_title=title,
                                          description=self._og_search_description(webpage, default=None))


code:
import functools
import re

from .common import InfoExtractor
from .kaltura import KalturaIE
from ..utils import (
    extract_attributes,
    int_or_none,
    OnDemandPagedList,
    parse_age_limit,
    strip_or_none,
    try_get,
)

class AsianCrushBaseIE(InfoExtractor):
    _VALID_URL_BASE = r'https?://(?:www\.)?(?P<host>(?:(?:asiancrush|yuyutv|midnightpulp)\.com|(?:cocoro|retrocrush)\.tv))'
    _KALTURA_KEYS = [
        'video_url', 'progressive_url', 'download_url', 'thumbnail_url',
        'widescreen_thumbnail_url', 'screencap_widescreen',
    ]
    _API_SUFFIX = {'retrocrush.tv': '-ott'}

    def _call_api(self, host, endpoint, video_id, query, resource):
        return self._download_json(
            'https://api%s.%s/%s' % (self._API_SUFFIX.get(host, ''), host, endpoint), video_id,
            'Downloading %s JSON metadata' % resource, query=query,
            headers=self.geo_verification_headers())['objects']

    def _download_object_data(self, host, object_id, resource):
        return self._call_api(
            host, 'search', object_id, {'id': object_id}, resource)[0]

    def _get_object_description(self, obj):
        return strip_or_none(obj.get('long_description') or obj.get('short_description'))

    def _parse_video_data(self, video):
        title = video['name']

        entry_id, partner_id = [None] * 2
        for k in self._KALTURA_KEYS:
            k_url = video.get(k)
            if k_url:
                mobj = re.search(r'/p/(\d+)/.+?/entryId/([^/]+)/', k_url)
                if mobj:
                    partner_id, entry_id = mobj.groups()
                    break

        meta_categories = try_get(video, lambda x: x['meta']['categories'], list) or []
        categories = list(filter(None, [c.get('name') for c in meta_categories]))

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

        return {
            '_type': 'url_transparent',
            'url': 'kaltura:%s:%s' % (partner_id, entry_id),
            'ie_key': KalturaIE.ie_key(),
            'id': entry_id,
            'title': title,
            'description': self._get_object_description(video),
            'age_limit': parse_age_limit(video.get('mpaa_rating') or video.get('tv_rating')),
            'categories': categories,
            'series': show_info.get('show_name'),
            'season_number': int_or_none(show_info.get('season_num')),
            'season_id': show_info.get('season_id'),
            'episode_number': int_or_none(show_info.get('episode_num')),
        }

class AsianCrushIE(AsianCrushBaseIE):
    _VALID_URL = r'%s/video/(?:[^/]+/)?0+(?P<id>\d+)v\b' % AsianCrushBaseIE._VALID_URL_BASE
    _TESTS = [{
        'url': 'https://www.asiancrush.com/video/004289v/women-who-flirt',
        'md5': 'c3b740e48d0ba002a42c0b72857beae6',
        'info_dict': {
            'id': '1_y4tmjm5r',
            'ext': 'mp4',
            'title': 'Women Who Flirt',
            'description': 'md5:b65c7e0ae03a85585476a62a186f924c',
            'timestamp': 1496936429,
            'upload_date': '20170608',
            'uploader_id': 'craig@crifkin.com',
            'age_limit': 13,
            'categories': 'count:5',
            'duration': 5812,
        },
    }, {
        'url': 'https://www.asiancrush.com/video/she-was-pretty/011886v-pretty-episode-3/',
        'only_matching': True,
    }, {
        'url': 'https://www.yuyutv.com/video/013886v/the-act-of-killing/',
        'only_matching': True,
    }, {
        'url': 'https://www.yuyutv.com/video/peep-show/013922v-warring-factions/',
        'only_matching': True,
    }, {
        'url': 'https://www.midnightpulp.com/video/010400v/drifters/',
        'only_matching': True,
    }, {
        'url': 'https://www.midnightpulp.com/video/mononoke/016378v-zashikiwarashi-part-1/',
        'only_matching': True,
    }, {
        'url': 'https://www.cocoro.tv/video/the-wonderful-wizard-of-oz/008878v-the-wonderful-wizard-of-oz-ep01/',
        'only_matching': True,
    }, {
        'url': 'https://www.retrocrush.tv/video/true-tears/012328v-i...gave-away-my-tears',
        'only_matching': True,
    }]

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

        if host == 'cocoro.tv':
            webpage = self._download_webpage(url, video_id)
            embed_vars = self._parse_json(self._search_regex(
                r'iEmbedVars\s*=\s*({.+?})', webpage, 'embed vars',
                default='{}'), video_id, fatal=False) or {}
            video_id = embed_vars.get('entry_id') or video_id

        video = self._download_object_data(host, video_id, 'video')
        return self._parse_video_data(video)

class AsianCrushPlaylistIE(AsianCrushBaseIE):
    _VALID_URL = r'%s/series/0+(?P<id>\d+)s\b' % AsianCrushBaseIE._VALID_URL_BASE
    _TESTS = [{
        'url': 'https://www.asiancrush.com/series/006447s/fruity-samurai',
        'info_dict': {
            'id': '6447',
            'title': 'Fruity Samurai',
            'description': 'md5:7535174487e4a202d3872a7fc8f2f154',
        },
        'playlist_count': 13,
    }, {
        'url': 'https://www.yuyutv.com/series/013920s/peep-show/',
        'only_matching': True,
    }, {
        'url': 'https://www.midnightpulp.com/series/016375s/mononoke/',
        'only_matching': True,
    }, {
        'url': 'https://www.cocoro.tv/series/008549s/the-wonderful-wizard-of-oz/',
        'only_matching': True,
    }, {
        'url': 'https://www.retrocrush.tv/series/012355s/true-tears',
        'only_matching': True,
    }]
    _PAGE_SIZE = 1000000000

    def _fetch_page(self, domain, parent_id, page):
        videos = self._call_api(
            domain, 'getreferencedobjects', parent_id, {
                'max': self._PAGE_SIZE,
                'object_type': 'video',
                'parent_id': parent_id,
                'start': page * self._PAGE_SIZE,
            }, 'page %d' % (page + 1))
        for video in videos:
            yield self._parse_video_data(video)

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

        if host == 'cocoro.tv':
            webpage = self._download_webpage(url, playlist_id)

            entries = []

            for mobj in re.finditer(
                    r'<a[^>]+href=(["\'])(?P<url>%s.*?)\1[^>]*>' % AsianCrushIE._VALID_URL,
                    webpage):
                attrs = extract_attributes(mobj.group(0))
                if attrs.get('class') == 'clearfix':
                    entries.append(self.url_result(
                        mobj.group('url'), ie=AsianCrushIE.ie_key()))

            title = self._html_search_regex(
                r'(?s)<h1\b[^>]\bid=["\']movieTitle[^>]+>(.+?)</h1>', webpage,
                'title', default=None) or self._og_search_title(
                webpage, default=None) or self._html_search_meta(
                'twitter:title', webpage, 'title',
                default=None) or self._html_extract_title(webpage)
            if title:
                title = re.sub(r'\s*\|\s*.+?$', '', title)

            description = self._og_search_description(
                webpage, default=None) or self._html_search_meta(
                'twitter:description', webpage, 'description', fatal=False)
        else:
            show = self._download_object_data(host, playlist_id, 'show')
            title = show.get('name')
            description = self._get_object_description(show)
            entries = OnDemandPagedList(
                functools.partial(self._fetch_page, host, playlist_id),
                self._PAGE_SIZE)

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


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

import re

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

class AskMonaIE(InfoExtractor):
    IE_NAME = 'askmona'
    _VALID_URL = r'https?://(?:www\.)?askmona.org/(?P<id>\d+)/?'
    ALL_POSTS_URL = 'https://askmona.org/%s?n=1000'
    YOUTUBE_RE = r'<a\s+class="youtube"\s*name="([0-9A-Za-z_-]{11})"\s*href="[^"]*"\s*>'

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

        youtube_results = [self.url_result('https://youtu.be/%s' % frg.group(1))
                           for frg in re.finditer(self.YOUTUBE_RE, webpage)]
        if not youtube_results:
            raise ExtractorError('No videos found', expected=True)
        return self.playlist_result(youtube_results, video_id)

class AskMona3IE(AskMonaIE):
    IE_NAME = 'askmona3'
    _VALID_URL = r'https?://web3\.askmona.org/(?P<id>\d+)/?'
    ALL_POSTS_URL = 'https://web3.askmona.org/%s?n=1000'


code:
from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    ExtractorError,
    int_or_none,
    urlencode_postdata,
)

class AtresPlayerIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?atresplayer\.com/[^/]+/[^/]+/[^/]+/[^/]+/(?P<display_id>.+?)_(?P<id>[0-9a-f]{24})'
    _NETRC_MACHINE = 'atresplayer'
    _TESTS = [
        {
            'url': 'https://www.atresplayer.com/antena3/series/pequenas-coincidencias/temporada-1/capitulo-7-asuntos-pendientes_5d4aa2c57ed1a88fc715a615/',
            'info_dict': {
                'id': '5d4aa2c57ed1a88fc715a615',
                'ext': 'mp4',
                'title': 'Capítulo 7: Asuntos pendientes',
                'description': 'md5:7634cdcb4d50d5381bedf93efb537fbc',
                'duration': 3413,
            },
            'skip': 'This video is only available for registered users'
        },
        {
            'url': 'https://www.atresplayer.com/lasexta/programas/el-club-de-la-comedia/temporada-4/capitulo-10-especial-solidario-nochebuena_5ad08edf986b2855ed47adc4/',
            'only_matching': True,
        },
        {
            'url': 'https://www.atresplayer.com/antena3/series/el-secreto-de-puente-viejo/el-chico-de-los-tres-lunares/capitulo-977-29-12-14_5ad51046986b2886722ccdea/',
            'only_matching': True,
        },
    ]
    _API_BASE = 'https://api.atresplayer.com/'

    def _handle_error(self, e, code):
        if isinstance(e.cause, compat_HTTPError) and e.cause.code == code:
            error = self._parse_json(e.cause.read(), None)
            if error.get('error') == 'required_registered':
                self.raise_login_required()
            raise ExtractorError(error['error_description'], expected=True)
        raise

    def _perform_login(self, username, password):
        self._request_webpage(
            self._API_BASE + 'login', None, 'Downloading login page')

        try:
            target_url = self._download_json(
                'https://account.atresmedia.com/api/login', None,
                'Logging in', headers={
                    'Content-Type': 'application/x-www-form-urlencoded'
                }, data=urlencode_postdata({
                    'username': username,
                    'password': password,
                }))['targetUrl']
        except ExtractorError as e:
            self._handle_error(e, 400)

        self._request_webpage(target_url, None, 'Following Target URL')

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

        try:
            episode = self._download_json(
                self._API_BASE + 'client/v1/player/episode/' + video_id, video_id)
        except ExtractorError as e:
            self._handle_error(e, 403)

        title = episode['titulo']

        formats = []
        subtitles = {}
        for source in episode.get('sources', []):
            src = source.get('src')
            if not src:
                continue
            src_type = source.get('type')
            if src_type == 'application/vnd.apple.mpegurl':
                formats, subtitles = self._extract_m3u8_formats(
                    src, video_id, 'mp4', 'm3u8_native',
                    m3u8_id='hls', fatal=False)
            elif src_type == 'application/dash+xml':
                formats, subtitles = self._extract_mpd_formats(
                    src, video_id, mpd_id='dash', fatal=False)
        self._sort_formats(formats)

        heartbeat = episode.get('heartbeat') or {}
        omniture = episode.get('omniture') or {}
        get_meta = lambda x: heartbeat.get(x) or omniture.get(x)

        return {
            'display_id': display_id,
            'id': video_id,
            'title': title,
            'description': episode.get('descripcion'),
            'thumbnail': episode.get('imgPoster'),
            'duration': int_or_none(episode.get('duration')),
            'formats': formats,
            'channel': get_meta('channel'),
            'season': get_meta('season'),
            'episode_number': int_or_none(get_meta('episodeNumber')),
            'subtitles': subtitles,
        }


code:
import re

from .common import InfoExtractor

class AtScaleConfEventIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?atscaleconference\.com/events/(?P<id>[^/&$?]+)'

    _TESTS = [{
        'url': 'https://atscaleconference.com/events/data-scale-spring-2022/',
        'playlist_mincount': 13,
        'info_dict': {
            'id': 'data-scale-spring-2022',
            'title': 'Data @Scale Spring 2022',
            'description': 'md5:7d7ca1c42ac9c6d8a785092a1aea4b55'
        },
    }, {
        'url': 'https://atscaleconference.com/events/video-scale-2021/',
        'playlist_mincount': 14,
        'info_dict': {
            'id': 'video-scale-2021',
            'title': 'Video @Scale 2021',
            'description': 'md5:7d7ca1c42ac9c6d8a785092a1aea4b55'
        },
    }]

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

        return self.playlist_from_matches(
            re.findall(r'data-url\s*=\s*"(https?://(?:www\.)?atscaleconference\.com/videos/[^"]+)"', webpage),
            ie='Generic', playlist_id=id,
            title=self._og_search_title(webpage), description=self._og_search_description(webpage))


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

class ATTTechChannelIE(InfoExtractor):
    _VALID_URL = r'https?://techchannel\.att\.com/play-video\.cfm/([^/]+/)*(?P<id>.+)'
    _TEST = {
        'url': 'http://techchannel.att.com/play-video.cfm/2014/1/27/ATT-Archives-The-UNIX-System-Making-Computers-Easier-to-Use',
        'info_dict': {
            'id': '11316',
            'display_id': 'ATT-Archives-The-UNIX-System-Making-Computers-Easier-to-Use',
            'ext': 'flv',
            'title': 'AT&T Archives : The UNIX System: Making Computers Easier to Use',
            'description': 'A 1982 film about UNIX is the foundation for software in use around Bell Labs and AT&T.',
            'thumbnail': r're:^https?://.*\.jpg$',
            'upload_date': '20140127',
        },
        'params': {
            # rtmp download
            'skip_download': True,
        },
    }

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

        webpage = self._download_webpage(url, display_id)

        video_url = self._search_regex(
            r"url\s*:\s*'(rtmp://[^']+)'",
            webpage, 'video URL')

        video_id = self._search_regex(
            r'mediaid\s*=\s*(\d+)',
            webpage, 'video id', fatal=False)

        title = self._og_search_title(webpage)
        description = self._og_search_description(webpage)
        thumbnail = self._og_search_thumbnail(webpage)
        upload_date = unified_strdate(self._search_regex(
            r'[Rr]elease\s+date:\s*(\d{1,2}/\d{1,2}/\d{4})',
            webpage, 'upload date', fatal=False), False)

        return {
            'id': video_id,
            'display_id': display_id,
            'url': video_url,
            'ext': 'flv',
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'upload_date': upload_date,
        }


code:
import datetime

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

class ATVAtIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?atv\.at/tv/(?:[^/]+/){2,3}(?P<id>.*)'

    _TESTS = [{
        'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/bauer-sucht-frau/bauer-sucht-frau-staffel-18-folge-3-die-hofwochen',
        'md5': '3c3b4aaca9f63e32b35e04a9c2515903',
        'info_dict': {
            'id': 'v-ce9cgn1e70n5-1',
            'ext': 'mp4',
            'title': 'Bauer sucht Frau - Staffel 18 Folge 3 - Die Hofwochen',
        }
    }, {
        'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/episode-01/bauer-sucht-frau-staffel-18-vorstellungsfolge-1',
        'only_matching': True,
    }]

    # extracted from bootstrap.js function (search for e.encryption_key and use your browser's debugger)
    _ACCESS_ID = 'x_atv'
    _ENCRYPTION_KEY = 'Hohnaekeishoogh2omaeghooquooshia'

    def _extract_video_info(self, url, content, video):
        clip_id = content.get('splitId', content['id'])
        formats = []
        clip_urls = video['urls']
        for protocol, variant in clip_urls.items():
            source_url = try_get(variant, lambda x: x['clear']['url'])
            if not source_url:
                continue
            if protocol == 'dash':
                formats.extend(self._extract_mpd_formats(
                    source_url, clip_id, mpd_id=protocol, fatal=False))
            elif protocol == 'hls':
                formats.extend(self._extract_m3u8_formats(
                    source_url, clip_id, 'mp4', 'm3u8_native',
                    m3u8_id=protocol, fatal=False))
            else:
                formats.append({
                    'url': source_url,
                    'format_id': protocol,
                })
        self._sort_formats(formats)

        return {
            'id': clip_id,
            'title': content.get('title'),
            'duration': float_or_none(content.get('duration')),
            'series': content.get('tvShowTitle'),
            'formats': formats,
        }

    def _real_extract(self, url):
        video_id = self._match_id(url)
        webpage = self._download_webpage(url, video_id)
        json_data = self._parse_json(
            self._search_regex(r'<script id="state" type="text/plain">(.*)</script>', webpage, 'json_data'),
            video_id=video_id)

        video_title = json_data['views']['default']['page']['title']
        contentResource = json_data['views']['default']['page']['contentResource']
        content_id = contentResource[0]['id']
        content_ids = [{'id': id, 'subclip_start': content['start'], 'subclip_end': content['end']}
                       for id, content in enumerate(contentResource)]

        time_of_request = datetime.datetime.now()
        not_before = time_of_request - datetime.timedelta(minutes=5)
        expire = time_of_request + datetime.timedelta(minutes=5)
        payload = {
            'content_ids': {
                content_id: content_ids,
            },
            'secure_delivery': True,
            'iat': int(time_of_request.timestamp()),
            'nbf': int(not_before.timestamp()),
            'exp': int(expire.timestamp()),
        }
        jwt_token = jwt_encode_hs256(payload, self._ENCRYPTION_KEY, headers={'kid': self._ACCESS_ID})
        videos = self._download_json(
            'https://vas-v4.p7s1video.net/4.0/getsources',
            content_id, 'Downloading videos JSON', query={
                'token': jwt_token.decode('utf-8')
            })

        video_id, videos_data = list(videos['data'].items())[0]
        error_msg = try_get(videos_data, lambda x: x['error']['title'])
        if error_msg == 'Geo check failed':
            self.raise_geo_restricted(error_msg)
        elif error_msg:
            raise ExtractorError(error_msg)
        entries = [
            self._extract_video_info(url, contentResource[video['id']], video)
            for video in videos_data]

        return {
            '_type': 'multi_video',
            'id': video_id,
            'title': video_title,
            'entries': entries,
        }


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

class AudiMediaIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?audi-mediacenter\.com/(?:en|de)/audimediatv/(?:video/)?(?P<id>[^/?#]+)'
    _TESTS = [{
        'url': 'https://www.audi-mediacenter.com/en/audimediatv/60-seconds-of-audi-sport-104-2015-wec-bahrain-rookie-test-1467',
        'md5': '79a8b71c46d49042609795ab59779b66',
        'info_dict': {
            'id': '1565',
            'ext': 'mp4',
            'title': '60 Seconds of Audi Sport 104/2015 - WEC Bahrain, Rookie Test',
            'description': 'md5:60e5d30a78ced725f7b8d34370762941',
            'upload_date': '20151124',
            'timestamp': 1448354940,
            'duration': 74022,
            'view_count': int,
        }
    }, {
        'url': 'https://www.audi-mediacenter.com/en/audimediatv/video/60-seconds-of-audi-sport-104-2015-wec-bahrain-rookie-test-2991',
        'only_matching': True,
    }]

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

        raw_payload = self._search_regex([
            r'class="amtv-embed"[^>]+id="([0-9a-z-]+)"',
            r'id="([0-9a-z-]+)"[^>]+class="amtv-embed"',
            r'class=\\"amtv-embed\\"[^>]+id=\\"([0-9a-z-]+)\\"',
            r'id=\\"([0-9a-z-]+)\\"[^>]+class=\\"amtv-embed\\"',
            r'id=(?:\\)?"(amtve-[a-z]-\d+-[a-z]{2})',
        ], webpage, 'raw payload')
        _, stage_mode, video_id, _ = raw_payload.split('-')

        # TODO: handle s and e stage_mode (live streams and ended live streams)
        if stage_mode not in ('s', 'e'):
            video_data = self._download_json(
                'https://www.audimedia.tv/api/video/v1/videos/' + video_id,
                video_id, query={
                    'embed[]': ['video_versions', 'thumbnail_image'],
                })['results']
            formats = []

            stream_url_hls = video_data.get('stream_url_hls')
            if stream_url_hls:
                formats.extend(self._extract_m3u8_formats(
                    stream_url_hls, video_id, 'mp4',
                    entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))

            stream_url_hds = video_data.get('stream_url_hds')
            if stream_url_hds:
                formats.extend(self._extract_f4m_formats(
                    stream_url_hds + '?hdcore=3.4.0',
                    video_id, f4m_id='hds', fatal=False))

            for video_version in video_data.get('video_versions', []):
                video_version_url = video_version.get('download_url') or video_version.get('stream_url')
                if not video_version_url:
                    continue
                f = {
                    'url': video_version_url,
                    'width': int_or_none(video_version.get('width')),
                    'height': int_or_none(video_version.get('height')),
                    'abr': int_or_none(video_version.get('audio_bitrate')),
                    'vbr': int_or_none(video_version.get('video_bitrate')),
                }
                bitrate = self._search_regex(r'(\d+)k', video_version_url, 'bitrate', default=None)
                if bitrate:
                    f.update({
                        'format_id': 'http-%s' % bitrate,
                    })
                formats.append(f)
            self._sort_formats(formats)

            return {
                'id': video_id,
                'title': video_data['title'],
                'description': video_data.get('subtitle'),
                'thumbnail': video_data.get('thumbnail_image', {}).get('file'),
                'timestamp': parse_iso8601(video_data.get('publication_date')),
                'duration': int_or_none(video_data.get('duration')),
                'view_count': int_or_none(video_data.get('view_count')),
                'formats': formats,
            }


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

class AudioBoomIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?audioboom\.com/(?:boos|posts)/(?P<id>[0-9]+)'
    _TESTS = [{
        'url': 'https://audioboom.com/posts/7398103-asim-chaudhry',
        'md5': '7b00192e593ff227e6a315486979a42d',
        'info_dict': {
            'id': '7398103',
            'ext': 'mp3',
            'title': 'Asim Chaudhry',
            'description': 'md5:2f3fef17dacc2595b5362e1d7d3602fc',
            'duration': 4000.99,
            'uploader': 'Sue Perkins: An hour or so with...',
            'uploader_url': r're:https?://(?:www\.)?audioboom\.com/channel/perkins',
        }
    }, {
        'url': 'https://audioboom.com/posts/4279833-3-09-2016-czaban-hour-3?t=0',
        'only_matching': True,
    }]

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

        webpage = self._download_webpage(url, video_id)

        clip = None

        clip_store = self._parse_json(
            self._html_search_regex(
                r'data-new-clip-store=(["\'])(?P<json>{.+?})\1',
                webpage, 'clip store', default='{}', group='json'),
            video_id, fatal=False)
        if clip_store:
            clips = clip_store.get('clips')
            if clips and isinstance(clips, list) and isinstance(clips[0], dict):
                clip = clips[0]

        def from_clip(field):
            if clip:
                return clip.get(field)

        audio_url = from_clip('clipURLPriorToLoading') or self._og_search_property(
            'audio', webpage, 'audio url')
        title = from_clip('title') or self._html_search_meta(
            ['og:title', 'og:audio:title', 'audio_title'], webpage)
        description = from_clip('description') or clean_html(from_clip('formattedDescription')) or self._og_search_description(webpage)

        duration = float_or_none(from_clip('duration') or self._html_search_meta(
            'weibo:audio:duration', webpage))

        uploader = from_clip('author') or self._html_search_meta(
            ['og:audio:artist', 'twitter:audio:artist_name', 'audio_artist'], webpage, 'uploader')
        uploader_url = from_clip('author_url') or self._html_search_meta(
            'audioboo:channel', webpage, 'uploader url')

        return {
            'id': video_id,
            'url': audio_url,
            'title': title,
            'description': description,
            'duration': duration,
            'uploader': uploader,
            'uploader_url': uploader_url,
        }


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

class AudiodraftBaseIE(InfoExtractor):
    def _audiodraft_extract_from_id(self, player_entry_id):
        data_json = self._download_json(
            'https://www.audiodraft.com/scripts/general/player/getPlayerInfoNew.php', player_entry_id,
            headers={
                'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'X-Requested-With': 'XMLHttpRequest',
            }, data=f'id={player_entry_id}'.encode('utf-8'))

        return {
            'id': str(data_json['entry_id']),
            'title': data_json.get('entry_title'),
            'url': data_json['path'],
            'vcodec': 'none',
            'ext': 'mp3',
            'uploader': data_json.get('designer_name'),
            'uploader_id': data_json.get('designer_id'),
            'webpage_url': data_json.get('entry_url'),
            'like_count': int_or_none(data_json.get('entry_likes')),
            'average_rating': int_or_none(data_json.get('entry_rating')),
        }

class AudiodraftCustomIE(AudiodraftBaseIE):
    IE_NAME = 'Audiodraft:custom'
    _VALID_URL = r'https?://(?:[-\w]+)\.audiodraft\.com/entry/(?P<id>\d+)'

    _TESTS = [{
        'url': 'http://nokiatune.audiodraft.com/entry/5874',
        'info_dict': {
            'id': '9485',
            'ext': 'mp3',
            'title': 'Hula Hula Calls',
            'uploader': 'unclemaki',
            'uploader_id': '13512',
            'average_rating': 5,
            'like_count': int,
        },
    }, {
        'url': 'http://vikinggrace.audiodraft.com/entry/501',
        'info_dict': {
            'id': '22241',
            'ext': 'mp3',
            'title': 'MVG Happy',
            'uploader': 'frog',
            'uploader_id': '19142',
            'average_rating': 5,
            'like_count': int,
        },
    }, {
        'url': 'http://timferriss.audiodraft.com/entry/765',
        'info_dict': {
            'id': '19710',
            'ext': 'mp3',
            'title': 'ferris03',
            'uploader': 'malex',
            'uploader_id': '17335',
            'average_rating': 5,
            'like_count': int,
        },
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        webpage = self._download_webpage(url, id)
        player_entry_id = self._search_regex(r'playAudio\(\'(player_entry_\d+)\'\);', webpage, id, 'play entry id')
        return self._audiodraft_extract_from_id(player_entry_id)

class AudiodraftGenericIE(AudiodraftBaseIE):
    IE_NAME = 'Audiodraft:generic'
    _VALID_URL = r'https?://www\.audiodraft\.com/contests/[^/#]+#entries&eid=(?P<id>\d+)'

    _TESTS = [{
        'url': 'https://www.audiodraft.com/contests/570-Score-A-Video-Surprise-Us#entries&eid=30138',
        'info_dict': {
            'id': '30138',
            'ext': 'mp3',
            'title': 'DROP in sound_V2',
            'uploader': 'TiagoSilva',
            'uploader_id': '19452',
            'average_rating': 4,
            'like_count': int,
        },
    }]

    def _real_extract(self, url):
        id = self._match_id(url)
        return self._audiodraft_extract_from_id(f'player_entry_{id}')


code:
import itertools
import time

from .common import InfoExtractor
from .soundcloud import SoundcloudIE
from ..compat import compat_str
from ..utils import (
    ExtractorError,
    url_basename,
)

class AudiomackIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?audiomack\.com/(?:song/|(?=.+/song/))(?P<id>[\w/-]+)'
    IE_NAME = 'audiomack'
    _TESTS = [
        # hosted on audiomack
        {
            'url': 'http://www.audiomack.com/song/roosh-williams/extraordinary',
            'info_dict':
            {
                'id': '310086',
                'ext': 'mp3',
                'uploader': 'Roosh Williams',
                'title': 'Extraordinary'
            }
        },
        # audiomack wrapper around soundcloud song
        # Needs new test URL.
        {
            'add_ie': ['Soundcloud'],
            'url': 'http://www.audiomack.com/song/hip-hop-daily/black-mamba-freestyle',
            'info_dict': {
                'id': '258901379',
                'ext': 'mp3',
                'description': 'mamba day freestyle for the legend Kobe Bryant ',
                'title': 'Black Mamba Freestyle [Prod. By Danny Wolf]',
                'uploader': 'ILOVEMAKONNEN',
                'upload_date': '20160414',
            },
            'skip': 'Song has been removed from the site',
        },
    ]

    def _real_extract(self, url):
        # URLs end with [uploader name]/song/[uploader title]
        # this title is whatever the user types in, and is rarely
        # the proper song title.  Real metadata is in the api response
        album_url_tag = self._match_id(url).replace('/song/', '/')

        # Request the extended version of the api for extra fields like artist and title
        api_response = self._download_json(
            'http://www.audiomack.com/api/music/url/song/%s?extended=1&_=%d' % (
                album_url_tag, time.time()),
            album_url_tag)

        # API is inconsistent with errors
        if 'url' not in api_response or not api_response['url'] or 'error' in api_response:
            raise ExtractorError('Invalid url %s' % url)

        # Audiomack wraps a lot of soundcloud tracks in their branded wrapper
        # if so, pass the work off to the soundcloud extractor
        if SoundcloudIE.suitable(api_response['url']):
            return self.url_result(api_response['url'], SoundcloudIE.ie_key())

        return {
            'id': compat_str(api_response.get('id', album_url_tag)),
            'uploader': api_response.get('artist'),
            'title': api_response.get('title'),
            'url': api_response['url'],
        }

class AudiomackAlbumIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?audiomack\.com/(?:album/|(?=.+/album/))(?P<id>[\w/-]+)'
    IE_NAME = 'audiomack:album'
    _TESTS = [
        # Standard album playlist
        {
            'url': 'http://www.audiomack.com/album/flytunezcom/tha-tour-part-2-mixtape',
            'playlist_count': 11,
            'info_dict':
            {
                'id': '812251',
                'title': 'Tha Tour: Part 2 (Official Mixtape)'
            }
        },
        # Album playlist ripped from fakeshoredrive with no metadata
        {
            'url': 'http://www.audiomack.com/album/fakeshoredrive/ppp-pistol-p-project',
            'info_dict': {
                'title': 'PPP (Pistol P Project)',
                'id': '837572',
            },
            'playlist': [{
                'info_dict': {
                    'title': 'PPP (Pistol P Project) - 8. Real (prod by SYK SENSE  )',
                    'id': '837576',
                    'ext': 'mp3',
                    'uploader': 'Lil Herb a.k.a. G Herbo',
                }
            }, {
                'info_dict': {
                    'title': 'PPP (Pistol P Project) - 10. 4 Minutes Of Hell Part 4 (prod by DY OF 808 MAFIA)',
                    'id': '837580',
                    'ext': 'mp3',
                    'uploader': 'Lil Herb a.k.a. G Herbo',
                }
            }],
        }
    ]

    def _real_extract(self, url):
        # URLs end with [uploader name]/album/[uploader title]
        # this title is whatever the user types in, and is rarely
        # the proper song title.  Real metadata is in the api response
        album_url_tag = self._match_id(url).replace('/album/', '/')
        result = {'_type': 'playlist', 'entries': []}
        # There is no one endpoint for album metadata - instead it is included/repeated in each song's metadata
        # Therefore we don't know how many songs the album has and must infi-loop until failure
        for track_no in itertools.count():
            # Get song's metadata
            api_response = self._download_json(
                'http://www.audiomack.com/api/music/url/album/%s/%d?extended=1&_=%d'
                % (album_url_tag, track_no, time.time()), album_url_tag,
                note='Querying song information (%d)' % (track_no + 1))

            # Total failure, only occurs when url is totally wrong
            # Won't happen in middle of valid playlist (next case)
            if 'url' not in api_response or 'error' in api_response:
                raise ExtractorError('Invalid url for track %d of album url %s' % (track_no, url))
            # URL is good but song id doesn't exist - usually means end of playlist
            elif not api_response['url']:
                break
            else:
                # Pull out the album metadata and add to result (if it exists)
                for resultkey, apikey in [('id', 'album_id'), ('title', 'album_title')]:
                    if apikey in api_response and resultkey not in result:
                        result[resultkey] = compat_str(api_response[apikey])
                song_id = url_basename(api_response['url']).rpartition('.')[0]
                result['entries'].append({
                    'id': compat_str(api_response.get('id', song_id)),
                    'uploader': api_response.get('artist'),
                    'title': api_response.get('title', song_id),
                    'url': api_response['url'],
                })
        return result


code:
import random

from .common import InfoExtractor
from ..compat import compat_str, compat_urllib_parse_unquote
from ..utils import ExtractorError, str_or_none, try_get

class AudiusBaseIE(InfoExtractor):
    _API_BASE = None
    _API_V = '/v1'

    def _get_response_data(self, response):
        if isinstance(response, dict):
            response_data = response.get('data')
            if response_data is not None:
                return response_data
            if len(response) == 1 and 'message' in response:
                raise ExtractorError('API error: %s' % response['message'],
                                     expected=True)
        raise ExtractorError('Unexpected API response')

    def _select_api_base(self):
        """Selecting one of the currently available API hosts"""
        response = super(AudiusBaseIE, self)._download_json(
            'https://api.audius.co/', None,
            note='Requesting available API hosts',
            errnote='Unable to request available API hosts')
        hosts = self._get_response_data(response)
        if isinstance(hosts, list):
            self._API_BASE = random.choice(hosts)
            return
        raise ExtractorError('Unable to get available API hosts')

    @staticmethod
    def _prepare_url(url, title):
        """
        Audius removes forward slashes from the uri, but leaves backslashes.
        The problem is that the current version of Chrome replaces backslashes
        in the address bar with a forward slashes, so if you copy the link from
        there and paste it into youtube-dl, you won't be able to download
        anything from this link, since the Audius API won't be able to resolve
        this url
        """
        url = compat_urllib_parse_unquote(url)
        title = compat_urllib_parse_unquote(title)
        if '/' in title or '%2F' in title:
            fixed_title = title.replace('/', '%5C').replace('%2F', '%5C')
            return url.replace(title, fixed_title)
        return url

    def _api_request(self, path, item_id=None, note='Downloading JSON metadata',
                     errnote='Unable to download JSON metadata',
                     expected_status=None):
        if self._API_BASE is None:
            self._select_api_base()
        try:
            response = super(AudiusBaseIE, self)._download_json(
                '%s%s%s' % (self._API_BASE, self._API_V, path), item_id, note=note,
                errnote=errnote, expected_status=expected_status)
        except ExtractorError as exc:
            # some of Audius API hosts may not work as expected and return HTML
            if 'Failed to parse JSON' in compat_str(exc):
                raise ExtractorError('An error occurred while receiving data. Try again',
                                     expected=True)
            raise exc
        return self._get_response_data(response)

    def _resolve_url(self, url, item_id):
        return self._api_request('/resolve?url=%s' % url, item_id,
                                 expected_status=404)

class AudiusIE(AudiusBaseIE):
    _VALID_URL = r'''(?x)https?://(?:www\.)?(?:audius\.co/(?P<uploader>[\w\d-]+)(?!/album|/playlist)/(?P<title>\S+))'''
    IE_DESC = 'Audius.co'
    _TESTS = [
        {
            # URL from Chrome address bar which replace backslash to forward slash
            'url': 'https://audius.co/test_acc/t%D0%B5%D0%B5%D0%B5est-1.%5E_%7B%7D/%22%3C%3E.%E2%84%96~%60-198631',
            'md5': '92c35d3e754d5a0f17eef396b0d33582',
            'info_dict': {
                'id': 'xd8gY',
                'title': '''Tеееest/ 1.!@#$%^&*()_+=[]{};'\\\":<>,.?/№~`''',
                'ext': 'mp3',
                'description': 'Description',
                'duration': 30,
                'track': '''Tеееest/ 1.!@#$%^&*()_+=[]{};'\\\":<>,.?/№~`''',
                'artist': 'test',
                'genre': 'Electronic',
                'thumbnail': r're:https?://.*\.jpg',
                'view_count': int,
                'like_count': int,
                'repost_count': int,
            }
        },
        {
            # Regular track
            'url': 'https://audius.co/voltra/radar-103692',
            'md5': '491898a0a8de39f20c5d6a8a80ab5132',
            'info_dict': {
                'id': 'KKdy2',
                'title': 'RADAR',
                'ext': 'mp3',
                'duration': 318,
                'track': 'RADAR',
                'artist': 'voltra',
                'genre': 'Trance',
                'thumbnail': r're:https?://.*\.jpg',
                'view_count': int,
                'like_count': int,
                'repost_count': int,
            }
        },
    ]

    _ARTWORK_MAP = {
        "150x150": 150,
        "480x480": 480,
        "1000x1000": 1000
    }

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        track_id = try_get(mobj, lambda x: x.group('track_id'))
        if track_id is None:
            title = mobj.group('title')
            # uploader = mobj.group('uploader')
            url = self._prepare_url(url, title)
            track_data = self._resolve_url(url, title)
        else:  # API link
            title = None
            # uploader = None
            track_data = self._api_request('/tracks/%s' % track_id, track_id)

        if not isinstance(track_data, dict):
            raise ExtractorError('Unexpected API response')

        track_id = track_data.get('id')
        if track_id is None:
            raise ExtractorError('Unable to get ID of the track')

        artworks_data = track_data.get('artwork')
        thumbnails = []
        if isinstance(artworks_data, dict):
            for quality_key, thumbnail_url in artworks_data.items():
                thumbnail = {
                    "url": thumbnail_url
                }
                quality_code = self._ARTWORK_MAP.get(quality_key)
                if quality_code is not None:
                    thumbnail['preference'] = quality_code
                thumbnails.append(thumbnail)

        return {
            'id': track_id,
            'title': track_data.get('title', title),
            'url': '%s/v1/tracks/%s/stream' % (self._API_BASE, track_id),
            'ext': 'mp3',
            'description': track_data.get('description'),
            'duration': track_data.get('duration'),
            'track': track_data.get('title'),
            'artist': try_get(track_data, lambda x: x['user']['name'], compat_str),
            'genre': track_data.get('genre'),
            'thumbnails': thumbnails,
            'view_count': track_data.get('play_count'),
            'like_count': track_data.get('favorite_count'),
            'repost_count': track_data.get('repost_count'),
        }

class AudiusTrackIE(AudiusIE):
    _VALID_URL = r'''(?x)(?:audius:)(?:https?://(?:www\.)?.+/v1/tracks/)?(?P<track_id>\w+)'''
    IE_NAME = 'audius:track'
    IE_DESC = 'Audius track ID or API link. Prepend with "audius:"'
    _TESTS = [
        {
            'url': 'audius:9RWlo',
            'only_matching': True
        },
        {
            'url': 'audius:http://discoveryprovider.audius.prod-us-west-2.staked.cloud/v1/tracks/9RWlo',
            'only_matching': True
        },
    ]

class AudiusPlaylistIE(AudiusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?audius\.co/(?P<uploader>[\w\d-]+)/(?:album|playlist)/(?P<title>\S+)'
    IE_NAME = 'audius:playlist'
    IE_DESC = 'Audius.co playlists'
    _TEST = {
        'url': 'https://audius.co/test_acc/playlist/test-playlist-22910',
        'info_dict': {
            'id': 'DNvjN',
            'title': 'test playlist',
            'description': 'Test description\n\nlol',
        },
        'playlist_count': 175,
    }

    def _build_playlist(self, tracks):
        entries = []
        for track in tracks:
            if not isinstance(track, dict):
                raise ExtractorError('Unexpected API response')
            track_id = str_or_none(track.get('id'))
            if not track_id:
                raise ExtractorError('Unable to get track ID from playlist')
            entries.append(self.url_result(
                'audius:%s' % track_id,
                ie=AudiusTrackIE.ie_key(), video_id=track_id))
        return entries

    def _real_extract(self, url):
        self._select_api_base()
        mobj = self._match_valid_url(url)
        title = mobj.group('title')
        # uploader = mobj.group('uploader')
        url = self._prepare_url(url, title)
        playlist_response = self._resolve_url(url, title)

        if not isinstance(playlist_response, list) or len(playlist_response) != 1:
            raise ExtractorError('Unexpected API response')

        playlist_data = playlist_response[0]
        if not isinstance(playlist_data, dict):
            raise ExtractorError('Unexpected API response')

        playlist_id = playlist_data.get('id')
        if playlist_id is None:
            raise ExtractorError('Unable to get playlist ID')

        playlist_tracks = self._api_request(
            '/playlists/%s/tracks' % playlist_id,
            title, note='Downloading playlist tracks metadata',
            errnote='Unable to download playlist tracks metadata')
        if not isinstance(playlist_tracks, list):
            raise ExtractorError('Unexpected API response')

        entries = self._build_playlist(playlist_tracks)
        return self.playlist_result(entries, playlist_id,
                                    playlist_data.get('playlist_name', title),
                                    playlist_data.get('description'))

class AudiusProfileIE(AudiusPlaylistIE):
    IE_NAME = 'audius:artist'
    IE_DESC = 'Audius.co profile/artist pages'
    _VALID_URL = r'https?://(?:www)?audius\.co/(?P<id>[^\/]+)/?(?:[?#]|$)'
    _TEST = {
        'url': 'https://audius.co/pzl/',
        'info_dict': {
            'id': 'ezRo7',
            'description': 'TAMALE\n\nContact: officialpzl@gmail.com',
            'title': 'pzl',
        },
        'playlist_count': 24,
    }

    def _real_extract(self, url):
        self._select_api_base()
        profile_id = self._match_id(url)
        try:
            _profile_data = self._api_request('/full/users/handle/' + profile_id, profile_id)
        except ExtractorError as e:
            raise ExtractorError('Could not download profile info; ' + str(e))
        profile_audius_id = _profile_data[0]['id']
        profile_bio = _profile_data[0].get('bio')

        api_call = self._api_request('/full/users/handle/%s/tracks' % profile_id, profile_id)
        return self.playlist_result(self._build_playlist(api_call), profile_audius_id, profile_id, profile_bio)