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

class DigitekaIE(InfoExtractor):
    _VALID_URL = r'''(?x)
        https?://(?:www\.)?(?:digiteka\.net|ultimedia\.com)/
        (?:
            deliver/
            (?P<embed_type>
                generic|
                musique
            )
            (?:/[^/]+)*/
            (?:
                src|
                article
            )|
            default/index/video
            (?P<site_type>
                generic|
                music
            )
            /id
        )/(?P<id>[\d+a-z]+)'''
    _EMBED_REGEX = [r'<(?:iframe|script)[^>]+src=["\'](?P<url>(?:https?:)?//(?:www\.)?ultimedia\.com/deliver/(?:generic|musique)(?:/[^/]+)*/(?:src|article)/[\d+a-z]+)']
    _TESTS = [{
        # news
        'url': 'https://www.ultimedia.com/default/index/videogeneric/id/s8uk0r',
        'md5': '276a0e49de58c7e85d32b057837952a2',
        'info_dict': {
            'id': 's8uk0r',
            'ext': 'mp4',
            'title': 'Loi sur la fin de vie: le texte prévoit un renforcement des directives anticipées',
            'thumbnail': r're:^https?://.*\.jpg',
            'duration': 74,
            'upload_date': '20150317',
            'timestamp': 1426604939,
            'uploader_id': '3fszv',
        },
    }, {
        # music
        'url': 'https://www.ultimedia.com/default/index/videomusic/id/xvpfp8',
        'md5': '2ea3513813cf230605c7e2ffe7eca61c',
        'info_dict': {
            'id': 'xvpfp8',
            'ext': 'mp4',
            'title': 'Two - C\'est La Vie (clip)',
            'thumbnail': r're:^https?://.*\.jpg',
            'duration': 233,
            'upload_date': '20150224',
            'timestamp': 1424760500,
            'uploader_id': '3rfzk',
        },
    }, {
        'url': 'https://www.digiteka.net/deliver/generic/iframe/mdtk/01637594/src/lqm3kl/zone/1/showtitle/1/autoplay/yes',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        video_id = mobj.group('id')
        video_type = mobj.group('embed_type') or mobj.group('site_type')
        if video_type == 'music':
            video_type = 'musique'

        deliver_info = self._download_json(
            'http://www.ultimedia.com/deliver/video?video=%s&topic=%s' % (video_id, video_type),
            video_id)

        yt_id = deliver_info.get('yt_id')
        if yt_id:
            return self.url_result(yt_id, 'Youtube')

        jwconf = deliver_info['jwconf']

        formats = []
        for source in jwconf['playlist'][0]['sources']:
            formats.append({
                'url': source['file'],
                'format_id': source.get('label'),
            })

        self._sort_formats(formats)

        title = deliver_info['title']
        thumbnail = jwconf.get('image')
        duration = int_or_none(deliver_info.get('duration'))
        timestamp = int_or_none(deliver_info.get('release_time'))
        uploader_id = deliver_info.get('owner_id')

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


code:
import random
import string

from .discoverygo import DiscoveryGoBaseIE
from ..compat import compat_urllib_parse_unquote
from ..utils import ExtractorError
from ..compat import compat_HTTPError

class DiscoveryIE(DiscoveryGoBaseIE):
    _VALID_URL = r'''(?x)https?://
        (?P<site>
            go\.discovery|
            www\.
                (?:
                    investigationdiscovery|
                    discoverylife|
                    animalplanet|
                    ahctv|
                    destinationamerica|
                    sciencechannel|
                    tlc
                )|
            watch\.
                (?:
                    hgtv|
                    foodnetwork|
                    travelchannel|
                    diynetwork|
                    cookingchanneltv|
                    motortrend
                )
        )\.com/tv-shows/(?P<show_slug>[^/]+)/(?:video|full-episode)s/(?P<id>[^./?#]+)'''
    _TESTS = [{
        'url': 'https://go.discovery.com/tv-shows/cash-cab/videos/riding-with-matthew-perry',
        'info_dict': {
            'id': '5a2f35ce6b66d17a5026e29e',
            'ext': 'mp4',
            'title': 'Riding with Matthew Perry',
            'description': 'md5:a34333153e79bc4526019a5129e7f878',
            'duration': 84,
        },
        'params': {
            'skip_download': True,  # requires ffmpeg
        }
    }, {
        'url': 'https://www.investigationdiscovery.com/tv-shows/final-vision/full-episodes/final-vision',
        'only_matching': True,
    }, {
        'url': 'https://go.discovery.com/tv-shows/alaskan-bush-people/videos/follow-your-own-road',
        'only_matching': True,
    }, {
        # using `show_slug` is important to get the correct video data
        'url': 'https://www.sciencechannel.com/tv-shows/mythbusters-on-science/full-episodes/christmas-special',
        'only_matching': True,
    }]
    _GEO_COUNTRIES = ['US']
    _GEO_BYPASS = False
    _API_BASE_URL = 'https://api.discovery.com/v1/'

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

        access_token = None
        cookies = self._get_cookies(url)

        # prefer Affiliate Auth Token over Anonymous Auth Token
        auth_storage_cookie = cookies.get('eosAf') or cookies.get('eosAn')
        if auth_storage_cookie and auth_storage_cookie.value:
            auth_storage = self._parse_json(compat_urllib_parse_unquote(
                compat_urllib_parse_unquote(auth_storage_cookie.value)),
                display_id, fatal=False) or {}
            access_token = auth_storage.get('a') or auth_storage.get('access_token')

        if not access_token:
            access_token = self._download_json(
                'https://%s.com/anonymous' % site, display_id,
                'Downloading token JSON metadata', query={
                    'authRel': 'authorization',
                    'client_id': '3020a40c2356a645b4b4',
                    'nonce': ''.join([random.choice(string.ascii_letters) for _ in range(32)]),
                    'redirectUri': 'https://www.discovery.com/',
                })['access_token']

        headers = self.geo_verification_headers()
        headers['Authorization'] = 'Bearer ' + access_token

        try:
            video = self._download_json(
                self._API_BASE_URL + 'content/videos',
                display_id, 'Downloading content JSON metadata',
                headers=headers, query={
                    'embed': 'show.name',
                    'fields': 'authenticated,description.detailed,duration,episodeNumber,id,name,parental.rating,season.number,show,tags',
                    'slug': display_id,
                    'show_slug': show_slug,
                })[0]
            video_id = video['id']
            stream = self._download_json(
                self._API_BASE_URL + 'streaming/video/' + video_id,
                display_id, 'Downloading streaming JSON metadata', headers=headers)
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code in (401, 403):
                e_description = self._parse_json(
                    e.cause.read().decode(), display_id)['description']
                if 'resource not available for country' in e_description:
                    self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
                if 'Authorized Networks' in e_description:
                    raise ExtractorError(
                        'This video is only available via cable service provider subscription that'
                        ' is not currently supported. You may want to use --cookies.', expected=True)
                raise ExtractorError(e_description)
            raise

        return self._extract_video_info(video, stream, display_id)

repo_name: Lesmiscore/ytdl


code:
import re

from .common import InfoExtractor
from ..utils import (
    determine_ext,
    extract_attributes,
    ExtractorError,
    int_or_none,
    parse_age_limit,
    remove_end,
    unescapeHTML,
    url_or_none,
)

class DiscoveryGoBaseIE(InfoExtractor):
    _VALID_URL_TEMPLATE = r'''(?x)https?://(?:www\.)?(?:
            discovery|
            investigationdiscovery|
            discoverylife|
            animalplanet|
            ahctv|
            destinationamerica|
            sciencechannel|
            tlc|
            velocitychannel
        )go\.com/%s(?P<id>[^/?#&]+)'''

    def _extract_video_info(self, video, stream, display_id):
        title = video['name']

        if not stream:
            if video.get('authenticated') is True:
                raise ExtractorError(
                    'This video is only available via cable service provider subscription that'
                    ' is not currently supported. You may want to use --cookies.', expected=True)
            else:
                raise ExtractorError('Unable to find stream')
        STREAM_URL_SUFFIX = 'streamUrl'
        formats = []
        for stream_kind in ('', 'hds'):
            suffix = STREAM_URL_SUFFIX.capitalize() if stream_kind else STREAM_URL_SUFFIX
            stream_url = stream.get('%s%s' % (stream_kind, suffix))
            if not stream_url:
                continue
            if stream_kind == '':
                formats.extend(self._extract_m3u8_formats(
                    stream_url, display_id, 'mp4', entry_protocol='m3u8_native',
                    m3u8_id='hls', fatal=False))
            elif stream_kind == 'hds':
                formats.extend(self._extract_f4m_formats(
                    stream_url, display_id, f4m_id=stream_kind, fatal=False))
        self._sort_formats(formats)

        video_id = video.get('id') or display_id
        description = video.get('description', {}).get('detailed')
        duration = int_or_none(video.get('duration'))

        series = video.get('show', {}).get('name')
        season_number = int_or_none(video.get('season', {}).get('number'))
        episode_number = int_or_none(video.get('episodeNumber'))

        tags = video.get('tags')
        age_limit = parse_age_limit(video.get('parental', {}).get('rating'))

        subtitles = {}
        captions = stream.get('captions')
        if isinstance(captions, list):
            for caption in captions:
                subtitle_url = url_or_none(caption.get('fileUrl'))
                if not subtitle_url or not subtitle_url.startswith('http'):
                    continue
                lang = caption.get('fileLang', 'en')
                ext = determine_ext(subtitle_url)
                subtitles.setdefault(lang, []).append({
                    'url': subtitle_url,
                    'ext': 'ttml' if ext == 'xml' else ext,
                })

        return {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': description,
            'duration': duration,
            'series': series,
            'season_number': season_number,
            'episode_number': episode_number,
            'tags': tags,
            'age_limit': age_limit,
            'formats': formats,
            'subtitles': subtitles,
        }

class DiscoveryGoIE(DiscoveryGoBaseIE):
    _VALID_URL = DiscoveryGoBaseIE._VALID_URL_TEMPLATE % r'(?:[^/]+/)+'
    _GEO_COUNTRIES = ['US']
    _TEST = {
        'url': 'https://www.discoverygo.com/bering-sea-gold/reaper-madness/',
        'info_dict': {
            'id': '58c167d86b66d12f2addeb01',
            'ext': 'mp4',
            'title': 'Reaper Madness',
            'description': 'md5:09f2c625c99afb8946ed4fb7865f6e78',
            'duration': 2519,
            'series': 'Bering Sea Gold',
            'season_number': 8,
            'episode_number': 6,
            'age_limit': 14,
        },
    }

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

        webpage = self._download_webpage(url, display_id)

        container = extract_attributes(
            self._search_regex(
                r'(<div[^>]+class=["\']video-player-container[^>]+>)',
                webpage, 'video container'))

        video = self._parse_json(
            container.get('data-video') or container.get('data-json'),
            display_id)

        stream = video.get('stream')

        return self._extract_video_info(video, stream, display_id)

class DiscoveryGoPlaylistIE(DiscoveryGoBaseIE):
    _VALID_URL = DiscoveryGoBaseIE._VALID_URL_TEMPLATE % ''
    _TEST = {
        'url': 'https://www.discoverygo.com/bering-sea-gold/',
        'info_dict': {
            'id': 'bering-sea-gold',
            'title': 'Bering Sea Gold',
            'description': 'md5:cc5c6489835949043c0cc3ad66c2fa0e',
        },
        'playlist_mincount': 6,
    }

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

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

        webpage = self._download_webpage(url, display_id)

        entries = []
        for mobj in re.finditer(r'data-json=(["\'])(?P<json>{.+?})\1', webpage):
            data = self._parse_json(
                mobj.group('json'), display_id,
                transform_source=unescapeHTML, fatal=False)
            if not isinstance(data, dict) or data.get('type') != 'episode':
                continue
            episode_url = data.get('socialUrl')
            if not episode_url:
                continue
            entries.append(self.url_result(
                episode_url, ie=DiscoveryGoIE.ie_key(),
                video_id=data.get('id')))

        return self.playlist_result(
            entries, display_id,
            remove_end(self._og_search_title(
                webpage, fatal=False), ' | Discovery GO'),
            self._og_search_description(webpage))


code:
import re

from .common import InfoExtractor
from ..utils import (
    int_or_none,
    unified_strdate,
    determine_ext,
    join_nonempty,
    update_url_query,
)

class DisneyIE(InfoExtractor):
    _VALID_URL = r'''(?x)
        https?://(?P<domain>(?:[^/]+\.)?(?:disney\.[a-z]{2,3}(?:\.[a-z]{2})?|disney(?:(?:me|latino)\.com|turkiye\.com\.tr|channel\.de)|(?:starwars|marvelkids)\.com))/(?:(?:embed/|(?:[^/]+/)+[\w-]+-)(?P<id>[a-z0-9]{24})|(?:[^/]+/)?(?P<display_id>[^/?#]+))'''
    _TESTS = [{
        # Disney.EmbedVideo
        'url': 'http://video.disney.com/watch/moana-trailer-545ed1857afee5a0ec239977',
        'info_dict': {
            'id': '545ed1857afee5a0ec239977',
            'ext': 'mp4',
            'title': 'Moana - Trailer',
            'description': 'A fun adventure for the entire Family!  Bring home Moana on Digital HD Feb 21 & Blu-ray March 7',
            'upload_date': '20170112',
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        }
    }, {
        # Grill.burger
        'url': 'http://www.starwars.com/video/rogue-one-a-star-wars-story-intro-featurette',
        'info_dict': {
            'id': '5454e9f4e9804a552e3524c8',
            'ext': 'mp4',
            'title': '"Intro" Featurette: Rogue One: A Star Wars Story',
            'upload_date': '20170104',
            'description': 'Go behind-the-scenes of Rogue One: A Star Wars Story in this featurette with Director Gareth Edwards and the cast of the film.',
        },
        'params': {
            # m3u8 download
            'skip_download': True,
        }
    }, {
        'url': 'http://videos.disneylatino.com/ver/spider-man-de-regreso-a-casa-primer-adelanto-543a33a1850bdcfcca13bae2',
        'only_matching': True,
    }, {
        'url': 'http://video.en.disneyme.com/watch/future-worm/robo-carp-2001-544b66002aa7353cdd3f5114',
        'only_matching': True,
    }, {
        'url': 'http://video.disneyturkiye.com.tr/izle/7c-7-cuceler/kimin-sesi-zaten-5456f3d015f6b36c8afdd0e2',
        'only_matching': True,
    }, {
        'url': 'http://disneyjunior.disney.com/embed/546a4798ddba3d1612e4005d',
        'only_matching': True,
    }, {
        'url': 'http://www.starwars.com/embed/54690d1e6c42e5f09a0fb097',
        'only_matching': True,
    }, {
        'url': 'http://spiderman.marvelkids.com/embed/522900d2ced3c565e4cc0677',
        'only_matching': True,
    }, {
        'url': 'http://spiderman.marvelkids.com/videos/contest-of-champions-part-four-clip-1',
        'only_matching': True,
    }, {
        'url': 'http://disneyjunior.en.disneyme.com/dj/watch-my-friends-tigger-and-pooh-promo',
        'only_matching': True,
    }, {
        'url': 'http://disneychannel.de/sehen/soy-luna-folge-118-5518518987ba27f3cc729268',
        'only_matching': True,
    }, {
        'url': 'http://disneyjunior.disney.com/galactech-the-galactech-grab-galactech-an-admiral-rescue',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        domain, video_id, display_id = self._match_valid_url(url).groups()
        if not video_id:
            webpage = self._download_webpage(url, display_id)
            grill = re.sub(r'"\s*\+\s*"', '', self._search_regex(
                r'Grill\.burger\s*=\s*({.+})\s*:',
                webpage, 'grill data'))
            page_data = next(s for s in self._parse_json(grill, display_id)['stack'] if s.get('type') == 'video')
            video_data = page_data['data'][0]
        else:
            webpage = self._download_webpage(
                'http://%s/embed/%s' % (domain, video_id), video_id)
            page_data = self._parse_json(self._search_regex(
                r'Disney\.EmbedVideo\s*=\s*({.+});',
                webpage, 'embed data'), video_id)
            video_data = page_data['video']

        for external in video_data.get('externals', []):
            if external.get('source') == 'vevo':
                return self.url_result('vevo:' + external['data_id'], 'Vevo')

        video_id = video_data['id']
        title = video_data['title']

        formats = []
        for flavor in video_data.get('flavors', []):
            flavor_format = flavor.get('format')
            flavor_url = flavor.get('url')
            if not flavor_url or not re.match(r'https?://', flavor_url) or flavor_format == 'mp4_access':
                continue
            tbr = int_or_none(flavor.get('bitrate'))
            if tbr == 99999:
                # wrong ks(Kaltura Signature) causes 404 Error
                flavor_url = update_url_query(flavor_url, {'ks': ''})
                m3u8_formats = self._extract_m3u8_formats(
                    flavor_url, video_id, 'mp4',
                    m3u8_id=flavor_format, fatal=False)
                for f in m3u8_formats:
                    # Apple FairPlay
                    if '/fpshls/' in f['url']:
                        continue
                    formats.append(f)
                continue
            ext = determine_ext(flavor_url)
            if flavor_format == 'applehttp' or ext == 'm3u8':
                ext = 'mp4'
            width = int_or_none(flavor.get('width'))
            height = int_or_none(flavor.get('height'))
            formats.append({
                'format_id': join_nonempty(flavor_format, tbr),
                'url': flavor_url,
                'width': width,
                'height': height,
                'tbr': tbr,
                'ext': ext,
                'vcodec': 'none' if (width == 0 and height == 0) else None,
            })
        if not formats and video_data.get('expired'):
            self.raise_no_formats(
                '%s said: %s' % (self.IE_NAME, page_data['translations']['video_expired']),
                expected=True)
        self._sort_formats(formats)

        subtitles = {}
        for caption in video_data.get('captions', []):
            caption_url = caption.get('url')
            caption_format = caption.get('format')
            if not caption_url or caption_format.startswith('unknown'):
                continue
            subtitles.setdefault(caption.get('language', 'en'), []).append({
                'url': caption_url,
                'ext': {
                    'webvtt': 'vtt',
                }.get(caption_format, caption_format),
            })

        return {
            'id': video_id,
            'title': title,
            'description': video_data.get('description') or video_data.get('short_desc'),
            'thumbnail': video_data.get('thumb') or video_data.get('thumb_secure'),
            'duration': int_or_none(video_data.get('duration_sec')),
            'upload_date': unified_strdate(video_data.get('publish_date')),
            'formats': formats,
            'subtitles': subtitles,
        }


code:
from __future__ import unicode_literals

import re

from .common import InfoExtractor
from ..utils import (
    determine_ext,
    int_or_none,
)

class DisneyChrisIE(InfoExtractor):
    IE_NAME = 'disneychris'
    _VALID_URL = r'https?://(?:www\.)?disneychris\.com/(?P<id>a-day-at-disneyland|15-disneyland-soundtracks/.+?)\.html'

    def _real_extract(self, url):
        long_id = self._match_id(url)
        video_id = long_id.rsplit('/', 1)[-1]
        webpage = self._download_webpage(url, video_id)

        title = next(
            (x.group(2) for x in re.finditer(r'href="/(.+?)\.html">(.+?)', webpage) if x.group(1) == long_id),
            None) or self._html_extract_title(webpage, 'playlist title', default=None)

        return self.playlist_result(self.extract_article_sections(webpage, video_id), video_id, title)

    def extract_article_sections(self, webpage, base_id):
        for idx, x in enumerate(re.finditer(r'(?s)(.+?)	', webpage)):
            content = x.group(1)

            title = self._html_search_regex(r'class="ch\d{2}Ttl[AB]">(.+?)\s* (.+?)
', content, 'description', fatal=False),
                'url': audio_url,
                'ext': determine_ext(audio_url, 'mp3'),
                'vcodec': 'none',
                'acodec': 'mp3',
                'track_number': int_or_none(track_number),
            }


code:
import re

from .common import InfoExtractor
from ..utils import (
    int_or_none,
    parse_duration,
    remove_end,
    xpath_element,
    xpath_text,
)

class DigitallySpeakingIE(InfoExtractor):
    _VALID_URL = r'https?://(?:s?evt\.dispeak|events\.digitallyspeaking)\.com/(?:[^/]+/)+xml/(?P<id>[^.]+)\.xml'

    _TESTS = [{
        # From http://gdcvault.com/play/1023460/Tenacious-Design-and-The-Interface
        'url': 'http://evt.dispeak.com/ubm/gdc/sf16/xml/840376_BQRC.xml',
        'md5': 'a8efb6c31ed06ca8739294960b2dbabd',
        'info_dict': {
            'id': '840376_BQRC',
            'ext': 'mp4',
            'title': 'Tenacious Design and The Interface of \'Destiny\'',
        },
    }, {
        # From http://www.gdcvault.com/play/1014631/Classic-Game-Postmortem-PAC
        'url': 'http://events.digitallyspeaking.com/gdc/sf11/xml/12396_1299111843500GMPX.xml',
        'only_matching': True,
    }, {
        # From http://www.gdcvault.com/play/1013700/Advanced-Material
        'url': 'http://sevt.dispeak.com/ubm/gdc/eur10/xml/11256_1282118587281VNIT.xml',
        'only_matching': True,
    }, {
        # From https://gdcvault.com/play/1016624, empty speakerVideo
        'url': 'https://sevt.dispeak.com/ubm/gdc/online12/xml/201210-822101_1349794556671DDDD.xml',
        'info_dict': {
            'id': '201210-822101_1349794556671DDDD',
            'ext': 'flv',
            'title': 'Pre-launch - Preparing to Take the Plunge',
        },
    }, {
        # From http://www.gdcvault.com/play/1014846/Conference-Keynote-Shigeru, empty slideVideo
        'url': 'http://events.digitallyspeaking.com/gdc/project25/xml/p25-miyamoto1999_1282467389849HSVB.xml',
        'only_matching': True,
    }]

    def _parse_mp4(self, metadata):
        video_formats = []
        video_root = None

        mp4_video = xpath_text(metadata, './mp4video', default=None)
        if mp4_video is not None:
            mobj = re.match(r'(?P<root>https?://.*?/).*', mp4_video)
            video_root = mobj.group('root')
        if video_root is None:
            http_host = xpath_text(metadata, 'httpHost', default=None)
            if http_host:
                video_root = 'http://%s/' % http_host
        if video_root is None:
            # Hard-coded in http://evt.dispeak.com/ubm/gdc/sf16/custom/player2.js
            # Works for GPUTechConf, too
            video_root = 'http://s3-2u.digitallyspeaking.com/'

        formats = metadata.findall('./MBRVideos/MBRVideo')
        if not formats:
            return None
        for a_format in formats:
            stream_name = xpath_text(a_format, 'streamName', fatal=True)
            video_path = re.match(r'mp4\:(?P<path>.*)', stream_name).group('path')
            url = video_root + video_path
            bitrate = xpath_text(a_format, 'bitrate')
            tbr = int_or_none(bitrate)
            vbr = int_or_none(self._search_regex(
                r'-(\d+)\.mp4', video_path, 'vbr', default=None))
            video_formats.append({
                'format_id': bitrate,
                'url': url,
                'tbr': tbr,
                'vbr': vbr,
            })
        return video_formats

    def _parse_flv(self, metadata):
        formats = []
        akamai_url = xpath_text(metadata, './akamaiHost', fatal=True)
        audios = metadata.findall('./audios/audio')
        for audio in audios:
            formats.append({
                'url': 'rtmp://%s/ondemand?ovpfv=1.1' % akamai_url,
                'play_path': remove_end(audio.get('url'), '.flv'),
                'ext': 'flv',
                'vcodec': 'none',
                'quality': 1,
                'format_id': audio.get('code'),
            })
        for video_key, format_id, preference in (
                ('slide', 'slides', -2), ('speaker', 'speaker', -1)):
            video_path = xpath_text(metadata, './%sVideo' % video_key)
            if not video_path:
                continue
            formats.append({
                'url': 'rtmp://%s/ondemand?ovpfv=1.1' % akamai_url,
                'play_path': remove_end(video_path, '.flv'),
                'ext': 'flv',
                'format_note': '%s video' % video_key,
                'quality': preference,
                'format_id': format_id,
            })
        return formats

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

        xml_description = self._download_xml(url, video_id)
        metadata = xpath_element(xml_description, 'metadata')

        video_formats = self._parse_mp4(metadata)
        if video_formats is None:
            video_formats = self._parse_flv(metadata)
        self._sort_formats(video_formats)

        return {
            'id': video_id,
            'formats': video_formats,
            'title': xpath_text(metadata, 'title', fatal=True),
            'duration': parse_duration(xpath_text(metadata, 'endTime')),
            'creator': xpath_text(metadata, 'speaker'),
        }


code:
import json

from .common import InfoExtractor
from ..utils import int_or_none

class DLiveVODIE(InfoExtractor):
    IE_NAME = 'dlive:vod'
    _VALID_URL = r'https?://(?:www\.)?dlive\.tv/p/(?P<uploader_id>.+?)\+(?P<id>[^/?#&]+)'
    _TESTS = [{
        'url': 'https://dlive.tv/p/pdp+3mTzOl4WR',
        'info_dict': {
            'id': '3mTzOl4WR',
            'ext': 'mp4',
            'title': 'Minecraft with james charles epic',
            'upload_date': '20190701',
            'timestamp': 1562011015,
            'uploader_id': 'pdp',
        }
    }, {
        'url': 'https://dlive.tv/p/pdpreplay+D-RD-xSZg',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        uploader_id, vod_id = self._match_valid_url(url).groups()
        broadcast = self._download_json(
            'https://graphigo.prd.dlive.tv/', vod_id,
            data=json.dumps({'query': '''query {
  pastBroadcast(permlink:"%s+%s") {
    content
    createdAt
    length
    playbackUrl
    title
    thumbnailUrl
    viewCount
  }
}''' % (uploader_id, vod_id)}).encode())['data']['pastBroadcast']
        title = broadcast['title']
        formats = self._extract_m3u8_formats(
            broadcast['playbackUrl'], vod_id, 'mp4', 'm3u8_native')
        self._sort_formats(formats)
        return {
            'id': vod_id,
            'title': title,
            'uploader_id': uploader_id,
            'formats': formats,
            'description': broadcast.get('content'),
            'thumbnail': broadcast.get('thumbnailUrl'),
            'timestamp': int_or_none(broadcast.get('createdAt'), 1000),
            'view_count': int_or_none(broadcast.get('viewCount')),
        }

class DLiveStreamIE(InfoExtractor):
    IE_NAME = 'dlive:stream'
    _VALID_URL = r'https?://(?:www\.)?dlive\.tv/(?!p/)(?P<id>[\w.-]+)'

    def _real_extract(self, url):
        display_name = self._match_id(url)
        user = self._download_json(
            'https://graphigo.prd.dlive.tv/', display_name,
            data=json.dumps({'query': '''query {
  userByDisplayName(displayname:"%s") {
    livestream {
      content
      createdAt
      title
      thumbnailUrl
      watchingCount
    }
    username
  }
}''' % display_name}).encode())['data']['userByDisplayName']
        livestream = user['livestream']
        title = livestream['title']
        username = user['username']
        formats = self._extract_m3u8_formats(
            'https://live.prd.dlive.tv/hls/live/%s.m3u8' % username,
            display_name, 'mp4')
        self._sort_formats(formats)
        return {
            'id': display_name,
            'title': title,
            'uploader': display_name,
            'uploader_id': username,
            'formats': formats,
            'description': livestream.get('content'),
            'thumbnail': livestream.get('thumbnailUrl'),
            'is_live': True,
            'timestamp': int_or_none(livestream.get('createdAt'), 1000),
            'view_count': int_or_none(livestream.get('watchingCount')),
        }


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

from .common import InfoExtractor

class DnaTubeBaseIE(InfoExtractor):
    pass

class DnaTubePlaylistBaseIE(DnaTubeBaseIE):
    pass

class DnaTubeIE(DnaTubeBaseIE):
    IE_NAME = 'dnatube'
    _VALID_URL = r'https?://(www\.)?dnatube\.com/video/(?P<id>\d+)/(?:[^/]+)/?'
    _TEST = {}

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

        title = self._og_search_title(webpage, default=None)

        entries = self._parse_html5_media_entries(url, webpage, video_id, m3u8_id='hls')
        entry = entries[0]
        self._sort_formats(entry['formats'])
        entry.update({
            'id': video_id,
            'title': title,
        })
        return entry


code:
import string
import random
import time

from .common import InfoExtractor

class DoodStreamIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?dood\.(?:to|watch|so|pm|wf)/[ed]/(?P<id>[a-z0-9]+)'
    _TESTS = [{
        'url': 'http://dood.to/e/5s1wmbdacezb',
        'md5': '4568b83b31e13242b3f1ff96c55f0595',
        'info_dict': {
            'id': '5s1wmbdacezb',
            'ext': 'mp4',
            'title': 'Kat Wonders - Monthly May 2020',
            'description': 'Kat Wonders - Monthly May 2020 | DoodStream.com',
            'thumbnail': 'https://img.doodcdn.com/snaps/flyus84qgl2fsk4g.jpg',
        }
    }, {
        'url': 'http://dood.watch/d/5s1wmbdacezb',
        'md5': '4568b83b31e13242b3f1ff96c55f0595',
        'info_dict': {
            'id': '5s1wmbdacezb',
            'ext': 'mp4',
            'title': 'Kat Wonders - Monthly May 2020',
            'description': 'Kat Wonders - Monthly May 2020 | DoodStream.com',
            'thumbnail': 'https://img.doodcdn.com/snaps/flyus84qgl2fsk4g.jpg',
        }
    }, {
        'url': 'https://dood.to/d/jzrxn12t2s7n',
        'md5': '3207e199426eca7c2aa23c2872e6728a',
        'info_dict': {
            'id': 'jzrxn12t2s7n',
            'ext': 'mp4',
            'title': 'Stacy Cruz Cute ALLWAYSWELL',
            'description': 'Stacy Cruz Cute ALLWAYSWELL | DoodStream.com',
            'thumbnail': 'https://img.doodcdn.com/snaps/8edqd5nppkac3x8u.jpg',
        }
    }, {
        'url': 'https://dood.so/d/jzrxn12t2s7n',
        'only_matching': True
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        url = f'https://dood.to/e/{video_id}'
        webpage = self._download_webpage(url, video_id)

        title = self._html_search_meta(
            ('og:title', 'twitter:title'), webpage, default=None) or self._html_extract_title(webpage)
        thumb = self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None)
        token = self._html_search_regex(r'[?&]token=([a-z0-9]+)[&\']', webpage, 'token')
        description = self._html_search_meta(
            ['og:description', 'description', 'twitter:description'], webpage, default=None)

        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/66.0',
            'referer': url
        }

        pass_md5 = self._html_search_regex(r'(/pass_md5.*?)\'', webpage, 'pass_md5')
        final_url = ''.join((
            self._download_webpage(f'https://dood.to{pass_md5}', video_id, headers=headers),
            *(random.choice(string.ascii_letters + string.digits) for _ in range(10)),
            f'?token={token}&expiry={int(time.time() * 1000)}',
        ))

        return {
            'id': video_id,
            'title': title,
            'url': final_url,
            'http_headers': headers,
            'ext': 'mp4',
            'description': description,
            'thumbnail': thumb,
        }


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

class DotsubIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?dotsub\.com/view/(?P<id>[^/]+)'
    _TESTS = [{
        'url': 'https://dotsub.com/view/9c63db2a-fa95-4838-8e6e-13deafe47f09',
        'md5': '21c7ff600f545358134fea762a6d42b6',
        'info_dict': {
            'id': '9c63db2a-fa95-4838-8e6e-13deafe47f09',
            'ext': 'flv',
            'title': 'MOTIVATION - "It\'s Possible" Best Inspirational Video Ever',
            'description': 'md5:41af1e273edbbdfe4e216a78b9d34ac6',
            'thumbnail': 're:^https?://dotsub.com/media/9c63db2a-fa95-4838-8e6e-13deafe47f09/p',
            'duration': 198,
            'uploader': 'liuxt',
            'timestamp': 1385778501.104,
            'upload_date': '20131130',
            'view_count': int,
        }
    }, {
        'url': 'https://dotsub.com/view/747bcf58-bd59-45b7-8c8c-ac312d084ee6',
        'md5': '2bb4a83896434d5c26be868c609429a3',
        'info_dict': {
            'id': '168006778',
            'ext': 'mp4',
            'title': 'Apartments and flats in Raipur the white symphony',
            'description': 'md5:784d0639e6b7d1bc29530878508e38fe',
            'thumbnail': 're:^https?://dotsub.com/media/747bcf58-bd59-45b7-8c8c-ac312d084ee6/p',
            'duration': 290,
            'timestamp': 1476767794.2809999,
            'upload_date': '20161018',
            'uploader': 'parthivi001',
            'uploader_id': 'user52596202',
            'view_count': int,
        },
        'add_ie': ['Vimeo'],
    }]

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

        info = self._download_json(
            'https://dotsub.com/api/media/%s/metadata' % video_id, video_id)
        video_url = info.get('mediaURI')

        if not video_url:
            webpage = self._download_webpage(url, video_id)
            video_url = self._search_regex(
                [r'<source[^>]+src="([^"]+)"', r'"file"\s*:\s*\'([^\']+)'],
                webpage, 'video url', default=None)
            info_dict = {
                'id': video_id,
                'url': video_url,
                'ext': 'flv',
            }

        if not video_url:
            setup_data = self._parse_json(self._html_search_regex(
                r'(?s)data-setup=([\'"])(?P<content>(?!\1).+?)\1',
                webpage, 'setup data', group='content'), video_id)
            info_dict = {
                '_type': 'url_transparent',
                'url': setup_data['src'],
            }

        info_dict.update({
            'title': info['title'],
            'description': info.get('description'),
            'thumbnail': info.get('screenshotURI'),
            'duration': int_or_none(info.get('duration'), 1000),
            'uploader': info.get('user'),
            'timestamp': float_or_none(info.get('dateCreated'), 1000),
            'view_count': int_or_none(info.get('numberOfViews')),
        })

        return info_dict


code:
import time
import hashlib
import re

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    unescapeHTML,
    unified_strdate,
    urljoin,
)

class DouyuTVIE(InfoExtractor):
    IE_DESC = '斗鱼'
    _VALID_URL = r'https?://(?:www\.)?douyu(?:tv)?\.com/(?:[^/]+/)*(?P<id>[A-Za-z0-9]+)'
    _TESTS = [{
        'url': 'http://www.douyutv.com/iseven',
        'info_dict': {
            'id': '17732',
            'display_id': 'iseven',
            'ext': 'flv',
            'title': 're:^清晨醒脑！根本停不下来！ [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'description': r're:.*m7show@163\.com.*',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': '7师傅',
            'is_live': True,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.douyutv.com/85982',
        'info_dict': {
            'id': '85982',
            'display_id': '85982',
            'ext': 'flv',
            'title': 're:^小漠从零单排记！——CSOL2躲猫猫 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'description': 'md5:746a2f7a253966a06755a912f0acc0d2',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': 'douyu小漠',
            'is_live': True,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'Room not found',
    }, {
        'url': 'http://www.douyutv.com/17732',
        'info_dict': {
            'id': '17732',
            'display_id': '17732',
            'ext': 'flv',
            'title': 're:^清晨醒脑！根本停不下来！ [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
            'description': r're:.*m7show@163\.com.*',
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': '7师傅',
            'is_live': True,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'http://www.douyu.com/xiaocang',
        'only_matching': True,
    }, {
        # \"room_id\"
        'url': 'http://www.douyu.com/t/lpl',
        'only_matching': True,
    }]

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

        if video_id.isdigit():
            room_id = video_id
        else:
            page = self._download_webpage(url, video_id)
            room_id = self._html_search_regex(
                r'"room_id\\?"\s*:\s*(\d+),', page, 'room id')

        # Grab metadata from mobile API
        room = self._download_json(
            'http://m.douyu.com/html5/live?roomId=%s' % room_id, video_id,
            note='Downloading room info')['data']

        # 1 = live, 2 = offline
        if room.get('show_status') == '2':
            raise ExtractorError('Live stream is offline', expected=True)

        # Grab the URL from PC client API
        # The m3u8 url from mobile API requires re-authentication every 5 minutes
        tt = int(time.time())
        signContent = 'lapi/live/thirdPart/getPlay/%s?aid=pcclient&rate=0&time=%d9TUk5fjjUjg9qIMH3sdnh' % (room_id, tt)
        sign = hashlib.md5(signContent.encode('ascii')).hexdigest()
        video_url = self._download_json(
            'http://coapi.douyucdn.cn/lapi/live/thirdPart/getPlay/' + room_id,
            video_id, note='Downloading video URL info',
            query={'rate': 0}, headers={
                'auth': sign,
                'time': str(tt),
                'aid': 'pcclient'
            })['data']['live_url']

        title = unescapeHTML(room['room_name'])
        description = room.get('show_details')
        thumbnail = room.get('room_src')
        uploader = room.get('nickname')

        return {
            'id': room_id,
            'display_id': video_id,
            'url': video_url,
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'uploader': uploader,
            'is_live': True,
        }

class DouyuShowIE(InfoExtractor):
    _VALID_URL = r'https?://v(?:mobile)?\.douyu\.com/show/(?P<id>[0-9a-zA-Z]+)'

    _TESTS = [{
        'url': 'https://v.douyu.com/show/rjNBdvnVXNzvE2yw',
        'md5': '0c2cfd068ee2afe657801269b2d86214',
        'info_dict': {
            'id': 'rjNBdvnVXNzvE2yw',
            'ext': 'mp4',
            'title': '陈一发儿：砒霜 我有个室友系列！04-01 22点场',
            'duration': 7150.08,
            'thumbnail': r're:^https?://.*\.jpg$',
            'uploader': '陈一发儿',
            'uploader_id': 'XrZwYelr5wbK',
            'uploader_url': 'https://v.douyu.com/author/XrZwYelr5wbK',
            'upload_date': '20170402',
        },
    }, {
        'url': 'https://vmobile.douyu.com/show/rjNBdvnVXNzvE2yw',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        url = url.replace('vmobile.', 'v.')
        video_id = self._match_id(url)

        webpage = self._download_webpage(url, video_id)

        room_info = self._parse_json(self._search_regex(
            r'var\s+\$ROOM\s*=\s*({.+});', webpage, 'room info'), video_id)

        video_info = None

        for trial in range(5):
            # Sometimes Douyu rejects our request. Let's try it more times
            try:
                video_info = self._download_json(
                    'https://vmobile.douyu.com/video/getInfo', video_id,
                    query={'vid': video_id},
                    headers={
                        'Referer': url,
                        'x-requested-with': 'XMLHttpRequest',
                    })
                break
            except ExtractorError:
                self._sleep(1, video_id)

        if not video_info:
            raise ExtractorError('Can\'t fetch video info')

        formats = self._extract_m3u8_formats(
            video_info['data']['video_url'], video_id,
            entry_protocol='m3u8_native', ext='mp4')

        upload_date = unified_strdate(self._html_search_regex(
            r'上传时间：([^<]+)', webpage,
            'upload date', fatal=False))

        uploader = uploader_id = uploader_url = None
        mobj = re.search(
            r'(?m)]+href="/author/([0-9a-zA-Z]+)".+?]+title="([^"]+)"',
            webpage)
        if mobj:
            uploader_id, uploader = mobj.groups()
            uploader_url = urljoin(url, '/author/' + uploader_id)

        return {
            'id': video_id,
            'title': room_info['name'],
            'formats': formats,
            'duration': room_info.get('duration'),
            'thumbnail': room_info.get('pic'),
            'upload_date': upload_date,
            'uploader': uploader,
            'uploader_id': uploader_id,
            'uploader_url': uploader_url,
        }


code:
import json
import uuid

from .common import InfoExtractor
from ..compat import compat_HTTPError
from ..utils import (
    determine_ext,
    ExtractorError,
    float_or_none,
    int_or_none,
    remove_start,
    strip_or_none,
    try_get,
    unified_timestamp,
)

class DPlayBaseIE(InfoExtractor):
    _PATH_REGEX = r'/(?P<id>[^/]+/[^/?#]+)'
    _auth_token_cache = {}

    def _get_auth(self, disco_base, display_id, realm, needs_device_id=True):
        key = (disco_base, realm)
        st = self._get_cookies(disco_base).get('st')
        token = (st and st.value) or self._auth_token_cache.get(key)

        if not token:
            query = {'realm': realm}
            if needs_device_id:
                query['deviceId'] = uuid.uuid4().hex
            token = self._download_json(
                disco_base + 'token', display_id, 'Downloading token',
                query=query)['data']['attributes']['token']

            # Save cache only if cookies are not being set
            if not self._get_cookies(disco_base).get('st'):
                self._auth_token_cache[key] = token

        return f'Bearer {token}'

    def _process_errors(self, e, geo_countries):
        info = self._parse_json(e.cause.read().decode('utf-8'), None)
        error = info['errors'][0]
        error_code = error.get('code')
        if error_code == 'access.denied.geoblocked':
            self.raise_geo_restricted(countries=geo_countries)
        elif error_code in ('access.denied.missingpackage', 'invalid.token'):
            raise ExtractorError(
                'This video is only available for registered users. You may want to use --cookies.', expected=True)
        raise ExtractorError(info['errors'][0]['detail'], expected=True)

    def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
        headers['Authorization'] = self._get_auth(disco_base, display_id, realm, False)

    def _download_video_playback_info(self, disco_base, video_id, headers):
        streaming = self._download_json(
            disco_base + 'playback/videoPlaybackInfo/' + video_id,
            video_id, headers=headers)['data']['attributes']['streaming']
        streaming_list = []
        for format_id, format_dict in streaming.items():
            streaming_list.append({
                'type': format_id,
                'url': format_dict.get('url'),
            })
        return streaming_list

    def _get_disco_api_info(self, url, display_id, disco_host, realm, country, domain=''):
        geo_countries = [country.upper()]
        self._initialize_geo_bypass({
            'countries': geo_countries,
        })
        disco_base = 'https://%s/' % disco_host
        headers = {
            'Referer': url,
        }
        self._update_disco_api_headers(headers, disco_base, display_id, realm)
        try:
            video = self._download_json(
                disco_base + 'content/videos/' + display_id, display_id,
                headers=headers, query={
                    'fields[channel]': 'name',
                    'fields[image]': 'height,src,width',
                    'fields[show]': 'name',
                    'fields[tag]': 'name',
                    'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
                    'include': 'images,primaryChannel,show,tags'
                })
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
                self._process_errors(e, geo_countries)
            raise
        video_id = video['data']['id']
        info = video['data']['attributes']
        title = info['name'].strip()
        formats = []
        subtitles = {}
        try:
            streaming = self._download_video_playback_info(
                disco_base, video_id, headers)
        except ExtractorError as e:
            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
                self._process_errors(e, geo_countries)
            raise
        for format_dict in streaming:
            if not isinstance(format_dict, dict):
                continue
            format_url = format_dict.get('url')
            if not format_url:
                continue
            format_id = format_dict.get('type')
            ext = determine_ext(format_url)
            if format_id == 'dash' or ext == 'mpd':
                dash_fmts, dash_subs = self._extract_mpd_formats_and_subtitles(
                    format_url, display_id, mpd_id='dash', fatal=False)
                formats.extend(dash_fmts)
                subtitles = self._merge_subtitles(subtitles, dash_subs)
            elif format_id == 'hls' or ext == 'm3u8':
                m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
                    format_url, display_id, 'mp4',
                    entry_protocol='m3u8_native', m3u8_id='hls',
                    fatal=False)
                formats.extend(m3u8_fmts)
                subtitles = self._merge_subtitles(subtitles, m3u8_subs)
            else:
                formats.append({
                    'url': format_url,
                    'format_id': format_id,
                })
        self._sort_formats(formats)

        creator = series = None
        tags = []
        thumbnails = []
        included = video.get('included') or []
        if isinstance(included, list):
            for e in included:
                attributes = e.get('attributes')
                if not attributes:
                    continue
                e_type = e.get('type')
                if e_type == 'channel':
                    creator = attributes.get('name')
                elif e_type == 'image':
                    src = attributes.get('src')
                    if src:
                        thumbnails.append({
                            'url': src,
                            'width': int_or_none(attributes.get('width')),
                            'height': int_or_none(attributes.get('height')),
                        })
                if e_type == 'show':
                    series = attributes.get('name')
                elif e_type == 'tag':
                    name = attributes.get('name')
                    if name:
                        tags.append(name)
        return {
            'id': video_id,
            'display_id': display_id,
            'title': title,
            'description': strip_or_none(info.get('description')),
            'duration': float_or_none(info.get('videoDuration'), 1000),
            'timestamp': unified_timestamp(info.get('publishStart')),
            'series': series,
            'season_number': int_or_none(info.get('seasonNumber')),
            'episode_number': int_or_none(info.get('episodeNumber')),
            'creator': creator,
            'tags': tags,
            'thumbnails': thumbnails,
            'formats': formats,
            'subtitles': subtitles,
            'http_headers': {
                'referer': domain,
            },
        }

class DPlayIE(DPlayBaseIE):
    _VALID_URL = r'''(?x)https?://
        (?P<domain>
            (?:www\.)?(?P<host>d
                (?:
                    play\.(?P<country>dk|fi|jp|se|no)|
                    iscoveryplus\.(?P<plus_country>dk|es|fi|it|se|no)
                )
            )|
            (?P<subdomain_country>es|it)\.dplay\.com
        )/[^/]+''' + DPlayBaseIE._PATH_REGEX

    _TESTS = [{
        # non geo restricted, via secure api, unsigned download hls URL
        'url': 'https://www.dplay.se/videos/nugammalt-77-handelser-som-format-sverige/nugammalt-77-handelser-som-format-sverige-101',
        'info_dict': {
            'id': '13628',
            'display_id': 'nugammalt-77-handelser-som-format-sverige/nugammalt-77-handelser-som-format-sverige-101',
            'ext': 'mp4',
            'title': 'Svensken lär sig njuta av livet',
            'description': 'md5:d3819c9bccffd0fe458ca42451dd50d8',
            'duration': 2649.856,
            'timestamp': 1365453720,
            'upload_date': '20130408',
            'creator': 'Kanal 5',
            'series': 'Nugammalt - 77 händelser som format Sverige',
            'season_number': 1,
            'episode_number': 1,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # geo restricted, via secure api, unsigned download hls URL
        'url': 'http://www.dplay.dk/videoer/ted-bundy-mind-of-a-monster/ted-bundy-mind-of-a-monster',
        'info_dict': {
            'id': '104465',
            'display_id': 'ted-bundy-mind-of-a-monster/ted-bundy-mind-of-a-monster',
            'ext': 'mp4',
            'title': 'Ted Bundy: Mind Of A Monster',
            'description': 'md5:8b780f6f18de4dae631668b8a9637995',
            'duration': 5290.027,
            'timestamp': 1570694400,
            'upload_date': '20191010',
            'creator': 'ID - Investigation Discovery',
            'series': 'Ted Bundy: Mind Of A Monster',
            'season_number': 1,
            'episode_number': 1,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        # disco-api
        'url': 'https://www.dplay.no/videoer/i-kongens-klr/sesong-1-episode-7',
        'info_dict': {
            'id': '40206',
            'display_id': 'i-kongens-klr/sesong-1-episode-7',
            'ext': 'mp4',
            'title': 'Episode 7',
            'description': 'md5:e3e1411b2b9aebeea36a6ec5d50c60cf',
            'duration': 2611.16,
            'timestamp': 1516726800,
            'upload_date': '20180123',
            'series': 'I kongens klær',
            'season_number': 1,
            'episode_number': 7,
        },
        'params': {
            'skip_download': True,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'http://it.dplay.com/nove/biografie-imbarazzanti/luigi-di-maio-la-psicosi-di-stanislawskij/',
        'md5': '2b808ffb00fc47b884a172ca5d13053c',
        'info_dict': {
            'id': '6918',
            'display_id': 'biografie-imbarazzanti/luigi-di-maio-la-psicosi-di-stanislawskij',
            'ext': 'mp4',
            'title': 'Luigi Di Maio: la psicosi di Stanislawskij',
            'description': 'md5:3c7a4303aef85868f867a26f5cc14813',
            'thumbnail': r're:^https?://.*\.jpe?g',
            'upload_date': '20160524',
            'timestamp': 1464076800,
            'series': 'Biografie imbarazzanti',
            'season_number': 1,
            'episode': 'Episode 1',
            'episode_number': 1,
        },
    }, {
        'url': 'https://es.dplay.com/dmax/la-fiebre-del-oro/temporada-8-episodio-1/',
        'info_dict': {
            'id': '21652',
            'display_id': 'la-fiebre-del-oro/temporada-8-episodio-1',
            'ext': 'mp4',
            'title': 'Episodio 1',
            'description': 'md5:b9dcff2071086e003737485210675f69',
            'thumbnail': r're:^https?://.*\.png',
            'upload_date': '20180709',
            'timestamp': 1531173540,
            'series': 'La fiebre del oro',
            'season_number': 8,
            'episode': 'Episode 1',
            'episode_number': 1,
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://www.dplay.fi/videot/shifting-gears-with-aaron-kaufman/episode-16',
        'only_matching': True,
    }, {
        'url': 'https://www.dplay.jp/video/gold-rush/24086',
        'only_matching': True,
    }, {
        'url': 'https://www.discoveryplus.se/videos/nugammalt-77-handelser-som-format-sverige/nugammalt-77-handelser-som-format-sverige-101',
        'only_matching': True,
    }, {
        'url': 'https://www.discoveryplus.dk/videoer/ted-bundy-mind-of-a-monster/ted-bundy-mind-of-a-monster',
        'only_matching': True,
    }, {
        'url': 'https://www.discoveryplus.no/videoer/i-kongens-klr/sesong-1-episode-7',
        'only_matching': True,
    }, {
        'url': 'https://www.discoveryplus.it/videos/biografie-imbarazzanti/luigi-di-maio-la-psicosi-di-stanislawskij',
        'only_matching': True,
    }, {
        'url': 'https://www.discoveryplus.es/videos/la-fiebre-del-oro/temporada-8-episodio-1',
        'only_matching': True,
    }, {
        'url': 'https://www.discoveryplus.fi/videot/shifting-gears-with-aaron-kaufman/episode-16',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        display_id = mobj.group('id')
        domain = remove_start(mobj.group('domain'), 'www.')
        country = mobj.group('country') or mobj.group('subdomain_country') or mobj.group('plus_country')
        host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
        return self._get_disco_api_info(
            url, display_id, host, 'dplay' + country, country, domain)

class HGTVDeIE(DPlayBaseIE):
    _VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
        'info_dict': {
            'id': '151205',
            'display_id': 'tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette',
            'ext': 'mp4',
            'title': 'Wer braucht schon eine Toilette',
            'description': 'md5:05b40a27e7aed2c9172de34d459134e2',
            'duration': 1177.024,
            'timestamp': 1595705400,
            'upload_date': '20200725',
            'creator': 'HGTV',
            'series': 'Tiny House - klein, aber oho',
            'season_number': 3,
            'episode_number': 3,
        },
    }]

    def _real_extract(self, url):
        display_id = self._match_id(url)
        return self._get_disco_api_info(
            url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')

class DiscoveryPlusBaseIE(DPlayBaseIE):
    def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
        headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6'

    def _download_video_playback_info(self, disco_base, video_id, headers):
        return self._download_json(
            disco_base + 'playback/v3/videoPlaybackInfo',
            video_id, headers=headers, data=json.dumps({
                'deviceInfo': {
                    'adBlocker': False,
                },
                'videoId': video_id,
                'wisteriaProperties': {
                    'platform': 'desktop',
                    'product': self._PRODUCT,
                },
            }).encode('utf-8'))['data']['attributes']['streaming']

    def _real_extract(self, url):
        return self._get_disco_api_info(url, self._match_id(url), **self._DISCO_API_PARAMS)

class GoDiscoveryIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:go\.)?discovery\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://go.discovery.com/video/dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
        'info_dict': {
            'id': '4164906',
            'display_id': 'dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
            'ext': 'mp4',
            'title': 'Rodbuster / Galvanizer',
            'description': 'Mike installs rebar with a team of rodbusters, then he galvanizes steel.',
            'season_number': 9,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://discovery.com/video/dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
        'only_matching': True,
    }]

    _PRODUCT = 'dsc'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.go.discovery.com',
        'realm': 'go',
        'country': 'us',
    }

class TravelChannelIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:watch\.)?travelchannel\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://watch.travelchannel.com/video/ghost-adventures-travel-channel/ghost-train-of-ely',
        'info_dict': {
            'id': '2220256',
            'display_id': 'ghost-adventures-travel-channel/ghost-train-of-ely',
            'ext': 'mp4',
            'title': 'Ghost Train of Ely',
            'description': 'The crew investigates the dark history of the Nevada Northern Railway.',
            'season_number': 24,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://watch.travelchannel.com/video/ghost-adventures-travel-channel/ghost-train-of-ely',
        'only_matching': True,
    }]

    _PRODUCT = 'trav'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.watch.travelchannel.com',
        'realm': 'go',
        'country': 'us',
    }

class CookingChannelIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:watch\.)?cookingchanneltv\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://watch.cookingchanneltv.com/video/carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
        'info_dict': {
            'id': '2348634',
            'display_id': 'carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
            'ext': 'mp4',
            'title': 'The Postman Always Brings Rice',
            'description': 'Noah visits the Maui Fair and the Aurora Winter Festival in Vancouver.',
            'season_number': 9,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://watch.cookingchanneltv.com/video/carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
        'only_matching': True,
    }]

    _PRODUCT = 'cook'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.watch.cookingchanneltv.com',
        'realm': 'go',
        'country': 'us',
    }

class HGTVUsaIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:watch\.)?hgtv\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://watch.hgtv.com/video/home-inspector-joe-hgtv-atve-us/this-mold-house',
        'info_dict': {
            'id': '4289736',
            'display_id': 'home-inspector-joe-hgtv-atve-us/this-mold-house',
            'ext': 'mp4',
            'title': 'This Mold House',
            'description': 'Joe and Noel help take a familys dream home from hazardous to fabulous.',
            'season_number': 1,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://watch.hgtv.com/video/home-inspector-joe-hgtv-atve-us/this-mold-house',
        'only_matching': True,
    }]

    _PRODUCT = 'hgtv'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.watch.hgtv.com',
        'realm': 'go',
        'country': 'us',
    }

class FoodNetworkIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:watch\.)?foodnetwork\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://watch.foodnetwork.com/video/kids-baking-championship-food-network/float-like-a-butterfly',
        'info_dict': {
            'id': '4116449',
            'display_id': 'kids-baking-championship-food-network/float-like-a-butterfly',
            'ext': 'mp4',
            'title': 'Float Like a Butterfly',
            'description': 'The 12 kid bakers create colorful carved butterfly cakes.',
            'season_number': 10,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://watch.foodnetwork.com/video/kids-baking-championship-food-network/float-like-a-butterfly',
        'only_matching': True,
    }]

    _PRODUCT = 'food'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.watch.foodnetwork.com',
        'realm': 'go',
        'country': 'us',
    }

class DestinationAmericaIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?destinationamerica\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.destinationamerica.com/video/alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
        'info_dict': {
            'id': '4210904',
            'display_id': 'alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
            'ext': 'mp4',
            'title': 'Central Alaskas Bigfoot',
            'description': 'A team heads to central Alaska to investigate an aggressive Bigfoot.',
            'season_number': 1,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://www.destinationamerica.com/video/alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
        'only_matching': True,
    }]

    _PRODUCT = 'dam'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.destinationamerica.com',
        'realm': 'go',
        'country': 'us',
    }

class InvestigationDiscoveryIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?investigationdiscovery\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.investigationdiscovery.com/video/unmasked-investigation-discovery/the-killer-clown',
        'info_dict': {
            'id': '2139409',
            'display_id': 'unmasked-investigation-discovery/the-killer-clown',
            'ext': 'mp4',
            'title': 'The Killer Clown',
            'description': 'A wealthy Florida woman is fatally shot in the face by a clown at her door.',
            'season_number': 1,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://www.investigationdiscovery.com/video/unmasked-investigation-discovery/the-killer-clown',
        'only_matching': True,
    }]

    _PRODUCT = 'ids'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.investigationdiscovery.com',
        'realm': 'go',
        'country': 'us',
    }

class AmHistoryChannelIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?ahctv\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.ahctv.com/video/modern-sniper-ahc/army',
        'info_dict': {
            'id': '2309730',
            'display_id': 'modern-sniper-ahc/army',
            'ext': 'mp4',
            'title': 'Army',
            'description': 'Snipers today face challenges their predecessors couldve only dreamed of.',
            'season_number': 1,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://www.ahctv.com/video/modern-sniper-ahc/army',
        'only_matching': True,
    }]

    _PRODUCT = 'ahc'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.ahctv.com',
        'realm': 'go',
        'country': 'us',
    }

class ScienceChannelIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
        'info_dict': {
            'id': '2842849',
            'display_id': 'strangest-things-science-atve-us/nazi-mystery-machine',
            'ext': 'mp4',
            'title': 'Nazi Mystery Machine',
            'description': 'Experts investigate the secrets of a revolutionary encryption machine.',
            'season_number': 1,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
        'only_matching': True,
    }]

    _PRODUCT = 'sci'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.sciencechannel.com',
        'realm': 'go',
        'country': 'us',
    }

class DIYNetworkIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
        'info_dict': {
            'id': '2309730',
            'display_id': 'pool-kings-diy-network/bringing-beach-life-to-texas',
            'ext': 'mp4',
            'title': 'Bringing Beach Life to Texas',
            'description': 'The Pool Kings give a family a day at the beach in their own backyard.',
            'season_number': 10,
            'episode_number': 2,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
        'only_matching': True,
    }]

    _PRODUCT = 'diy'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.watch.diynetwork.com',
        'realm': 'go',
        'country': 'us',
    }

class DiscoveryLifeIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?discoverylife\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.discoverylife.com/video/surviving-death-discovery-life-atve-us/bodily-trauma',
        'info_dict': {
            'id': '2218238',
            'display_id': 'surviving-death-discovery-life-atve-us/bodily-trauma',
            'ext': 'mp4',
            'title': 'Bodily Trauma',
            'description': 'Meet three people who tested the limits of the human body.',
            'season_number': 1,
            'episode_number': 2,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://www.discoverylife.com/video/surviving-death-discovery-life-atve-us/bodily-trauma',
        'only_matching': True,
    }]

    _PRODUCT = 'dlf'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.discoverylife.com',
        'realm': 'go',
        'country': 'us',
    }

class AnimalPlanetIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
        'info_dict': {
            'id': '3338923',
            'display_id': 'north-woods-law-animal-planet/squirrel-showdown',
            'ext': 'mp4',
            'title': 'Squirrel Showdown',
            'description': 'A woman is suspected of being in possession of flying squirrel kits.',
            'season_number': 16,
            'episode_number': 11,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
        'only_matching': True,
    }]

    _PRODUCT = 'apl'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.animalplanet.com',
        'realm': 'go',
        'country': 'us',
    }

class TLCIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:go\.)?tlc\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://go.tlc.com/video/my-600-lb-life-tlc/melissas-story-part-1',
        'info_dict': {
            'id': '2206540',
            'display_id': 'my-600-lb-life-tlc/melissas-story-part-1',
            'ext': 'mp4',
            'title': 'Melissas Story (Part 1)',
            'description': 'At 650 lbs, Melissa is ready to begin her seven-year weight loss journey.',
            'season_number': 1,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://go.tlc.com/video/my-600-lb-life-tlc/melissas-story-part-1',
        'only_matching': True,
    }]

    _PRODUCT = 'tlc'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.tlc.com',
        'realm': 'go',
        'country': 'us',
    }

class MotorTrendIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:watch\.)?motortrend\.com/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://watch.motortrend.com/video/car-issues-motortrend-atve-us/double-dakotas',
        'info_dict': {
            'id': '"4859182"',
            'display_id': 'double-dakotas',
            'ext': 'mp4',
            'title': 'Double Dakotas',
            'description': 'Tylers buy-one-get-one Dakota deal has the Wizard pulling double duty.',
            'season_number': 2,
            'episode_number': 3,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://watch.motortrend.com/video/car-issues-motortrend-atve-us/double-dakotas',
        'only_matching': True,
    }]

    _PRODUCT = 'vel'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.watch.motortrend.com',
        'realm': 'go',
        'country': 'us',
    }

class DiscoveryPlusIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?!it/)(?:\w{2}/)?video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
        'info_dict': {
            'id': '1140794',
            'display_id': 'property-brothers-forever-home/food-and-family',
            'ext': 'mp4',
            'title': 'Food and Family',
            'description': 'The brothers help a Richmond family expand their single-level home.',
            'duration': 2583.113,
            'timestamp': 1609304400,
            'upload_date': '20201230',
            'creator': 'HGTV',
            'series': 'Property Brothers: Forever Home',
            'season_number': 1,
            'episode_number': 1,
        },
        'skip': 'Available for Premium users',
    }, {
        'url': 'https://discoveryplus.com/ca/video/bering-sea-gold-discovery-ca/goldslingers',
        'only_matching': True,
    }]

    _PRODUCT = 'dplus_us'
    _DISCO_API_PARAMS = {
        'disco_host': 'us1-prod-direct.discoveryplus.com',
        'realm': 'go',
        'country': 'us',
    }

class DiscoveryPlusIndiaIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/videos?' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.discoveryplus.in/videos/how-do-they-do-it/fugu-and-more?seasonId=8&type=EPISODE',
        'info_dict': {
            'id': '27104',
            'ext': 'mp4',
            'display_id': 'how-do-they-do-it/fugu-and-more',
            'title': 'Fugu and More',
            'description': 'The Japanese catch, prepare and eat the deadliest fish on the planet.',
            'duration': 1319.32,
            'timestamp': 1582309800,
            'upload_date': '20200221',
            'series': 'How Do They Do It?',
            'season_number': 8,
            'episode_number': 2,
            'creator': 'Discovery Channel',
            'thumbnail': r're:https://.+\.jpeg',
            'episode': 'Episode 2',
            'season': 'Season 8',
            'tags': [],
        },
        'params': {
            'skip_download': True,
        }
    }]

    _PRODUCT = 'dplus-india'
    _DISCO_API_PARAMS = {
        'disco_host': 'ap2-prod-direct.discoveryplus.in',
        'realm': 'dplusindia',
        'country': 'in',
        'domain': 'https://www.discoveryplus.in/',
    }

    def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
        headers.update({
            'x-disco-params': 'realm=%s' % realm,
            'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:17.0.0',
            'Authorization': self._get_auth(disco_base, display_id, realm),
        })

