channel9.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # encoding: utf-8
  2. from __future__ import unicode_literals
  3. import re
  4. from .common import InfoExtractor
  5. from ..utils import ExtractorError
  6. class Channel9IE(InfoExtractor):
  7. '''
  8. Common extractor for channel9.msdn.com.
  9. The type of provided URL (video or playlist) is determined according to
  10. meta Search.PageType from web page HTML rather than URL itself, as it is
  11. not always possible to do.
  12. '''
  13. IE_DESC = 'Channel 9'
  14. IE_NAME = 'channel9'
  15. _VALID_URL = r'^https?://(?:www\.)?channel9\.msdn\.com/(?P<contentpath>.+)/?'
  16. _TESTS = [
  17. {
  18. 'url': 'http://channel9.msdn.com/Events/TechEd/Australia/2013/KOS002',
  19. 'file': 'Events_TechEd_Australia_2013_KOS002.mp4',
  20. 'md5': 'bbd75296ba47916b754e73c3a4bbdf10',
  21. 'info_dict': {
  22. 'title': 'Developer Kick-Off Session: Stuff We Love',
  23. 'description': 'md5:c08d72240b7c87fcecafe2692f80e35f',
  24. 'duration': 4576,
  25. 'thumbnail': 'http://media.ch9.ms/ch9/9d51/03902f2d-fc97-4d3c-b195-0bfe15a19d51/KOS002_220.jpg',
  26. 'session_code': 'KOS002',
  27. 'session_day': 'Day 1',
  28. 'session_room': 'Arena 1A',
  29. 'session_speakers': [ 'Ed Blankenship', 'Andrew Coates', 'Brady Gaster', 'Patrick Klug', 'Mads Kristensen' ],
  30. },
  31. },
  32. {
  33. 'url': 'http://channel9.msdn.com/posts/Self-service-BI-with-Power-BI-nuclear-testing',
  34. 'file': 'posts_Self-service-BI-with-Power-BI-nuclear-testing.mp4',
  35. 'md5': 'b43ee4529d111bc37ba7ee4f34813e68',
  36. 'info_dict': {
  37. 'title': 'Self-service BI with Power BI - nuclear testing',
  38. 'description': 'md5:d1e6ecaafa7fb52a2cacdf9599829f5b',
  39. 'duration': 1540,
  40. 'thumbnail': 'http://media.ch9.ms/ch9/87e1/0300391f-a455-4c72-bec3-4422f19287e1/selfservicenuk_512.jpg',
  41. 'authors': [ 'Mike Wilmot' ],
  42. },
  43. }
  44. ]
  45. _RSS_URL = 'http://channel9.msdn.com/%s/RSS'
  46. # Sorted by quality
  47. _known_formats = ['MP3', 'MP4', 'Mid Quality WMV', 'Mid Quality MP4', 'High Quality WMV', 'High Quality MP4']
  48. def _restore_bytes(self, formatted_size):
  49. if not formatted_size:
  50. return 0
  51. m = re.match(r'^(?P<size>\d+(?:\.\d+)?)\s+(?P<units>[a-zA-Z]+)', formatted_size)
  52. if not m:
  53. return 0
  54. units = m.group('units')
  55. try:
  56. exponent = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'].index(units.upper())
  57. except ValueError:
  58. return 0
  59. size = float(m.group('size'))
  60. return int(size * (1024 ** exponent))
  61. def _formats_from_html(self, html):
  62. FORMAT_REGEX = r'''
  63. (?x)
  64. <a\s+href="(?P<url>[^"]+)">(?P<quality>[^<]+)</a>\s*
  65. <span\s+class="usage">\((?P<note>[^\)]+)\)</span>\s*
  66. (?:<div\s+class="popup\s+rounded">\s*
  67. <h3>File\s+size</h3>\s*(?P<filesize>.*?)\s*
  68. </div>)? # File size part may be missing
  69. '''
  70. # Extract known formats
  71. formats = [{
  72. 'url': x.group('url'),
  73. 'format_id': x.group('quality'),
  74. 'format_note': x.group('note'),
  75. 'format': '%s (%s)' % (x.group('quality'), x.group('note')),
  76. 'filesize': self._restore_bytes(x.group('filesize')), # File size is approximate
  77. 'preference': self._known_formats.index(x.group('quality')),
  78. 'vcodec': 'none' if x.group('note') == 'Audio only' else None,
  79. } for x in list(re.finditer(FORMAT_REGEX, html)) if x.group('quality') in self._known_formats]
  80. self._sort_formats(formats)
  81. return formats
  82. def _extract_title(self, html):
  83. title = self._html_search_meta('title', html, 'title')
  84. if title is None:
  85. title = self._og_search_title(html)
  86. TITLE_SUFFIX = ' (Channel 9)'
  87. if title is not None and title.endswith(TITLE_SUFFIX):
  88. title = title[:-len(TITLE_SUFFIX)]
  89. return title
  90. def _extract_description(self, html):
  91. DESCRIPTION_REGEX = r'''(?sx)
  92. <div\s+class="entry-content">\s*
  93. <div\s+id="entry-body">\s*
  94. (?P<description>.+?)\s*
  95. </div>\s*
  96. </div>
  97. '''
  98. m = re.search(DESCRIPTION_REGEX, html)
  99. if m is not None:
  100. return m.group('description')
  101. return self._html_search_meta('description', html, 'description')
  102. def _extract_duration(self, html):
  103. m = re.search(r'data-video_duration="(?P<hours>\d{2}):(?P<minutes>\d{2}):(?P<seconds>\d{2})"', html)
  104. return ((int(m.group('hours')) * 60 * 60) + (int(m.group('minutes')) * 60) + int(m.group('seconds'))) if m else None
  105. def _extract_slides(self, html):
  106. m = re.search(r'<a href="(?P<slidesurl>[^"]+)" class="slides">Slides</a>', html)
  107. return m.group('slidesurl') if m is not None else None
  108. def _extract_zip(self, html):
  109. m = re.search(r'<a href="(?P<zipurl>[^"]+)" class="zip">Zip</a>', html)
  110. return m.group('zipurl') if m is not None else None
  111. def _extract_avg_rating(self, html):
  112. m = re.search(r'<p class="avg-rating">Avg Rating: <span>(?P<avgrating>[^<]+)</span></p>', html)
  113. return float(m.group('avgrating')) if m is not None else 0
  114. def _extract_rating_count(self, html):
  115. m = re.search(r'<div class="rating-count">\((?P<ratingcount>[^<]+)\)</div>', html)
  116. return int(self._fix_count(m.group('ratingcount'))) if m is not None else 0
  117. def _extract_view_count(self, html):
  118. m = re.search(r'<li class="views">\s*<span class="count">(?P<viewcount>[^<]+)</span> Views\s*</li>', html)
  119. return int(self._fix_count(m.group('viewcount'))) if m is not None else 0
  120. def _extract_comment_count(self, html):
  121. m = re.search(r'<li class="comments">\s*<a href="#comments">\s*<span class="count">(?P<commentcount>[^<]+)</span> Comments\s*</a>\s*</li>', html)
  122. return int(self._fix_count(m.group('commentcount'))) if m is not None else 0
  123. def _fix_count(self, count):
  124. return int(str(count).replace(',', '')) if count is not None else None
  125. def _extract_authors(self, html):
  126. m = re.search(r'(?s)<li class="author">(.*?)</li>', html)
  127. if m is None:
  128. return None
  129. return re.findall(r'<a href="/Niners/[^"]+">([^<]+)</a>', m.group(1))
  130. def _extract_session_code(self, html):
  131. m = re.search(r'<li class="code">\s*(?P<code>.+?)\s*</li>', html)
  132. return m.group('code') if m is not None else None
  133. def _extract_session_day(self, html):
  134. m = re.search(r'<li class="day">\s*<a href="/Events/[^"]+">(?P<day>[^<]+)</a>\s*</li>', html)
  135. return m.group('day') if m is not None else None
  136. def _extract_session_room(self, html):
  137. m = re.search(r'<li class="room">\s*(?P<room>.+?)\s*</li>', html)
  138. return m.group('room') if m is not None else None
  139. def _extract_session_speakers(self, html):
  140. return re.findall(r'<a href="/Events/Speakers/[^"]+">([^<]+)</a>', html)
  141. def _extract_content(self, html, content_path):
  142. # Look for downloadable content
  143. formats = self._formats_from_html(html)
  144. slides = self._extract_slides(html)
  145. zip_ = self._extract_zip(html)
  146. # Nothing to download
  147. if len(formats) == 0 and slides is None and zip_ is None:
  148. self._downloader.report_warning('None of recording, slides or zip are available for %s' % content_path)
  149. return
  150. # Extract meta
  151. title = self._extract_title(html)
  152. description = self._extract_description(html)
  153. thumbnail = self._og_search_thumbnail(html)
  154. duration = self._extract_duration(html)
  155. avg_rating = self._extract_avg_rating(html)
  156. rating_count = self._extract_rating_count(html)
  157. view_count = self._extract_view_count(html)
  158. comment_count = self._extract_comment_count(html)
  159. common = {'_type': 'video',
  160. 'id': content_path,
  161. 'description': description,
  162. 'thumbnail': thumbnail,
  163. 'duration': duration,
  164. 'avg_rating': avg_rating,
  165. 'rating_count': rating_count,
  166. 'view_count': view_count,
  167. 'comment_count': comment_count,
  168. }
  169. result = []
  170. if slides is not None:
  171. d = common.copy()
  172. d.update({ 'title': title + '-Slides', 'url': slides })
  173. result.append(d)
  174. if zip_ is not None:
  175. d = common.copy()
  176. d.update({ 'title': title + '-Zip', 'url': zip_ })
  177. result.append(d)
  178. if len(formats) > 0:
  179. d = common.copy()
  180. d.update({ 'title': title, 'formats': formats })
  181. result.append(d)
  182. return result
  183. def _extract_entry_item(self, html, content_path):
  184. contents = self._extract_content(html, content_path)
  185. if contents is None:
  186. return contents
  187. authors = self._extract_authors(html)
  188. for content in contents:
  189. content['authors'] = authors
  190. return contents
  191. def _extract_session(self, html, content_path):
  192. contents = self._extract_content(html, content_path)
  193. if contents is None:
  194. return contents
  195. session_meta = {'session_code': self._extract_session_code(html),
  196. 'session_day': self._extract_session_day(html),
  197. 'session_room': self._extract_session_room(html),
  198. 'session_speakers': self._extract_session_speakers(html),
  199. }
  200. for content in contents:
  201. content.update(session_meta)
  202. return contents
  203. def _extract_list(self, content_path):
  204. rss = self._download_xml(self._RSS_URL % content_path, content_path, 'Downloading RSS')
  205. entries = [self.url_result(session_url.text, 'Channel9')
  206. for session_url in rss.findall('./channel/item/link')]
  207. title_text = rss.find('./channel/title').text
  208. return self.playlist_result(entries, content_path, title_text)
  209. def _real_extract(self, url):
  210. mobj = re.match(self._VALID_URL, url)
  211. content_path = mobj.group('contentpath')
  212. webpage = self._download_webpage(url, content_path, 'Downloading web page')
  213. page_type_m = re.search(r'<meta name="Search.PageType" content="(?P<pagetype>[^"]+)"/>', webpage)
  214. if page_type_m is None:
  215. raise ExtractorError('Search.PageType not found, don\'t know how to process this page', expected=True)
  216. page_type = page_type_m.group('pagetype')
  217. if page_type == 'List': # List page, may contain list of 'item'-like objects
  218. return self._extract_list(content_path)
  219. elif page_type == 'Entry.Item': # Any 'item'-like page, may contain downloadable content
  220. return self._extract_entry_item(webpage, content_path)
  221. elif page_type == 'Session': # Event session page, may contain downloadable content
  222. return self._extract_session(webpage, content_path)
  223. else:
  224. raise ExtractorError('Unexpected Search.PageType %s' % page_type, expected=True)