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.

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