class DiscoveryNetworksDeIE(DPlayBaseIE):
    _VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de|dplay\.co\.uk)/(?:programme|show|sendungen)/(?P<programme>[^/]+)/(?:video/)?(?P<alternate_id>[^/]+)'

    _TESTS = [{
        'url': 'https://www.tlc.de/programme/breaking-amish/video/die-welt-da-drauen/DCB331270001100',
        'info_dict': {
            'id': '78867',
            'ext': 'mp4',
            'title': 'Die Welt da draußen',
            'description': 'md5:61033c12b73286e409d99a41742ef608',
            'timestamp': 1554069600,
            'upload_date': '20190331',
            'creator': 'TLC',
            'season': 'Season 1',
            'series': 'Breaking Amish',
            'episode_number': 1,
            'tags': ['new york', 'großstadt', 'amische', 'landleben', 'modern', 'infos', 'tradition', 'herausforderung'],
            'display_id': 'breaking-amish/die-welt-da-drauen',
            'episode': 'Episode 1',
            'duration': 2625.024,
            'season_number': 1,
            'thumbnail': r're:https://.+\.jpg',
        },
        'params': {
            'skip_download': True,
        },
    }, {
        'url': 'https://www.dmax.de/programme/dmax-highlights/video/tuning-star-sidney-hoffmann-exklusiv-bei-dmax/191023082312316',
        'only_matching': True,
    }, {
        'url': 'https://www.dplay.co.uk/show/ghost-adventures/video/hotel-leger-103620/EHD_280313B',
        'only_matching': True,
    }, {
        'url': 'https://tlc.de/sendungen/breaking-amish/die-welt-da-drauen/',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        domain, programme, alternate_id = self._match_valid_url(url).groups()
        country = 'GB' if domain == 'dplay.co.uk' else 'DE'
        realm = 'questuk' if country == 'GB' else domain.replace('.', '')
        return self._get_disco_api_info(
            url, '%s/%s' % (programme, alternate_id),
            'sonic-eu1-prod.disco-api.com', realm, country)

class DiscoveryPlusShowBaseIE(DPlayBaseIE):

    def _entries(self, show_name):
        headers = {
            'x-disco-client': self._X_CLIENT,
            'x-disco-params': f'realm={self._REALM}',
            'referer': self._DOMAIN,
            'Authentication': self._get_auth(self._BASE_API, None, self._REALM),
        }
        show_json = self._download_json(
            f'{self._BASE_API}cms/routes/{self._SHOW_STR}/{show_name}?include=default',
            video_id=show_name, headers=headers)['included'][self._INDEX]['attributes']['component']
        show_id = show_json['mandatoryParams'].split('=')[-1]
        season_url = self._BASE_API + 'content/videos?sort=episodeNumber&filter[seasonNumber]={}&filter[show.id]={}&page[size]=100&page[number]={}'
        for season in show_json['filters'][0]['options']:
            season_id = season['id']
            total_pages, page_num = 1, 0
            while page_num < total_pages:
                season_json = self._download_json(
                    season_url.format(season_id, show_id, str(page_num + 1)), show_name, headers=headers,
                    note='Downloading season %s JSON metadata%s' % (season_id, ' page %d' % page_num if page_num else ''))
                if page_num == 0:
                    total_pages = try_get(season_json, lambda x: x['meta']['totalPages'], int) or 1
                episodes_json = season_json['data']
                for episode in episodes_json:
                    video_path = episode['attributes']['path']
                    yield self.url_result(
                        '%svideos/%s' % (self._DOMAIN, video_path),
                        ie=self._VIDEO_IE.ie_key(), video_id=episode.get('id') or video_path)
                page_num += 1

    def _real_extract(self, url):
        show_name = self._match_valid_url(url).group('show_name')
        return self.playlist_result(self._entries(show_name), playlist_id=show_name)

class DiscoveryPlusItalyIE(DiscoveryPlusBaseIE):
    _VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/it/video' + DPlayBaseIE._PATH_REGEX
    _TESTS = [{
        'url': 'https://www.discoveryplus.com/it/video/i-signori-della-neve/stagione-2-episodio-1-i-preparativi',
        'only_matching': True,
    }]

    _PRODUCT = 'dplus_us'
    _DISCO_API_PARAMS = {
        'disco_host': 'eu1-prod-direct.discoveryplus.com',
        'realm': 'dplay',
        'country': 'it',
    }

class DiscoveryPlusItalyShowIE(DiscoveryPlusShowBaseIE):
    _VALID_URL = r'https?://(?:www\.)?discoveryplus\.it/programmi/(?P<show_name>[^/]+)/?(?:[?#]|$)'
    _TESTS = [{
        'url': 'https://www.discoveryplus.it/programmi/deal-with-it-stai-al-gioco',
        'playlist_mincount': 168,
        'info_dict': {
            'id': 'deal-with-it-stai-al-gioco',
        },
    }]

    _BASE_API = 'https://disco-api.discoveryplus.it/'
    _DOMAIN = 'https://www.discoveryplus.it/'
    _X_CLIENT = 'WEB:UNKNOWN:dplay-client:2.6.0'
    _REALM = 'dplayit'
    _SHOW_STR = 'programmi'
    _INDEX = 1
    _VIDEO_IE = DPlayIE

class DiscoveryPlusIndiaShowIE(DiscoveryPlusShowBaseIE):
    _VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/show/(?P<show_name>[^/]+)/?(?:[?#]|$)'
    _TESTS = [{
        'url': 'https://www.discoveryplus.in/show/how-do-they-do-it',
        'playlist_mincount': 140,
        'info_dict': {
            'id': 'how-do-they-do-it',
        },
    }]

    _BASE_API = 'https://ap2-prod-direct.discoveryplus.in/'
    _DOMAIN = 'https://www.discoveryplus.in/'
    _X_CLIENT = 'WEB:UNKNOWN:dplus-india:prod'
    _REALM = 'dplusindia'
    _SHOW_STR = 'show'
    _INDEX = 4
    _VIDEO_IE = DiscoveryPlusIndiaIE


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

class DRBonanzaIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?dr\.dk/bonanza/[^/]+/\d+/[^/]+/(?P<id>\d+)/(?P<display_id>[^/?#&]+)'
    _TEST = {
        'url': 'http://www.dr.dk/bonanza/serie/154/matador/40312/matador---0824-komme-fremmede-',
        'info_dict': {
            'id': '40312',
            'display_id': 'matador---0824-komme-fremmede-',
            'ext': 'mp4',
            'title': 'MATADOR - 08:24. "Komme fremmede".',
            'description': 'md5:77b4c1ac4d4c1b9d610ab4395212ff84',
            'thumbnail': r're:^https?://.*\.(?:gif|jpg)$',
            'duration': 4613,
        },
    }

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

        webpage = self._download_webpage(url, display_id)

        info = self._parse_html5_media_entries(
            url, webpage, display_id, m3u8_id='hls',
            m3u8_entry_protocol='m3u8_native')[0]
        self._sort_formats(info['formats'])

        asset = self._parse_json(
            self._search_regex(
                r'(?s)currentAsset\s*=\s*({.+?})\s*</script', webpage, 'asset'),
            display_id, transform_source=js_to_json)

        title = unescapeHTML(asset['AssetTitle']).strip()

        def extract(field):
            return self._search_regex(
                r'
]+>\s*
%s:
\s*
\s*
]+>\s*
([^<]+)
' % field,
                webpage, field, default=None)

        info.update({
            'id': asset.get('AssetId') or video_id,
            'display_id': display_id,
            'title': title,
            'description': extract('Programinfo'),
            'duration': parse_duration(extract('Tid')),
            'thumbnail': asset.get('AssetImageUrl'),
        })
        return info


code:
from .zdf import ZDFIE

class DreiSatIE(ZDFIE):
    IE_NAME = '3sat'
    _VALID_URL = r'https?://(?:www\.)?3sat\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
    _TESTS = [{
        # Same as https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html
        'url': 'https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html',
        'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
        'info_dict': {
            'id': '141007_ab18_10wochensommer_film',
            'ext': 'mp4',
            'title': 'Ab 18! - 10 Wochen Sommer',
            'description': 'md5:8253f41dc99ce2c3ff892dac2d65fe26',
            'duration': 2660,
            'timestamp': 1608604200,
            'upload_date': '20201222',
        },
    }, {
        'url': 'https://www.3sat.de/gesellschaft/schweizweit/waidmannsheil-100.html',
        'info_dict': {
            'id': '140913_sendung_schweizweit',
            'ext': 'mp4',
            'title': 'Waidmannsheil',
            'description': 'md5:cce00ca1d70e21425e72c86a98a56817',
            'timestamp': 1410623100,
            'upload_date': '20140913'
        },
        'params': {
            'skip_download': True,
        }
    }, {
        # Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html
        'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html',
        'only_matching': True,
    }, {
        # Same as https://www.zdf.de/wissen/nano/nano-21-mai-2019-102.html, equal media ids
        'url': 'https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html',
        'only_matching': True,
    }]


code:
import json

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

class DroobleIE(InfoExtractor):
    _VALID_URL = r'''(?x)https?://drooble\.com/(?:
        (?:(?P<user>[^/]+)/)?(?P<kind>song|videos|music/albums)/(?P<id>\d+)|
        (?P<user_2>[^/]+)/(?P<kind_2>videos|music))
    '''
    _TESTS = [{
        'url': 'https://drooble.com/song/2858030',
        'md5': '5ffda90f61c7c318dc0c3df4179eb064',
        'info_dict': {
            'id': '2858030',
            'ext': 'mp3',
            'title': 'Skankocillin',
            'upload_date': '20200801',
            'timestamp': 1596241390,
            'uploader_id': '95894',
            'uploader': 'Bluebeat Shelter',
        }
    }, {
        'url': 'https://drooble.com/karl340758/videos/2859183',
        'info_dict': {
            'id': 'J6QCQY_I5Tk',
            'ext': 'mp4',
            'title': 'Skankocillin',
            'uploader_id': 'UCrSRoI5vVyeYihtWEYua7rg',
            'description': 'md5:ffc0bd8ba383db5341a86a6cd7d9bcca',
            'upload_date': '20200731',
            'uploader': 'Bluebeat Shelter',
        }
    }, {
        'url': 'https://drooble.com/karl340758/music/albums/2858031',
        'info_dict': {
            'id': '2858031',
        },
        'playlist_mincount': 8,
    }, {
        'url': 'https://drooble.com/karl340758/music',
        'info_dict': {
            'id': 'karl340758',
        },
        'playlist_mincount': 8,
    }, {
        'url': 'https://drooble.com/karl340758/videos',
        'info_dict': {
            'id': 'karl340758',
        },
        'playlist_mincount': 8,
    }]

    def _call_api(self, method, video_id, data=None):
        response = self._download_json(
            f'https://drooble.com/api/dt/{method}', video_id, data=json.dumps(data).encode())
        if not response[0]:
            raise ExtractorError('Unable to download JSON metadata')
        return response[1]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        user = mobj.group('user') or mobj.group('user_2')
        kind = mobj.group('kind') or mobj.group('kind_2')
        display_id = mobj.group('id') or user

        if mobj.group('kind_2') == 'videos':
            data = {'from_user': display_id, 'album': -1, 'limit': 18, 'offset': 0, 'order': 'new2old', 'type': 'video'}
        elif kind in ('music/albums', 'music'):
            data = {'user': user, 'public_only': True, 'individual_limit': {'singles': 1, 'albums': 1, 'playlists': 1}}
        else:
            data = {'url_slug': display_id, 'children': 10, 'order': 'old2new'}

        method = 'getMusicOverview' if kind in ('music/albums', 'music') else 'getElements'
        json_data = self._call_api(method, display_id, data=data)
        if kind in ('music/albums', 'music'):
            json_data = json_data['singles']['list']

        entites = []
        for media in json_data:
            url = media.get('external_media_url') or media.get('link')
            if url.startswith('https://www.youtube.com'):
                entites.append({
                    '_type': 'url',
                    'url': url,
                    'ie_key': 'Youtube'
                })
                continue
            is_audio = (media.get('type') or '').lower() == 'audio'
            entites.append({
                'url': url,
                'id': media['id'],
                'title': media['title'],
                'duration': int_or_none(media.get('duration')),
                'timestamp': int_or_none(media.get('timestamp')),
                'album': try_get(media, lambda x: x['album']['title']),
                'uploader': try_get(media, lambda x: x['creator']['display_name']),
                'uploader_id': try_get(media, lambda x: x['creator']['id']),
                'thumbnail': media.get('image_comment'),
                'like_count': int_or_none(media.get('likes')),
                'vcodec': 'none' if is_audio else None,
                'ext': 'mp3' if is_audio else None,
            })

        if len(entites) > 1:
            return self.playlist_result(entites, display_id)

        return entites[0]


code:
import os.path
import re

from .common import InfoExtractor
from ..compat import compat_urllib_parse_unquote
from ..utils import (
    ExtractorError,
    traverse_obj,
    try_get,
    url_basename,
)

class DropboxIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?dropbox[.]com/sh?/(?P<id>[a-zA-Z0-9]{15})/.*'
    _TESTS = [
        {
            'url': 'https://www.dropbox.com/s/nelirfsxnmcfbfh/youtube-dl%20test%20video%20%27%C3%A4%22BaW_jenozKc.mp4?dl=0',
            'info_dict': {
                'id': 'nelirfsxnmcfbfh',
                'ext': 'mp4',
                'title': 'youtube-dl test video \'ä"BaW_jenozKc'
            }
        }, {
            'url': 'https://www.dropbox.com/sh/662glsejgzoj9sr/AAByil3FGH9KFNZ13e08eSa1a/Pregame%20Ceremony%20Program%20PA%2020140518.m4v',
            'only_matching': True,
        },
    ]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        video_id = mobj.group('id')
        webpage = self._download_webpage(url, video_id)
        fn = compat_urllib_parse_unquote(url_basename(url))
        title = os.path.splitext(fn)[0]

        password = self.get_param('videopassword')
        if (self._og_search_title(webpage) == 'Dropbox - Password Required'
                or 'Enter the password for this link' in webpage):

            if password:
                content_id = self._search_regex(r'content_id=(.*?)["\']', webpage, 'content_id')
                payload = f'is_xhr=true&t={self._get_cookies("https://www.dropbox.com").get("t").value}&content_id={content_id}&password={password}&url={url}'
                response = self._download_json(
                    'https://www.dropbox.com/sm/auth', video_id, 'POSTing video password', data=payload.encode('UTF-8'),
                    headers={'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'})

                if response.get('status') != 'authed':
                    raise ExtractorError('Authentication failed!', expected=True)
                webpage = self._download_webpage(url, video_id)
            elif self._get_cookies('https://dropbox.com').get('sm_auth'):
                webpage = self._download_webpage(url, video_id)
            else:
                raise ExtractorError('Password protected video, use --video-password <password>', expected=True)

        info_json = self._search_json(r'InitReact\.mountComponent\(.*?,', webpage, 'mountComponent', video_id,
                                      contains_pattern=r'.+?"preview".+?', end_pattern=r'\)')['props']
        transcode_url = traverse_obj(info_json, ((None, 'preview'), 'file', 'preview', 'content', 'transcode_url'), get_all=False)
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(transcode_url, video_id)

        # downloads enabled we can get the original file
        if 'anonymous' in (try_get(info_json, lambda x: x['sharePermission']['canDownloadRoles']) or []):
            video_url = re.sub(r'[?&]dl=0', '', url)
            video_url += ('?' if '?' not in video_url else '&') + 'dl=1'
            formats.append({'url': video_url, 'format_id': 'original', 'format_note': 'Original', 'quality': 1})
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': title,
            'formats': formats,
            'subtitles': subtitles
        }