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

Code before:
            api_data = self._parse_json(self._html_search_regex(
                'data-api-data="([^"]+)"', webpage,
                'API data', default='{}'), video_id)
        except ExtractorError as e:
            try:
                api_data = self._download_json(
                    'https://www.nicovideo.jp/api/watch/v3/%s?_frontendId=6&_frontendVersion=0&actionTrackId=AAAAAAAAAA_%d' % (video_id, round(time.time() * 1000)), video_id,
                    note='Downloading API JSON', errnote='Unable to fetch data')['data']
            except ExtractorError:
                if not isinstance(e.cause, HTTPError):
                    raise
                webpage = e.cause.response.read().decode('utf-8', 'replace')
                error_msg = self._html_search_regex(
                    r'(?s)<section\s+class="(?:(?:ErrorMessage|WatchExceptionPage-message)\s*)+">(.+?)</section>',
                    webpage, 'error reason', default=None)
                if not error_msg:
                    raise
                raise ExtractorError(re.sub(r'\s+', ' ', error_msg), expected=True)

        club_joined = traverse_obj(api_data, ('channel', 'viewer', 'follow', 'isFollowed', {bool}))
        if club_joined is None:
            fail_msg = self._html_search_regex(
                r'<p[^>]+\bclass="fail-message"[^>]*>(?P<msg>.+)</p>',
                webpage, 'fail message', default=None, group='msg')
            if fail_msg:
                self.raise_login_required(re.sub(r'\s+', ' ', fail_msg), metadata_available=True)
        elif not club_joined:
            self.raise_login_required('This video is for members only', metadata_available=True)

        formats = []

        def get_video_info(*items, get_first=True, **kwargs):
            return traverse_obj(api_data, ('video', *items), get_all=not get_first, **kwargs)

        dmc_data = traverse_obj(api_data, (
            'media', 'delivery', 'movie', {
                'audios': 'audios',
                'videos': 'videos',
                'protocols': ('session', 'protocols'),
            }, {lambda data: data if all(key in data for key in ['audios', 'videos', 'protocols']) else None},
        ))
        if dmc_data:
            for (audio_quality, video_quality, protocol) in itertools.product(
                    dmc_data['audios'], dmc_data['videos'], dmc_data['protocols']):
                fmt = self._extract_format_for_quality(video_id, audio_quality, video_quality, protocol)
                if fmt:
                    formats.append(fmt)

        dms_data = traverse_obj(api_data, ({
            'videos': ('media', 'domand', 'videos', ..., {
                lambda item: item['id'] if item['isAvailable'] else None

Review comment (bug):
```suggestion
                self.raise_login_required(clean_html(fail_msg), metadata_available=True)
```

Code after:
    def _yield_dms_formats(self, api_data, video_id):
        fmt_filter = lambda _, v: v['isAvailable'] and v['id']
        videos = traverse_obj(api_data, ('media', 'domand', 'videos', fmt_filter))
        audios = traverse_obj(api_data, ('media', 'domand', 'audios', fmt_filter))
        access_key = traverse_obj(api_data, ('media', 'domand', 'accessRightKey', {str}))
        track_id = traverse_obj(api_data, ('client', 'watchTrackId', {str}))
        if not all((videos, audios, access_key, track_id)):
            return

        dms_m3u8_url = self._download_json(
            f'https://nvapi.nicovideo.jp/v1/watch/{video_id}/access-rights/hls', video_id,
            data=json.dumps({
                'outputs': list(itertools.product((v['id'] for v in videos), (a['id'] for a in audios)))
            }).encode(), query={'actionTrackId': track_id}, headers={
                'x-access-right-key': access_key,
                'x-frontend-id': 6,
                'x-frontend-version': 0,
                'x-request-with': 'https://www.nicovideo.jp',
            })['data']['contentUrl']
        # Getting all audio formats results in duplicate video formats which we filter out later
        dms_fmts = self._extract_m3u8_formats(dms_m3u8_url, video_id)

        # m3u8 extraction does not provide audio bitrates, so extract from the API data and fix
        for audio_fmt in traverse_obj(dms_fmts, lambda _, v: v['vcodec'] == 'none'):
            yield {
                **audio_fmt,
                **traverse_obj(audios, (lambda _, v: audio_fmt['format_id'].startswith(v['id']), {
                    'format_id': ('id', {str}),
                    'abr': ('bitRate', {functools.partial(float_or_none, scale=1000)}),
                    'asr': ('samplingRate', {int_or_none}),
                }), get_all=False),
                'acodec': 'aac',
                'ext': 'm4a',
            }

        # Sort before removing dupes to keep the format dicts with the lowest tbr
        video_fmts = sorted((fmt for fmt in dms_fmts if fmt['vcodec'] != 'none'), key=lambda f: f['tbr'])
        self._remove_duplicate_formats(video_fmts)
        # Calculate the true vbr/tbr by subtracting the lowest abr
        min_abr = min(traverse_obj(audios, (..., 'bitRate', {float_or_none})), default=0) / 1000
        for video_fmt in video_fmts:
            video_fmt['tbr'] -= min_abr
            video_fmt['format_id'] = f'video-{video_fmt["tbr"]:.0f}'
            yield video_fmt

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

        try:
            webpage, handle = self._download_webpage_handle(
                'https://www.nicovideo.jp/watch/' + video_id, video_id)