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

Code before:
        """
        Get a Bearer token for the Nebula API. This will be required to fetch video meta data.
        """
        response = self._call_nebula_api('https://api.watchnebula.com/api/v1/authorization/',
                                         method='POST',
                                         note='Authorizing to Nebula')
        return response['token']

    def _fetch_video_formats(self, slug):
        stream_info = self._call_nebula_api(f'https://content.api.nebula.app/video/{slug}/stream/',
                                            video_id=slug,
                                            auth_type='bearer',
                                            note='Fetching video stream info')
        manifest_url = stream_info['manifest']
        return self._extract_m3u8_formats_and_subtitles(manifest_url, slug)

    def _build_video_info(self, episode):
        fmts, subs = self._fetch_video_formats(episode['slug'])
        channel_slug = episode['channel_slug']
        channel_title = episode['channel_title']
        if episode['zype_id']:
            zype_id = episode['zype_id']
        else:
            zype_id = None
        return {
            'id': episode['id'].replace('video_episode:', ''),
            'display_id': episode['slug'],
            'formats': fmts,
            'subtitles': subs,
            'webpage_url': f'https://nebula.tv/{episode["slug"]}',
            'title': episode['title'],
            'description': episode['description'],
            'timestamp': parse_iso8601(episode['published_at']),
            'thumbnails': [{
                # 'id': tn.get('name'),  # this appears to be null
                'url': tn['original'],
                'height': key,
            } for key, tn in episode['assets']['thumbnail'].items()],
            'duration': episode['duration'],
            'channel': channel_title,
            'channel_id': channel_slug,
            'channel_url': f'https://nebula.tv/{channel_slug}',
            'uploader': channel_title,
            'uploader_id': channel_slug,
            'uploader_url': f'https://nebula.tv/{channel_slug}',
            'series': channel_title,
            'creator': channel_title,
            '_old_archive_ids': [make_archive_id(self, zype_id)] if zype_id else None,
        }

    def _perform_login(self, username=None, password=None):

Review comment (bug):
This isn't absolutely necessary, but it's more precise (remove from the start) and would give us a more descriptive error if something goes wrong (`Extractor failed to obtain "id"` instead of `NoneType has no attr replace`)
```suggestion
            'id': remove_start(episode['id'], 'video_episode:'),
```

Code after:
                                         method='POST',
                                         note='Authorizing to Nebula')
        return response['token']

    def _fetch_video_formats(self, slug):
        stream_info = self._call_nebula_api(f'https://content.api.nebula.app/video/{slug}/stream/',
                                            video_id=slug,
                                            auth_type='bearer',
                                            note='Fetching video stream info')
        manifest_url = stream_info['manifest']
        return self._extract_m3u8_formats_and_subtitles(manifest_url, slug, 'mp4')

    def _build_video_info(self, episode):
        fmts, subs = self._fetch_video_formats(episode['slug'])
        channel_slug = episode['channel_slug']
        channel_title = episode['channel_title']
        zype_id = episode.get('zype_id')
        return {
            'id': remove_start(episode['id'], 'video_episode:'),
            'display_id': episode['slug'],
            'formats': fmts,
            'subtitles': subs,
            'webpage_url': f'https://nebula.tv/{episode["slug"]}',
            'title': episode['title'],
            'description': episode['description'],
            'timestamp': parse_iso8601(episode['published_at']),
            'thumbnails': [{
                # 'id': tn.get('name'),  # this appears to be null
                'url': tn['original'],
                'height': key,
            } for key, tn in episode['assets']['thumbnail'].items()],
            'duration': episode['duration'],
            'channel': channel_title,
            'channel_id': channel_slug,
            'channel_url': f'https://nebula.tv/{channel_slug}',
            'uploader': channel_title,
            'uploader_id': channel_slug,
            'uploader_url': f'https://nebula.tv/{channel_slug}',
            'series': channel_title,
            'creator': channel_title,
            'extractor_key': NebulaIE.ie_key(),
            'extractor': NebulaIE.IE_NAME,
            '_old_archive_ids': [make_archive_id(NebulaIE, zype_id)] if zype_id else None,
        }

    def _perform_login(self, username=None, password=None):
        self._nebula_api_token = self._perform_nebula_auth(username, password)
        self._nebula_bearer_token = self._fetch_nebula_bearer_token()


class NebulaIE(NebulaBaseIE):