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

Code before:
        'url': 'http://funhaus.roosterteeth.com/episode/funhaus-shorts-2016-austin-sucks-funhaus-shorts',
        'only_matching': True,
    }, {
        'url': 'http://screwattack.roosterteeth.com/episode/death-battle-season-3-mewtwo-vs-shadow',
        'only_matching': True,
    }, {
        'url': 'http://theknow.roosterteeth.com/episode/the-know-game-news-season-1-boring-steam-sales-are-better',
        'only_matching': True,
    }, {
        # only available for FIRST members
        'url': 'http://roosterteeth.com/episode/rt-docs-the-world-s-greatest-head-massage-the-world-s-greatest-head-massage-an-asmr-journey-part-one',
        'only_matching': True,
    }, {
        'url': 'https://roosterteeth.com/watch/million-dollars-but-season-2-million-dollars-but-the-game-announcement',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        display_id = self._match_id(url)
        api_episode_url = f'{self._API_BASE_URL}/watch/{display_id}'

        try:
            video_data = self._download_json(
                api_episode_url + '/videos', display_id, 'Downloading video JSON metadata',
                headers={'Client-Type': 'web'})['data'][0]
            m3u8_url = traverse_obj(video_data, ('links', 'download')) or traverse_obj(video_data, ('attributes', 'url'))
        except ExtractorError as e:
            if isinstance(e.cause, HTTPError) and e.cause.status == 403:
                if self._parse_json(e.cause.response.read().decode(), display_id).get('access') is False:
                    self.raise_login_required(
                        '%s is only available for FIRST members' % display_id)
            raise

        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
            m3u8_url, display_id, 'mp4', 'm3u8_native', m3u8_id='hls')

        episode = self._download_json(
            api_episode_url, display_id,
            'Downloading episode JSON metadata')['data'][0]

        return {
            'display_id': display_id,
            'formats': formats,
            'subtitles': subtitles,
            **self._extract_video_info(episode)
        }


class RoosterTeethSeriesIE(RoosterTeethBaseIE):
    _VALID_URL = r'https?://(?:.+?\.)?roosterteeth\.com/series/(?P<id>[^/?#&]+)'
    _TESTS = [{

Review comment (suggestion):
`video_data['links']['download']` URLs were giving 403 errors
```suggestion
                headers={'Client-Type': 'web'})['data'][0]  # web client-type yields ad-free streams
            m3u8_url = video_data['attributes']['url']
            # XXX: additional ad-free URL at video_data['links']['download'] but often gives 403 errors
```

Code after:
        'url': 'http://funhaus.roosterteeth.com/episode/funhaus-shorts-2016-austin-sucks-funhaus-shorts',
        'only_matching': True,
    }, {
        'url': 'http://screwattack.roosterteeth.com/episode/death-battle-season-3-mewtwo-vs-shadow',
        'only_matching': True,
    }, {
        'url': 'http://theknow.roosterteeth.com/episode/the-know-game-news-season-1-boring-steam-sales-are-better',
        'only_matching': True,
    }, {
        # only available for FIRST members
        'url': 'http://roosterteeth.com/episode/rt-docs-the-world-s-greatest-head-massage-the-world-s-greatest-head-massage-an-asmr-journey-part-one',
        'only_matching': True,
    }, {
        'url': 'https://roosterteeth.com/watch/million-dollars-but-season-2-million-dollars-but-the-game-announcement',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        display_id = self._match_id(url)
        api_episode_url = f'{self._API_BASE_URL}/watch/{display_id}'

        try:
            video_data = self._download_json(
                api_episode_url + '/videos', display_id, 'Downloading video JSON metadata',
                headers={'Client-Type': 'web'})['data'][0]  # web client-type yields ad-free streams
            m3u8_url = video_data['attributes']['url']
            # XXX: additional ad-free URL at video_data['links']['download'] but often gives 403 errors
        except ExtractorError as e:
            if isinstance(e.cause, HTTPError) and e.cause.status == 403:
                if self._parse_json(e.cause.response.read().decode(), display_id).get('access') is False:
                    self.raise_login_required(
                        '%s is only available for FIRST members' % display_id)
            raise

        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
            m3u8_url, display_id, 'mp4', 'm3u8_native', m3u8_id='hls')

        episode = self._download_json(
            api_episode_url, display_id,
            'Downloading episode JSON metadata')['data'][0]

        return {
            'display_id': display_id,
            'formats': formats,
            'subtitles': subtitles,
            **self._extract_video_info(episode)
        }


class RoosterTeethSeriesIE(RoosterTeethBaseIE):
    _VALID_URL = r'https?://(?:.+?\.)?roosterteeth\.com/series/(?P<id>[^/?#&]+)'