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

Code before:
                    lecture['video']['playListUrl'], display_id, 'mp4', m3u8_id='hls')
                yield {
                    **course_info,
                    **section_info,
                    'id': display_id,  # fallback
                    'display_id': display_id,
                    'formats': formats,
                    'subtitles': subtitles,
                    'series': course_info.get('title'),
                    'series_id': course_id,
                    **traverse_obj(lecture, {
                        'id': ('video', 'guid', {str}),
                        'title': ('title', {str}),
                        'alt_title': ('video', 'title', {str}),
                        'description': ('description', {clean_html}),
                        'episode': ('title', {str}),
                        'episode_number': ('order', {int_or_none}),
                        'duration': ('video', 'duration_in_sec', {int_or_none}),
                        'timestamp': ('video', 'created_at', {parse_iso8601}),
                        'modified_timestamp': ('video', 'updated_at', {parse_iso8601}),
                        'thumbnail': ('video', 'thumbnailUrl', {url_or_none}),
                    }),
                }

    def _real_extract(self, url):
        course_id = self._match_id(url)
        data = self._download_json(
            f'https://api.gamedev.tv/api/courses/my/{course_id}', course_id,
            headers=self._API_HEADERS)['data']

        course_info = traverse_obj(data, {
            'title': ('title', {str}),
            'tags': ('tags', ..., 'name', {str}),
            'categories': ('categories', ..., 'title', {str}),
            'timestamp': ('created_at', {parse_iso8601}),
            'modified_timestamp': ('updated_at', {parse_iso8601}),
            'thumbnail': ('image', {url_or_none}),
        })

        return self.playlist_result(self._entries(data, course_id, course_info), course_id, **course_info)


Review comment (suggestion):
```suggestion
        course_id, lecture_id = self._match_valid_url(url).group('course_id', 'lecture_id')
```

Code after:
                    'password': password,
                    'cart_items': [],
                }).encode())
        except ExtractorError as e:
            if isinstance(e.cause, HTTPError) and e.cause.status == 401:
                raise ExtractorError('Invalid username/password', expected=True)
            raise

        self._API_HEADERS['Authorization'] = f'{response["token_type"]} {response["access_token"]}'

    def _real_initialize(self):
        if not self._API_HEADERS.get('Authorization'):
            self.raise_login_required(
                'This content is only available with purchase', method='password')

    def _entries(self, data, course_id, course_info, selected_lecture):
        for section in traverse_obj(data, ('sections', ..., {dict})):
            section_info = traverse_obj(section, {
                'season_id': ('id', {str_or_none}),
                'season': ('title', {str}),
                'season_number': ('order', {int_or_none}),
            })
            for lecture in traverse_obj(section, ('lectures', lambda _, v: url_or_none(v['video']['playListUrl']))):
                if selected_lecture and str(lecture.get('id')) != selected_lecture:
                    continue
                display_id = join_nonempty(course_id, section_info.get('season_id'), lecture.get('id'))
                formats, subtitles = self._extract_m3u8_formats_and_subtitles(
                    lecture['video']['playListUrl'], display_id, 'mp4', m3u8_id='hls')
                yield {
                    **course_info,
                    **section_info,
                    'id': display_id,  # fallback
                    'display_id': display_id,
                    'formats': formats,
                    'subtitles': subtitles,
                    'series': course_info.get('title'),
                    'series_id': course_id,
                    **traverse_obj(lecture, {
                        'id': ('video', 'guid', {str}),
                        'title': ('title', {str}),
                        'alt_title': ('video', 'title', {str}),
                        'description': ('description', {clean_html}),
                        'episode': ('title', {str}),
                        'episode_number': ('order', {int_or_none}),
                        'duration': ('video', 'duration_in_sec', {int_or_none}),
                        'timestamp': ('video', 'created_at', {parse_iso8601}),
                        'modified_timestamp': ('video', 'updated_at', {parse_iso8601}),
                        'thumbnail': ('video', 'thumbnailUrl', {url_or_none}),
                    }),
                }