2 from __future__ 
import unicode_literals
 
   7 from .common 
import InfoExtractor
 
  24 from ..compat 
import ( 
  31 class BBCCoUkIE(InfoExtractor
): 
  33     IE_DESC 
= 'BBC iPlayer' 
  34     _ID_REGEX 
= r
'(?:[pbm][\da-z]{7}|w[\da-z]{7,14})' 
  37                         (?:www\.)?bbc\.co\.uk/ 
  39                             programmes/(?!articles/)| 
  40                             iplayer(?:/[^/]+)?/(?:episode/|playlist/)| 
  41                             music/(?:clips|audiovideo/popular)[/#]| 
  44                             events/[^/]+/play/[^/]+/ 
  46                         (?P<id>%s)(?!/(?:episodes|broadcasts|clips)) 
  49     _LOGIN_URL 
= 'https://account.bbc.com/signin' 
  50     _NETRC_MACHINE 
= 'bbc' 
  52     _MEDIASELECTOR_URLS 
= [ 
  53         # Provides HQ HLS streams with even better quality that pc mediaset but fails 
  54         # with geolocation in some cases when it's even not geo restricted at all (e.g. 
  55         # http://www.bbc.co.uk/programmes/b06bp7lf). Also may fail with selectionunavailable. 
  56         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/%s', 
  57         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s', 
  60     _MEDIASELECTION_NS 
= 'http://bbc.co.uk/2008/mp/mediaselection' 
  61     _EMP_PLAYLIST_NS 
= 'http://bbc.co.uk/2008/emp/playlist' 
  70             'url': 'http://www.bbc.co.uk/programmes/b039g8p7', 
  74                 'title': 'Kaleidoscope, Leonard Cohen', 
  75                 'description': 'The Canadian poet and songwriter reflects on his musical career.', 
  79                 'skip_download': True, 
  83             'url': 'http://www.bbc.co.uk/iplayer/episode/b00yng5w/The_Man_in_Black_Series_3_The_Printed_Name/', 
  87                 'title': 'The Man in Black: Series 3: The Printed Name', 
  88                 'description': "Mark Gatiss introduces Nicholas Pierpan's chilling tale of a writer's devilish pact with a mysterious man. Stars Ewan Bailey.", 
  93                 'skip_download': True, 
  95             'skip': 'Episode is no longer available on BBC iPlayer Radio', 
  98             'url': 'http://www.bbc.co.uk/iplayer/episode/b03vhd1f/The_Voice_UK_Series_3_Blind_Auditions_5/', 
 102                 'title': 'The Voice UK: Series 3: Blind Auditions 5', 
 103                 '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.', 
 108                 'skip_download': True, 
 110             'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only', 
 113             'url': 'http://www.bbc.co.uk/iplayer/episode/p026c7jt/tomorrows-worlds-the-unearthly-history-of-science-fiction-2-invasion', 
 117                 'title': "Tomorrow's Worlds: The Unearthly History of Science Fiction", 
 118                 'description': '2. Invasion', 
 123                 'skip_download': True, 
 125             'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only', 
 127             'url': 'http://www.bbc.co.uk/programmes/b04v20dw', 
 131                 'title': 'Pete Tong, The Essential New Tune Special', 
 132                 'description': "Pete has a very special mix - all of 2014's Essential New Tunes!", 
 137                 'skip_download': True, 
 139             'skip': 'Episode is no longer available on BBC iPlayer Radio', 
 141             'url': 'http://www.bbc.co.uk/music/clips/p022h44b', 
 146                 'title': 'BBC Proms Music Guides, Rachmaninov: Symphonic Dances', 
 147                 'description': "In this Proms Music Guide, Andrew McGregor looks at Rachmaninov's Symphonic Dances.", 
 152                 'skip_download': True, 
 155             'url': 'http://www.bbc.co.uk/music/clips/p025c0zz', 
 160                 'title': 'Reading and Leeds Festival, 2014, Rae Morris - Closer (Live on BBC Three)', 
 161                 'description': 'Rae Morris performs Closer for BBC Three at Reading 2014', 
 166                 'skip_download': True, 
 169             'url': 'http://www.bbc.co.uk/iplayer/episode/b054fn09/ad/natural-world-20152016-2-super-powered-owls', 
 173                 'title': 'Natural World, 2015-2016: 2. Super Powered Owls', 
 174                 'description': 'md5:e4db5c937d0e95a7c6b5e654d429183d', 
 179                 'skip_download': True, 
 181             'skip': 'geolocation', 
 183             'url': 'http://www.bbc.co.uk/iplayer/episode/b05zmgwn/royal-academy-summer-exhibition', 
 187                 '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.', 
 188                 'title': 'Royal Academy Summer Exhibition', 
 193                 'skip_download': True, 
 195             'skip': 'geolocation', 
 197             # iptv-all mediaset fails with geolocation however there is no geo restriction 
 198             # for this programme at all 
 199             'url': 'http://www.bbc.co.uk/programmes/b06rkn85', 
 203                 'title': "Best of the Mini-Mixes 2015: Part 3, Annie Mac's Friday Night - BBC Radio 1", 
 204                 'description': "Annie has part three in the Best of the Mini-Mixes 2015, plus the year's Most Played!", 
 208                 'skip_download': True, 
 210             'skip': 'Now it\'s really geo-restricted', 
 212             # compact player (https://github.com/ytdl-org/youtube-dl/issues/8147) 
 213             'url': 'http://www.bbc.co.uk/programmes/p028bfkf/player', 
 217                 'title': 'Extract from BBC documentary Look Stranger - Giant Leeks and Magic Brews', 
 218                 'description': 'Extract from BBC documentary Look Stranger - Giant Leeks and Magic Brews', 
 222                 'skip_download': True, 
 225             'url': 'https://www.bbc.co.uk/sounds/play/m0007jzb', 
 230                 'title': 'BBC Proms, 2019, Prom 34: WestāEastern Divan Orchestra', 
 231                 'description': "Live BBC Proms. WestāEastern Divan Orchestra with Daniel Barenboim and Martha Argerich.", 
 236                 'skip_download': True, 
 239             'url': 'http://www.bbc.co.uk/iplayer/playlist/p01dvks4', 
 240             'only_matching': True, 
 242             'url': 'http://www.bbc.co.uk/music/clips#p02frcc3', 
 243             'only_matching': True, 
 245             'url': 'http://www.bbc.co.uk/iplayer/cbeebies/episode/b0480276/bing-14-atchoo', 
 246             'only_matching': True, 
 248             'url': 'http://www.bbc.co.uk/radio/player/p03cchwf', 
 249             'only_matching': True, 
 251             'url': 'https://www.bbc.co.uk/music/audiovideo/popular#p055bc55', 
 252             'only_matching': True, 
 254             'url': 'http://www.bbc.co.uk/programmes/w3csv1y9', 
 255             'only_matching': True, 
 257             'url': 'https://www.bbc.co.uk/programmes/m00005xn', 
 258             'only_matching': True, 
 260             'url': 'https://www.bbc.co.uk/programmes/w172w4dww1jqt5s', 
 261             'only_matching': True, 
 264     _USP_RE 
= r
'/([^/]+?)\.ism(?:\.hlsv2\.ism)?/[^/]+\.m3u8' 
 267         username
, password 
= self
._get
_login
_info
() 
 271         login_page 
= self
._download
_webpage
( 
 272             self
._LOGIN
_URL
, None, 'Downloading signin page') 
 274         login_form 
= self
._hidden
_inputs
(login_page
) 
 277             'username': username
, 
 278             'password': password
, 
 281         post_url 
= urljoin(self
._LOGIN
_URL
, self
._search
_regex
( 
 282             r
'<form[^>]+action=(["\'])(?P
<url
>.+?
)\
1', login_page, 
 283             'post url
', default=self._LOGIN_URL, group='url
')) 
 285         response, urlh = self._download_webpage_handle( 
 286             post_url, None, 'Logging 
in', data=urlencode_postdata(login_form), 
 287             headers={'Referer
': self._LOGIN_URL}) 
 289         if self._LOGIN_URL in urlh.geturl(): 
 290             error = clean_html(get_element_by_class('form
-message
', response)) 
 292                 raise ExtractorError( 
 293                     'Unable to login
: %s' % error, expected=True) 
 294             raise ExtractorError('Unable to log 
in') 
 296     def _real_initialize(self): 
 299     class MediaSelectionError(Exception): 
 300         def __init__(self, id): 
 303     def _extract_asx_playlist(self, connection, programme_id): 
 304         asx = self._download_xml(connection.get('href
'), programme_id, 'Downloading ASX playlist
') 
 305         return [ref.get('href
') for ref in asx.findall('./Entry
/ref
')] 
 307     def _extract_items(self, playlist): 
 308         return playlist.findall('./{%s}item
' % self._EMP_PLAYLIST_NS) 
 310     def _findall_ns(self, element, xpath): 
 312         for ns in self._NAMESPACES: 
 313             elements.extend(element.findall(xpath % ns)) 
 316     def _extract_medias(self, media_selection): 
 317         error = media_selection.find('./{%s}error
' % self._MEDIASELECTION_NS) 
 319             media_selection.find('./{%s}error
' % self._EMP_PLAYLIST_NS) 
 320         if error is not None: 
 321             raise BBCCoUkIE.MediaSelectionError(error.get('id')) 
 322         return self._findall_ns(media_selection, './{%s}media
') 
 324     def _extract_connections(self, media): 
 325         return self._findall_ns(media, './{%s}connection
') 
 327     def _get_subtitles(self, media, programme_id): 
 329         for connection in self._extract_connections(media): 
 330             cc_url = url_or_none(connection.get('href
')) 
 333             captions = self._download_xml( 
 334                 cc_url, programme_id, 'Downloading captions
', fatal=False) 
 335             if not isinstance(captions, compat_etree_Element): 
 337             lang = captions.get('{http
://www
.w3
.org
/XML
/1998/namespace
}lang
', 'en
') 
 340                     'url
': connection.get('href
'), 
 346     def _raise_extractor_error(self, media_selection_error): 
 347         raise ExtractorError( 
 348             '%s returned error
: %s' % (self.IE_NAME, media_selection_error.id), 
 351     def _download_media_selector(self, programme_id): 
 352         last_exception = None 
 353         for mediaselector_url in self._MEDIASELECTOR_URLS: 
 355                 return self._download_media_selector_url( 
 356                     mediaselector_url % programme_id, programme_id) 
 357             except BBCCoUkIE.MediaSelectionError as e: 
 358                 if e.id in ('notukerror
', 'geolocation
', 'selectionunavailable
'): 
 361                 self._raise_extractor_error(e) 
 362         self._raise_extractor_error(last_exception) 
 364     def _download_media_selector_url(self, url, programme_id=None): 
 365         media_selection = self._download_xml( 
 366             url, programme_id, 'Downloading media selection XML
', 
 367             expected_status=(403, 404)) 
 368         return self._process_media_selector(media_selection, programme_id) 
 370     def _process_media_selector(self, media_selection, programme_id): 
 375         for media in self._extract_medias(media_selection): 
 376             kind = media.get('kind
') 
 377             if kind in ('video
', 'audio
'): 
 378                 bitrate = int_or_none(media.get('bitrate
')) 
 379                 encoding = media.get('encoding
') 
 380                 service = media.get('service
') 
 381                 width = int_or_none(media.get('width
')) 
 382                 height = int_or_none(media.get('height
')) 
 383                 file_size = int_or_none(media.get('media_file_size
')) 
 384                 for connection in self._extract_connections(media): 
 385                     href = connection.get('href
') 
 390                     conn_kind = connection.get('kind
') 
 391                     protocol = connection.get('protocol
') 
 392                     supplier = connection.get('supplier
') 
 393                     transfer_format = connection.get('transferFormat
') 
 394                     format_id = supplier or conn_kind or protocol 
 396                         format_id = '%s_%s' % (service, format_id) 
 398                     if supplier == 'asx
': 
 399                         for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)): 
 402                                 'format_id
': 'ref
%s_%s' % (i, format_id), 
 404                     elif transfer_format == 'dash
': 
 405                         formats.extend(self._extract_mpd_formats( 
 406                             href, programme_id, mpd_id=format_id, fatal=False)) 
 407                     elif transfer_format == 'hls
': 
 408                         formats.extend(self._extract_m3u8_formats( 
 409                             href, programme_id, ext='mp4
', entry_protocol='m3u8_native
', 
 410                             m3u8_id=format_id, fatal=False)) 
 411                         if re.search(self._USP_RE, href): 
 412                             usp_formats = self._extract_m3u8_formats( 
 413                                 re.sub(self._USP_RE, r'/\
1.ism
/\
1.m3u8
', href), 
 414                                 programme_id, ext='mp4
', entry_protocol='m3u8_native
', 
 415                                 m3u8_id=format_id, fatal=False) 
 416                             for f in usp_formats: 
 417                                 if f.get('height
') and f['height
'] > 720: 
 420                     elif transfer_format == 'hds
': 
 421                         formats.extend(self._extract_f4m_formats( 
 422                             href, programme_id, f4m_id=format_id, fatal=False)) 
 424                         if not service and not supplier and bitrate: 
 425                             format_id += '-%d' % bitrate 
 427                             'format_id
': format_id, 
 428                             'filesize
': file_size, 
 443                         if protocol in ('http
', 'https
'): 
 448                         elif protocol == 'rtmp
': 
 449                             application = connection.get('application
', 'ondemand
') 
 450                             auth_string = connection.get('authString
') 
 451                             identifier = connection.get('identifier
') 
 452                             server = connection.get('server
') 
 454                                 'url
': '%s://%s/%s?
%s' % (protocol, server, application, auth_string), 
 455                                 'play_path
': identifier, 
 456                                 'app
': '%s?
%s' % (application, auth_string), 
 457                                 'page_url
': 'http
://www
.bbc
.co
.uk
', 
 458                                 'player_url
': 'http
://www
.bbc
.co
.uk
/emp
/releases
/iplayer
/revisions
/617463_618125_4/617463_618125_4_emp
.swf
', 
 465             elif kind == 'captions
': 
 466                 subtitles = self.extract_subtitles(media, programme_id) 
 467         return formats, subtitles 
 469     def _download_playlist(self, playlist_id): 
 471             playlist = self._download_json( 
 472                 'http
://www
.bbc
.co
.uk
/programmes
/%s/playlist
.json
' % playlist_id, 
 473                 playlist_id, 'Downloading playlist JSON
') 
 475             version = playlist.get('defaultAvailableVersion
') 
 477                 smp_config = version['smpConfig
'] 
 478                 title = smp_config['title
'] 
 479                 description = smp_config['summary
'] 
 480                 for item in smp_config['items
']: 
 482                     if kind not in ('programme
', 'radioProgramme
'): 
 484                     programme_id = item.get('vpid
') 
 485                     duration = int_or_none(item.get('duration
')) 
 486                     formats, subtitles = self._download_media_selector(programme_id) 
 487                 return programme_id, title, description, duration, formats, subtitles 
 488         except ExtractorError as ee: 
 489             if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404): 
 492         # fallback to legacy playlist 
 493         return self._process_legacy_playlist(playlist_id) 
 495     def _process_legacy_playlist_url(self, url, display_id): 
 496         playlist = self._download_legacy_playlist_url(url, display_id) 
 497         return self._extract_from_legacy_playlist(playlist, display_id) 
 499     def _process_legacy_playlist(self, playlist_id): 
 500         return self._process_legacy_playlist_url( 
 501             'http
://www
.bbc
.co
.uk
/iplayer
/playlist
/%s' % playlist_id, playlist_id) 
 503     def _download_legacy_playlist_url(self, url, playlist_id=None): 
 504         return self._download_xml( 
 505             url, playlist_id, 'Downloading legacy playlist XML
') 
 507     def _extract_from_legacy_playlist(self, playlist, playlist_id): 
 508         no_items = playlist.find('./{%s}noItems
' % self._EMP_PLAYLIST_NS) 
 509         if no_items is not None: 
 510             reason = no_items.get('reason
') 
 511             if reason == 'preAvailability
': 
 512                 msg = 'Episode 
%s is not yet available
' % playlist_id 
 513             elif reason == 'postAvailability
': 
 514                 msg = 'Episode 
%s is no longer available
' % playlist_id 
 515             elif reason == 'noMedia
': 
 516                 msg = 'Episode 
%s is not currently available
' % playlist_id 
 518                 msg = 'Episode 
%s is not available
: %s' % (playlist_id, reason) 
 519             raise ExtractorError(msg, expected=True) 
 521         for item in self._extract_items(playlist): 
 522             kind = item.get('kind
') 
 523             if kind not in ('programme
', 'radioProgramme
'): 
 525             title = playlist.find('./{%s}title
' % self._EMP_PLAYLIST_NS).text 
 526             description_el = playlist.find('./{%s}summary
' % self._EMP_PLAYLIST_NS) 
 527             description = description_el.text if description_el is not None else None 
 529             def get_programme_id(item): 
 530                 def get_from_attributes(item): 
 531                     for p in('identifier
', 'group
'): 
 533                         if value and re.match(r'^
[pb
][\da
-z
]{7}$
', value): 
 535                 get_from_attributes(item) 
 536                 mediator = item.find('./{%s}mediator
' % self._EMP_PLAYLIST_NS) 
 537                 if mediator is not None: 
 538                     return get_from_attributes(mediator) 
 540             programme_id = get_programme_id(item) 
 541             duration = int_or_none(item.get('duration
')) 
 544                 formats, subtitles = self._download_media_selector(programme_id) 
 546                 formats, subtitles = self._process_media_selector(item, playlist_id) 
 547                 programme_id = playlist_id 
 549         return programme_id, title, description, duration, formats, subtitles 
 551     def _real_extract(self, url): 
 552         group_id = self._match_id(url) 
 554         webpage = self._download_webpage(url, group_id, 'Downloading video page
') 
 556         error = self._search_regex( 
 557             r'<div
\b[^
>]+\bclass
=["\']smp__message delta["\'][^
>]*>([^
<]+)<', 
 558             webpage, 'error
', default=None) 
 560             raise ExtractorError(error, expected=True) 
 565         tviplayer = self._search_regex( 
 566             r'mediator\
.bind\
(({.+?
})\s
*,\s
*document\
.getElementById
', 
 567             webpage, 'player
', default=None) 
 570             player = self._parse_json(tviplayer, group_id).get('player
', {}) 
 571             duration = int_or_none(player.get('duration
')) 
 572             programme_id = player.get('vpid
') 
 575             programme_id = self._search_regex( 
 576                 r'"vpid"\s
*:\s
*"(%s)"' % self._ID_REGEX, webpage, 'vpid
', fatal=False, default=None) 
 579             formats, subtitles = self._download_media_selector(programme_id) 
 580             title = self._og_search_title(webpage, default=None) or self._html_search_regex( 
 581                 (r'<h2
[^
>]+id="parent-title"[^
>]*>(.+?
)</h2
>', 
 582                  r'<div
[^
>]+class="info"[^
>]*>\s
*<h1
>(.+?
)</h1
>'), webpage, 'title
') 
 583             description = self._search_regex( 
 584                 (r'<p 
class="[^"]*medium
-description
[^
"]*">([^
<]+)</p
>', 
 585                  r'<div
[^
>]+class="info_+synopsis"[^
>]*>([^
<]+)</div
>'), 
 586                 webpage, 'description
', default=None) 
 588                 description = self._html_search_meta('description
', webpage) 
 590             programme_id, title, description, duration, formats, subtitles = self._download_playlist(group_id) 
 592         self._sort_formats(formats) 
 597             'description
': description, 
 598             'thumbnail
': self._og_search_thumbnail(webpage, default=None), 
 599             'duration
': duration, 
 601             'subtitles
': subtitles, 
 605 class BBCIE(BBCCoUkIE): 
 608     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.(?
:com|co\
.uk
)/(?
:[^
/]+/)+(?P
<id>[^
/#?]+)' 
 610     _MEDIASELECTOR_URLS 
= [ 
 611         # Provides HQ HLS streams but fails with geolocation in some cases when it's 
 612         # even not geo restricted at all 
 613         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/%s', 
 614         # Provides more formats, namely direct mp4 links, but fails on some videos with 
 615         # notukerror for non UK (?) users (e.g. 
 616         # http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret) 
 617         'http://open.live.bbc.co.uk/mediaselector/4/mtis/stream/%s', 
 618         # Provides fewer formats, but works everywhere for everybody (hopefully) 
 619         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/journalism-pc/vpid/%s', 
 623         # article with multiple videos embedded with data-playable containing vpids 
 624         'url': 'http://www.bbc.com/news/world-europe-32668511', 
 626             'id': 'world-europe-32668511', 
 627             'title': 'Russia stages massive WW2 parade', 
 628             'description': 'md5:00ff61976f6081841f759a08bf78cc9c', 
 632         # article with multiple videos embedded with data-playable (more videos) 
 633         'url': 'http://www.bbc.com/news/business-28299555', 
 635             'id': 'business-28299555', 
 636             'title': 'Farnborough Airshow: Video highlights', 
 637             'description': 'BBC reports and video highlights at the Farnborough Airshow.', 
 642         # article with multiple videos embedded with `new SMP()` 
 644         'url': 'http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460', 
 646             'id': '3662a707-0af9-3149-963f-47bea720b460', 
 649         'playlist_count': 18, 
 651         # single video embedded with data-playable containing vpid 
 652         'url': 'http://www.bbc.com/news/world-europe-32041533', 
 656             'title': 'Aerial footage showed the site of the crash in the Alps - courtesy BFM TV', 
 657             'description': 'md5:2868290467291b37feda7863f7a83f54', 
 659             'timestamp': 1427219242, 
 660             'upload_date': '20150324', 
 664             'skip_download': True, 
 667         # article with single video embedded with data-playable containing XML playlist 
 668         # with direct video links as progressiveDownloadUrl (for now these are extracted) 
 669         # and playlist with f4m and m3u8 as streamingUrl 
 670         'url': 'http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu', 
 672             'id': '150615_telabyad_kentin_cogu', 
 674             'title': "YPG: Tel Abyad'ın tamamı kontrolümüzde", 
 675             'description': 'md5:33a4805a855c9baf7115fcbde57e7025', 
 676             'timestamp': 1434397334, 
 677             'upload_date': '20150615', 
 680             'skip_download': True, 
 683         # single video embedded with data-playable containing XML playlists (regional section) 
 684         'url': 'http://www.bbc.com/mundo/video_fotos/2015/06/150619_video_honduras_militares_hospitales_corrupcion_aw', 
 686             'id': '150619_video_honduras_militares_hospitales_corrupcion_aw', 
 688             'title': 'Honduras militariza sus hospitales por nuevo escÔndalo de corrupción', 
 689             'description': 'md5:1525f17448c4ee262b64b8f0c9ce66c8', 
 690             'timestamp': 1434713142, 
 691             'upload_date': '20150619', 
 694             'skip_download': True, 
 697         # single video from video playlist embedded with vxp-playlist-data JSON 
 698         'url': 'http://www.bbc.com/news/video_and_audio/must_see/33376376', 
 702             'title': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''', 
 704             'description': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''', 
 707             'skip_download': True, 
 710         # single video story with digitalData 
 711         'url': 'http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret', 
 715             'title': 'Sri Lankaās spicy secret', 
 716             'description': 'As a new train line to Jaffna opens up the countryās north, travellers can experience a truly distinct slice of Tamil culture.', 
 717             'timestamp': 1437674293, 
 718             'upload_date': '20150723', 
 722             'skip_download': True, 
 725         # single video story without digitalData 
 726         'url': 'http://www.bbc.com/autos/story/20130513-hyundais-rock-star', 
 730             'title': 'Hyundai Santa Fe Sport: Rock star', 
 731             'description': 'md5:b042a26142c4154a6e472933cf20793d', 
 732             'timestamp': 1415867444, 
 733             'upload_date': '20141113', 
 737             'skip_download': True, 
 740         # single video embedded with Morph 
 741         'url': 'http://www.bbc.co.uk/sport/live/olympics/36895975', 
 745             'title': "Nigeria v Japan - Men's First Round", 
 746             'description': 'Live coverage of the first round from Group B at the Amazonia Arena.', 
 748             'uploader': 'BBC Sport', 
 749             'uploader_id': 'bbc_sport', 
 753             'skip_download': True, 
 755         'skip': 'Georestricted to UK', 
 757         # single video with playlist.sxml URL in playlist param 
 758         'url': 'http://www.bbc.com/sport/0/football/33653409', 
 762             'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?', 
 763             'description': 'BBC Sport\'s David Ornstein has the latest transfer gossip, including rumours of a Manchester United return for Cristiano Ronaldo.', 
 768             'skip_download': True, 
 771         # article with multiple videos embedded with playlist.sxml in playlist param 
 772         'url': 'http://www.bbc.com/sport/0/football/34475836', 
 775             'title': 'Jurgen Klopp: Furious football from a witty and winning coach', 
 776             'description': 'Fast-paced football, wit, wisdom and a ready smile - why Liverpool fans should come to love new boss Jurgen Klopp.', 
 780         # school report article with single video 
 781         'url': 'http://www.bbc.co.uk/schoolreport/35744779', 
 784             'title': 'School which breaks down barriers in Jerusalem', 
 788         # single video with playlist URL from weather section 
 789         'url': 'http://www.bbc.com/weather/features/33601775', 
 790         'only_matching': True, 
 792         # custom redirection to www.bbc.com 
 793         'url': 'http://www.bbc.co.uk/news/science-environment-33661876', 
 794         'only_matching': True, 
 796         # single video article embedded with data-media-vpid 
 797         'url': 'http://www.bbc.co.uk/sport/rowing/35908187', 
 798         'only_matching': True, 
 800         'url': 'https://www.bbc.co.uk/bbcthree/clip/73d0bbd0-abc3-4cea-b3c0-cdae21905eb1', 
 804             'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?', 
 805             'description': 'md5:4b7dfd063d5a789a1512e99662be3ddd', 
 808             'skip_download': True, 
 811         # window.__PRELOADED_STATE__ 
 812         'url': 'https://www.bbc.co.uk/radio/play/b0b9z4yl', 
 816             'title': 'Prom 6: An American in Paris and Turangalila', 
 817             'description': 'md5:51cf7d6f5c8553f197e58203bc78dff8', 
 818             'uploader': 'Radio 3', 
 819             'uploader_id': 'bbc_radio_three', 
 822         'url': 'http://www.bbc.co.uk/learningenglish/chinese/features/lingohack/ep-181227', 
 826             'title': 'md5:2fabf12a726603193a2879a055f72514', 
 827             'description': 'Learn English words and phrases from this story', 
 829         'add_ie': [BBCCoUkIE
.ie_key()], 
 833     def suitable(cls
, url
): 
 834         EXCLUDE_IE 
= (BBCCoUkIE
, BBCCoUkArticleIE
, BBCCoUkIPlayerPlaylistIE
, BBCCoUkPlaylistIE
) 
 835         return (False if any(ie
.suitable(url
) for ie 
in EXCLUDE_IE
) 
 836                 else super(BBCIE
, cls
).suitable(url
)) 
 838     def _extract_from_media_meta(self
, media_meta
, video_id
): 
 839         # Direct links to media in media metadata (e.g. 
 840         # http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu) 
 841         # TODO: there are also f4m and m3u8 streams incorporated in playlist.sxml 
 842         source_files 
= media_meta
.get('sourceFiles') 
 846                 'format_id': format_id
, 
 847                 'ext': f
.get('encoding'), 
 848                 'tbr': float_or_none(f
.get('bitrate'), 1000), 
 849                 'filesize': int_or_none(f
.get('filesize')), 
 850             } for format_id
, f 
in source_files
.items() if f
.get('url')], [] 
 852         programme_id 
= media_meta
.get('externalId') 
 854             return self
._download
_media
_selector
(programme_id
) 
 856         # Process playlist.sxml as legacy playlist 
 857         href 
= media_meta
.get('href') 
 859             playlist 
= self
._download
_legacy
_playlist
_url
(href
) 
 860             _
, _
, _
, _
, formats
, subtitles 
= self
._extract
_from
_legacy
_playlist
(playlist
, video_id
) 
 861             return formats
, subtitles
 
 865     def _extract_from_playlist_sxml(self
, url
, playlist_id
, timestamp
): 
 866         programme_id
, title
, description
, duration
, formats
, subtitles 
= \
 
 867             self
._process
_legacy
_playlist
_url
(url
, playlist_id
) 
 868         self
._sort
_formats
(formats
) 
 872             'description': description
, 
 873             'duration': duration
, 
 874             'timestamp': timestamp
, 
 876             'subtitles': subtitles
, 
 879     def _real_extract(self
, url
): 
 880         playlist_id 
= self
._match
_id
(url
) 
 882         webpage 
= self
._download
_webpage
(url
, playlist_id
) 
 884         json_ld_info 
= self
._search
_json
_ld
(webpage
, playlist_id
, default
={}) 
 885         timestamp 
= json_ld_info
.get('timestamp') 
 887         playlist_title 
= json_ld_info
.get('title') 
 888         if not playlist_title
: 
 889             playlist_title 
= self
._og
_search
_title
( 
 890                 webpage
, default
=None) or self
._html
_search
_regex
( 
 891                 r
'<title>(.+?)</title>', webpage
, 'playlist title', default
=None) 
 893                 playlist_title 
= re
.sub(r
'(.+)\s*-\s*BBC.*?$', r
'\1', playlist_title
).strip() 
 895         playlist_description 
= json_ld_info
.get( 
 896             'description') or self
._og
_search
_description
(webpage
, default
=None) 
 899             timestamp 
= parse_iso8601(self
._search
_regex
( 
 900                 [r
'<meta[^>]+property="article:published_time"[^>]+content="([^"]+)"', 
 901                  r
'itemprop="datePublished"[^>]+datetime="([^"]+)"', 
 902                  r
'"datePublished":\s*"([^"]+)'], 
 903                 webpage
, 'date', default
=None)) 
 907         # article with multiple videos embedded with playlist.sxml (e.g. 
 908         # http://www.bbc.com/sport/0/football/34475836) 
 909         playlists 
= re
.findall(r
'<param[^>]+name="playlist"[^>]+value="([^"]+)"', webpage
) 
 910         playlists
.extend(re
.findall(r
'data-media-id="([^"]+/playlist\.sxml)"', webpage
)) 
 913                 self
._extract
_from
_playlist
_sxml
(playlist_url
, playlist_id
, timestamp
) 
 914                 for playlist_url 
in playlists
] 
 916         # news article with multiple videos embedded with data-playable 
 917         data_playables 
= re
.findall(r
'data-playable=(["\'])({.+?
})\
1', webpage) 
 919             for _, data_playable_json in data_playables: 
 920                 data_playable = self._parse_json( 
 921                     unescapeHTML(data_playable_json), playlist_id, fatal=False) 
 922                 if not data_playable: 
 924                 settings = data_playable.get('settings
', {}) 
 926                     # data-playable with video vpid in settings.playlistObject.items (e.g. 
 927                     # http://www.bbc.com/news/world-us-canada-34473351) 
 928                     playlist_object = settings.get('playlistObject
', {}) 
 930                         items = playlist_object.get('items
') 
 931                         if items and isinstance(items, list): 
 932                             title = playlist_object['title
'] 
 933                             description = playlist_object.get('summary
') 
 934                             duration = int_or_none(items[0].get('duration
')) 
 935                             programme_id = items[0].get('vpid
') 
 936                             formats, subtitles = self._download_media_selector(programme_id) 
 937                             self._sort_formats(formats) 
 941                                 'description
': description, 
 942                                 'timestamp
': timestamp, 
 943                                 'duration
': duration, 
 945                                 'subtitles
': subtitles, 
 948                         # data-playable without vpid but with a playlist.sxml URLs 
 949                         # in otherSettings.playlist (e.g. 
 950                         # http://www.bbc.com/turkce/multimedya/2015/10/151010_vid_ankara_patlama_ani) 
 951                         playlist = data_playable.get('otherSettings
', {}).get('playlist
', {}) 
 954                             for key in ('streaming
', 'progressiveDownload
'): 
 955                                 playlist_url = playlist.get('%sUrl
' % key) 
 959                                     info = self._extract_from_playlist_sxml( 
 960                                         playlist_url, playlist_id, timestamp) 
 964                                         entry['title
'] = info['title
'] 
 965                                         entry['formats
'].extend(info['formats
']) 
 966                                 except Exception as e: 
 967                                     # Some playlist URL may fail with 500, at the same time 
 968                                     # the other one may work fine (e.g. 
 969                                     # http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu) 
 970                                     if isinstance(e.cause, compat_HTTPError) and e.cause.code == 500: 
 974                                 self._sort_formats(entry['formats
']) 
 975                                 entries.append(entry) 
 978             return self.playlist_result(entries, playlist_id, playlist_title, playlist_description) 
 980         # http://www.bbc.co.uk/learningenglish/chinese/features/lingohack/ep-181227 
 981         group_id = self._search_regex( 
 982             r'<div
[^
>]+\bclass
=["\']video["\'][^
>]+\bdata
-pid
=["\'](%s)' % self._ID_REGEX, 
 983             webpage, 'group id', default=None) 
 985             return self.url_result( 
 986                 'https://www.bbc.co.uk/programmes/%s' % group_id, 
 987                 ie=BBCCoUkIE.ie_key()) 
 989         # single video story (e.g. http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret) 
 990         programme_id = self._search_regex( 
 991             [r'data-(?:video-player|media)-vpid="(%s)"' % self._ID_REGEX, 
 992              r'<param[^>]+name="externalIdentifier
"[^>]+value="(%s)"' % self._ID_REGEX, 
 993              r'videoId\s*:\s*["\'](%s)["\']' % self._ID_REGEX], 
 994             webpage, 'vpid', default=None) 
 997             formats, subtitles = self._download_media_selector(programme_id) 
 998             self._sort_formats(formats) 
 999             # digitalData may be missing (e.g. http://www.bbc.com/autos/story/20130513-hyundais-rock-star) 
1000             digital_data = self._parse_json( 
1002                     r'var\s+digitalData\s*=\s*({.+?});?\n', webpage, 'digital data', default='{}'), 
1003                 programme_id, fatal=False) 
1004             page_info = digital_data.get('page', {}).get('pageInfo', {}) 
1005             title = page_info.get('pageName') or self._og_search_title(webpage) 
1006             description = page_info.get('description') or self._og_search_description(webpage) 
1007             timestamp = parse_iso8601(page_info.get('publicationDate')) or timestamp 
1011                 'description': description, 
1012                 'timestamp': timestamp, 
1014                 'subtitles': subtitles, 
1017         # Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975) 
1018         # There are several setPayload calls may be present but the video 
1019         # seems to be always related to the first one 
1020         morph_payload = self._parse_json( 
1022                 r'Morph\.setPayload\([^,]+,\s*({.+?})\);', 
1023                 webpage, 'morph payload', default='{}'), 
1024             playlist_id, fatal=False) 
1026             components = try_get(morph_payload, lambda x: x['body']['components'], list) or [] 
1027             for component in components: 
1028                 if not isinstance(component, dict): 
1030                 lead_media = try_get(component, lambda x: x['props']['leadMedia'], dict) 
1033                 identifiers = lead_media.get('identifiers') 
1034                 if not identifiers or not isinstance(identifiers, dict): 
1036                 programme_id = identifiers.get('vpid') or identifiers.get('playablePid') 
1037                 if not programme_id: 
1039                 title = lead_media.get('title') or self._og_search_title(webpage) 
1040                 formats, subtitles = self._download_media_selector(programme_id) 
1041                 self._sort_formats(formats) 
1042                 description = lead_media.get('summary') 
1043                 uploader = lead_media.get('masterBrand') 
1044                 uploader_id = lead_media.get('mid') 
1046                 duration_d = lead_media.get('duration') 
1047                 if isinstance(duration_d, dict): 
1048                     duration = parse_duration(dict_get( 
1049                         duration_d, ('rawDuration', 'formattedDuration', 'spokenDuration'))) 
1053                     'description': description, 
1054                     'duration': duration, 
1055                     'uploader': uploader, 
1056                     'uploader_id': uploader_id, 
1058                     'subtitles': subtitles, 
1061         preload_state = self._parse_json(self._search_regex( 
1062             r'window\.__PRELOADED_STATE__\s*=\s*({.+?});', webpage, 
1063             'preload state', default='{}'), playlist_id, fatal=False) 
1065             current_programme = preload_state.get('programmes', {}).get('current') or {} 
1066             programme_id = current_programme.get('id') 
1067             if current_programme and programme_id and current_programme.get('type') == 'playable_item': 
1068                 title = current_programme.get('titles', {}).get('tertiary') or playlist_title 
1069                 formats, subtitles = self._download_media_selector(programme_id) 
1070                 self._sort_formats(formats) 
1071                 synopses = current_programme.get('synopses') or {} 
1072                 network = current_programme.get('network') or {} 
1073                 duration = int_or_none( 
1074                     current_programme.get('duration', {}).get('value')) 
1076                 image_url = current_programme.get('image_url') 
1078                     thumbnail = image_url.replace('{recipe}', '1920x1920') 
1082                     'description': dict_get(synopses, ('long', 'medium', 'short')), 
1083                     'thumbnail': thumbnail, 
1084                     'duration': duration, 
1085                     'uploader': network.get('short_title'), 
1086                     'uploader_id': network.get('id'), 
1088                     'subtitles': subtitles, 
1091         bbc3_config = self._parse_json( 
1093                 r'(?s)bbcthreeConfig\s*=\s*({.+?})\s*;\s*<', webpage, 
1094                 'bbcthree config', default='{}'), 
1095             playlist_id, transform_source=js_to_json, fatal=False) 
1097             bbc3_playlist = try_get( 
1098                 bbc3_config, lambda x: x['payload']['content']['bbcMedia']['playlist'], 
1101                 playlist_title = bbc3_playlist.get('title') or playlist_title 
1102                 thumbnail = bbc3_playlist.get('holdingImageURL') 
1104                 for bbc3_item in bbc3_playlist['items']: 
1105                     programme_id = bbc3_item.get('versionID') 
1106                     if not programme_id: 
1108                     formats, subtitles = self._download_media_selector(programme_id) 
1109                     self._sort_formats(formats) 
1112                         'title': playlist_title, 
1113                         'thumbnail': thumbnail, 
1114                         'timestamp': timestamp, 
1116                         'subtitles': subtitles, 
1118                 return self.playlist_result( 
1119                     entries, playlist_id, playlist_title, playlist_description) 
1121         def extract_all(pattern): 
1122             return list(filter(None, map( 
1123                 lambda s: self._parse_json(s, playlist_id, fatal=False), 
1124                 re.findall(pattern, webpage)))) 
1126         # Multiple video article (e.g. 
1127         # http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460) 
1128         EMBED_URL = r'https?://(?:www\.)?bbc\.co\.uk/(?:[^/]+/)+%s(?:\b[^"]+)?
' % self._ID_REGEX 
1130         for match in extract_all(r'new\s
+SMP\
(({.+?
})\
)'): 
1131             embed_url = match.get('playerSettings
', {}).get('externalEmbedUrl
') 
1132             if embed_url and re.match(EMBED_URL, embed_url): 
1133                 entries.append(embed_url) 
1134         entries.extend(re.findall( 
1135             r'setPlaylist\
("(%s)"\
)' % EMBED_URL, webpage)) 
1137             return self.playlist_result( 
1138                 [self.url_result(entry_, 'BBCCoUk
') for entry_ in entries], 
1139                 playlist_id, playlist_title, playlist_description) 
1141         # Multiple video article (e.g. http://www.bbc.com/news/world-europe-32668511) 
1142         medias = extract_all(r"data-media-meta='({[^
']+})'") 
1145             # Single video article (e.g. http://www.bbc.com/news/video_and_audio/international) 
1146             media_asset = self._search_regex( 
1147                 r'mediaAssetPage\.init\(\s*({.+?}), "/', 
1148                 webpage, 'media asset
', default=None) 
1150                 media_asset_page = self._parse_json(media_asset, playlist_id, fatal=False) 
1152                 for video in media_asset_page.get('videos
', {}).values(): 
1153                     medias.extend(video.values()) 
1156             # Multiple video playlist with single `now playing` entry (e.g. 
1157             # http://www.bbc.com/news/video_and_audio/must_see/33767813) 
1158             vxp_playlist = self._parse_json( 
1160                     r'<script
[^
>]+class="vxp-playlist-data"[^
>]+type="application/json"[^
>]*>([^
<]+)</script
>', 
1161                     webpage, 'playlist data
'), 
1163             playlist_medias = [] 
1164             for item in vxp_playlist: 
1165                 media = item.get('media
') 
1168                 playlist_medias.append(media) 
1169                 # Download single video if found media with asset id matching the video id from URL 
1170                 if item.get('advert
', {}).get('assetId
') == playlist_id: 
1173             # Fallback to the whole playlist 
1175                 medias = playlist_medias 
1178         for num, media_meta in enumerate(medias, start=1): 
1179             formats, subtitles = self._extract_from_media_meta(media_meta, playlist_id) 
1182             self._sort_formats(formats) 
1184             video_id = media_meta.get('externalId
') 
1186                 video_id = playlist_id if len(medias) == 1 else '%s-%s' % (playlist_id, num) 
1188             title = media_meta.get('caption
') 
1190                 title = playlist_title if len(medias) == 1 else '%s - Video 
%s' % (playlist_title, num) 
1192             duration = int_or_none(media_meta.get('durationInSeconds
')) or parse_duration(media_meta.get('duration
')) 
1195             for image in media_meta.get('images
', {}).values(): 
1196                 images.extend(image.values()) 
1197             if 'image
' in media_meta: 
1198                 images.append(media_meta['image
']) 
1201                 'url
': image.get('href
'), 
1202                 'width
': int_or_none(image.get('width
')), 
1203                 'height
': int_or_none(image.get('height
')), 
1204             } for image in images] 
1209                 'thumbnails
': thumbnails, 
1210                 'duration
': duration, 
1211                 'timestamp
': timestamp, 
1213                 'subtitles
': subtitles, 
1216         return self.playlist_result(entries, playlist_id, playlist_title, playlist_description) 
1219 class BBCCoUkArticleIE(InfoExtractor): 
1220     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.co\
.uk
/programmes
/articles
/(?P
<id>[a
-zA
-Z0
-9]+)' 
1221     IE_NAME = 'bbc
.co
.uk
:article
' 
1222     IE_DESC = 'BBC articles
' 
1225         'url
': 'http
://www
.bbc
.co
.uk
/programmes
/articles
/3jNQLTMrPlYGTBn0WV6M2MS
/not-your
-typical
-role
-model
-ada
-lovelace
-the
-19th
-century
-programmer
', 
1227             'id': '3jNQLTMrPlYGTBn0WV6M2MS
', 
1228             'title
': 'Calculating Ada
: The Countess of Computing 
- Not your typical role model
: Ada Lovelace the 
19th century programmer 
- BBC Four
', 
1229             'description
': 'Hannah Fry reveals some of her surprising discoveries about Ada Lovelace during filming
.', 
1231         'playlist_count
': 4, 
1232         'add_ie
': ['BBCCoUk
'], 
1235     def _real_extract(self, url): 
1236         playlist_id = self._match_id(url) 
1238         webpage = self._download_webpage(url, playlist_id) 
1240         title = self._og_search_title(webpage) 
1241         description = self._og_search_description(webpage).strip() 
1243         entries = [self.url_result(programme_url) for programme_url in re.findall( 
1244             r'<div
[^
>]+typeof
="Clip"[^
>]+resource
="([^"]+)"', webpage)] 
1246         return self.playlist_result(entries, playlist_id, title, description) 
1249 class BBCCoUkPlaylistBaseIE(InfoExtractor): 
1250     def _entries(self, webpage, url, playlist_id): 
1251         single_page = 'page' in compat_urlparse.parse_qs( 
1252             compat_urlparse.urlparse(url).query) 
1253         for page_num in itertools.count(2): 
1254             for video_id in re.findall( 
1255                     self._VIDEO_ID_TEMPLATE % BBCCoUkIE._ID_REGEX, webpage): 
1256                 yield self.url_result( 
1257                     self._URL_TEMPLATE % video_id, BBCCoUkIE.ie_key()) 
1260             next_page = self._search_regex( 
1261                 r'<li[^>]+class=(["\'])pagination_
+next\
1[^
>]*><a
[^
>]+href
=(["\'])(?P<url>(?:(?!\2).)+)\2', 
1262                 webpage, 'next page url', default=None, group='url') 
1265             webpage = self._download_webpage( 
1266                 compat_urlparse.urljoin(url, next_page), playlist_id, 
1267                 'Downloading page %d' % page_num, page_num) 
1269     def _real_extract(self, url): 
1270         playlist_id = self._match_id(url) 
1272         webpage = self._download_webpage(url, playlist_id) 
1274         title, description = self._extract_title_and_description(webpage) 
1276         return self.playlist_result( 
1277             self._entries(webpage, url, playlist_id), 
1278             playlist_id, title, description) 
1281 class BBCCoUkIPlayerPlaylistIE(BBCCoUkPlaylistBaseIE): 
1282     IE_NAME = 'bbc.co.uk:iplayer:playlist' 
1283     _VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/iplayer/(?:episodes|group)/(?P<id>%s)' % BBCCoUkIE._ID_REGEX 
1284     _URL_TEMPLATE = 'http://www.bbc.co.uk/iplayer/episode/%s' 
1285     _VIDEO_ID_TEMPLATE = r'data-ip-id=["\'](%s)' 
1287         'url
': 'http
://www
.bbc
.co
.uk
/iplayer
/episodes
/b05rcz9v
', 
1290             'title
': 'The Disappearance
', 
1291             'description
': 'French thriller serial about a missing teenager
.', 
1293         'playlist_mincount
': 6, 
1294         'skip
': 'This programme 
is not currently available on BBC iPlayer
', 
1296         # Available for over a year unlike 30 days for most other programmes 
1297         'url
': 'http
://www
.bbc
.co
.uk
/iplayer
/group
/p02tcc32
', 
1300             'title
': 'Bohemian Icons
', 
1301             'description
': 'md5
:683e901041b2fe9ba596f2ab04c4dbe7
', 
1303         'playlist_mincount
': 10, 
1306     def _extract_title_and_description(self, webpage): 
1307         title = self._search_regex(r'<h1
>([^
<]+)</h1
>', webpage, 'title
', fatal=False) 
1308         description = self._search_regex( 
1309             r'<p
[^
>]+class=(["\'])subtitle\1[^>]*>(?P<value>[^<]+)</p>', 
1310             webpage, 'description', fatal=False, group='value') 
1311         return title, description 
1314 class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE): 
1315     IE_NAME = 'bbc.co.uk:playlist' 
1316     _VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/programmes/(?P<id>%s)/(?:episodes|broadcasts|clips)' % BBCCoUkIE._ID_REGEX 
1317     _URL_TEMPLATE = 'http://www.bbc.co.uk/programmes/%s' 
1318     _VIDEO_ID_TEMPLATE = r'data-pid=["\'](%s)' 
1320         'url
': 'http
://www
.bbc
.co
.uk
/programmes
/b05rcz9v
/clips
', 
1323             'title
': 'The Disappearance 
- Clips 
- BBC Four
', 
1324             'description
': 'French thriller serial about a missing teenager
.', 
1326         'playlist_mincount
': 7, 
1328         # multipage playlist, explicit page 
1329         'url
': 'http
://www
.bbc
.co
.uk
/programmes
/b00mfl7n
/clips?page
=1', 
1332             'title
': 'Frozen Planet 
- Clips 
- BBC One
', 
1333             'description
': 'md5
:65dcbf591ae628dafe32aa6c4a4a0d8c
', 
1335         'playlist_mincount
': 24, 
1337         # multipage playlist, all pages 
1338         'url
': 'http
://www
.bbc
.co
.uk
/programmes
/b00mfl7n
/clips
', 
1341             'title
': 'Frozen Planet 
- Clips 
- BBC One
', 
1342             'description
': 'md5
:65dcbf591ae628dafe32aa6c4a4a0d8c
', 
1344         'playlist_mincount
': 142, 
1346         'url
': 'http
://www
.bbc
.co
.uk
/programmes
/b05rcz9v
/broadcasts
/2016/06', 
1347         'only_matching
': True, 
1349         'url
': 'http
://www
.bbc
.co
.uk
/programmes
/b05rcz9v
/clips
', 
1350         'only_matching
': True, 
1352         'url
': 'http
://www
.bbc
.co
.uk
/programmes
/b055jkys
/episodes
/player
', 
1353         'only_matching
': True, 
1356     def _extract_title_and_description(self, webpage): 
1357         title = self._og_search_title(webpage, fatal=False) 
1358         description = self._og_search_description(webpage) 
1359         return title, description