2 from __future__ 
import unicode_literals
 
   7 from .common 
import InfoExtractor
 
  22 from ..compat 
import ( 
  23     compat_etree_fromstring
, 
  29 class BBCCoUkIE(InfoExtractor
): 
  31     IE_DESC 
= 'BBC iPlayer' 
  32     _ID_REGEX 
= r
'[pbw][\da-z]{7}' 
  35                         (?:www\.)?bbc\.co\.uk/ 
  37                             programmes/(?!articles/)| 
  38                             iplayer(?:/[^/]+)?/(?:episode/|playlist/)| 
  39                             music/(?:clips|audiovideo/popular)[/#]| 
  41                             events/[^/]+/play/[^/]+/ 
  43                         (?P<id>%s)(?!/(?:episodes|broadcasts|clips)) 
  46     _LOGIN_URL 
= 'https://account.bbc.com/signin' 
  47     _NETRC_MACHINE 
= 'bbc' 
  49     _MEDIASELECTOR_URLS 
= [ 
  50         # Provides HQ HLS streams with even better quality that pc mediaset but fails 
  51         # with geolocation in some cases when it's even not geo restricted at all (e.g. 
  52         # http://www.bbc.co.uk/programmes/b06bp7lf). Also may fail with selectionunavailable. 
  53         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/%s', 
  54         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s', 
  57     _MEDIASELECTION_NS 
= 'http://bbc.co.uk/2008/mp/mediaselection' 
  58     _EMP_PLAYLIST_NS 
= 'http://bbc.co.uk/2008/emp/playlist' 
  67             'url': 'http://www.bbc.co.uk/programmes/b039g8p7', 
  71                 'title': 'Leonard Cohen, Kaleidoscope - BBC Radio 4', 
  72                 'description': 'The Canadian poet and songwriter reflects on his musical career.', 
  76                 'skip_download': True, 
  80             'url': 'http://www.bbc.co.uk/iplayer/episode/b00yng5w/The_Man_in_Black_Series_3_The_Printed_Name/', 
  84                 'title': 'The Man in Black: Series 3: The Printed Name', 
  85                 'description': "Mark Gatiss introduces Nicholas Pierpan's chilling tale of a writer's devilish pact with a mysterious man. Stars Ewan Bailey.", 
  90                 'skip_download': True, 
  92             'skip': 'Episode is no longer available on BBC iPlayer Radio', 
  95             'url': 'http://www.bbc.co.uk/iplayer/episode/b03vhd1f/The_Voice_UK_Series_3_Blind_Auditions_5/', 
  99                 'title': 'The Voice UK: Series 3: Blind Auditions 5', 
 100                 '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.', 
 105                 'skip_download': True, 
 107             'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only', 
 110             'url': 'http://www.bbc.co.uk/iplayer/episode/p026c7jt/tomorrows-worlds-the-unearthly-history-of-science-fiction-2-invasion', 
 114                 'title': "Tomorrow's Worlds: The Unearthly History of Science Fiction", 
 115                 'description': '2. Invasion', 
 120                 'skip_download': True, 
 122             'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only', 
 124             'url': 'http://www.bbc.co.uk/programmes/b04v20dw', 
 128                 'title': 'Pete Tong, The Essential New Tune Special', 
 129                 'description': "Pete has a very special mix - all of 2014's Essential New Tunes!", 
 134                 'skip_download': True, 
 136             'skip': 'Episode is no longer available on BBC iPlayer Radio', 
 138             'url': 'http://www.bbc.co.uk/music/clips/p022h44b', 
 143                 'title': 'BBC Proms Music Guides, Rachmaninov: Symphonic Dances', 
 144                 'description': "In this Proms Music Guide, Andrew McGregor looks at Rachmaninov's Symphonic Dances.", 
 149                 'skip_download': True, 
 152             'url': 'http://www.bbc.co.uk/music/clips/p025c0zz', 
 157                 'title': 'Reading and Leeds Festival, 2014, Rae Morris - Closer (Live on BBC Three)', 
 158                 'description': 'Rae Morris performs Closer for BBC Three at Reading 2014', 
 163                 'skip_download': True, 
 166             'url': 'http://www.bbc.co.uk/iplayer/episode/b054fn09/ad/natural-world-20152016-2-super-powered-owls', 
 170                 'title': 'Natural World, 2015-2016: 2. Super Powered Owls', 
 171                 'description': 'md5:e4db5c937d0e95a7c6b5e654d429183d', 
 176                 'skip_download': True, 
 178             'skip': 'geolocation', 
 180             'url': 'http://www.bbc.co.uk/iplayer/episode/b05zmgwn/royal-academy-summer-exhibition', 
 184                 'description': 'Kirsty Wark and Morgan Quaintance visit the Royal Academy as it prepares for its annual artistic extravaganza, meeting people who have come together to make the show unique.', 
 185                 'title': 'Royal Academy Summer Exhibition', 
 190                 'skip_download': True, 
 192             'skip': 'geolocation', 
 194             # iptv-all mediaset fails with geolocation however there is no geo restriction 
 195             # for this programme at all 
 196             'url': 'http://www.bbc.co.uk/programmes/b06rkn85', 
 200                 'title': "Best of the Mini-Mixes 2015: Part 3, Annie Mac's Friday Night - BBC Radio 1", 
 201                 'description': "Annie has part three in the Best of the Mini-Mixes 2015, plus the year's Most Played!", 
 205                 'skip_download': True, 
 207             'skip': 'Now it\'s really geo-restricted', 
 209             # compact player (https://github.com/rg3/youtube-dl/issues/8147) 
 210             'url': 'http://www.bbc.co.uk/programmes/p028bfkf/player', 
 214                 'title': 'Extract from BBC documentary Look Stranger - Giant Leeks and Magic Brews', 
 215                 'description': 'Extract from BBC documentary Look Stranger - Giant Leeks and Magic Brews', 
 219                 'skip_download': True, 
 222             'url': 'http://www.bbc.co.uk/iplayer/playlist/p01dvks4', 
 223             'only_matching': True, 
 225             'url': 'http://www.bbc.co.uk/music/clips#p02frcc3', 
 226             'only_matching': True, 
 228             'url': 'http://www.bbc.co.uk/iplayer/cbeebies/episode/b0480276/bing-14-atchoo', 
 229             'only_matching': True, 
 231             'url': 'http://www.bbc.co.uk/radio/player/p03cchwf', 
 232             'only_matching': True, 
 234             'url': 'https://www.bbc.co.uk/music/audiovideo/popular#p055bc55', 
 235             'only_matching': True, 
 237             'url': 'http://www.bbc.co.uk/programmes/w3csv1y9', 
 238             'only_matching': True, 
 241     _USP_RE 
= r
'/([^/]+?)\.ism(?:\.hlsv2\.ism)?/[^/]+\.m3u8' 
 244         username
, password 
= self
._get
_login
_info
() 
 248         login_page 
= self
._download
_webpage
( 
 249             self
._LOGIN
_URL
, None, 'Downloading signin page') 
 251         login_form 
= self
._hidden
_inputs
(login_page
) 
 254             'username': username
, 
 255             'password': password
, 
 258         post_url 
= urljoin(self
._LOGIN
_URL
, self
._search
_regex
( 
 259             r
'<form[^>]+action=(["\'])(?P
<url
>.+?
)\
1', login_page, 
 260             'post url
', default=self._LOGIN_URL, group='url
')) 
 262         response, urlh = self._download_webpage_handle( 
 263             post_url, None, 'Logging 
in', data=urlencode_postdata(login_form), 
 264             headers={'Referer
': self._LOGIN_URL}) 
 266         if self._LOGIN_URL in urlh.geturl(): 
 267             error = clean_html(get_element_by_class('form
-message
', response)) 
 269                 raise ExtractorError( 
 270                     'Unable to login
: %s' % error, expected=True) 
 271             raise ExtractorError('Unable to log 
in') 
 273     def _real_initialize(self): 
 276     class MediaSelectionError(Exception): 
 277         def __init__(self, id): 
 280     def _extract_asx_playlist(self, connection, programme_id): 
 281         asx = self._download_xml(connection.get('href
'), programme_id, 'Downloading ASX playlist
') 
 282         return [ref.get('href
') for ref in asx.findall('./Entry
/ref
')] 
 284     def _extract_items(self, playlist): 
 285         return playlist.findall('./{%s}item
' % self._EMP_PLAYLIST_NS) 
 287     def _findall_ns(self, element, xpath): 
 289         for ns in self._NAMESPACES: 
 290             elements.extend(element.findall(xpath % ns)) 
 293     def _extract_medias(self, media_selection): 
 294         error = media_selection.find('./{%s}error
' % self._MEDIASELECTION_NS) 
 296             media_selection.find('./{%s}error
' % self._EMP_PLAYLIST_NS) 
 297         if error is not None: 
 298             raise BBCCoUkIE.MediaSelectionError(error.get('id')) 
 299         return self._findall_ns(media_selection, './{%s}media
') 
 301     def _extract_connections(self, media): 
 302         return self._findall_ns(media, './{%s}connection
') 
 304     def _get_subtitles(self, media, programme_id): 
 306         for connection in self._extract_connections(media): 
 307             captions = self._download_xml(connection.get('href
'), programme_id, 'Downloading captions
') 
 308             lang = captions.get('{http
://www
.w3
.org
/XML
/1998/namespace
}lang
', 'en
') 
 311                     'url
': connection.get('href
'), 
 317     def _raise_extractor_error(self, media_selection_error): 
 318         raise ExtractorError( 
 319             '%s returned error
: %s' % (self.IE_NAME, media_selection_error.id), 
 322     def _download_media_selector(self, programme_id): 
 323         last_exception = None 
 324         for mediaselector_url in self._MEDIASELECTOR_URLS: 
 326                 return self._download_media_selector_url( 
 327                     mediaselector_url % programme_id, programme_id) 
 328             except BBCCoUkIE.MediaSelectionError as e: 
 329                 if e.id in ('notukerror
', 'geolocation
', 'selectionunavailable
'): 
 332                 self._raise_extractor_error(e) 
 333         self._raise_extractor_error(last_exception) 
 335     def _download_media_selector_url(self, url, programme_id=None): 
 337             media_selection = self._download_xml( 
 338                 url, programme_id, 'Downloading media selection XML
') 
 339         except ExtractorError as ee: 
 340             if isinstance(ee.cause, compat_HTTPError) and ee.cause.code in (403, 404): 
 341                 media_selection = compat_etree_fromstring(ee.cause.read().decode('utf
-8')) 
 344         return self._process_media_selector(media_selection, programme_id) 
 346     def _process_media_selector(self, media_selection, programme_id): 
 351         for media in self._extract_medias(media_selection): 
 352             kind = media.get('kind
') 
 353             if kind in ('video
', 'audio
'): 
 354                 bitrate = int_or_none(media.get('bitrate
')) 
 355                 encoding = media.get('encoding
') 
 356                 service = media.get('service
') 
 357                 width = int_or_none(media.get('width
')) 
 358                 height = int_or_none(media.get('height
')) 
 359                 file_size = int_or_none(media.get('media_file_size
')) 
 360                 for connection in self._extract_connections(media): 
 361                     href = connection.get('href
') 
 366                     conn_kind = connection.get('kind
') 
 367                     protocol = connection.get('protocol
') 
 368                     supplier = connection.get('supplier
') 
 369                     transfer_format = connection.get('transferFormat
') 
 370                     format_id = supplier or conn_kind or protocol 
 372                         format_id = '%s_%s' % (service, format_id) 
 374                     if supplier == 'asx
': 
 375                         for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)): 
 378                                 'format_id
': 'ref
%s_%s' % (i, format_id), 
 380                     elif transfer_format == 'dash
': 
 381                         formats.extend(self._extract_mpd_formats( 
 382                             href, programme_id, mpd_id=format_id, fatal=False)) 
 383                     elif transfer_format == 'hls
': 
 384                         formats.extend(self._extract_m3u8_formats( 
 385                             href, programme_id, ext='mp4
', entry_protocol='m3u8_native
', 
 386                             m3u8_id=format_id, fatal=False)) 
 387                         if re.search(self._USP_RE, href): 
 388                             usp_formats = self._extract_m3u8_formats( 
 389                                 re.sub(self._USP_RE, r'/\
1.ism
/\
1.m3u8
', href), 
 390                                 programme_id, ext='mp4
', entry_protocol='m3u8_native
', 
 391                                 m3u8_id=format_id, fatal=False) 
 392                             for f in usp_formats: 
 393                                 if f.get('height
') and f['height
'] > 720: 
 396                     elif transfer_format == 'hds
': 
 397                         formats.extend(self._extract_f4m_formats( 
 398                             href, programme_id, f4m_id=format_id, fatal=False)) 
 400                         if not service and not supplier and bitrate: 
 401                             format_id += '-%d' % bitrate 
 403                             'format_id
': format_id, 
 404                             'filesize
': file_size, 
 419                         if protocol in ('http
', 'https
'): 
 424                         elif protocol == 'rtmp
': 
 425                             application = connection.get('application
', 'ondemand
') 
 426                             auth_string = connection.get('authString
') 
 427                             identifier = connection.get('identifier
') 
 428                             server = connection.get('server
') 
 430                                 'url
': '%s://%s/%s?
%s' % (protocol, server, application, auth_string), 
 431                                 'play_path
': identifier, 
 432                                 'app
': '%s?
%s' % (application, auth_string), 
 433                                 'page_url
': 'http
://www
.bbc
.co
.uk
', 
 434                                 'player_url
': 'http
://www
.bbc
.co
.uk
/emp
/releases
/iplayer
/revisions
/617463_618125_4/617463_618125_4_emp
.swf
', 
 441             elif kind == 'captions
': 
 442                 subtitles = self.extract_subtitles(media, programme_id) 
 443         return formats, subtitles 
 445     def _download_playlist(self, playlist_id): 
 447             playlist = self._download_json( 
 448                 'http
://www
.bbc
.co
.uk
/programmes
/%s/playlist
.json
' % playlist_id, 
 449                 playlist_id, 'Downloading playlist JSON
') 
 451             version = playlist.get('defaultAvailableVersion
') 
 453                 smp_config = version['smpConfig
'] 
 454                 title = smp_config['title
'] 
 455                 description = smp_config['summary
'] 
 456                 for item in smp_config['items
']: 
 458                     if kind not in ('programme
', 'radioProgramme
'): 
 460                     programme_id = item.get('vpid
') 
 461                     duration = int_or_none(item.get('duration
')) 
 462                     formats, subtitles = self._download_media_selector(programme_id) 
 463                 return programme_id, title, description, duration, formats, subtitles 
 464         except ExtractorError as ee: 
 465             if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404): 
 468         # fallback to legacy playlist 
 469         return self._process_legacy_playlist(playlist_id) 
 471     def _process_legacy_playlist_url(self, url, display_id): 
 472         playlist = self._download_legacy_playlist_url(url, display_id) 
 473         return self._extract_from_legacy_playlist(playlist, display_id) 
 475     def _process_legacy_playlist(self, playlist_id): 
 476         return self._process_legacy_playlist_url( 
 477             'http
://www
.bbc
.co
.uk
/iplayer
/playlist
/%s' % playlist_id, playlist_id) 
 479     def _download_legacy_playlist_url(self, url, playlist_id=None): 
 480         return self._download_xml( 
 481             url, playlist_id, 'Downloading legacy playlist XML
') 
 483     def _extract_from_legacy_playlist(self, playlist, playlist_id): 
 484         no_items = playlist.find('./{%s}noItems
' % self._EMP_PLAYLIST_NS) 
 485         if no_items is not None: 
 486             reason = no_items.get('reason
') 
 487             if reason == 'preAvailability
': 
 488                 msg = 'Episode 
%s is not yet available
' % playlist_id 
 489             elif reason == 'postAvailability
': 
 490                 msg = 'Episode 
%s is no longer available
' % playlist_id 
 491             elif reason == 'noMedia
': 
 492                 msg = 'Episode 
%s is not currently available
' % playlist_id 
 494                 msg = 'Episode 
%s is not available
: %s' % (playlist_id, reason) 
 495             raise ExtractorError(msg, expected=True) 
 497         for item in self._extract_items(playlist): 
 498             kind = item.get('kind
') 
 499             if kind not in ('programme
', 'radioProgramme
'): 
 501             title = playlist.find('./{%s}title
' % self._EMP_PLAYLIST_NS).text 
 502             description_el = playlist.find('./{%s}summary
' % self._EMP_PLAYLIST_NS) 
 503             description = description_el.text if description_el is not None else None 
 505             def get_programme_id(item): 
 506                 def get_from_attributes(item): 
 507                     for p in('identifier
', 'group
'): 
 509                         if value and re.match(r'^
[pb
][\da
-z
]{7}$
', value): 
 511                 get_from_attributes(item) 
 512                 mediator = item.find('./{%s}mediator
' % self._EMP_PLAYLIST_NS) 
 513                 if mediator is not None: 
 514                     return get_from_attributes(mediator) 
 516             programme_id = get_programme_id(item) 
 517             duration = int_or_none(item.get('duration
')) 
 520                 formats, subtitles = self._download_media_selector(programme_id) 
 522                 formats, subtitles = self._process_media_selector(item, playlist_id) 
 523                 programme_id = playlist_id 
 525         return programme_id, title, description, duration, formats, subtitles 
 527     def _real_extract(self, url): 
 528         group_id = self._match_id(url) 
 530         webpage = self._download_webpage(url, group_id, 'Downloading video page
') 
 532         error = self._search_regex( 
 533             r'<div
\b[^
>]+\bclass
=["\']smp__message delta["\'][^
>]*>([^
<]+)<', 
 534             webpage, 'error
', default=None) 
 536             raise ExtractorError(error, expected=True) 
 541         tviplayer = self._search_regex( 
 542             r'mediator\
.bind\
(({.+?
})\s
*,\s
*document\
.getElementById
', 
 543             webpage, 'player
', default=None) 
 546             player = self._parse_json(tviplayer, group_id).get('player
', {}) 
 547             duration = int_or_none(player.get('duration
')) 
 548             programme_id = player.get('vpid
') 
 551             programme_id = self._search_regex( 
 552                 r'"vpid"\s
*:\s
*"(%s)"' % self._ID_REGEX, webpage, 'vpid
', fatal=False, default=None) 
 555             formats, subtitles = self._download_media_selector(programme_id) 
 556             title = self._og_search_title(webpage, default=None) or self._html_search_regex( 
 557                 (r'<h2
[^
>]+id="parent-title"[^
>]*>(.+?
)</h2
>', 
 558                  r'<div
[^
>]+class="info"[^
>]*>\s
*<h1
>(.+?
)</h1
>'), webpage, 'title
') 
 559             description = self._search_regex( 
 560                 (r'<p 
class="[^"]*medium
-description
[^
"]*">([^
<]+)</p
>', 
 561                  r'<div
[^
>]+class="info_+synopsis"[^
>]*>([^
<]+)</div
>'), 
 562                 webpage, 'description
', default=None) 
 564                 description = self._html_search_meta('description
', webpage) 
 566             programme_id, title, description, duration, formats, subtitles = self._download_playlist(group_id) 
 568         self._sort_formats(formats) 
 573             'description
': description, 
 574             'thumbnail
': self._og_search_thumbnail(webpage, default=None), 
 575             'duration
': duration, 
 577             'subtitles
': subtitles, 
 581 class BBCIE(BBCCoUkIE): 
 584     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.(?
:com|co\
.uk
)/(?
:[^
/]+/)+(?P
<id>[^
/#?]+)' 
 586     _MEDIASELECTOR_URLS 
= [ 
 587         # Provides HQ HLS streams but fails with geolocation in some cases when it's 
 588         # even not geo restricted at all 
 589         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/%s', 
 590         # Provides more formats, namely direct mp4 links, but fails on some videos with 
 591         # notukerror for non UK (?) users (e.g. 
 592         # http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret) 
 593         'http://open.live.bbc.co.uk/mediaselector/4/mtis/stream/%s', 
 594         # Provides fewer formats, but works everywhere for everybody (hopefully) 
 595         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/journalism-pc/vpid/%s', 
 599         # article with multiple videos embedded with data-playable containing vpids 
 600         'url': 'http://www.bbc.com/news/world-europe-32668511', 
 602             'id': 'world-europe-32668511', 
 603             'title': 'Russia stages massive WW2 parade despite Western boycott', 
 604             'description': 'md5:00ff61976f6081841f759a08bf78cc9c', 
 608         # article with multiple videos embedded with data-playable (more videos) 
 609         'url': 'http://www.bbc.com/news/business-28299555', 
 611             'id': 'business-28299555', 
 612             'title': 'Farnborough Airshow: Video highlights', 
 613             'description': 'BBC reports and video highlights at the Farnborough Airshow.', 
 618         # article with multiple videos embedded with `new SMP()` 
 620         'url': 'http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460', 
 622             'id': '3662a707-0af9-3149-963f-47bea720b460', 
 625         'playlist_count': 18, 
 627         # single video embedded with data-playable containing vpid 
 628         'url': 'http://www.bbc.com/news/world-europe-32041533', 
 632             'title': 'Aerial footage showed the site of the crash in the Alps - courtesy BFM TV', 
 633             'description': 'md5:2868290467291b37feda7863f7a83f54', 
 635             'timestamp': 1427219242, 
 636             'upload_date': '20150324', 
 640             'skip_download': True, 
 643         # article with single video embedded with data-playable containing XML playlist 
 644         # with direct video links as progressiveDownloadUrl (for now these are extracted) 
 645         # and playlist with f4m and m3u8 as streamingUrl 
 646         'url': 'http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu', 
 648             'id': '150615_telabyad_kentin_cogu', 
 650             'title': "YPG: Tel Abyad'ın tamamı kontrolümüzde", 
 651             'description': 'md5:33a4805a855c9baf7115fcbde57e7025', 
 652             'timestamp': 1434397334, 
 653             'upload_date': '20150615', 
 656             'skip_download': True, 
 659         # single video embedded with data-playable containing XML playlists (regional section) 
 660         'url': 'http://www.bbc.com/mundo/video_fotos/2015/06/150619_video_honduras_militares_hospitales_corrupcion_aw', 
 662             'id': '150619_video_honduras_militares_hospitales_corrupcion_aw', 
 664             'title': 'Honduras militariza sus hospitales por nuevo escÔndalo de corrupción', 
 665             'description': 'md5:1525f17448c4ee262b64b8f0c9ce66c8', 
 666             'timestamp': 1434713142, 
 667             'upload_date': '20150619', 
 670             'skip_download': True, 
 673         # single video from video playlist embedded with vxp-playlist-data JSON 
 674         'url': 'http://www.bbc.com/news/video_and_audio/must_see/33376376', 
 678             'title': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''', 
 680             'description': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''', 
 683             'skip_download': True, 
 686         # single video story with digitalData 
 687         'url': 'http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret', 
 691             'title': 'Sri Lankaās spicy secret', 
 692             'description': 'As a new train line to Jaffna opens up the countryās north, travellers can experience a truly distinct slice of Tamil culture.', 
 693             'timestamp': 1437674293, 
 694             'upload_date': '20150723', 
 698             'skip_download': True, 
 701         # single video story without digitalData 
 702         'url': 'http://www.bbc.com/autos/story/20130513-hyundais-rock-star', 
 706             'title': 'Hyundai Santa Fe Sport: Rock star', 
 707             'description': 'md5:b042a26142c4154a6e472933cf20793d', 
 708             'timestamp': 1415867444, 
 709             'upload_date': '20141113', 
 713             'skip_download': True, 
 716         # single video embedded with Morph 
 717         'url': 'http://www.bbc.co.uk/sport/live/olympics/36895975', 
 721             'title': "Nigeria v Japan - Men's First Round", 
 722             'description': 'Live coverage of the first round from Group B at the Amazonia Arena.', 
 724             'uploader': 'BBC Sport', 
 725             'uploader_id': 'bbc_sport', 
 729             'skip_download': True, 
 731         'skip': 'Georestricted to UK', 
 733         # single video with playlist.sxml URL in playlist param 
 734         'url': 'http://www.bbc.com/sport/0/football/33653409', 
 738             'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?', 
 739             'description': 'BBC Sport\'s David Ornstein has the latest transfer gossip, including rumours of a Manchester United return for Cristiano Ronaldo.', 
 744             'skip_download': True, 
 747         # article with multiple videos embedded with playlist.sxml in playlist param 
 748         'url': 'http://www.bbc.com/sport/0/football/34475836', 
 751             'title': 'Jurgen Klopp: Furious football from a witty and winning coach', 
 752             'description': 'Fast-paced football, wit, wisdom and a ready smile - why Liverpool fans should come to love new boss Jurgen Klopp.', 
 756         # school report article with single video 
 757         'url': 'http://www.bbc.co.uk/schoolreport/35744779', 
 760             'title': 'School which breaks down barriers in Jerusalem', 
 764         # single video with playlist URL from weather section 
 765         'url': 'http://www.bbc.com/weather/features/33601775', 
 766         'only_matching': True, 
 768         # custom redirection to www.bbc.com 
 769         'url': 'http://www.bbc.co.uk/news/science-environment-33661876', 
 770         'only_matching': True, 
 772         # single video article embedded with data-media-vpid 
 773         'url': 'http://www.bbc.co.uk/sport/rowing/35908187', 
 774         'only_matching': True, 
 778     def suitable(cls
, url
): 
 779         EXCLUDE_IE 
= (BBCCoUkIE
, BBCCoUkArticleIE
, BBCCoUkIPlayerPlaylistIE
, BBCCoUkPlaylistIE
) 
 780         return (False if any(ie
.suitable(url
) for ie 
in EXCLUDE_IE
) 
 781                 else super(BBCIE
, cls
).suitable(url
)) 
 783     def _extract_from_media_meta(self
, media_meta
, video_id
): 
 784         # Direct links to media in media metadata (e.g. 
 785         # http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu) 
 786         # TODO: there are also f4m and m3u8 streams incorporated in playlist.sxml 
 787         source_files 
= media_meta
.get('sourceFiles') 
 791                 'format_id': format_id
, 
 792                 'ext': f
.get('encoding'), 
 793                 'tbr': float_or_none(f
.get('bitrate'), 1000), 
 794                 'filesize': int_or_none(f
.get('filesize')), 
 795             } for format_id
, f 
in source_files
.items() if f
.get('url')], [] 
 797         programme_id 
= media_meta
.get('externalId') 
 799             return self
._download
_media
_selector
(programme_id
) 
 801         # Process playlist.sxml as legacy playlist 
 802         href 
= media_meta
.get('href') 
 804             playlist 
= self
._download
_legacy
_playlist
_url
(href
) 
 805             _
, _
, _
, _
, formats
, subtitles 
= self
._extract
_from
_legacy
_playlist
(playlist
, video_id
) 
 806             return formats
, subtitles
 
 810     def _extract_from_playlist_sxml(self
, url
, playlist_id
, timestamp
): 
 811         programme_id
, title
, description
, duration
, formats
, subtitles 
= \
 
 812             self
._process
_legacy
_playlist
_url
(url
, playlist_id
) 
 813         self
._sort
_formats
(formats
) 
 817             'description': description
, 
 818             'duration': duration
, 
 819             'timestamp': timestamp
, 
 821             'subtitles': subtitles
, 
 824     def _real_extract(self
, url
): 
 825         playlist_id 
= self
._match
_id
(url
) 
 827         webpage 
= self
._download
_webpage
(url
, playlist_id
) 
 829         json_ld_info 
= self
._search
_json
_ld
(webpage
, playlist_id
, default
={}) 
 830         timestamp 
= json_ld_info
.get('timestamp') 
 832         playlist_title 
= json_ld_info
.get('title') 
 833         if not playlist_title
: 
 834             playlist_title 
= self
._og
_search
_title
( 
 835                 webpage
, default
=None) or self
._html
_search
_regex
( 
 836                 r
'<title>(.+?)</title>', webpage
, 'playlist title', default
=None) 
 838                 playlist_title 
= re
.sub(r
'(.+)\s*-\s*BBC.*?$', r
'\1', playlist_title
).strip() 
 840         playlist_description 
= json_ld_info
.get( 
 841             'description') or self
._og
_search
_description
(webpage
, default
=None) 
 844             timestamp 
= parse_iso8601(self
._search
_regex
( 
 845                 [r
'<meta[^>]+property="article:published_time"[^>]+content="([^"]+)"', 
 846                  r
'itemprop="datePublished"[^>]+datetime="([^"]+)"', 
 847                  r
'"datePublished":\s*"([^"]+)'], 
 848                 webpage
, 'date', default
=None)) 
 852         # article with multiple videos embedded with playlist.sxml (e.g. 
 853         # http://www.bbc.com/sport/0/football/34475836) 
 854         playlists 
= re
.findall(r
'<param[^>]+name="playlist"[^>]+value="([^"]+)"', webpage
) 
 855         playlists
.extend(re
.findall(r
'data-media-id="([^"]+/playlist\.sxml)"', webpage
)) 
 858                 self
._extract
_from
_playlist
_sxml
(playlist_url
, playlist_id
, timestamp
) 
 859                 for playlist_url 
in playlists
] 
 861         # news article with multiple videos embedded with data-playable 
 862         data_playables 
= re
.findall(r
'data-playable=(["\'])({.+?
})\
1', webpage) 
 864             for _, data_playable_json in data_playables: 
 865                 data_playable = self._parse_json( 
 866                     unescapeHTML(data_playable_json), playlist_id, fatal=False) 
 867                 if not data_playable: 
 869                 settings = data_playable.get('settings
', {}) 
 871                     # data-playable with video vpid in settings.playlistObject.items (e.g. 
 872                     # http://www.bbc.com/news/world-us-canada-34473351) 
 873                     playlist_object = settings.get('playlistObject
', {}) 
 875                         items = playlist_object.get('items
') 
 876                         if items and isinstance(items, list): 
 877                             title = playlist_object['title
'] 
 878                             description = playlist_object.get('summary
') 
 879                             duration = int_or_none(items[0].get('duration
')) 
 880                             programme_id = items[0].get('vpid
') 
 881                             formats, subtitles = self._download_media_selector(programme_id) 
 882                             self._sort_formats(formats) 
 886                                 'description
': description, 
 887                                 'timestamp
': timestamp, 
 888                                 'duration
': duration, 
 890                                 'subtitles
': subtitles, 
 893                         # data-playable without vpid but with a playlist.sxml URLs 
 894                         # in otherSettings.playlist (e.g. 
 895                         # http://www.bbc.com/turkce/multimedya/2015/10/151010_vid_ankara_patlama_ani) 
 896                         playlist = data_playable.get('otherSettings
', {}).get('playlist
', {}) 
 899                             for key in ('streaming
', 'progressiveDownload
'): 
 900                                 playlist_url = playlist.get('%sUrl
' % key) 
 904                                     info = self._extract_from_playlist_sxml( 
 905                                         playlist_url, playlist_id, timestamp) 
 909                                         entry['title
'] = info['title
'] 
 910                                         entry['formats
'].extend(info['formats
']) 
 911                                 except Exception as e: 
 912                                     # Some playlist URL may fail with 500, at the same time 
 913                                     # the other one may work fine (e.g. 
 914                                     # http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu) 
 915                                     if isinstance(e.cause, compat_HTTPError) and e.cause.code == 500: 
 919                                 self._sort_formats(entry['formats
']) 
 920                                 entries.append(entry) 
 923             return self.playlist_result(entries, playlist_id, playlist_title, playlist_description) 
 925         # single video story (e.g. http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret) 
 926         programme_id = self._search_regex( 
 927             [r'data
-(?
:video
-player|media
)-vpid
="(%s)"' % self._ID_REGEX, 
 928              r'<param
[^
>]+name
="externalIdentifier"[^
>]+value
="(%s)"' % self._ID_REGEX, 
 929              r'videoId\s
*:\s
*["\'](%s)["\']' % self._ID_REGEX], 
 930             webpage, 'vpid
', default=None) 
 933             formats, subtitles = self._download_media_selector(programme_id) 
 934             self._sort_formats(formats) 
 935             # digitalData may be missing (e.g. http://www.bbc.com/autos/story/20130513-hyundais-rock-star) 
 936             digital_data = self._parse_json( 
 938                     r'var\s
+digitalData\s
*=\s
*({.+?
});?
\n', webpage, 'digital data
', default='{}'), 
 939                 programme_id, fatal=False) 
 940             page_info = digital_data.get('page
', {}).get('pageInfo
', {}) 
 941             title = page_info.get('pageName
') or self._og_search_title(webpage) 
 942             description = page_info.get('description
') or self._og_search_description(webpage) 
 943             timestamp = parse_iso8601(page_info.get('publicationDate
')) or timestamp 
 947                 'description
': description, 
 948                 'timestamp
': timestamp, 
 950                 'subtitles
': subtitles, 
 953         # Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975) 
 954         # There are several setPayload calls may be present but the video 
 955         # seems to be always related to the first one 
 956         morph_payload = self._parse_json( 
 958                 r'Morph\
.setPayload\
([^
,]+,\s
*({.+?
})\
);', 
 959                 webpage, 'morph payload
', default='{}'), 
 960             playlist_id, fatal=False) 
 962             components = try_get(morph_payload, lambda x: x['body
']['components
'], list) or [] 
 963             for component in components: 
 964                 if not isinstance(component, dict): 
 966                 lead_media = try_get(component, lambda x: x['props
']['leadMedia
'], dict) 
 969                 identifiers = lead_media.get('identifiers
') 
 970                 if not identifiers or not isinstance(identifiers, dict): 
 972                 programme_id = identifiers.get('vpid
') or identifiers.get('playablePid
') 
 975                 title = lead_media.get('title
') or self._og_search_title(webpage) 
 976                 formats, subtitles = self._download_media_selector(programme_id) 
 977                 self._sort_formats(formats) 
 978                 description = lead_media.get('summary
') 
 979                 uploader = lead_media.get('masterBrand
') 
 980                 uploader_id = lead_media.get('mid
') 
 982                 duration_d = lead_media.get('duration
') 
 983                 if isinstance(duration_d, dict): 
 984                     duration = parse_duration(dict_get( 
 985                         duration_d, ('rawDuration
', 'formattedDuration
', 'spokenDuration
'))) 
 989                     'description
': description, 
 990                     'duration
': duration, 
 991                     'uploader
': uploader, 
 992                     'uploader_id
': uploader_id, 
 994                     'subtitles
': subtitles, 
 997         def extract_all(pattern): 
 998             return list(filter(None, map( 
 999                 lambda s: self._parse_json(s, playlist_id, fatal=False), 
1000                 re.findall(pattern, webpage)))) 
1002         # Multiple video article (e.g. 
1003         # http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460) 
1004         EMBED_URL = r'https?
://(?
:www\
.)?bbc\
.co\
.uk
/(?
:[^
/]+/)+%s(?
:\b[^
"]+)?' % self._ID_REGEX 
1006         for match in extract_all(r'new\s+SMP\(({.+?})\)'): 
1007             embed_url = match.get('playerSettings', {}).get('externalEmbedUrl') 
1008             if embed_url and re.match(EMBED_URL, embed_url): 
1009                 entries.append(embed_url) 
1010         entries.extend(re.findall( 
1011             r'setPlaylist\("(%s)"\)' % EMBED_URL, webpage)) 
1013             return self.playlist_result( 
1014                 [self.url_result(entry_, 'BBCCoUk') for entry_ in entries], 
1015                 playlist_id, playlist_title, playlist_description) 
1017         # Multiple video article (e.g. http://www.bbc.com/news/world-europe-32668511) 
1018         medias = extract_all(r"data
-media
-meta
='({[^']+})'") 
1021             # Single video article (e.g. http://www.bbc.com/news/video_and_audio/international) 
1022             media_asset = self._search_regex( 
1023                 r'mediaAssetPage\
.init\
(\s
*({.+?
}), "/', 
1024                 webpage, 'media asset', default=None) 
1026                 media_asset_page = self._parse_json(media_asset, playlist_id, fatal=False) 
1028                 for video in media_asset_page.get('videos', {}).values(): 
1029                     medias.extend(video.values()) 
1032             # Multiple video playlist with single `now playing` entry (e.g. 
1033             # http://www.bbc.com/news/video_and_audio/must_see/33767813) 
1034             vxp_playlist = self._parse_json( 
1036                     r'<script[^>]+class="vxp
-playlist
-data
"[^>]+type="application
/json
"[^>]*>([^<]+)</script>', 
1037                     webpage, 'playlist data'), 
1039             playlist_medias = [] 
1040             for item in vxp_playlist: 
1041                 media = item.get('media') 
1044                 playlist_medias.append(media) 
1045                 # Download single video if found media with asset id matching the video id from URL 
1046                 if item.get('advert', {}).get('assetId') == playlist_id: 
1049             # Fallback to the whole playlist 
1051                 medias = playlist_medias 
1054         for num, media_meta in enumerate(medias, start=1): 
1055             formats, subtitles = self._extract_from_media_meta(media_meta, playlist_id) 
1058             self._sort_formats(formats) 
1060             video_id = media_meta.get('externalId') 
1062                 video_id = playlist_id if len(medias) == 1 else '%s-%s' % (playlist_id, num) 
1064             title = media_meta.get('caption') 
1066                 title = playlist_title if len(medias) == 1 else '%s - Video %s' % (playlist_title, num) 
1068             duration = int_or_none(media_meta.get('durationInSeconds')) or parse_duration(media_meta.get('duration')) 
1071             for image in media_meta.get('images', {}).values(): 
1072                 images.extend(image.values()) 
1073             if 'image' in media_meta: 
1074                 images.append(media_meta['image']) 
1077                 'url': image.get('href'), 
1078                 'width': int_or_none(image.get('width')), 
1079                 'height': int_or_none(image.get('height')), 
1080             } for image in images] 
1085                 'thumbnails': thumbnails, 
1086                 'duration': duration, 
1087                 'timestamp': timestamp, 
1089                 'subtitles': subtitles, 
1092         return self.playlist_result(entries, playlist_id, playlist_title, playlist_description) 
1095 class BBCCoUkArticleIE(InfoExtractor): 
1096     _VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/programmes/articles/(?P<id>[a-zA-Z0-9]+)' 
1097     IE_NAME = 'bbc.co.uk:article' 
1098     IE_DESC = 'BBC articles' 
1101         'url': 'http://www.bbc.co.uk/programmes/articles/3jNQLTMrPlYGTBn0WV6M2MS/not-your-typical-role-model-ada-lovelace-the-19th-century-programmer', 
1103             'id': '3jNQLTMrPlYGTBn0WV6M2MS', 
1104             'title': 'Calculating Ada: The Countess of Computing - Not your typical role model: Ada Lovelace the 19th century programmer - BBC Four', 
1105             'description': 'Hannah Fry reveals some of her surprising discoveries about Ada Lovelace during filming.', 
1107         'playlist_count': 4, 
1108         'add_ie': ['BBCCoUk'], 
1111     def _real_extract(self, url): 
1112         playlist_id = self._match_id(url) 
1114         webpage = self._download_webpage(url, playlist_id) 
1116         title = self._og_search_title(webpage) 
1117         description = self._og_search_description(webpage).strip() 
1119         entries = [self.url_result(programme_url) for programme_url in re.findall( 
1120             r'<div[^>]+typeof="Clip
"[^>]+resource="([^
"]+)"', webpage)] 
1122         return self.playlist_result(entries, playlist_id, title, description) 
1125 class BBCCoUkPlaylistBaseIE(InfoExtractor): 
1126     def _entries(self, webpage, url, playlist_id): 
1127         single_page = 'page
' in compat_urlparse.parse_qs( 
1128             compat_urlparse.urlparse(url).query) 
1129         for page_num in itertools.count(2): 
1130             for video_id in re.findall( 
1131                     self._VIDEO_ID_TEMPLATE % BBCCoUkIE._ID_REGEX, webpage): 
1132                 yield self.url_result( 
1133                     self._URL_TEMPLATE % video_id, BBCCoUkIE.ie_key()) 
1136             next_page = self._search_regex( 
1137                 r'<li
[^
>]+class=(["\'])pagination_+next\1[^>]*><a[^>]+href=(["\'])(?P
<url
>(?
:(?
!\
2).)+)\
2', 
1138                 webpage, 'next page url
', default=None, group='url
') 
1141             webpage = self._download_webpage( 
1142                 compat_urlparse.urljoin(url, next_page), playlist_id, 
1143                 'Downloading page 
%d' % page_num, page_num) 
1145     def _real_extract(self, url): 
1146         playlist_id = self._match_id(url) 
1148         webpage = self._download_webpage(url, playlist_id) 
1150         title, description = self._extract_title_and_description(webpage) 
1152         return self.playlist_result( 
1153             self._entries(webpage, url, playlist_id), 
1154             playlist_id, title, description) 
1157 class BBCCoUkIPlayerPlaylistIE(BBCCoUkPlaylistBaseIE): 
1158     IE_NAME = 'bbc
.co
.uk
:iplayer
:playlist
' 
1159     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.co\
.uk
/iplayer
/(?
:episodes|group
)/(?P
<id>%s)' % BBCCoUkIE._ID_REGEX 
1160     _URL_TEMPLATE = 'http
://www
.bbc
.co
.uk
/iplayer
/episode
/%s' 
1161     _VIDEO_ID_TEMPLATE = r'data
-ip
-id=["\'](%s)' 
1163         'url': 'http://www.bbc.co.uk/iplayer/episodes/b05rcz9v', 
1166             'title': 'The Disappearance', 
1167             'description': 'French thriller serial about a missing teenager.', 
1169         'playlist_mincount': 6, 
1170         'skip': 'This programme is not currently available on BBC iPlayer', 
1172         # Available for over a year unlike 30 days for most other programmes 
1173         'url': 'http://www.bbc.co.uk/iplayer/group/p02tcc32', 
1176             'title': 'Bohemian Icons', 
1177             'description': 'md5:683e901041b2fe9ba596f2ab04c4dbe7', 
1179         'playlist_mincount': 10, 
1182     def _extract_title_and_description(self, webpage): 
1183         title = self._search_regex(r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False) 
1184         description = self._search_regex( 
1185             r'<p[^>]+class=(["\'])subtitle\
1[^
>]*>(?P
<value
>[^
<]+)</p
>', 
1186             webpage, 'description
', fatal=False, group='value
') 
1187         return title, description 
1190 class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE): 
1191     IE_NAME = 'bbc
.co
.uk
:playlist
' 
1192     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.co\
.uk
/programmes
/(?P
<id>%s)/(?
:episodes|broadcasts|clips
)' % BBCCoUkIE._ID_REGEX 
1193     _URL_TEMPLATE = 'http
://www
.bbc
.co
.uk
/programmes
/%s' 
1194     _VIDEO_ID_TEMPLATE = r'data
-pid
=["\'](%s)' 
1196         'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/clips', 
1199             'title': 'The Disappearance - Clips - BBC Four', 
1200             'description': 'French thriller serial about a missing teenager.', 
1202         'playlist_mincount': 7, 
1204         # multipage playlist, explicit page 
1205         'url': 'http://www.bbc.co.uk/programmes/b00mfl7n/clips?page=1', 
1208             'title': 'Frozen Planet - Clips - BBC One', 
1209             'description': 'md5:65dcbf591ae628dafe32aa6c4a4a0d8c', 
1211         'playlist_mincount': 24, 
1213         # multipage playlist, all pages 
1214         'url': 'http://www.bbc.co.uk/programmes/b00mfl7n/clips', 
1217             'title': 'Frozen Planet - Clips - BBC One', 
1218             'description': 'md5:65dcbf591ae628dafe32aa6c4a4a0d8c', 
1220         'playlist_mincount': 142, 
1222         'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/broadcasts/2016/06', 
1223         'only_matching': True, 
1225         'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/clips', 
1226         'only_matching': True, 
1228         'url': 'http://www.bbc.co.uk/programmes/b055jkys/episodes/player', 
1229         'only_matching': True, 
1232     def _extract_title_and_description(self, webpage): 
1233         title = self._og_search_title(webpage, fatal=False) 
1234         description = self._og_search_description(webpage) 
1235         return title, description