3 from .common
import InfoExtractor
4 from ..networking
.exceptions
import HTTPError
14 from ..utils
.traversal
import traverse_obj
17 class GameDevTVDashboardIE(InfoExtractor
):
18 _VALID_URL
= r
'https?://(?:www\.)?gamedev\.tv/dashboard/courses/(?P<course_id>\d+)(?:/(?P<lecture_id>\d+))?'
19 _NETRC_MACHINE
= 'gamedevtv'
21 'url': 'https://www.gamedev.tv/dashboard/courses/25',
24 'title': 'Complete Blender Creator 3: Learn 3D Modelling for Beginners',
25 'tags': ['blender', 'course', 'all', 'box modelling', 'sculpting'],
26 'categories': ['Blender', '3D Art'],
27 'thumbnail': 'https://gamedev-files.b-cdn.net/courses/qisc9pmu1jdc.jpg',
28 'upload_date': '20220516',
29 'timestamp': 1652694420,
30 'modified_date': '20241027',
31 'modified_timestamp': 1730049658,
33 'playlist_count': 100,
35 'url': 'https://www.gamedev.tv/dashboard/courses/63/2279',
37 'id': 'df04f4d8-68a4-4756-a71b-9ca9446c3a01',
39 'modified_timestamp': 1701695752,
40 'upload_date': '20230504',
41 'episode': 'MagicaVoxel Community Course Introduction',
43 'title': 'MagicaVoxel Community Course Introduction',
44 'timestamp': 1683195397,
45 'modified_date': '20231204',
46 'categories': ['3D Art', 'MagicaVoxel'],
47 'season': 'MagicaVoxel Community Course',
48 'tags': ['MagicaVoxel', 'all', 'course'],
49 'series': 'MagicaVoxel 3D Art Mini Course',
54 'description': 'md5:a378738c5bbec1c785d76c067652d650',
55 'display_id': '63-219-2279',
56 'alt_title': '1_CC_MVX MagicaVoxel Community Course Introduction.mp4',
57 'thumbnail': 'https://vz-23691c65-6fa.b-cdn.net/df04f4d8-68a4-4756-a71b-9ca9446c3a01/thumbnail.jpg',
62 def _perform_login(self
, username
, password
):
64 response
= self
._download
_json
(
65 'https://api.gamedev.tv/api/students/login', None, 'Logging in',
66 headers
={'Content-Type': 'application/json'},
72 except ExtractorError
as e
:
73 if isinstance(e
.cause
, HTTPError
) and e
.cause
.status
== 401:
74 raise ExtractorError('Invalid username/password', expected
=True)
77 self
._API
_HEADERS
['Authorization'] = f
'{response["token_type"]} {response["access_token"]}'
79 def _real_initialize(self
):
80 if not self
._API
_HEADERS
.get('Authorization'):
81 self
.raise_login_required(
82 'This content is only available with purchase', method
='password')
84 def _entries(self
, data
, course_id
, course_info
, selected_lecture
):
85 for section
in traverse_obj(data
, ('sections', ..., {dict}
)):
86 section_info
= traverse_obj(section
, {
87 'season_id': ('id', {str_or_none}
),
88 'season': ('title', {str}
),
89 'season_number': ('order', {int_or_none}
),
91 for lecture
in traverse_obj(section
, ('lectures', lambda _
, v
: url_or_none(v
['video']['playListUrl']))):
92 if selected_lecture
and str(lecture
.get('id')) != selected_lecture
:
94 display_id
= join_nonempty(course_id
, section_info
.get('season_id'), lecture
.get('id'))
95 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(
96 lecture
['video']['playListUrl'], display_id
, 'mp4', m3u8_id
='hls')
100 'id': display_id
, # fallback
101 'display_id': display_id
,
103 'subtitles': subtitles
,
104 'series': course_info
.get('title'),
105 'series_id': course_id
,
106 **traverse_obj(lecture
, {
107 'id': ('video', 'guid', {str}
),
108 'title': ('title', {str}
),
109 'alt_title': ('video', 'title', {str}
),
110 'description': ('description', {clean_html}
),
111 'episode': ('title', {str}
),
112 'episode_number': ('order', {int_or_none}
),
113 'duration': ('video', 'duration_in_sec', {int_or_none}
),
114 'timestamp': ('video', 'created_at', {parse_iso8601}
),
115 'modified_timestamp': ('video', 'updated_at', {parse_iso8601}
),
116 'thumbnail': ('video', 'thumbnailUrl', {url_or_none}
),
120 def _real_extract(self
, url
):
121 course_id
, lecture_id
= self
._match
_valid
_url
(url
).group('course_id', 'lecture_id')
122 data
= self
._download
_json
(
123 f
'https://api.gamedev.tv/api/courses/my/{course_id}', course_id
,
124 headers
=self
._API
_HEADERS
)['data']
126 course_info
= traverse_obj(data
, {
127 'title': ('title', {str}
),
128 'tags': ('tags', ..., 'name', {str}
),
129 'categories': ('categories', ..., 'title', {str}
),
130 'timestamp': ('created_at', {parse_iso8601}
),
131 'modified_timestamp': ('updated_at', {parse_iso8601}
),
132 'thumbnail': ('image', {url_or_none}
),
135 entries
= self
._entries
(data
, course_id
, course_info
, lecture_id
)
137 lecture
= next(entries
, None)
139 raise ExtractorError('Lecture not found')
141 return self
.playlist_result(entries
, course_id
, **course_info
)