]>
Commit | Line | Data |
---|---|---|
7fc60f4e S |
1 | from __future__ import unicode_literals |
2 | ||
3 | import re | |
4 | ||
5 | from .common import InfoExtractor | |
6 | from .wistia import WistiaIE | |
7 | from ..compat import compat_str | |
8 | from ..utils import ( | |
9 | clean_html, | |
10 | ExtractorError, | |
11 | get_element_by_class, | |
12 | urlencode_postdata, | |
13 | urljoin, | |
14 | ) | |
15 | ||
16 | ||
5ee7ae5c S |
17 | class TeachableBaseIE(InfoExtractor): |
18 | _NETRC_MACHINE = 'teachable' | |
19 | _URL_PREFIX = 'teachable:' | |
20 | ||
21 | _SITES = { | |
22 | # Only notable ones here | |
23 | 'upskillcourses.com': 'upskill', | |
24 | 'academy.gns3.com': 'gns3', | |
25 | 'academyhacker.com': 'academyhacker', | |
26 | 'stackskills.com': 'stackskills', | |
27 | 'market.saleshacker.com': 'saleshacker', | |
28 | 'learnability.org': 'learnability', | |
29 | 'edurila.com': 'edurila', | |
eecf788b | 30 | 'courses.workitdaily.com': 'workitdaily', |
5ee7ae5c S |
31 | } |
32 | ||
33 | _VALID_URL_SUB_TUPLE = (_URL_PREFIX, '|'.join(re.escape(site) for site in _SITES.keys())) | |
7fc60f4e S |
34 | |
35 | def _real_initialize(self): | |
5ee7ae5c | 36 | self._logged_in = False |
7fc60f4e | 37 | |
5ee7ae5c S |
38 | def _login(self, site): |
39 | if self._logged_in: | |
40 | return | |
41 | ||
42 | username, password = self._get_login_info( | |
43 | netrc_machine=self._SITES.get(site, site)) | |
7fc60f4e S |
44 | if username is None: |
45 | return | |
46 | ||
47 | login_page, urlh = self._download_webpage_handle( | |
5ee7ae5c S |
48 | 'https://%s/sign_in' % site, None, |
49 | 'Downloading %s login page' % site) | |
7fc60f4e S |
50 | |
51 | login_url = compat_str(urlh.geturl()) | |
52 | ||
53 | login_form = self._hidden_inputs(login_page) | |
54 | ||
55 | login_form.update({ | |
56 | 'user[email]': username, | |
57 | 'user[password]': password, | |
58 | }) | |
59 | ||
60 | post_url = self._search_regex( | |
61 | r'<form[^>]+action=(["\'])(?P<url>(?:(?!\1).)+)\1', login_page, | |
62 | 'post url', default=login_url, group='url') | |
63 | ||
64 | if not post_url.startswith('http'): | |
65 | post_url = urljoin(login_url, post_url) | |
66 | ||
67 | response = self._download_webpage( | |
5ee7ae5c | 68 | post_url, None, 'Logging in to %s' % site, |
7fc60f4e S |
69 | data=urlencode_postdata(login_form), |
70 | headers={ | |
71 | 'Content-Type': 'application/x-www-form-urlencoded', | |
72 | 'Referer': login_url, | |
73 | }) | |
74 | ||
5ee7ae5c S |
75 | if '>I accept the new Privacy Policy<' in response: |
76 | raise ExtractorError( | |
77 | 'Unable to login: %s asks you to accept new Privacy Policy. ' | |
78 | 'Go to https://%s/ and accept.' % (site, site), expected=True) | |
79 | ||
7fc60f4e S |
80 | # Successful login |
81 | if any(re.search(p, response) for p in ( | |
82 | r'class=["\']user-signout', | |
83 | r'<a[^>]+\bhref=["\']/sign_out', | |
84 | r'>\s*Log out\s*<')): | |
5ee7ae5c | 85 | self._logged_in = True |
7fc60f4e S |
86 | return |
87 | ||
88 | message = get_element_by_class('alert', response) | |
89 | if message is not None: | |
90 | raise ExtractorError( | |
91 | 'Unable to login: %s' % clean_html(message), expected=True) | |
92 | ||
93 | raise ExtractorError('Unable to log in') | |
94 | ||
95 | ||
5ee7ae5c S |
96 | class TeachableIE(TeachableBaseIE): |
97 | _VALID_URL = r'''(?x) | |
98 | (?: | |
99 | %shttps?://(?P<site_t>[^/]+)| | |
100 | https?://(?:www\.)?(?P<site>%s) | |
101 | ) | |
102 | /courses/[^/]+/lectures/(?P<id>\d+) | |
103 | ''' % TeachableBaseIE._VALID_URL_SUB_TUPLE | |
7fc60f4e S |
104 | |
105 | _TESTS = [{ | |
106 | 'url': 'http://upskillcourses.com/courses/essential-web-developer-course/lectures/1747100', | |
107 | 'info_dict': { | |
108 | 'id': 'uzw6zw58or', | |
109 | 'ext': 'mp4', | |
110 | 'title': 'Welcome to the Course!', | |
5ee7ae5c | 111 | 'description': 'md5:65edb0affa582974de4625b9cdea1107', |
7fc60f4e S |
112 | 'duration': 138.763, |
113 | 'timestamp': 1479846621, | |
114 | 'upload_date': '20161122', | |
115 | }, | |
116 | 'params': { | |
117 | 'skip_download': True, | |
118 | }, | |
119 | }, { | |
120 | 'url': 'http://upskillcourses.com/courses/119763/lectures/1747100', | |
121 | 'only_matching': True, | |
5ee7ae5c S |
122 | }, { |
123 | 'url': 'https://academy.gns3.com/courses/423415/lectures/6885939', | |
124 | 'only_matching': True, | |
125 | }, { | |
126 | 'url': 'teachable:https://upskillcourses.com/courses/essential-web-developer-course/lectures/1747100', | |
127 | 'only_matching': True, | |
7fc60f4e S |
128 | }] |
129 | ||
5ee7ae5c S |
130 | @staticmethod |
131 | def _is_teachable(webpage): | |
132 | return 'teachableTracker.linker:autoLink' in webpage and re.search( | |
133 | r'<link[^>]+href=["\']https?://process\.fs\.teachablecdn\.com', | |
134 | webpage) | |
135 | ||
136 | @staticmethod | |
137 | def _extract_url(webpage, source_url): | |
138 | if not TeachableIE._is_teachable(webpage): | |
5ee7ae5c S |
139 | return |
140 | if re.match(r'https?://[^/]+/(?:courses|p)', source_url): | |
141 | return '%s%s' % (TeachableBaseIE._URL_PREFIX, source_url) | |
142 | ||
7fc60f4e | 143 | def _real_extract(self, url): |
5ee7ae5c S |
144 | mobj = re.match(self._VALID_URL, url) |
145 | site = mobj.group('site') or mobj.group('site_t') | |
146 | video_id = mobj.group('id') | |
147 | ||
148 | self._login(site) | |
149 | ||
150 | prefixed = url.startswith(self._URL_PREFIX) | |
151 | if prefixed: | |
152 | url = url[len(self._URL_PREFIX):] | |
7fc60f4e S |
153 | |
154 | webpage = self._download_webpage(url, video_id) | |
155 | ||
156 | wistia_url = WistiaIE._extract_url(webpage) | |
157 | if not wistia_url: | |
158 | if any(re.search(p, webpage) for p in ( | |
159 | r'class=["\']lecture-contents-locked', | |
160 | r'>\s*Lecture contents locked', | |
161 | r'id=["\']lecture-locked')): | |
162 | self.raise_login_required('Lecture contents locked') | |
163 | ||
164 | title = self._og_search_title(webpage, default=None) | |
165 | ||
166 | return { | |
167 | '_type': 'url_transparent', | |
168 | 'url': wistia_url, | |
169 | 'ie_key': WistiaIE.ie_key(), | |
170 | 'title': title, | |
171 | } | |
172 | ||
173 | ||
5ee7ae5c S |
174 | class TeachableCourseIE(TeachableBaseIE): |
175 | _VALID_URL = r'''(?x) | |
176 | (?: | |
177 | %shttps?://(?P<site_t>[^/]+)| | |
178 | https?://(?:www\.)?(?P<site>%s) | |
179 | ) | |
180 | /(?:courses|p)/(?:enrolled/)?(?P<id>[^/?#&]+) | |
181 | ''' % TeachableBaseIE._VALID_URL_SUB_TUPLE | |
7fc60f4e S |
182 | _TESTS = [{ |
183 | 'url': 'http://upskillcourses.com/courses/essential-web-developer-course/', | |
184 | 'info_dict': { | |
5ee7ae5c | 185 | 'id': 'essential-web-developer-course', |
7fc60f4e S |
186 | 'title': 'The Essential Web Developer Course (Free)', |
187 | }, | |
188 | 'playlist_count': 192, | |
189 | }, { | |
190 | 'url': 'http://upskillcourses.com/courses/119763/', | |
191 | 'only_matching': True, | |
192 | }, { | |
193 | 'url': 'http://upskillcourses.com/courses/enrolled/119763', | |
194 | 'only_matching': True, | |
5ee7ae5c S |
195 | }, { |
196 | 'url': 'https://academy.gns3.com/courses/enrolled/423415', | |
197 | 'only_matching': True, | |
198 | }, { | |
199 | 'url': 'teachable:https://learn.vrdev.school/p/gear-vr-developer-mini', | |
200 | 'only_matching': True, | |
201 | }, { | |
202 | 'url': 'teachable:https://filmsimplified.com/p/davinci-resolve-15-crash-course', | |
203 | 'only_matching': True, | |
7fc60f4e S |
204 | }] |
205 | ||
206 | @classmethod | |
207 | def suitable(cls, url): | |
5ee7ae5c S |
208 | return False if TeachableIE.suitable(url) else super( |
209 | TeachableCourseIE, cls).suitable(url) | |
7fc60f4e S |
210 | |
211 | def _real_extract(self, url): | |
5ee7ae5c S |
212 | mobj = re.match(self._VALID_URL, url) |
213 | site = mobj.group('site') or mobj.group('site_t') | |
214 | course_id = mobj.group('id') | |
215 | ||
216 | self._login(site) | |
217 | ||
218 | prefixed = url.startswith(self._URL_PREFIX) | |
219 | if prefixed: | |
220 | prefix = self._URL_PREFIX | |
221 | url = url[len(prefix):] | |
7fc60f4e S |
222 | |
223 | webpage = self._download_webpage(url, course_id) | |
224 | ||
5ee7ae5c | 225 | url_base = 'https://%s/' % site |
7fc60f4e S |
226 | |
227 | entries = [] | |
228 | ||
229 | for mobj in re.finditer( | |
230 | r'(?s)(?P<li><li[^>]+class=(["\'])(?:(?!\2).)*?section-item[^>]+>.+?</li>)', | |
231 | webpage): | |
232 | li = mobj.group('li') | |
233 | if 'fa-youtube-play' not in li: | |
234 | continue | |
235 | lecture_url = self._search_regex( | |
236 | r'<a[^>]+href=(["\'])(?P<url>(?:(?!\1).)+)\1', li, | |
237 | 'lecture url', default=None, group='url') | |
238 | if not lecture_url: | |
239 | continue | |
240 | lecture_id = self._search_regex( | |
241 | r'/lectures/(\d+)', lecture_url, 'lecture id', default=None) | |
242 | title = self._html_search_regex( | |
243 | r'<span[^>]+class=["\']lecture-name[^>]+>([^<]+)', li, | |
244 | 'title', default=None) | |
5ee7ae5c S |
245 | entry_url = urljoin(url_base, lecture_url) |
246 | if prefixed: | |
247 | entry_url = self._URL_PREFIX + entry_url | |
7fc60f4e S |
248 | entries.append( |
249 | self.url_result( | |
5ee7ae5c S |
250 | entry_url, |
251 | ie=TeachableIE.ie_key(), video_id=lecture_id, | |
7fc60f4e S |
252 | video_title=clean_html(title))) |
253 | ||
254 | course_title = self._html_search_regex( | |
255 | (r'(?s)<img[^>]+class=["\']course-image[^>]+>\s*<h\d>(.+?)</h', | |
256 | r'(?s)<h\d[^>]+class=["\']course-title[^>]+>(.+?)</h'), | |
257 | webpage, 'course title', fatal=False) | |
258 | ||
259 | return self.playlist_result(entries, course_id, course_title) |