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

Code before:
        'info_dict': {
            'id': 'jetlag_15',
        },
        'playlist_count': 6,
    }, {
        'url': 'https://nebula.tv/jetlag/season/14',
        'info_dict': {
            'id': 'jetlag_14',
        },
        'playlist_mincount': 6,
    }, {
        'url': 'https://nebula.tv/jetlag/season/10',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        season_name, season_id = self._match_valid_url(url).group('sn', 'sid')
        video_id = f'{season_name}_{season_id}'
        data = self._call_api(f'https://content.api.nebula.app/content/{season_name}/season/{season_id}', video_id)
        if not data.get('episodes'):
            raise ExtractorError('No Episodes Found')
        return self.playlist_result((
            self.url_result(f'https://nebula.tv/{traverse_obj(ep, ("video", "app_path"))}')
            for ep in traverse_obj(data, ('episodes')) if ep.get('video')),
            video_id, data.get('video_channel_slug'), data.get('short_description'),
        )


Review comment (refactor):
For the playlist entries:
- we should use the base `_extract_video_metadata` method and return `url_transparent` results
- we should mimic the other playlist extractors' strategy of using `smuggle_data` to smuggle the `id` in the URL. If we do this along with returning the metadata in a `url_transparent` result, then the video extractor doesn't need to make another API call
- we should also extract the `extras` and `trailers`, so let's write an `_entries()` method that yields everything from the season API response

Code after:
        '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, {
                'title': ('title', {str}),
                'description': ('description', {str}),
            }))