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.

337 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. ]
  118. def _extract_asx_playlist(self, connection, programme_id):
  119. asx = self._download_xml(connection.get('href'), programme_id, 'Downloading ASX playlist')
  120. return [ref.get('href') for ref in asx.findall('./Entry/ref')]
  121. def _extract_connection(self, connection, programme_id):
  122. formats = []
  123. protocol = connection.get('protocol')
  124. supplier = connection.get('supplier')
  125. if protocol == 'http':
  126. href = connection.get('href')
  127. # ASX playlist
  128. if supplier == 'asx':
  129. for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)):
  130. formats.append({
  131. 'url': ref,
  132. 'format_id': 'ref%s_%s' % (i, supplier),
  133. })
  134. # Direct link
  135. else:
  136. formats.append({
  137. 'url': href,
  138. 'format_id': supplier,
  139. })
  140. elif protocol == 'rtmp':
  141. application = connection.get('application', 'ondemand')
  142. auth_string = connection.get('authString')
  143. identifier = connection.get('identifier')
  144. server = connection.get('server')
  145. formats.append({
  146. 'url': '%s://%s/%s?%s' % (protocol, server, application, auth_string),
  147. 'play_path': identifier,
  148. 'app': '%s?%s' % (application, auth_string),
  149. 'page_url': 'http://www.bbc.co.uk',
  150. 'player_url': 'http://www.bbc.co.uk/emp/releases/iplayer/revisions/617463_618125_4/617463_618125_4_emp.swf',
  151. 'rtmp_live': False,
  152. 'ext': 'flv',
  153. 'format_id': supplier,
  154. })
  155. return formats
  156. def _extract_items(self, playlist):
  157. return playlist.findall('./{http://bbc.co.uk/2008/emp/playlist}item')
  158. def _extract_medias(self, media_selection):
  159. error = media_selection.find('./{http://bbc.co.uk/2008/mp/mediaselection}error')
  160. if error is not None:
  161. raise ExtractorError(
  162. '%s returned error: %s' % (self.IE_NAME, error.get('id')), expected=True)
  163. return media_selection.findall('./{http://bbc.co.uk/2008/mp/mediaselection}media')
  164. def _extract_connections(self, media):
  165. return media.findall('./{http://bbc.co.uk/2008/mp/mediaselection}connection')
  166. def _extract_video(self, media, programme_id):
  167. formats = []
  168. vbr = int(media.get('bitrate'))
  169. vcodec = media.get('encoding')
  170. service = media.get('service')
  171. width = int(media.get('width'))
  172. height = int(media.get('height'))
  173. file_size = int(media.get('media_file_size'))
  174. for connection in self._extract_connections(media):
  175. conn_formats = self._extract_connection(connection, programme_id)
  176. for format in conn_formats:
  177. format.update({
  178. 'format_id': '%s_%s' % (service, format['format_id']),
  179. 'width': width,
  180. 'height': height,
  181. 'vbr': vbr,
  182. 'vcodec': vcodec,
  183. 'filesize': file_size,
  184. })
  185. formats.extend(conn_formats)
  186. return formats
  187. def _extract_audio(self, media, programme_id):
  188. formats = []
  189. abr = int(media.get('bitrate'))
  190. acodec = media.get('encoding')
  191. service = media.get('service')
  192. for connection in self._extract_connections(media):
  193. conn_formats = self._extract_connection(connection, programme_id)
  194. for format in conn_formats:
  195. format.update({
  196. 'format_id': '%s_%s' % (service, format['format_id']),
  197. 'abr': abr,
  198. 'acodec': acodec,
  199. })
  200. formats.extend(conn_formats)
  201. return formats
  202. def _extract_captions(self, media, programme_id):
  203. subtitles = {}
  204. for connection in self._extract_connections(media):
  205. captions = self._download_xml(connection.get('href'), programme_id, 'Downloading captions')
  206. lang = captions.get('{http://www.w3.org/XML/1998/namespace}lang', 'en')
  207. ps = captions.findall('./{0}body/{0}div/{0}p'.format('{http://www.w3.org/2006/10/ttaf1}'))
  208. srt = ''
  209. for pos, p in enumerate(ps):
  210. srt += '%s\r\n%s --> %s\r\n%s\r\n\r\n' % (str(pos), p.get('begin'), p.get('end'),
  211. p.text.strip() if p.text is not None else '')
  212. subtitles[lang] = srt
  213. return subtitles
  214. def _download_media_selector(self, programme_id):
  215. try:
  216. media_selection = self._download_xml(
  217. 'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s' % programme_id,
  218. programme_id, 'Downloading media selection XML')
  219. except ExtractorError as ee:
  220. if isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 403:
  221. media_selection = xml.etree.ElementTree.fromstring(ee.cause.read().encode('utf-8'))
  222. else:
  223. raise
  224. formats = []
  225. subtitles = None
  226. for media in self._extract_medias(media_selection):
  227. kind = media.get('kind')
  228. if kind == 'audio':
  229. formats.extend(self._extract_audio(media, programme_id))
  230. elif kind == 'video':
  231. formats.extend(self._extract_video(media, programme_id))
  232. elif kind == 'captions':
  233. subtitles = self._extract_captions(media, programme_id)
  234. return formats, subtitles
  235. def _download_playlist(self, playlist_id):
  236. try:
  237. playlist = self._download_json(
  238. 'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
  239. playlist_id, 'Downloading playlist JSON')
  240. version = playlist.get('defaultAvailableVersion')
  241. if version:
  242. smp_config = version['smpConfig']
  243. title = smp_config['title']
  244. description = smp_config['summary']
  245. for item in smp_config['items']:
  246. kind = item['kind']
  247. if kind != 'programme' and kind != 'radioProgramme':
  248. continue
  249. programme_id = item.get('vpid')
  250. duration = int(item.get('duration'))
  251. formats, subtitles = self._download_media_selector(programme_id)
  252. return programme_id, title, description, duration, formats, subtitles
  253. except ExtractorError as ee:
  254. if not isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404:
  255. raise
  256. # fallback to legacy playlist
  257. playlist = self._download_xml(
  258. 'http://www.bbc.co.uk/iplayer/playlist/%s' % playlist_id,
  259. playlist_id, 'Downloading legacy playlist XML')
  260. no_items = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}noItems')
  261. if no_items is not None:
  262. reason = no_items.get('reason')
  263. if reason == 'preAvailability':
  264. msg = 'Episode %s is not yet available' % playlist_id
  265. elif reason == 'postAvailability':
  266. msg = 'Episode %s is no longer available' % playlist_id
  267. elif reason == 'noMedia':
  268. msg = 'Episode %s is not currently available' % playlist_id
  269. else:
  270. msg = 'Episode %s is not available: %s' % (playlist_id, reason)
  271. raise ExtractorError(msg, expected=True)
  272. for item in self._extract_items(playlist):
  273. kind = item.get('kind')
  274. if kind != 'programme' and kind != 'radioProgramme':
  275. continue
  276. title = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}title').text
  277. description = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}summary').text
  278. programme_id = item.get('identifier')
  279. duration = int(item.get('duration'))
  280. formats, subtitles = self._download_media_selector(programme_id)
  281. return programme_id, title, description, duration, formats, subtitles
  282. def _real_extract(self, url):
  283. group_id = self._match_id(url)
  284. webpage = self._download_webpage(url, group_id, 'Downloading video page')
  285. programme_id = self._search_regex(
  286. r'"vpid"\s*:\s*"([\da-z]{8})"', webpage, 'vpid', fatal=False, default=None)
  287. if programme_id:
  288. player = self._download_json(
  289. 'http://www.bbc.co.uk/iplayer/episode/%s.json' % group_id,
  290. group_id)['jsConf']['player']
  291. title = player['title']
  292. description = player['subtitle']
  293. duration = player['duration']
  294. formats, subtitles = self._download_media_selector(programme_id)
  295. else:
  296. programme_id, title, description, duration, formats, subtitles = self._download_playlist(group_id)
  297. if self._downloader.params.get('listsubtitles', False):
  298. self._list_available_subtitles(programme_id, subtitles)
  299. return
  300. self._sort_formats(formats)
  301. return {
  302. 'id': programme_id,
  303. 'title': title,
  304. 'description': description,
  305. 'duration': duration,
  306. 'formats': formats,
  307. 'subtitles': subtitles,
  308. }