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

Code before:
            entries=entries,
            playlist_id=collection_slug,
            playlist_title=channel.get('title'),
            playlist_description=channel.get('description'))


class NebulaSeasonIE(NebulaBaseIE):
    IE_NAME = 'nebula:season'
    _VALID_URL = rf'{_BASE_URL_RE}/(?P<series>[\w-]+)/?season/(?P<season_number>\d+)'
    _TESTS = [{
        'url': 'https://nebula.tv/jetlag/season/15',
        'info_dict': {
            'id': 'jetlag_15',
        },
        'playlist_count': 8,
    }, {
        'url': 'https://nebula.tv/jetlag/season/14',
        'info_dict': {
            'id': 'jetlag_14',
        },
        'playlist_mincount': 8,
    }]

    def _entries(self, data):
        base_url = 'https://nebula.tv/videos'
        for episode in traverse_obj(data, ('episodes')):
            episode_id = traverse_obj(episode, ('video', 'id'))
            metadata = self._extract_video_metadata(episode)
            yield self.url_result(smuggle_url(f'{base_url}/{episode_id}', {'id': episode_id}),
                                  NebulaIE, episode_id, url_transparent=True, **metadata)
        for extra in traverse_obj(data, ('extras')):
            extra_id = traverse_obj(extra, ('items', ..., 'id'))[0]
            metadata = self._extract_video_metadata(traverse_obj(extra, ('items', 0)))
            yield self.url_result(smuggle_url(f'{base_url}/{extra_id}', {'id': extra_id}),
                                  NebulaIE, extra_id, url_transparent=True, **metadata)
        for trailer in traverse_obj(data, ('trailers')):
            trailer_id = trailer.get('id')
            metadata = self._extract_video_metadata(trailer)
            yield self.url_result(smuggle_url(f'{base_url}/{trailer_id}', {'id': trailer_id}),
                                  NebulaIE, trailer_id, url_transparent=True, **metadata)

    def _real_extract(self, url):
        season_name, season_id = self._match_valid_url(url).group('series', 'season_number')
        playlist_id = f'{season_name}_{season_id}'
        data = self._call_api(f'https://content.api.nebula.app/content/{season_name}/season/{season_id}', playlist_id)
        if not traverse_obj(data, ('episodes'), ('extras'), ('trailers')):
            traverse_obj(data, ('episodes'), ('extras'), ('trailers'))
            raise ExtractorError('No Episodes, Outtakes, Trailes Found.')
        return self.playlist_result(self._entries(data), playlist_id, data.get('video_channel_slug'), data.get('short_description'))


Review comment (refactor):
One of the main reasons to use `traverse_obj` when iterating over data from the web is that you can *branch* to guarantee that `traverse_obj` will return a list result (so that you don't throw a `TypeError`)

You can branch in a traversal path by using `...` (to traverse all branches), or by using a tuple of multiple keys (e.g. `'start', ('path_a', 'path_b'), 'end'`), or by using a filter (e.g. `lambda _, v: v['id']` to traverse all objects that have an `id` key with a truthy value).

When you just use `traverse_obj(data, 'episodes')`, if `episodes` is not a list or if `data` does not have an `episodes` key, then the extractor will throw a `TypeError`. (BTW, the parentheses around `('episodes')` are not doing anything)

So let's do this to filter for objects that contain an `id` key with a truthy value:
```suggestion
        for episode in traverse_obj(data, ('episodes', lambda _, v: v['id'])):
```

Code after:
            playlist_description=channel.get('description'))


class NebulaSeasonIE(NebulaBaseIE):
    IE_NAME = 'nebula:season'
    _VALID_URL = rf'{_BASE_URL_RE}/(?P<series>[\w-]+)/season/(?P<season_number>\d+)'
    _TESTS = [{
        'url': 'https://nebula.tv/jetlag/season/15',
        'info_dict': {
            'id': 'jetlag_15',
            'title': 'Tag: All Stars',
            'description': 'md5:5aa5b8abf3de71756448dc44ffebb674',
        },
        'playlist_count': 8,
    }, {
        'url': 'https://nebula.tv/jetlag/season/14',
        'info_dict': {
            'id': 'jetlag_14',
            'title': 'Snake',
            'description': 'md5:6da9040f1c2ac559579738bfb6919d1e',
        },
        'playlist_count': 8,
    }]

    def _build_url_result(self, item):
        url = (
            traverse_obj(item, ('share_url', {url_or_none}))
            or urljoin('https://nebula.tv/', item.get('app_path'))
            or f'https://nebula.tv/videos/{item["slug"]}')
        return self.url_result(
            smuggle_url(url, {'id': item['id']}),
            NebulaIE, url_transparent=True,
            **self._extract_video_metadata(item))

    def _entries(self, data):
        for episode in traverse_obj(data, ('episodes', lambda _, v: v['video']['id'], 'video')):
            yield self._build_url_result(episode)
        for extra in traverse_obj(data, ('extras', ..., 'items', lambda _, v: v['id'])):
            yield self._build_url_result(extra)
        for trailer in traverse_obj(data, ('trailers', lambda _, v: v['id'])):
            yield self._build_url_result(trailer)

    def _real_extract(self, url):
        series, season_id = self._match_valid_url(url).group('series', 'season_number')
        playlist_id = f'{series}_{season_id}'
        data = self._call_api(
            f'https://content.api.nebula.app/content/{series}/season/{season_id}', playlist_id)

        return self.playlist_result(
            self._entries(data), playlist_id,
            **traverse_obj(data, {