1 from __future__ 
import unicode_literals
 
   5 from .common 
import InfoExtractor
 
  17 class LyndaBaseIE(InfoExtractor
): 
  18     _SIGNIN_URL 
= 'https://www.lynda.com/signin/lynda' 
  19     _PASSWORD_URL 
= 'https://www.lynda.com/signin/password' 
  20     _USER_URL 
= 'https://www.lynda.com/signin/user' 
  21     _ACCOUNT_CREDENTIALS_HINT 
= 'Use --username and --password options to provide lynda.com account credentials.' 
  22     _NETRC_MACHINE 
= 'lynda' 
  24     def _real_initialize(self
): 
  28     def _check_error(json_string
, key_or_keys
): 
  29         keys 
= [key_or_keys
] if isinstance(key_or_keys
, compat_str
) else key_or_keys
 
  31             error 
= json_string
.get(key
) 
  33                 raise ExtractorError('Unable to login: %s' % error
, expected
=True) 
  35     def _login_step(self
, form_html
, fallback_action_url
, extra_form_data
, note
, referrer_url
): 
  36         action_url 
= self
._search
_regex
( 
  37             r
'<form[^>]+action=(["\'])(?P
<url
>.+?
)\
1', form_html, 
  38             'post url
', default=fallback_action_url, group='url
') 
  40         if not action_url.startswith('http
'): 
  41             action_url = compat_urlparse.urljoin(self._SIGNIN_URL, action_url) 
  43         form_data = self._hidden_inputs(form_html) 
  44         form_data.update(extra_form_data) 
  46         response = self._download_json( 
  47             action_url, None, note, 
  48             data=urlencode_postdata(form_data), 
  50                 'Referer
': referrer_url, 
  51                 'X
-Requested
-With
': 'XMLHttpRequest
', 
  52             }, expected_status=(418, 500, )) 
  54         self._check_error(response, ('email
', 'password
', 'ErrorMessage
')) 
  56         return response, action_url 
  59         username, password = self._get_login_info() 
  63         # Step 1: download signin page 
  64         signin_page = self._download_webpage( 
  65             self._SIGNIN_URL, None, 'Downloading signin page
') 
  68         if any(re.search(p, signin_page) for p in ( 
  69                 r'isLoggedIn\s
*:\s
*true
', r'logout\
.aspx
', r'>Log out
<')): 
  72         # Step 2: submit email 
  73         signin_form = self._search_regex( 
  74             r'(?s
)(<form
[^
>]+data
-form
-name
=["\']signin["\'][^
>]*>.+?
</form
>)', 
  75             signin_page, 'signin form
') 
  76         signin_page, signin_url = self._login_step( 
  77             signin_form, self._PASSWORD_URL, {'email
': username}, 
  78             'Submitting email
', self._SIGNIN_URL) 
  80         # Step 3: submit password 
  81         password_form = signin_page['body
'] 
  83             password_form, self._USER_URL, {'email
': username, 'password
': password}, 
  84             'Submitting password
', signin_url) 
  87 class LyndaIE(LyndaBaseIE): 
  89     IE_DESC = 'lynda
.com videos
' 
  92                         (?:www\.)?(?:lynda\.com|educourse\.ga)/ 
  94                             (?:[^/]+/){2,3}(?P<course_id>\d+)| 
 100     _TIMECODE_REGEX = r'\
[(?P
<timecode
>\d
+:\d
+:\d
+[\
.,]\d
+)\
]' 
 103         'url
': 'https
://www
.lynda
.com
/Bootstrap
-tutorials
/Using
-exercise
-files
/110885/114408-4.html
', 
 108             'title
': 'Using the exercise files
', 
 112         'url
': 'https
://www
.lynda
.com
/player
/embed
/133770?tr
=foo
=1;bar
=g
;fizz
=rt
&fs
=0', 
 113         'only_matching
': True, 
 115         'url
': 'https
://educourse
.ga
/Bootstrap
-tutorials
/Using
-exercise
-files
/110885/114408-4.html
', 
 116         'only_matching
': True, 
 118         'url
': 'https
://www
.lynda
.com
/de
/Graphic
-Design
-tutorials
/Willkommen
-Grundlagen
-guten
-Gestaltung
/393570/393572-4.html
', 
 119         'only_matching
': True, 
 122     def _raise_unavailable(self, video_id): 
 123         self.raise_login_required( 
 124             'Video 
%s is only available 
for members
' % video_id) 
 126     def _real_extract(self, url): 
 127         mobj = re.match(self._VALID_URL, url) 
 128         video_id = mobj.group('id') 
 129         course_id = mobj.group('course_id
') 
 136         video = self._download_json( 
 137             'https
://www
.lynda
.com
/ajax
/player
', video_id, 
 138             'Downloading video JSON
', fatal=False, query=query) 
 142             query['courseId
'] = course_id 
 144             play = self._download_json( 
 145                 'https
://www
.lynda
.com
/ajax
/course
/%s/%s/play
' 
 146                 % (course_id, video_id), video_id, 'Downloading play JSON
') 
 149                 self._raise_unavailable(video_id) 
 152             for formats_dict in play: 
 153                 urls = formats_dict.get('urls
') 
 154                 if not isinstance(urls, dict): 
 156                 cdn = formats_dict.get('name
') 
 157                 for format_id, format_url in urls.items(): 
 162                         'format_id
': '%s-%s' % (cdn, format_id) if cdn else format_id, 
 163                         'height
': int_or_none(format_id), 
 165             self._sort_formats(formats) 
 167             conviva = self._download_json( 
 168                 'https
://www
.lynda
.com
/ajax
/player
/conviva
', video_id, 
 169                 'Downloading conviva JSON
', query=query) 
 173                 'title
': conviva['VideoTitle
'], 
 174                 'description
': conviva.get('VideoDescription
'), 
 175                 'release_year
': int_or_none(conviva.get('ReleaseYear
')), 
 176                 'duration
': int_or_none(conviva.get('Duration
')), 
 177                 'creator
': conviva.get('Author
'), 
 181         if 'Status
' in video: 
 182             raise ExtractorError( 
 183                 'lynda returned error
: %s' % video['Message
'], expected=True) 
 185         if video.get('HasAccess
') is False: 
 186             self._raise_unavailable(video_id) 
 188         video_id = compat_str(video.get('ID
') or video_id) 
 189         duration = int_or_none(video.get('DurationInSeconds
')) 
 190         title = video['Title
'] 
 194         fmts = video.get('Formats
') 
 198                 'ext
': f.get('Extension
'), 
 199                 'width
': int_or_none(f.get('Width
')), 
 200                 'height
': int_or_none(f.get('Height
')), 
 201                 'filesize
': int_or_none(f.get('FileSize
')), 
 202                 'format_id
': compat_str(f.get('Resolution
')) if f.get('Resolution
') else None, 
 203             } for f in fmts if f.get('Url
')]) 
 205         prioritized_streams = video.get('PrioritizedStreams
') 
 206         if prioritized_streams: 
 207             for prioritized_stream_id, prioritized_stream in prioritized_streams.items(): 
 210                     'height
': int_or_none(format_id), 
 211                     'format_id
': '%s-%s' % (prioritized_stream_id, format_id), 
 212                 } for format_id, video_url in prioritized_stream.items()]) 
 214         self._check_formats(formats, video_id) 
 215         self._sort_formats(formats) 
 217         subtitles = self.extract_subtitles(video_id) 
 222             'duration
': duration, 
 223             'subtitles
': subtitles, 
 227     def _fix_subtitles(self, subs): 
 230         for pos in range(0, len(subs) - 1): 
 231             seq_current = subs[pos] 
 232             m_current = re.match(self._TIMECODE_REGEX, seq_current['Timecode
']) 
 233             if m_current is None: 
 235             seq_next = subs[pos + 1] 
 236             m_next = re.match(self._TIMECODE_REGEX, seq_next['Timecode
']) 
 239             appear_time = m_current.group('timecode
') 
 240             disappear_time = m_next.group('timecode
') 
 241             text = seq_current['Caption
'].strip() 
 244                 srt += '%s\r\n%s --> %s\r\n%s\r\n\r\n' % (seq_counter, appear_time, disappear_time, text) 
 248     def _get_subtitles(self, video_id): 
 249         url = 'https
://www
.lynda
.com
/ajax
/player?videoId
=%s&type=transcript
' % video_id 
 250         subs = self._download_json(url, None, False) 
 251         fixed_subs = self._fix_subtitles(subs) 
 253             return {'en
': [{'ext
': 'srt
', 'data
': fixed_subs}]} 
 258 class LyndaCourseIE(LyndaBaseIE): 
 259     IE_NAME = 'lynda
:course
' 
 260     IE_DESC = 'lynda
.com online courses
' 
 262     # Course link equals to welcome/introduction video link of same course 
 263     # We will recognize it as course link 
 264     _VALID_URL = r'https?
://(?
:www|m
)\
.(?
:lynda\
.com|educourse\
.ga
)/(?P
<coursepath
>(?
:[^
/]+/){2,3}(?P
<courseid
>\d
+))-2\
.html
' 
 267         'url
': 'https
://www
.lynda
.com
/Graphic
-Design
-tutorials
/Grundlagen
-guten
-Gestaltung
/393570-2.html
', 
 268         'only_matching
': True, 
 270         'url
': 'https
://www
.lynda
.com
/de
/Graphic
-Design
-tutorials
/Grundlagen
-guten
-Gestaltung
/393570-2.html
', 
 271         'only_matching
': True, 
 274     def _real_extract(self, url): 
 275         mobj = re.match(self._VALID_URL, url) 
 276         course_path = mobj.group('coursepath
') 
 277         course_id = mobj.group('courseid
') 
 279         item_template = 'https
://www
.lynda
.com
/%s/%%s-4.html
' % course_path 
 281         course = self._download_json( 
 282             'https
://www
.lynda
.com
/ajax
/player?courseId
=%s&type=course
' % course_id, 
 283             course_id, 'Downloading course JSON
', fatal=False) 
 286             webpage = self._download_webpage(url, course_id) 
 289                     item_template % video_id, ie=LyndaIE.ie_key(), 
 291                 for video_id in re.findall( 
 292                     r'data
-video
-id=["\'](\d+)', webpage)] 
 293             return self.playlist_result( 
 295                 self._og_search_title(webpage, fatal=False), 
 296                 self._og_search_description(webpage)) 
 298         if course.get('Status') == 'NotFound': 
 299             raise ExtractorError( 
 300                 'Course %s does not exist' % course_id, expected=True) 
 302         unaccessible_videos = 0 
 305         # Might want to extract videos right here from video['Formats'] as it seems 'Formats' is not provided 
 306         # by single video API anymore 
 308         for chapter in course['Chapters']: 
 309             for video in chapter.get('Videos', []): 
 310                 if video.get('HasAccess') is False: 
 311                     unaccessible_videos += 1 
 313                 video_id = video.get('ID') 
 316                         '_type': 'url_transparent', 
 317                         'url': item_template % video_id, 
 318                         'ie_key': LyndaIE.ie_key(), 
 319                         'chapter': chapter.get('Title'), 
 320                         'chapter_number': int_or_none(chapter.get('ChapterIndex')), 
 321                         'chapter_id': compat_str(chapter.get('ID')), 
 324         if unaccessible_videos > 0: 
 325             self._downloader.report_warning( 
 326                 '%s videos are only available for members (or paid members) and will not be downloaded. ' 
 327                 % unaccessible_videos + self._ACCOUNT_CREDENTIALS_HINT) 
 329         course_title = course.get('Title') 
 330         course_description = course.get('Description') 
 332         return self.playlist_result(entries, course_id, course_title, course_description)