channel9.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. from __future__ import unicode_literals
  2. import re
  3. from .common import InfoExtractor
  4. from ..utils import (
  5. ExtractorError,
  6. unescapeHTML,
  7. int_or_none,
  8. parse_iso8601,
  9. clean_html,
  10. )
  11. class Channel9IE(InfoExtractor):
  12. '''
  13. Common extractor for channel9.msdn.com.
  14. The type of provided URL (video or playlist) is determined according to
  15. meta Search.PageType from web page HTML rather than URL itself, as it is
  16. not always possible to do.
  17. '''
  18. IE_DESC = 'Channel 9'
  19. IE_NAME = 'channel9'
  20. _VALID_URL = r'https?://(?:www\.)?(?:channel9\.msdn\.com|s\.ch9\.ms)/(?P<contentpath>.+?)(?P<rss>/RSS)?/?(?:[?#&]|$)'
  21. _TESTS = [{
  22. 'url': 'http://channel9.msdn.com/Events/TechEd/Australia/2013/KOS002',
  23. 'md5': '32083d4eaf1946db6d454313f44510ca',
  24. 'info_dict': {
  25. 'id': '6c413323-383a-49dc-88f9-a22800cab024',
  26. 'ext': 'wmv',
  27. 'title': 'Developer Kick-Off Session: Stuff We Love',
  28. 'description': 'md5:b80bf9355a503c193aff7ec6cd5a7731',
  29. 'duration': 4576,
  30. 'thumbnail': r're:https?://.*\.jpg',
  31. 'timestamp': 1377717420,
  32. 'upload_date': '20130828',
  33. 'session_code': 'KOS002',
  34. 'session_room': 'Arena 1A',
  35. 'session_speakers': ['Andrew Coates', 'Brady Gaster', 'Mads Kristensen', 'Ed Blankenship', 'Patrick Klug'],
  36. },
  37. }, {
  38. 'url': 'http://channel9.msdn.com/posts/Self-service-BI-with-Power-BI-nuclear-testing',
  39. 'md5': 'dcf983ee6acd2088e7188c3cf79b46bc',
  40. 'info_dict': {
  41. 'id': 'fe8e435f-bb93-4e01-8e97-a28c01887024',
  42. 'ext': 'wmv',
  43. 'title': 'Self-service BI with Power BI - nuclear testing',
  44. 'description': 'md5:2d17fec927fc91e9e17783b3ecc88f54',
  45. 'duration': 1540,
  46. 'thumbnail': r're:https?://.*\.jpg',
  47. 'timestamp': 1386381991,
  48. 'upload_date': '20131207',
  49. 'authors': ['Mike Wilmot'],
  50. },
  51. }, {
  52. # low quality mp4 is best
  53. 'url': 'https://channel9.msdn.com/Events/CPP/CppCon-2015/Ranges-for-the-Standard-Library',
  54. 'info_dict': {
  55. 'id': '33ad69d2-6a4e-4172-83a1-a523013dec76',
  56. 'ext': 'mp4',
  57. 'title': 'Ranges for the Standard Library',
  58. 'description': 'md5:9895e0a9fd80822d2f01c454b8f4a372',
  59. 'duration': 5646,
  60. 'thumbnail': r're:https?://.*\.jpg',
  61. 'upload_date': '20150930',
  62. 'timestamp': 1443640735,
  63. },
  64. 'params': {
  65. 'skip_download': True,
  66. },
  67. }, {
  68. 'url': 'https://channel9.msdn.com/Niners/Splendid22/Queue/76acff796e8f411184b008028e0d492b/RSS',
  69. 'info_dict': {
  70. 'id': 'Niners/Splendid22/Queue/76acff796e8f411184b008028e0d492b',
  71. 'title': 'Channel 9',
  72. },
  73. 'playlist_mincount': 100,
  74. }, {
  75. 'url': 'https://channel9.msdn.com/Events/DEVintersection/DEVintersection-2016/RSS',
  76. 'only_matching': True,
  77. }, {
  78. 'url': 'https://channel9.msdn.com/Events/Speakers/scott-hanselman/RSS?UrlSafeName=scott-hanselman',
  79. 'only_matching': True,
  80. }]
  81. _RSS_URL = 'http://channel9.msdn.com/%s/RSS'
  82. def _extract_list(self, video_id, rss_url=None):
  83. if not rss_url:
  84. rss_url = self._RSS_URL % video_id
  85. rss = self._download_xml(rss_url, video_id, 'Downloading RSS')
  86. entries = [self.url_result(session_url.text, 'Channel9')
  87. for session_url in rss.findall('./channel/item/link')]
  88. title_text = rss.find('./channel/title').text
  89. return self.playlist_result(entries, video_id, title_text)
  90. def _real_extract(self, url):
  91. content_path, rss = re.match(self._VALID_URL, url).groups()
  92. if rss:
  93. return self._extract_list(content_path, url)
  94. webpage = self._download_webpage(
  95. url, content_path, 'Downloading web page')
  96. episode_data = self._search_regex(
  97. r"data-episode='([^']+)'", webpage, 'episode data', default=None)
  98. if episode_data:
  99. episode_data = self._parse_json(unescapeHTML(
  100. episode_data), content_path)
  101. content_id = episode_data['contentId']
  102. is_session = '/Sessions(' in episode_data['api']
  103. content_url = 'https://channel9.msdn.com/odata' + episode_data['api']
  104. if is_session:
  105. content_url += '?$expand=Speakers'
  106. else:
  107. content_url += '?$expand=Authors'
  108. content_data = self._download_json(content_url, content_id)
  109. title = content_data['Title']
  110. formats = []
  111. qualities = [
  112. 'VideoMP4Low',
  113. 'VideoWMV',
  114. 'VideoMP4Medium',
  115. 'VideoMP4High',
  116. 'VideoWMVHQ',
  117. ]
  118. for q in qualities:
  119. q_url = content_data.get(q)
  120. if not q_url:
  121. continue
  122. formats.append({
  123. 'format_id': q,
  124. 'url': q_url,
  125. })
  126. slides = content_data.get('Slides')
  127. zip_file = content_data.get('ZipFile')
  128. if not formats and not slides and not zip_file:
  129. raise ExtractorError(
  130. 'None of recording, slides or zip are available for %s' % content_path)
  131. subtitles = {}
  132. for caption in content_data.get('Captions', []):
  133. caption_url = caption.get('Url')
  134. if not caption_url:
  135. continue
  136. subtitles.setdefault(caption.get('Language', 'en'), []).append({
  137. 'url': caption_url,
  138. 'ext': 'vtt',
  139. })
  140. common = {
  141. 'id': content_id,
  142. 'title': title,
  143. 'description': clean_html(content_data.get('Description') or content_data.get('Body')),
  144. 'thumbnail': content_data.get('Thumbnail') or content_data.get('VideoPlayerPreviewImage'),
  145. 'duration': int_or_none(content_data.get('MediaLengthInSeconds')),
  146. 'timestamp': parse_iso8601(content_data.get('PublishedDate')),
  147. 'avg_rating': int_or_none(content_data.get('Rating')),
  148. 'rating_count': int_or_none(content_data.get('RatingCount')),
  149. 'view_count': int_or_none(content_data.get('Views')),
  150. 'comment_count': int_or_none(content_data.get('CommentCount')),
  151. 'subtitles': subtitles,
  152. }
  153. if is_session:
  154. speakers = []
  155. for s in content_data.get('Speakers', []):
  156. speaker_name = s.get('FullName')
  157. if not speaker_name:
  158. continue
  159. speakers.append(speaker_name)
  160. common.update({
  161. 'session_code': content_data.get('Code'),
  162. 'session_room': content_data.get('Room'),
  163. 'session_speakers': speakers,
  164. })
  165. else:
  166. authors = []
  167. for a in content_data.get('Authors', []):
  168. author_name = a.get('DisplayName')
  169. if not author_name:
  170. continue
  171. authors.append(author_name)
  172. common['authors'] = authors
  173. contents = []
  174. if slides:
  175. d = common.copy()
  176. d.update({'title': title + '-Slides', 'url': slides})
  177. contents.append(d)
  178. if zip_file:
  179. d = common.copy()
  180. d.update({'title': title + '-Zip', 'url': zip_file})
  181. contents.append(d)
  182. if formats:
  183. d = common.copy()
  184. d.update({'title': title, 'formats': formats})
  185. contents.append(d)
  186. return self.playlist_result(contents)
  187. else:
  188. return self._extract_list(content_path)