Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/ard.py (Python)

Code before:
            # 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+)?(?:\:| -|) ).*',
            # As a fallback use the full title
            r'(?P<title>.*)',
        ]

        return traverse_obj(patterns, (..., {partial(re.match, string=title)}, {
            'season_number': ('season_number', {int_or_none}),
            'episode_number': ('episode_number', {int_or_none}),
            'episode': ((
                ('episode', {str_or_none}),
                ('ep_info', {lambda x: title.replace(x, '')}),
                ('title', {str}),
            ), {str.strip}),
        }), get_all=False)

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

        token = self._download_json('https://sso.ardmediathek.de/sso/token', display_id)

        id_token = traverse_obj(token, 'idToken')

        if id_token:
            jwt_token_decoded = jwt_decode_hs256(id_token)
            user_id = traverse_obj(jwt_token_decoded, 'user_id', 'sub')

            page_data = self._download_json(
                f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}', display_id, query={
                    'embedded': 'false',
                    'mcV6': 'true',
                    'userId': user_id
                }, headers= {
                    'x-authorization': f'Bearer {id_token}'
                })
        else:
            page_data = self._download_json(
                f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}', display_id, query={
                    'embedded': 'false',
                    'mcV6': 'true'
                })


        # For user convenience we use the old contentId instead of the longer crid
        # Ref: https://github.com/yt-dlp/yt-dlp/issues/8731#issuecomment-1874398283

Review comment (bug):
when the cookie is not set, this fails, so I need a parameter here to ignore failures

ERROR: [ARDMediathek] Y3JpZDovL2Rhc2Vyc3RlLmRlL29kZXJicnVjaC8yMDI0LTAxLTIwXzAwLTAwLU1FWg: Unable to download JSON metadata: HTTP Error 400: Bad Request (caused by <HTTPError 400: Bad Request>)

Code after:
            # 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+)?(?:\:| -|) ).*',
            # As a fallback use the full title
            r'(?P<title>.*)',
        ]

        return traverse_obj(patterns, (..., {partial(re.match, string=title)}, {
            'season_number': ('season_number', {int_or_none}),
            'episode_number': ('episode_number', {int_or_none}),
            'episode': ((
                ('episode', {str_or_none}),
                ('ep_info', {lambda x: title.replace(x, '')}),
                ('title', {str}),
            ), {str.strip}),
        }), get_all=False)

    def _real_extract(self, url):
        display_id = self._match_id(url)
        query = {'embedded': 'false', 'mcV6': 'true'}
        headers = {}

        if self._get_cookies(self._TOKEN_URL).get('ams'):
            token = self._download_json(
                self._TOKEN_URL, display_id, 'Fetching token for age verification',
                'Unable to fetch age verification token', fatal=False)
            id_token = traverse_obj(token, ('idToken', {str}))
            decoded_token = traverse_obj(id_token, ({jwt_decode_hs256}, {dict}))
            user_id = traverse_obj(decoded_token, (('user_id', 'sub'), {str}), get_all=False)
            if not user_id:
                self.report_warning('Unable to extract token, continuing without authentication')
            else:
                headers['x-authorization'] = f'Bearer {id_token}'
                query['userId'] = user_id
                if decoded_token.get('age_rating') != 18:
                    self.report_warning('Account is not verified as 18+; video may be unavailable')

        page_data = self._download_json(
            f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}',
            display_id, query=query, headers=headers)

        # For user convenience we use the old contentId instead of the longer crid
        # Ref: https://github.com/yt-dlp/yt-dlp/issues/8731#issuecomment-1874398283
        old_id = traverse_obj(page_data, ('tracking', 'atiCustomVars', 'contentId', {int}))
        if old_id is not None: