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.

340 lines
14 KiB

10 years ago
10 years ago
  1. from __future__ import unicode_literals
  2. import xml.etree.ElementTree
  3. from .subtitles import SubtitlesInfoExtractor
  4. from ..utils import ExtractorError
  5. from ..compat import compat_HTTPError
  6. class BBCCoUkIE(SubtitlesInfoExtractor):
  7. IE_NAME = 'bbc.co.uk'
  8. IE_DESC = 'BBC iPlayer'
  9. _VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/(?:(?:(?:programmes|iplayer(?:/[^/]+)?/(?:episode|playlist))/)|music/clips[/#])(?P<id>[\da-z]{8})'
  10. _TESTS = [
  11. {
  12. 'url': 'http://www.bbc.co.uk/programmes/b039g8p7',
  13. 'info_dict': {
  14. 'id': 'b039d07m',
  15. 'ext': 'flv',
  16. 'title': 'Kaleidoscope, Leonard Cohen',
  17. 'description': 'The Canadian poet and songwriter reflects on his musical career.',
  18. 'duration': 1740,
  19. },
  20. 'params': {
  21. # rtmp download
  22. 'skip_download': True,
  23. }
  24. },
  25. {
  26. 'url': 'http://www.bbc.co.uk/iplayer/episode/b00yng5w/The_Man_in_Black_Series_3_The_Printed_Name/',
  27. 'info_dict': {
  28. 'id': 'b00yng1d',
  29. 'ext': 'flv',
  30. 'title': 'The Man in Black: Series 3: The Printed Name',
  31. 'description': "Mark Gatiss introduces Nicholas Pierpan's chilling tale of a writer's devilish pact with a mysterious man. Stars Ewan Bailey.",
  32. 'duration': 1800,
  33. },
  34. 'params': {
  35. # rtmp download
  36. 'skip_download': True,
  37. },
  38. 'skip': 'Episode is no longer available on BBC iPlayer Radio',
  39. },
  40. {
  41. 'url': 'http://www.bbc.co.uk/iplayer/episode/b03vhd1f/The_Voice_UK_Series_3_Blind_Auditions_5/',
  42. 'info_dict': {
  43. 'id': 'b00yng1d',
  44. 'ext': 'flv',
  45. 'title': 'The Voice UK: Series 3: Blind Auditions 5',
  46. 'description': "Emma Willis and Marvin Humes present the fifth set of blind auditions in the singing competition, as the coaches continue to build their teams based on voice alone.",
  47. 'duration': 5100,
  48. },
  49. 'params': {
  50. # rtmp download
  51. 'skip_download': True,
  52. },
  53. 'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only',
  54. },
  55. {
  56. 'url': 'http://www.bbc.co.uk/iplayer/episode/p026c7jt/tomorrows-worlds-the-unearthly-history-of-science-fiction-2-invasion',
  57. 'info_dict': {
  58. 'id': 'b03k3pb7',
  59. 'ext': 'flv',
  60. 'title': "Tomorrow's Worlds: The Unearthly History of Science Fiction",
  61. 'description': '2. Invasion',
  62. 'duration': 3600,
  63. },
  64. 'params': {
  65. # rtmp download
  66. 'skip_download': True,
  67. },
  68. 'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only',
  69. }, {
  70. 'url': 'http://www.bbc.co.uk/programmes/b04v20dw',
  71. 'info_dict': {
  72. 'id': 'b04v209v',
  73. 'ext': 'flv',
  74. 'title': 'Pete Tong, The Essential New Tune Special',
  75. 'description': "Pete has a very special mix - all of 2014's Essential New Tunes!",
  76. 'duration': 10800,
  77. },
  78. 'params': {
  79. # rtmp download
  80. 'skip_download': True,
  81. }
  82. }, {
  83. 'url': 'http://www.bbc.co.uk/music/clips/p02frcc3',
  84. 'note': 'Audio',
  85. 'info_dict': {
  86. 'id': 'p02frcch',
  87. 'ext': 'flv',
  88. 'title': 'Pete Tong, Past, Present and Future Special, Madeon - After Hours mix',
  89. 'description': 'French house superstar Madeon takes us out of the club and onto the after party.',
  90. 'duration': 3507,
  91. },
  92. 'params': {
  93. # rtmp download
  94. 'skip_download': True,
  95. }
  96. }, {
  97. 'url': 'http://www.bbc.co.uk/music/clips/p025c0zz',
  98. 'note': 'Video',
  99. 'info_dict': {
  100. 'id': 'p025c103',
  101. 'ext': 'flv',
  102. 'title': 'Reading and Leeds Festival, 2014, Rae Morris - Closer (Live on BBC Three)',
  103. 'description': 'Rae Morris performs Closer for BBC Three at Reading 2014',
  104. 'duration': 226,
  105. },
  106. 'params': {
  107. # rtmp download
  108. 'skip_download': True,
  109. }
  110. }, {
  111. 'url': 'http://www.bbc.co.uk/iplayer/playlist/p01dvks4',
  112. 'only_matching': True,
  113. }, {
  114. 'url': 'http://www.bbc.co.uk/music/clips#p02frcc3',
  115. 'only_matching': True,
  116. }, {
  117. 'url': 'http://www.bbc.co.uk/iplayer/cbeebies/episode/b0480276/bing-14-atchoo',
  118. 'only_matching': True,
  119. }
  120. ]
  121. def _extract_asx_playlist(self, connection, programme_id):
  122. asx = self._download_xml(connection.get('href'), programme_id, 'Downloading ASX playlist')
  123. return [ref.get('href') for ref in asx.findall('./Entry/ref')]
  124. def _extract_connection(self, connection, programme_id):
  125. formats = []
  126. protocol = connection.get('protocol')
  127. supplier = connection.get('supplier')
  128. if protocol == 'http':
  129. href = connection.get('href')
  130. # ASX playlist
  131. if supplier == 'asx':
  132. for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)):
  133. formats.append({
  134. 'url': ref,
  135. 'format_id': 'ref%s_%s' % (i, supplier),
  136. })
  137. # Direct link
  138. else:
  139. formats.append({
  140. 'url': href,
  141. 'format_id': supplier,
  142. })
  143. elif protocol == 'rtmp':
  144. application = connection.get('application', 'ondemand')
  145. auth_string = connection.get('authString')
  146. identifier = connection.get('identifier')
  147. server = connection.get('server')
  148. formats.append({
  149. 'url': '%s://%s/%s?%s' % (protocol, server, application, auth_string),
  150. 'play_path': identifier,
  151. 'app': '%s?%s' % (application, auth_string),
  152. 'page_url': 'http://www.bbc.co.uk',
  153. 'player_url': 'http://www.bbc.co.uk/emp/releases/iplayer/revisions/617463_618125_4/617463_618125_4_emp.swf',
  154. 'rtmp_live': False,
  155. 'ext': 'flv',
  156. 'format_id': supplier,
  157. })
  158. return formats
  159. def _extract_items(self, playlist):
  160. return playlist.findall('./{http://bbc.co.uk/2008/emp/playlist}item')
  161. def _extract_medias(self, media_selection):
  162. error = media_selection.find('./{http://bbc.co.uk/2008/mp/mediaselection}error')
  163. if error is not None:
  164. raise ExtractorError(
  165. '%s returned error: %s' % (self.IE_NAME, error.get('id')), expected=True)
  166. return media_selection.findall('./{http://bbc.co.uk/2008/mp/mediaselection}media')
  167. def _extract_connections(self, media):
  168. return media.findall('./{http://bbc.co.uk/2008/mp/mediaselection}connection')
  169. def _extract_video(self, media, programme_id):
  170. formats = []
  171. vbr = int(media.get('bitrate'))
  172. vcodec = media.get('encoding')
  173. service = media.get('service')
  174. width = int(media.get('width'))
  175. height = int(media.get('height'))
  176. file_size = int(media.get('media_file_size'))
  177. for connection in self._extract_connections(media):
  178. conn_formats = self._extract_connection(connection, programme_id)
  179. for format in conn_formats:
  180. format.update({
  181. 'format_id': '%s_%s' % (service, format['format_id']),
  182. 'width': width,
  183. 'height': height,
  184. 'vbr': vbr,
  185. 'vcodec': vcodec,
  186. 'filesize': file_size,
  187. })
  188. formats.extend(conn_formats)
  189. return formats
  190. def _extract_audio(self, media, programme_id):
  191. formats = []
  192. abr = int(media.get('bitrate'))
  193. acodec = media.get('encoding')
  194. service = media.get('service')
  195. for connection in self._extract_connections(media):
  196. conn_formats = self._extract_connection(connection, programme_id)
  197. for format in conn_formats:
  198. format.update({
  199. 'format_id': '%s_%s' % (service, format['format_id']),
  200. 'abr': abr,
  201. 'acodec': acodec,
  202. })
  203. formats.extend(conn_formats)
  204. return formats
  205. def _extract_captions(self, media, programme_id):
  206. subtitles = {}
  207. for connection in self._extract_connections(media):
  208. captions = self._download_xml(connection.get('href'), programme_id, 'Downloading captions')
  209. lang = captions.get('{http://www.w3.org/XML/1998/namespace}lang', 'en')
  210. ps = captions.findall('./{0}body/{0}div/{0}p'.format('{http://www.w3.org/2006/10/ttaf1}'))
  211. srt = ''
  212. for pos, p in enumerate(ps):
  213. srt += '%s\r\n%s --> %s\r\n%s\r\n\r\n' % (str(pos), p.get('begin'), p.get('end'),
  214. p.text.strip() if p.text is not None else '')
  215. subtitles[lang] = srt
  216. return subtitles
  217. def _download_media_selector(self, programme_id):
  218. try:
  219. media_selection = self._download_xml(
  220. 'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s' % programme_id,
  221. programme_id, 'Downloading media selection XML')
  222. except ExtractorError as ee:
  223. if isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 403:
  224. media_selection = xml.etree.ElementTree.fromstring(ee.cause.read().encode('utf-8'))
  225. else:
  226. raise
  227. formats = []
  228. subtitles = None
  229. for media in self._extract_medias(media_selection):
  230. kind = media.get('kind')
  231. if kind == 'audio':
  232. formats.extend(self._extract_audio(media, programme_id))
  233. elif kind == 'video':
  234. formats.extend(self._extract_video(media, programme_id))
  235. elif kind == 'captions':
  236. subtitles = self._extract_captions(media, programme_id)
  237. return formats, subtitles
  238. def _download_playlist(self, playlist_id):
  239. try:
  240. playlist = self._download_json(
  241. 'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
  242. playlist_id, 'Downloading playlist JSON')
  243. version = playlist.get('defaultAvailableVersion')
  244. if version:
  245. smp_config = version['smpConfig']
  246. title = smp_config['title']
  247. description = smp_config['summary']
  248. for item in smp_config['items']:
  249. kind = item['kind']
  250. if kind != 'programme' and kind != 'radioProgramme':
  251. continue
  252. programme_id = item.get('vpid')
  253. duration = int(item.get('duration'))
  254. formats, subtitles = self._download_media_selector(programme_id)
  255. return programme_id, title, description, duration, formats, subtitles
  256. except ExtractorError as ee:
  257. if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404):
  258. raise
  259. # fallback to legacy playlist
  260. playlist = self._download_xml(
  261. 'http://www.bbc.co.uk/iplayer/playlist/%s' % playlist_id,
  262. playlist_id, 'Downloading legacy playlist XML')
  263. no_items = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}noItems')
  264. if no_items is not None:
  265. reason = no_items.get('reason')
  266. if reason == 'preAvailability':
  267. msg = 'Episode %s is not yet available' % playlist_id
  268. elif reason == 'postAvailability':
  269. msg = 'Episode %s is no longer available' % playlist_id
  270. elif reason == 'noMedia':
  271. msg = 'Episode %s is not currently available' % playlist_id
  272. else:
  273. msg = 'Episode %s is not available: %s' % (playlist_id, reason)
  274. raise ExtractorError(msg, expected=True)
  275. for item in self._extract_items(playlist):
  276. kind = item.get('kind')
  277. if kind != 'programme' and kind != 'radioProgramme':
  278. continue
  279. title = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}title').text
  280. description = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}summary').text
  281. programme_id = item.get('identifier')
  282. duration = int(item.get('duration'))
  283. formats, subtitles = self._download_media_selector(programme_id)
  284. return programme_id, title, description, duration, formats, subtitles
  285. def _real_extract(self, url):
  286. group_id = self._match_id(url)
  287. webpage = self._download_webpage(url, group_id, 'Downloading video page')
  288. programme_id = self._search_regex(
  289. r'"vpid"\s*:\s*"([\da-z]{8})"', webpage, 'vpid', fatal=False, default=None)
  290. if programme_id:
  291. player = self._download_json(
  292. 'http://www.bbc.co.uk/iplayer/episode/%s.json' % group_id,
  293. group_id)['jsConf']['player']
  294. title = player['title']
  295. description = player['subtitle']
  296. duration = player['duration']
  297. formats, subtitles = self._download_media_selector(programme_id)
  298. else:
  299. programme_id, title, description, duration, formats, subtitles = self._download_playlist(group_id)
  300. if self._downloader.params.get('listsubtitles', False):
  301. self._list_available_subtitles(programme_id, subtitles)
  302. return
  303. self._sort_formats(formats)
  304. return {
  305. 'id': programme_id,
  306. 'title': title,
  307. 'description': description,
  308. 'duration': duration,
  309. 'formats': formats,
  310. 'subtitles': subtitles,
  311. }