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

Code before:
import json

from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
    ExtractorError,
    clean_html,
    int_or_none,
    join_nonempty,
    parse_iso8601,
    str_or_none,
    url_or_none,
)
from ..utils.traversal import traverse_obj


class GameDevTVDashboardIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?gamedev\.tv/dashboard/courses/(?P<id>\d+)'
    _NETRC_MACHINE = 'gamedevtv'
    _TESTS = [{
        'url': 'https://www.gamedev.tv/dashboard/courses/25',
        'info_dict': {
            'id': '25',
            'title': 'Complete Blender Creator 3: Learn 3D Modelling for Beginners',
            'tags': ['blender', 'course', 'all', 'box modelling', 'sculpting'],
            'categories': ['Blender', '3D Art'],
            'thumbnail': 'https://gamedev-files.b-cdn.net/courses/qisc9pmu1jdc.jpg',
            'upload_date': '20220516',
            'timestamp': 1652694420,
            'modified_date': '20241027',
            'modified_timestamp': 1730049658,
        },
        'playlist_count': 100,
    }]
    _API_HEADERS = {}

    def _perform_login(self, username, password):
        try:
            response = self._download_json(
                'https://api.gamedev.tv/api/students/login', None, 'Logging in',
                headers={'Content-Type': 'application/json'},
                data=json.dumps({
                    'email': username,

Review comment (refactor):
Imo. the extractor should support individual videos as well.
Given that only the course api request is needed, I think a single extractor is fine. Just add an optional sub_id to the regex and, if present, only return a single video.

Code after:
import json

from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
    ExtractorError,
    clean_html,
    int_or_none,
    join_nonempty,
    parse_iso8601,
    str_or_none,
    url_or_none,
)
from ..utils.traversal import traverse_obj


class GameDevTVDashboardIE(InfoExtractor):
    _VALID_URL = r'https?://(?:www\.)?gamedev\.tv/dashboard/courses/(?P<course_id>\d+)(?:/(?P<lecture_id>\d+))?'
    _NETRC_MACHINE = 'gamedevtv'
    _TESTS = [{
        'url': 'https://www.gamedev.tv/dashboard/courses/25',
        'info_dict': {
            'id': '25',
            'title': 'Complete Blender Creator 3: Learn 3D Modelling for Beginners',
            'tags': ['blender', 'course', 'all', 'box modelling', 'sculpting'],
            'categories': ['Blender', '3D Art'],
            'thumbnail': 'https://gamedev-files.b-cdn.net/courses/qisc9pmu1jdc.jpg',
            'upload_date': '20220516',
            'timestamp': 1652694420,
            'modified_date': '20241027',
            'modified_timestamp': 1730049658,
        },
        'playlist_count': 100,
    }, {
        'url': 'https://www.gamedev.tv/dashboard/courses/63/2279',
        'info_dict': {
            'id': 'df04f4d8-68a4-4756-a71b-9ca9446c3a01',
            'ext': 'mp4',
            'modified_timestamp': 1701695752,
            'upload_date': '20230504',
            'episode': 'MagicaVoxel Community Course Introduction',
            'series_id': '63',
            'title': 'MagicaVoxel Community Course Introduction',