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

Code before:
        if not path.startswith(f'/content/v2/{self._API_ENDPOINT}/'):
            path = f'/content/v2/{self._API_ENDPOINT}/{path}'

        try:
            result = self._call_base_api(
                path, internal_id, lang, f'Downloading {note} JSON ({self._API_ENDPOINT})', query=query)
        except ExtractorError as error:
            if isinstance(error.cause, HTTPError) and error.cause.status == 404:
                return None
            raise

        if not result:
            raise ExtractorError(f'Unexpected response when downloading {note} JSON')
        return result

    def _extract_stream(self, internal_id, is_music):
        if is_music:
            object_type = 'music/'
        else:
            object_type = ''

        return self._download_json(
            f'https://cr-play-service.prd.crunchyrollsvc.com/v1/{object_type}{internal_id}/console/switch/play',
            internal_id, note='stream info', headers=CrunchyrollBaseIE._AUTH_HEADERS)

    def _extract_formats(self, stream_response, display_id=None):
        available_formats = {'': ('', '', stream_response['url'])}
        for hardsub_lang, stream in traverse_obj(stream_response, ('hardSubs', {dict.items}, ...)):
            available_formats[hardsub_lang] = (f'hardsub-{hardsub_lang}', hardsub_lang, stream['url'])

        requested_hardsubs = [('' if val == 'none' else val) for val in (self._configuration_arg('hardsub') or ['none'])]

        audio_locale = stream_response.get('audioLocale')
        hardsub_preference = qualities(requested_hardsubs[::-1])
        formats = []
        for format_id, hardsub_lang, stream_url in available_formats.values():
            adaptive_formats = self._extract_mpd_formats(
                stream_url, display_id, mpd_id=format_id, headers=CrunchyrollBaseIE._AUTH_HEADERS,
                fatal=False, note=f'Downloading {f"hardsub {hardsub_lang} " if hardsub_lang else ""}MPD manifest')
            for f in adaptive_formats:
                if f.get('acodec') != 'none':
                    f['language'] = audio_locale
                f['quality'] = hardsub_preference(hardsub_lang.lower())
            formats.extend(adaptive_formats)

        return formats

    def _extract_subtitles(self, data):
        subtitles = {}

        for locale, subtitle in traverse_obj(data, ((None, 'meta'), 'subtitles', {dict.items}, ...)):

Review comment (security):
This function should be merged with `_extract_formats`. Remove the `is_music` parameter and just download the json first part in the function:
```suggestion
    def _extract_formats(self, identifier, display_id=None):
        if not display_id:
            display_id = identifier
        stream_response = self._download_json(
            f'https://cr-play-service.prd.crunchyrollsvc.com/v1/{identifier}/console/switch/play',
            display_id, note='stream info', headers=CrunchyrollBaseIE._AUTH_HEADERS)
```
You can then call it directly:
```py
# regular
result['formats'] = self._extract_stream(internal_id)
# music
result['formats'] = self._extract_stream(f'music/{internal_id}', internal_id)
```

Code after:
        try:
            result = self._call_base_api(
                path, internal_id, lang, f'Downloading {note} JSON ({self._API_ENDPOINT})', query=query)
        except ExtractorError as error:
            if isinstance(error.cause, HTTPError) and error.cause.status == 404:
                return None
            raise

        if not result:
            raise ExtractorError(f'Unexpected response when downloading {note} JSON')
        return result

    def _extract_chapters(self, internal_id):
        # if no skip events are available, a 403 xml error is returned
        skip_events = self._download_json(
            f'https://static.crunchyroll.com/skip-events/production/{internal_id}.json',
            internal_id, note='Downloading chapter info', fatal=False, errnote=False)
        if not skip_events:
            return None

        chapters = []
        for event in ('recap', 'intro', 'credits', 'preview'):
            start = traverse_obj(skip_events, (event, 'start', {float_or_none}))
            end = traverse_obj(skip_events, (event, 'end', {float_or_none}))
            # some chapters have no start and/or ending time, they will just be ignored
            if start is None or end is None:
                continue
            chapters.append({'title': event.capitalize(), 'start_time': start, 'end_time': end})

        return chapters

    def _extract_stream(self, identifier, display_id=None):
        if not display_id:
            display_id = identifier

        self._update_auth()
        stream_response = self._download_json(
            f'https://cr-play-service.prd.crunchyrollsvc.com/v1/{identifier}/console/switch/play',
            display_id, note='Downloading stream info', headers=CrunchyrollBaseIE._AUTH_HEADERS)

        available_formats = {'': ('', '', stream_response['url'])}
        for hardsub_lang, stream in traverse_obj(stream_response, ('hardSubs', {dict.items}, lambda _, v: v[1]['url'])):
            available_formats[hardsub_lang] = (f'hardsub-{hardsub_lang}', hardsub_lang, stream['url'])

        requested_hardsubs = [('' if val == 'none' else val) for val in (self._configuration_arg('hardsub') or ['none'])]
        hardsub_langs = [lang for lang in available_formats if lang]
        if hardsub_langs and 'all' not in requested_hardsubs:
            full_format_langs = set(requested_hardsubs)
            self.to_screen(f'Available hardsub languages: {", ".join(hardsub_langs)}')
            self.to_screen(
                'To extract formats of a hardsub language, use '