You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

174 lines
6.4 KiB

  1. from __future__ import unicode_literals
  2. import json
  3. import random
  4. import re
  5. from .common import InfoExtractor
  6. from ..compat import (
  7. compat_b64decode,
  8. compat_HTTPError,
  9. compat_str,
  10. )
  11. from ..utils import (
  12. ExtractorError,
  13. orderedSet,
  14. unescapeHTML,
  15. urlencode_postdata,
  16. urljoin,
  17. )
  18. class LinuxAcademyIE(InfoExtractor):
  19. _VALID_URL = r'''(?x)
  20. https?://
  21. (?:www\.)?linuxacademy\.com/cp/
  22. (?:
  23. courses/lesson/course/(?P<chapter_id>\d+)/lesson/(?P<lesson_id>\d+)|
  24. modules/view/id/(?P<course_id>\d+)
  25. )
  26. '''
  27. _TESTS = [{
  28. 'url': 'https://linuxacademy.com/cp/courses/lesson/course/1498/lesson/2/module/154',
  29. 'info_dict': {
  30. 'id': '1498-2',
  31. 'ext': 'mp4',
  32. 'title': "Introduction to the Practitioner's Brief",
  33. },
  34. 'params': {
  35. 'skip_download': True,
  36. },
  37. 'skip': 'Requires Linux Academy account credentials',
  38. }, {
  39. 'url': 'https://linuxacademy.com/cp/courses/lesson/course/1498/lesson/2',
  40. 'only_matching': True,
  41. }, {
  42. 'url': 'https://linuxacademy.com/cp/modules/view/id/154',
  43. 'info_dict': {
  44. 'id': '154',
  45. 'title': 'AWS Certified Cloud Practitioner',
  46. 'description': 'md5:039db7e60e4aac9cf43630e0a75fa834',
  47. },
  48. 'playlist_count': 41,
  49. 'skip': 'Requires Linux Academy account credentials',
  50. }]
  51. _AUTHORIZE_URL = 'https://login.linuxacademy.com/authorize'
  52. _ORIGIN_URL = 'https://linuxacademy.com'
  53. _CLIENT_ID = 'KaWxNn1C2Gc7n83W9OFeXltd8Utb5vvx'
  54. _NETRC_MACHINE = 'linuxacademy'
  55. def _real_initialize(self):
  56. self._login()
  57. def _login(self):
  58. username, password = self._get_login_info()
  59. if username is None:
  60. return
  61. def random_string():
  62. return ''.join([
  63. random.choice('0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._~')
  64. for _ in range(32)])
  65. webpage, urlh = self._download_webpage_handle(
  66. self._AUTHORIZE_URL, None, 'Downloading authorize page', query={
  67. 'client_id': self._CLIENT_ID,
  68. 'response_type': 'token id_token',
  69. 'redirect_uri': self._ORIGIN_URL,
  70. 'scope': 'openid email user_impersonation profile',
  71. 'audience': self._ORIGIN_URL,
  72. 'state': random_string(),
  73. 'nonce': random_string(),
  74. })
  75. login_data = self._parse_json(
  76. self._search_regex(
  77. r'atob\(\s*(["\'])(?P<value>(?:(?!\1).)+)\1', webpage,
  78. 'login info', group='value'), None,
  79. transform_source=lambda x: compat_b64decode(x).decode('utf-8')
  80. )['extraParams']
  81. login_data.update({
  82. 'client_id': self._CLIENT_ID,
  83. 'redirect_uri': self._ORIGIN_URL,
  84. 'tenant': 'lacausers',
  85. 'connection': 'Username-Password-Authentication',
  86. 'username': username,
  87. 'password': password,
  88. 'sso': 'true',
  89. })
  90. login_state_url = compat_str(urlh.geturl())
  91. try:
  92. login_page = self._download_webpage(
  93. 'https://login.linuxacademy.com/usernamepassword/login', None,
  94. 'Downloading login page', data=json.dumps(login_data).encode(),
  95. headers={
  96. 'Content-Type': 'application/json',
  97. 'Origin': 'https://login.linuxacademy.com',
  98. 'Referer': login_state_url,
  99. })
  100. except ExtractorError as e:
  101. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
  102. error = self._parse_json(e.cause.read(), None)
  103. message = error.get('description') or error['code']
  104. raise ExtractorError(
  105. '%s said: %s' % (self.IE_NAME, message), expected=True)
  106. raise
  107. callback_page, urlh = self._download_webpage_handle(
  108. 'https://login.linuxacademy.com/login/callback', None,
  109. 'Downloading callback page',
  110. data=urlencode_postdata(self._hidden_inputs(login_page)),
  111. headers={
  112. 'Content-Type': 'application/x-www-form-urlencoded',
  113. 'Origin': 'https://login.linuxacademy.com',
  114. 'Referer': login_state_url,
  115. })
  116. access_token = self._search_regex(
  117. r'access_token=([^=&]+)', compat_str(urlh.geturl()),
  118. 'access token')
  119. self._download_webpage(
  120. 'https://linuxacademy.com/cp/login/tokenValidateLogin/token/%s'
  121. % access_token, None, 'Downloading token validation page')
  122. def _real_extract(self, url):
  123. mobj = re.match(self._VALID_URL, url)
  124. chapter_id, lecture_id, course_id = mobj.group('chapter_id', 'lesson_id', 'course_id')
  125. item_id = course_id if course_id else '%s-%s' % (chapter_id, lecture_id)
  126. webpage = self._download_webpage(url, item_id)
  127. # course path
  128. if course_id:
  129. entries = [
  130. self.url_result(
  131. urljoin(url, lesson_url), ie=LinuxAcademyIE.ie_key())
  132. for lesson_url in orderedSet(re.findall(
  133. r'<a[^>]+\bhref=["\'](/cp/courses/lesson/course/\d+/lesson/\d+/module/\d+)',
  134. webpage))]
  135. title = unescapeHTML(self._html_search_regex(
  136. (r'class=["\']course-title["\'][^>]*>(?P<value>[^<]+)',
  137. r'var\s+title\s*=\s*(["\'])(?P<value>(?:(?!\1).)+)\1'),
  138. webpage, 'title', default=None, group='value'))
  139. description = unescapeHTML(self._html_search_regex(
  140. r'var\s+description\s*=\s*(["\'])(?P<value>(?:(?!\1).)+)\1',
  141. webpage, 'description', default=None, group='value'))
  142. return self.playlist_result(entries, course_id, title, description)
  143. # single video path
  144. info = self._extract_jwplayer_data(
  145. webpage, item_id, require_title=False, m3u8_id='hls',)
  146. title = self._search_regex(
  147. (r'>Lecture\s*:\s*(?P<value>[^<]+)',
  148. r'lessonName\s*=\s*(["\'])(?P<value>(?:(?!\1).)+)\1'), webpage,
  149. 'title', group='value')
  150. info.update({
  151. 'id': item_id,
  152. 'title': title,
  153. })
  154. return info