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

Code before:
            if not next_url:
                break

    def _real_extract(self, url):
        collection_slug = self._match_id(url)
        channel = self._call_api(
            f'https://content.api.nebula.app/content/{collection_slug}/?include=lessons',
            collection_slug, note='Retrieving channel')

        if channel.get('type') == 'class':
            entries = self._generate_class_entries(channel)
        elif channel.get('type') == 'podcast_channel':
            entries = self._generate_podcast_entries(channel['id'], collection_slug)
        else:
            entries = self._generate_playlist_entries(channel['id'], collection_slug)

        return self.playlist_result(
            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',
            '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,
    }, {
        'url': 'https://nebula.tv/jetlag/season/13-5',
        'info_dict': {
            'id': 'jetlag_13-5',
            'title': 'Hide + Seek Across NYC',
            'description': 'md5:5b87bb9acc6dcdff289bb4c71a2ad59f',
        },
        'playlist_count': 3,
    }]

Review comment (suggestion):
Expand regex in case they ever decide to add letters
```suggestion
    _VALID_URL = rf'{_BASE_URL_RE}/(?P<series>[\w-]+)/season/(?P<season_number>[\w-]+)'
```

Code after:
            if not next_url:
                break

    def _real_extract(self, url):
        collection_slug = self._match_id(url)
        channel = self._call_api(
            f'https://content.api.nebula.app/content/{collection_slug}/?include=lessons',
            collection_slug, note='Retrieving channel')

        if channel.get('type') == 'class':
            entries = self._generate_class_entries(channel)
        elif channel.get('type') == 'podcast_channel':
            entries = self._generate_podcast_entries(channel['id'], collection_slug)
        else:
            entries = self._generate_playlist_entries(channel['id'], collection_slug)

        return self.playlist_result(
            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>[\w-]+)'
    _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,
    }, {
        'url': 'https://nebula.tv/jetlag/season/13-5',
        'info_dict': {
            'id': 'jetlag_13-5',
            'title': 'Hide + Seek Across NYC',
            'description': 'md5:5b87bb9acc6dcdff289bb4c71a2ad59f',
        },
        'playlist_count': 3,
    }]