2 from __future__ 
import unicode_literals
 
   7 from .common 
import InfoExtractor
 
  23 from ..compat 
import ( 
  29 class BBCCoUkIE(InfoExtractor
): 
  31     IE_DESC 
= 'BBC iPlayer' 
  32     _ID_REGEX 
= r
'(?:[pbm][\da-z]{7}|w[\da-z]{7,14})' 
  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, 
 240             'url': 'https://www.bbc.co.uk/programmes/m00005xn', 
 241             'only_matching': True, 
 243             'url': 'https://www.bbc.co.uk/programmes/w172w4dww1jqt5s', 
 244             'only_matching': True, 
 247     _USP_RE 
= r
'/([^/]+?)\.ism(?:\.hlsv2\.ism)?/[^/]+\.m3u8' 
 250         username
, password 
= self
._get
_login
_info
() 
 254         login_page 
= self
._download
_webpage
( 
 255             self
._LOGIN
_URL
, None, 'Downloading signin page') 
 257         login_form 
= self
._hidden
_inputs
(login_page
) 
 260             'username': username
, 
 261             'password': password
, 
 264         post_url 
= urljoin(self
._LOGIN
_URL
, self
._search
_regex
( 
 265             r
'<form[^>]+action=(["\'])(?P
<url
>.+?
)\
1', login_page, 
 266             'post url
', default=self._LOGIN_URL, group='url
')) 
 268         response, urlh = self._download_webpage_handle( 
 269             post_url, None, 'Logging 
in', data=urlencode_postdata(login_form), 
 270             headers={'Referer
': self._LOGIN_URL}) 
 272         if self._LOGIN_URL in urlh.geturl(): 
 273             error = clean_html(get_element_by_class('form
-message
', response)) 
 275                 raise ExtractorError( 
 276                     'Unable to login
: %s' % error, expected=True) 
 277             raise ExtractorError('Unable to log 
in') 
 279     def _real_initialize(self): 
 282     class MediaSelectionError(Exception): 
 283         def __init__(self, id): 
 286     def _extract_asx_playlist(self, connection, programme_id): 
 287         asx = self._download_xml(connection.get('href
'), programme_id, 'Downloading ASX playlist
') 
 288         return [ref.get('href
') for ref in asx.findall('./Entry
/ref
')] 
 290     def _extract_items(self, playlist): 
 291         return playlist.findall('./{%s}item
' % self._EMP_PLAYLIST_NS) 
 293     def _findall_ns(self, element, xpath): 
 295         for ns in self._NAMESPACES: 
 296             elements.extend(element.findall(xpath % ns)) 
 299     def _extract_medias(self, media_selection): 
 300         error = media_selection.find('./{%s}error
' % self._MEDIASELECTION_NS) 
 302             media_selection.find('./{%s}error
' % self._EMP_PLAYLIST_NS) 
 303         if error is not None: 
 304             raise BBCCoUkIE.MediaSelectionError(error.get('id')) 
 305         return self._findall_ns(media_selection, './{%s}media
') 
 307     def _extract_connections(self, media): 
 308         return self._findall_ns(media, './{%s}connection
') 
 310     def _get_subtitles(self, media, programme_id): 
 312         for connection in self._extract_connections(media): 
 313             captions = self._download_xml(connection.get('href
'), programme_id, 'Downloading captions
') 
 314             lang = captions.get('{http
://www
.w3
.org
/XML
/1998/namespace
}lang
', 'en
') 
 317                     'url
': connection.get('href
'), 
 323     def _raise_extractor_error(self, media_selection_error): 
 324         raise ExtractorError( 
 325             '%s returned error
: %s' % (self.IE_NAME, media_selection_error.id), 
 328     def _download_media_selector(self, programme_id): 
 329         last_exception = None 
 330         for mediaselector_url in self._MEDIASELECTOR_URLS: 
 332                 return self._download_media_selector_url( 
 333                     mediaselector_url % programme_id, programme_id) 
 334             except BBCCoUkIE.MediaSelectionError as e: 
 335                 if e.id in ('notukerror
', 'geolocation
', 'selectionunavailable
'): 
 338                 self._raise_extractor_error(e) 
 339         self._raise_extractor_error(last_exception) 
 341     def _download_media_selector_url(self, url, programme_id=None): 
 342         media_selection = self._download_xml( 
 343             url, programme_id, 'Downloading media selection XML
', 
 344             expected_status=(403, 404)) 
 345         return self._process_media_selector(media_selection, programme_id) 
 347     def _process_media_selector(self, media_selection, programme_id): 
 352         for media in self._extract_medias(media_selection): 
 353             kind = media.get('kind
') 
 354             if kind in ('video
', 'audio
'): 
 355                 bitrate = int_or_none(media.get('bitrate
')) 
 356                 encoding = media.get('encoding
') 
 357                 service = media.get('service
') 
 358                 width = int_or_none(media.get('width
')) 
 359                 height = int_or_none(media.get('height
')) 
 360                 file_size = int_or_none(media.get('media_file_size
')) 
 361                 for connection in self._extract_connections(media): 
 362                     href = connection.get('href
') 
 367                     conn_kind = connection.get('kind
') 
 368                     protocol = connection.get('protocol
') 
 369                     supplier = connection.get('supplier
') 
 370                     transfer_format = connection.get('transferFormat
') 
 371                     format_id = supplier or conn_kind or protocol 
 373                         format_id = '%s_%s' % (service, format_id) 
 375                     if supplier == 'asx
': 
 376                         for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)): 
 379                                 'format_id
': 'ref
%s_%s' % (i, format_id), 
 381                     elif transfer_format == 'dash
': 
 382                         formats.extend(self._extract_mpd_formats( 
 383                             href, programme_id, mpd_id=format_id, fatal=False)) 
 384                     elif transfer_format == 'hls
': 
 385                         formats.extend(self._extract_m3u8_formats( 
 386                             href, programme_id, ext='mp4
', entry_protocol='m3u8_native
', 
 387                             m3u8_id=format_id, fatal=False)) 
 388                         if re.search(self._USP_RE, href): 
 389                             usp_formats = self._extract_m3u8_formats( 
 390                                 re.sub(self._USP_RE, r'/\
1.ism
/\
1.m3u8
', href), 
 391                                 programme_id, ext='mp4
', entry_protocol='m3u8_native
', 
 392                                 m3u8_id=format_id, fatal=False) 
 393                             for f in usp_formats: 
 394                                 if f.get('height
') and f['height
'] > 720: 
 397                     elif transfer_format == 'hds
': 
 398                         formats.extend(self._extract_f4m_formats( 
 399                             href, programme_id, f4m_id=format_id, fatal=False)) 
 401                         if not service and not supplier and bitrate: 
 402                             format_id += '-%d' % bitrate 
 404                             'format_id
': format_id, 
 405                             'filesize
': file_size, 
 420                         if protocol in ('http
', 'https
'): 
 425                         elif protocol == 'rtmp
': 
 426                             application = connection.get('application
', 'ondemand
') 
 427                             auth_string = connection.get('authString
') 
 428                             identifier = connection.get('identifier
') 
 429                             server = connection.get('server
') 
 431                                 'url
': '%s://%s/%s?
%s' % (protocol, server, application, auth_string), 
 432                                 'play_path
': identifier, 
 433                                 'app
': '%s?
%s' % (application, auth_string), 
 434                                 'page_url
': 'http
://www
.bbc
.co
.uk
', 
 435                                 'player_url
': 'http
://www
.bbc
.co
.uk
/emp
/releases
/iplayer
/revisions
/617463_618125_4/617463_618125_4_emp
.swf
', 
 442             elif kind == 'captions
': 
 443                 subtitles = self.extract_subtitles(media, programme_id) 
 444         return formats, subtitles 
 446     def _download_playlist(self, playlist_id): 
 448             playlist = self._download_json( 
 449                 'http
://www
.bbc
.co
.uk
/programmes
/%s/playlist
.json
' % playlist_id, 
 450                 playlist_id, 'Downloading playlist JSON
') 
 452             version = playlist.get('defaultAvailableVersion
') 
 454                 smp_config = version['smpConfig
'] 
 455                 title = smp_config['title
'] 
 456                 description = smp_config['summary
'] 
 457                 for item in smp_config['items
']: 
 459                     if kind not in ('programme
', 'radioProgramme
'): 
 461                     programme_id = item.get('vpid
') 
 462                     duration = int_or_none(item.get('duration
')) 
 463                     formats, subtitles = self._download_media_selector(programme_id) 
 464                 return programme_id, title, description, duration, formats, subtitles 
 465         except ExtractorError as ee: 
 466             if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404): 
 469         # fallback to legacy playlist 
 470         return self._process_legacy_playlist(playlist_id) 
 472     def _process_legacy_playlist_url(self, url, display_id): 
 473         playlist = self._download_legacy_playlist_url(url, display_id) 
 474         return self._extract_from_legacy_playlist(playlist, display_id) 
 476     def _process_legacy_playlist(self, playlist_id): 
 477         return self._process_legacy_playlist_url( 
 478             'http
://www
.bbc
.co
.uk
/iplayer
/playlist
/%s' % playlist_id, playlist_id) 
 480     def _download_legacy_playlist_url(self, url, playlist_id=None): 
 481         return self._download_xml( 
 482             url, playlist_id, 'Downloading legacy playlist XML
') 
 484     def _extract_from_legacy_playlist(self, playlist, playlist_id): 
 485         no_items = playlist.find('./{%s}noItems
' % self._EMP_PLAYLIST_NS) 
 486         if no_items is not None: 
 487             reason = no_items.get('reason
') 
 488             if reason == 'preAvailability
': 
 489                 msg = 'Episode 
%s is not yet available
' % playlist_id 
 490             elif reason == 'postAvailability
': 
 491                 msg = 'Episode 
%s is no longer available
' % playlist_id 
 492             elif reason == 'noMedia
': 
 493                 msg = 'Episode 
%s is not currently available
' % playlist_id 
 495                 msg = 'Episode 
%s is not available
: %s' % (playlist_id, reason) 
 496             raise ExtractorError(msg, expected=True) 
 498         for item in self._extract_items(playlist): 
 499             kind = item.get('kind
') 
 500             if kind not in ('programme
', 'radioProgramme
'): 
 502             title = playlist.find('./{%s}title
' % self._EMP_PLAYLIST_NS).text 
 503             description_el = playlist.find('./{%s}summary
' % self._EMP_PLAYLIST_NS) 
 504             description = description_el.text if description_el is not None else None 
 506             def get_programme_id(item): 
 507                 def get_from_attributes(item): 
 508                     for p in('identifier
', 'group
'): 
 510                         if value and re.match(r'^
[pb
][\da
-z
]{7}$
', value): 
 512                 get_from_attributes(item) 
 513                 mediator = item.find('./{%s}mediator
' % self._EMP_PLAYLIST_NS) 
 514                 if mediator is not None: 
 515                     return get_from_attributes(mediator) 
 517             programme_id = get_programme_id(item) 
 518             duration = int_or_none(item.get('duration
')) 
 521                 formats, subtitles = self._download_media_selector(programme_id) 
 523                 formats, subtitles = self._process_media_selector(item, playlist_id) 
 524                 programme_id = playlist_id 
 526         return programme_id, title, description, duration, formats, subtitles 
 528     def _real_extract(self, url): 
 529         group_id = self._match_id(url) 
 531         webpage = self._download_webpage(url, group_id, 'Downloading video page
') 
 533         error = self._search_regex( 
 534             r'<div
\b[^
>]+\bclass
=["\']smp__message delta["\'][^
>]*>([^
<]+)<', 
 535             webpage, 'error
', default=None) 
 537             raise ExtractorError(error, expected=True) 
 542         tviplayer = self._search_regex( 
 543             r'mediator\
.bind\
(({.+?
})\s
*,\s
*document\
.getElementById
', 
 544             webpage, 'player
', default=None) 
 547             player = self._parse_json(tviplayer, group_id).get('player
', {}) 
 548             duration = int_or_none(player.get('duration
')) 
 549             programme_id = player.get('vpid
') 
 552             programme_id = self._search_regex( 
 553                 r'"vpid"\s
*:\s
*"(%s)"' % self._ID_REGEX, webpage, 'vpid
', fatal=False, default=None) 
 556             formats, subtitles = self._download_media_selector(programme_id) 
 557             title = self._og_search_title(webpage, default=None) or self._html_search_regex( 
 558                 (r'<h2
[^
>]+id="parent-title"[^
>]*>(.+?
)</h2
>', 
 559                  r'<div
[^
>]+class="info"[^
>]*>\s
*<h1
>(.+?
)</h1
>'), webpage, 'title
') 
 560             description = self._search_regex( 
 561                 (r'<p 
class="[^"]*medium
-description
[^
"]*">([^
<]+)</p
>', 
 562                  r'<div
[^
>]+class="info_+synopsis"[^
>]*>([^
<]+)</div
>'), 
 563                 webpage, 'description
', default=None) 
 565                 description = self._html_search_meta('description
', webpage) 
 567             programme_id, title, description, duration, formats, subtitles = self._download_playlist(group_id) 
 569         self._sort_formats(formats) 
 574             'description
': description, 
 575             'thumbnail
': self._og_search_thumbnail(webpage, default=None), 
 576             'duration
': duration, 
 578             'subtitles
': subtitles, 
 582 class BBCIE(BBCCoUkIE): 
 585     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.(?
:com|co\
.uk
)/(?
:[^
/]+/)+(?P
<id>[^
/#?]+)' 
 587     _MEDIASELECTOR_URLS 
= [ 
 588         # Provides HQ HLS streams but fails with geolocation in some cases when it's 
 589         # even not geo restricted at all 
 590         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/%s', 
 591         # Provides more formats, namely direct mp4 links, but fails on some videos with 
 592         # notukerror for non UK (?) users (e.g. 
 593         # http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret) 
 594         'http://open.live.bbc.co.uk/mediaselector/4/mtis/stream/%s', 
 595         # Provides fewer formats, but works everywhere for everybody (hopefully) 
 596         'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/journalism-pc/vpid/%s', 
 600         # article with multiple videos embedded with data-playable containing vpids 
 601         'url': 'http://www.bbc.com/news/world-europe-32668511', 
 603             'id': 'world-europe-32668511', 
 604             'title': 'Russia stages massive WW2 parade despite Western boycott', 
 605             'description': 'md5:00ff61976f6081841f759a08bf78cc9c', 
 609         # article with multiple videos embedded with data-playable (more videos) 
 610         'url': 'http://www.bbc.com/news/business-28299555', 
 612             'id': 'business-28299555', 
 613             'title': 'Farnborough Airshow: Video highlights', 
 614             'description': 'BBC reports and video highlights at the Farnborough Airshow.', 
 619         # article with multiple videos embedded with `new SMP()` 
 621         'url': 'http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460', 
 623             'id': '3662a707-0af9-3149-963f-47bea720b460', 
 626         'playlist_count': 18, 
 628         # single video embedded with data-playable containing vpid 
 629         'url': 'http://www.bbc.com/news/world-europe-32041533', 
 633             'title': 'Aerial footage showed the site of the crash in the Alps - courtesy BFM TV', 
 634             'description': 'md5:2868290467291b37feda7863f7a83f54', 
 636             'timestamp': 1427219242, 
 637             'upload_date': '20150324', 
 641             'skip_download': True, 
 644         # article with single video embedded with data-playable containing XML playlist 
 645         # with direct video links as progressiveDownloadUrl (for now these are extracted) 
 646         # and playlist with f4m and m3u8 as streamingUrl 
 647         'url': 'http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu', 
 649             'id': '150615_telabyad_kentin_cogu', 
 651             'title': "YPG: Tel Abyad'ın tamamı kontrolümüzde", 
 652             'description': 'md5:33a4805a855c9baf7115fcbde57e7025', 
 653             'timestamp': 1434397334, 
 654             'upload_date': '20150615', 
 657             'skip_download': True, 
 660         # single video embedded with data-playable containing XML playlists (regional section) 
 661         'url': 'http://www.bbc.com/mundo/video_fotos/2015/06/150619_video_honduras_militares_hospitales_corrupcion_aw', 
 663             'id': '150619_video_honduras_militares_hospitales_corrupcion_aw', 
 665             'title': 'Honduras militariza sus hospitales por nuevo escÔndalo de corrupción', 
 666             'description': 'md5:1525f17448c4ee262b64b8f0c9ce66c8', 
 667             'timestamp': 1434713142, 
 668             'upload_date': '20150619', 
 671             'skip_download': True, 
 674         # single video from video playlist embedded with vxp-playlist-data JSON 
 675         'url': 'http://www.bbc.com/news/video_and_audio/must_see/33376376', 
 679             'title': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''', 
 681             'description': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''', 
 684             'skip_download': True, 
 687         # single video story with digitalData 
 688         'url': 'http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret', 
 692             'title': 'Sri Lankaās spicy secret', 
 693             'description': 'As a new train line to Jaffna opens up the countryās north, travellers can experience a truly distinct slice of Tamil culture.', 
 694             'timestamp': 1437674293, 
 695             'upload_date': '20150723', 
 699             'skip_download': True, 
 702         # single video story without digitalData 
 703         'url': 'http://www.bbc.com/autos/story/20130513-hyundais-rock-star', 
 707             'title': 'Hyundai Santa Fe Sport: Rock star', 
 708             'description': 'md5:b042a26142c4154a6e472933cf20793d', 
 709             'timestamp': 1415867444, 
 710             'upload_date': '20141113', 
 714             'skip_download': True, 
 717         # single video embedded with Morph 
 718         'url': 'http://www.bbc.co.uk/sport/live/olympics/36895975', 
 722             'title': "Nigeria v Japan - Men's First Round", 
 723             'description': 'Live coverage of the first round from Group B at the Amazonia Arena.', 
 725             'uploader': 'BBC Sport', 
 726             'uploader_id': 'bbc_sport', 
 730             'skip_download': True, 
 732         'skip': 'Georestricted to UK', 
 734         # single video with playlist.sxml URL in playlist param 
 735         'url': 'http://www.bbc.com/sport/0/football/33653409', 
 739             'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?', 
 740             'description': 'BBC Sport\'s David Ornstein has the latest transfer gossip, including rumours of a Manchester United return for Cristiano Ronaldo.', 
 745             'skip_download': True, 
 748         # article with multiple videos embedded with playlist.sxml in playlist param 
 749         'url': 'http://www.bbc.com/sport/0/football/34475836', 
 752             'title': 'Jurgen Klopp: Furious football from a witty and winning coach', 
 753             'description': 'Fast-paced football, wit, wisdom and a ready smile - why Liverpool fans should come to love new boss Jurgen Klopp.', 
 757         # school report article with single video 
 758         'url': 'http://www.bbc.co.uk/schoolreport/35744779', 
 761             'title': 'School which breaks down barriers in Jerusalem', 
 765         # single video with playlist URL from weather section 
 766         'url': 'http://www.bbc.com/weather/features/33601775', 
 767         'only_matching': True, 
 769         # custom redirection to www.bbc.com 
 770         'url': 'http://www.bbc.co.uk/news/science-environment-33661876', 
 771         'only_matching': True, 
 773         # single video article embedded with data-media-vpid 
 774         'url': 'http://www.bbc.co.uk/sport/rowing/35908187', 
 775         'only_matching': True, 
 777         'url': 'https://www.bbc.co.uk/bbcthree/clip/73d0bbd0-abc3-4cea-b3c0-cdae21905eb1', 
 781             'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?', 
 782             'description': 'md5:4b7dfd063d5a789a1512e99662be3ddd', 
 785             'skip_download': True, 
 788         # window.__PRELOADED_STATE__ 
 789         'url': 'https://www.bbc.co.uk/radio/play/b0b9z4yl', 
 793             'title': 'Prom 6: An American in Paris and Turangalila', 
 794             'description': 'md5:51cf7d6f5c8553f197e58203bc78dff8', 
 795             'uploader': 'Radio 3', 
 796             'uploader_id': 'bbc_radio_three', 
 801     def suitable(cls
, url
): 
 802         EXCLUDE_IE 
= (BBCCoUkIE
, BBCCoUkArticleIE
, BBCCoUkIPlayerPlaylistIE
, BBCCoUkPlaylistIE
) 
 803         return (False if any(ie
.suitable(url
) for ie 
in EXCLUDE_IE
) 
 804                 else super(BBCIE
, cls
).suitable(url
)) 
 806     def _extract_from_media_meta(self
, media_meta
, video_id
): 
 807         # Direct links to media in media metadata (e.g. 
 808         # http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu) 
 809         # TODO: there are also f4m and m3u8 streams incorporated in playlist.sxml 
 810         source_files 
= media_meta
.get('sourceFiles') 
 814                 'format_id': format_id
, 
 815                 'ext': f
.get('encoding'), 
 816                 'tbr': float_or_none(f
.get('bitrate'), 1000), 
 817                 'filesize': int_or_none(f
.get('filesize')), 
 818             } for format_id
, f 
in source_files
.items() if f
.get('url')], [] 
 820         programme_id 
= media_meta
.get('externalId') 
 822             return self
._download
_media
_selector
(programme_id
) 
 824         # Process playlist.sxml as legacy playlist 
 825         href 
= media_meta
.get('href') 
 827             playlist 
= self
._download
_legacy
_playlist
_url
(href
) 
 828             _
, _
, _
, _
, formats
, subtitles 
= self
._extract
_from
_legacy
_playlist
(playlist
, video_id
) 
 829             return formats
, subtitles
 
 833     def _extract_from_playlist_sxml(self
, url
, playlist_id
, timestamp
): 
 834         programme_id
, title
, description
, duration
, formats
, subtitles 
= \
 
 835             self
._process
_legacy
_playlist
_url
(url
, playlist_id
) 
 836         self
._sort
_formats
(formats
) 
 840             'description': description
, 
 841             'duration': duration
, 
 842             'timestamp': timestamp
, 
 844             'subtitles': subtitles
, 
 847     def _real_extract(self
, url
): 
 848         playlist_id 
= self
._match
_id
(url
) 
 850         webpage 
= self
._download
_webpage
(url
, playlist_id
) 
 852         json_ld_info 
= self
._search
_json
_ld
(webpage
, playlist_id
, default
={}) 
 853         timestamp 
= json_ld_info
.get('timestamp') 
 855         playlist_title 
= json_ld_info
.get('title') 
 856         if not playlist_title
: 
 857             playlist_title 
= self
._og
_search
_title
( 
 858                 webpage
, default
=None) or self
._html
_search
_regex
( 
 859                 r
'<title>(.+?)</title>', webpage
, 'playlist title', default
=None) 
 861                 playlist_title 
= re
.sub(r
'(.+)\s*-\s*BBC.*?$', r
'\1', playlist_title
).strip() 
 863         playlist_description 
= json_ld_info
.get( 
 864             'description') or self
._og
_search
_description
(webpage
, default
=None) 
 867             timestamp 
= parse_iso8601(self
._search
_regex
( 
 868                 [r
'<meta[^>]+property="article:published_time"[^>]+content="([^"]+)"', 
 869                  r
'itemprop="datePublished"[^>]+datetime="([^"]+)"', 
 870                  r
'"datePublished":\s*"([^"]+)'], 
 871                 webpage
, 'date', default
=None)) 
 875         # article with multiple videos embedded with playlist.sxml (e.g. 
 876         # http://www.bbc.com/sport/0/football/34475836) 
 877         playlists 
= re
.findall(r
'<param[^>]+name="playlist"[^>]+value="([^"]+)"', webpage
) 
 878         playlists
.extend(re
.findall(r
'data-media-id="([^"]+/playlist\.sxml)"', webpage
)) 
 881                 self
._extract
_from
_playlist
_sxml
(playlist_url
, playlist_id
, timestamp
) 
 882                 for playlist_url 
in playlists
] 
 884         # news article with multiple videos embedded with data-playable 
 885         data_playables 
= re
.findall(r
'data-playable=(["\'])({.+?
})\
1', webpage) 
 887             for _, data_playable_json in data_playables: 
 888                 data_playable = self._parse_json( 
 889                     unescapeHTML(data_playable_json), playlist_id, fatal=False) 
 890                 if not data_playable: 
 892                 settings = data_playable.get('settings
', {}) 
 894                     # data-playable with video vpid in settings.playlistObject.items (e.g. 
 895                     # http://www.bbc.com/news/world-us-canada-34473351) 
 896                     playlist_object = settings.get('playlistObject
', {}) 
 898                         items = playlist_object.get('items
') 
 899                         if items and isinstance(items, list): 
 900                             title = playlist_object['title
'] 
 901                             description = playlist_object.get('summary
') 
 902                             duration = int_or_none(items[0].get('duration
')) 
 903                             programme_id = items[0].get('vpid
') 
 904                             formats, subtitles = self._download_media_selector(programme_id) 
 905                             self._sort_formats(formats) 
 909                                 'description
': description, 
 910                                 'timestamp
': timestamp, 
 911                                 'duration
': duration, 
 913                                 'subtitles
': subtitles, 
 916                         # data-playable without vpid but with a playlist.sxml URLs 
 917                         # in otherSettings.playlist (e.g. 
 918                         # http://www.bbc.com/turkce/multimedya/2015/10/151010_vid_ankara_patlama_ani) 
 919                         playlist = data_playable.get('otherSettings
', {}).get('playlist
', {}) 
 922                             for key in ('streaming
', 'progressiveDownload
'): 
 923                                 playlist_url = playlist.get('%sUrl
' % key) 
 927                                     info = self._extract_from_playlist_sxml( 
 928                                         playlist_url, playlist_id, timestamp) 
 932                                         entry['title
'] = info['title
'] 
 933                                         entry['formats
'].extend(info['formats
']) 
 934                                 except Exception as e: 
 935                                     # Some playlist URL may fail with 500, at the same time 
 936                                     # the other one may work fine (e.g. 
 937                                     # http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu) 
 938                                     if isinstance(e.cause, compat_HTTPError) and e.cause.code == 500: 
 942                                 self._sort_formats(entry['formats
']) 
 943                                 entries.append(entry) 
 946             return self.playlist_result(entries, playlist_id, playlist_title, playlist_description) 
 948         # single video story (e.g. http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret) 
 949         programme_id = self._search_regex( 
 950             [r'data
-(?
:video
-player|media
)-vpid
="(%s)"' % self._ID_REGEX, 
 951              r'<param
[^
>]+name
="externalIdentifier"[^
>]+value
="(%s)"' % self._ID_REGEX, 
 952              r'videoId\s
*:\s
*["\'](%s)["\']' % self._ID_REGEX], 
 953             webpage, 'vpid
', default=None) 
 956             formats, subtitles = self._download_media_selector(programme_id) 
 957             self._sort_formats(formats) 
 958             # digitalData may be missing (e.g. http://www.bbc.com/autos/story/20130513-hyundais-rock-star) 
 959             digital_data = self._parse_json( 
 961                     r'var\s
+digitalData\s
*=\s
*({.+?
});?
\n', webpage, 'digital data
', default='{}'), 
 962                 programme_id, fatal=False) 
 963             page_info = digital_data.get('page
', {}).get('pageInfo
', {}) 
 964             title = page_info.get('pageName
') or self._og_search_title(webpage) 
 965             description = page_info.get('description
') or self._og_search_description(webpage) 
 966             timestamp = parse_iso8601(page_info.get('publicationDate
')) or timestamp 
 970                 'description
': description, 
 971                 'timestamp
': timestamp, 
 973                 'subtitles
': subtitles, 
 976         # Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975) 
 977         # There are several setPayload calls may be present but the video 
 978         # seems to be always related to the first one 
 979         morph_payload = self._parse_json( 
 981                 r'Morph\
.setPayload\
([^
,]+,\s
*({.+?
})\
);', 
 982                 webpage, 'morph payload
', default='{}'), 
 983             playlist_id, fatal=False) 
 985             components = try_get(morph_payload, lambda x: x['body
']['components
'], list) or [] 
 986             for component in components: 
 987                 if not isinstance(component, dict): 
 989                 lead_media = try_get(component, lambda x: x['props
']['leadMedia
'], dict) 
 992                 identifiers = lead_media.get('identifiers
') 
 993                 if not identifiers or not isinstance(identifiers, dict): 
 995                 programme_id = identifiers.get('vpid
') or identifiers.get('playablePid
') 
 998                 title = lead_media.get('title
') or self._og_search_title(webpage) 
 999                 formats, subtitles = self._download_media_selector(programme_id) 
1000                 self._sort_formats(formats) 
1001                 description = lead_media.get('summary
') 
1002                 uploader = lead_media.get('masterBrand
') 
1003                 uploader_id = lead_media.get('mid
') 
1005                 duration_d = lead_media.get('duration
') 
1006                 if isinstance(duration_d, dict): 
1007                     duration = parse_duration(dict_get( 
1008                         duration_d, ('rawDuration
', 'formattedDuration
', 'spokenDuration
'))) 
1012                     'description
': description, 
1013                     'duration
': duration, 
1014                     'uploader
': uploader, 
1015                     'uploader_id
': uploader_id, 
1017                     'subtitles
': subtitles, 
1020         preload_state = self._parse_json(self._search_regex( 
1021             r'window\
.__PRELOADED
_STATE
__\s
*=\s
*({.+?
});', webpage, 
1022             'preload state
', default='{}'), playlist_id, fatal=False) 
1024             current_programme = preload_state.get('programmes
', {}).get('current
') or {} 
1025             programme_id = current_programme.get('id') 
1026             if current_programme and programme_id and current_programme.get('type') == 'playable_item
': 
1027                 title = current_programme.get('titles
', {}).get('tertiary
') or playlist_title 
1028                 formats, subtitles = self._download_media_selector(programme_id) 
1029                 self._sort_formats(formats) 
1030                 synopses = current_programme.get('synopses
') or {} 
1031                 network = current_programme.get('network
') or {} 
1032                 duration = int_or_none( 
1033                     current_programme.get('duration
', {}).get('value
')) 
1035                 image_url = current_programme.get('image_url
') 
1037                     thumbnail = image_url.replace('{recipe}
', '1920x1920
') 
1041                     'description
': dict_get(synopses, ('long', 'medium
', 'short
')), 
1042                     'thumbnail
': thumbnail, 
1043                     'duration
': duration, 
1044                     'uploader
': network.get('short_title
'), 
1045                     'uploader_id
': network.get('id'), 
1047                     'subtitles
': subtitles, 
1050         bbc3_config = self._parse_json( 
1052                 r'(?s
)bbcthreeConfig\s
*=\s
*({.+?
})\s
*;\s
*<', webpage, 
1053                 'bbcthree config
', default='{}'), 
1054             playlist_id, transform_source=js_to_json, fatal=False) 
1056             bbc3_playlist = try_get( 
1057                 bbc3_config, lambda x: x['payload
']['content
']['bbcMedia
']['playlist
'], 
1060                 playlist_title = bbc3_playlist.get('title
') or playlist_title 
1061                 thumbnail = bbc3_playlist.get('holdingImageURL
') 
1063                 for bbc3_item in bbc3_playlist['items
']: 
1064                     programme_id = bbc3_item.get('versionID
') 
1065                     if not programme_id: 
1067                     formats, subtitles = self._download_media_selector(programme_id) 
1068                     self._sort_formats(formats) 
1071                         'title
': playlist_title, 
1072                         'thumbnail
': thumbnail, 
1073                         'timestamp
': timestamp, 
1075                         'subtitles
': subtitles, 
1077                 return self.playlist_result( 
1078                     entries, playlist_id, playlist_title, playlist_description) 
1080         def extract_all(pattern): 
1081             return list(filter(None, map( 
1082                 lambda s: self._parse_json(s, playlist_id, fatal=False), 
1083                 re.findall(pattern, webpage)))) 
1085         # Multiple video article (e.g. 
1086         # http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460) 
1087         EMBED_URL = r'https?
://(?
:www\
.)?bbc\
.co\
.uk
/(?
:[^
/]+/)+%s(?
:\b[^
"]+)?' % self._ID_REGEX 
1089         for match in extract_all(r'new\s+SMP\(({.+?})\)'): 
1090             embed_url = match.get('playerSettings', {}).get('externalEmbedUrl') 
1091             if embed_url and re.match(EMBED_URL, embed_url): 
1092                 entries.append(embed_url) 
1093         entries.extend(re.findall( 
1094             r'setPlaylist\("(%s)"\)' % EMBED_URL, webpage)) 
1096             return self.playlist_result( 
1097                 [self.url_result(entry_, 'BBCCoUk') for entry_ in entries], 
1098                 playlist_id, playlist_title, playlist_description) 
1100         # Multiple video article (e.g. http://www.bbc.com/news/world-europe-32668511) 
1101         medias = extract_all(r"data
-media
-meta
='({[^']+})'") 
1104             # Single video article (e.g. http://www.bbc.com/news/video_and_audio/international) 
1105             media_asset = self._search_regex( 
1106                 r'mediaAssetPage\
.init\
(\s
*({.+?
}), "/', 
1107                 webpage, 'media asset', default=None) 
1109                 media_asset_page = self._parse_json(media_asset, playlist_id, fatal=False) 
1111                 for video in media_asset_page.get('videos', {}).values(): 
1112                     medias.extend(video.values()) 
1115             # Multiple video playlist with single `now playing` entry (e.g. 
1116             # http://www.bbc.com/news/video_and_audio/must_see/33767813) 
1117             vxp_playlist = self._parse_json( 
1119                     r'<script[^>]+class="vxp
-playlist
-data
"[^>]+type="application
/json
"[^>]*>([^<]+)</script>', 
1120                     webpage, 'playlist data'), 
1122             playlist_medias = [] 
1123             for item in vxp_playlist: 
1124                 media = item.get('media') 
1127                 playlist_medias.append(media) 
1128                 # Download single video if found media with asset id matching the video id from URL 
1129                 if item.get('advert', {}).get('assetId') == playlist_id: 
1132             # Fallback to the whole playlist 
1134                 medias = playlist_medias 
1137         for num, media_meta in enumerate(medias, start=1): 
1138             formats, subtitles = self._extract_from_media_meta(media_meta, playlist_id) 
1141             self._sort_formats(formats) 
1143             video_id = media_meta.get('externalId') 
1145                 video_id = playlist_id if len(medias) == 1 else '%s-%s' % (playlist_id, num) 
1147             title = media_meta.get('caption') 
1149                 title = playlist_title if len(medias) == 1 else '%s - Video %s' % (playlist_title, num) 
1151             duration = int_or_none(media_meta.get('durationInSeconds')) or parse_duration(media_meta.get('duration')) 
1154             for image in media_meta.get('images', {}).values(): 
1155                 images.extend(image.values()) 
1156             if 'image' in media_meta: 
1157                 images.append(media_meta['image']) 
1160                 'url': image.get('href'), 
1161                 'width': int_or_none(image.get('width')), 
1162                 'height': int_or_none(image.get('height')), 
1163             } for image in images] 
1168                 'thumbnails': thumbnails, 
1169                 'duration': duration, 
1170                 'timestamp': timestamp, 
1172                 'subtitles': subtitles, 
1175         return self.playlist_result(entries, playlist_id, playlist_title, playlist_description) 
1178 class BBCCoUkArticleIE(InfoExtractor): 
1179     _VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/programmes/articles/(?P<id>[a-zA-Z0-9]+)' 
1180     IE_NAME = 'bbc.co.uk:article' 
1181     IE_DESC = 'BBC articles' 
1184         'url': 'http://www.bbc.co.uk/programmes/articles/3jNQLTMrPlYGTBn0WV6M2MS/not-your-typical-role-model-ada-lovelace-the-19th-century-programmer', 
1186             'id': '3jNQLTMrPlYGTBn0WV6M2MS', 
1187             'title': 'Calculating Ada: The Countess of Computing - Not your typical role model: Ada Lovelace the 19th century programmer - BBC Four', 
1188             'description': 'Hannah Fry reveals some of her surprising discoveries about Ada Lovelace during filming.', 
1190         'playlist_count': 4, 
1191         'add_ie': ['BBCCoUk'], 
1194     def _real_extract(self, url): 
1195         playlist_id = self._match_id(url) 
1197         webpage = self._download_webpage(url, playlist_id) 
1199         title = self._og_search_title(webpage) 
1200         description = self._og_search_description(webpage).strip() 
1202         entries = [self.url_result(programme_url) for programme_url in re.findall( 
1203             r'<div[^>]+typeof="Clip
"[^>]+resource="([^
"]+)"', webpage)] 
1205         return self.playlist_result(entries, playlist_id, title, description) 
1208 class BBCCoUkPlaylistBaseIE(InfoExtractor): 
1209     def _entries(self, webpage, url, playlist_id): 
1210         single_page = 'page
' in compat_urlparse.parse_qs( 
1211             compat_urlparse.urlparse(url).query) 
1212         for page_num in itertools.count(2): 
1213             for video_id in re.findall( 
1214                     self._VIDEO_ID_TEMPLATE % BBCCoUkIE._ID_REGEX, webpage): 
1215                 yield self.url_result( 
1216                     self._URL_TEMPLATE % video_id, BBCCoUkIE.ie_key()) 
1219             next_page = self._search_regex( 
1220                 r'<li
[^
>]+class=(["\'])pagination_+next\1[^>]*><a[^>]+href=(["\'])(?P
<url
>(?
:(?
!\
2).)+)\
2', 
1221                 webpage, 'next page url
', default=None, group='url
') 
1224             webpage = self._download_webpage( 
1225                 compat_urlparse.urljoin(url, next_page), playlist_id, 
1226                 'Downloading page 
%d' % page_num, page_num) 
1228     def _real_extract(self, url): 
1229         playlist_id = self._match_id(url) 
1231         webpage = self._download_webpage(url, playlist_id) 
1233         title, description = self._extract_title_and_description(webpage) 
1235         return self.playlist_result( 
1236             self._entries(webpage, url, playlist_id), 
1237             playlist_id, title, description) 
1240 class BBCCoUkIPlayerPlaylistIE(BBCCoUkPlaylistBaseIE): 
1241     IE_NAME = 'bbc
.co
.uk
:iplayer
:playlist
' 
1242     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.co\
.uk
/iplayer
/(?
:episodes|group
)/(?P
<id>%s)' % BBCCoUkIE._ID_REGEX 
1243     _URL_TEMPLATE = 'http
://www
.bbc
.co
.uk
/iplayer
/episode
/%s' 
1244     _VIDEO_ID_TEMPLATE = r'data
-ip
-id=["\'](%s)' 
1246         'url': 'http://www.bbc.co.uk/iplayer/episodes/b05rcz9v', 
1249             'title': 'The Disappearance', 
1250             'description': 'French thriller serial about a missing teenager.', 
1252         'playlist_mincount': 6, 
1253         'skip': 'This programme is not currently available on BBC iPlayer', 
1255         # Available for over a year unlike 30 days for most other programmes 
1256         'url': 'http://www.bbc.co.uk/iplayer/group/p02tcc32', 
1259             'title': 'Bohemian Icons', 
1260             'description': 'md5:683e901041b2fe9ba596f2ab04c4dbe7', 
1262         'playlist_mincount': 10, 
1265     def _extract_title_and_description(self, webpage): 
1266         title = self._search_regex(r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False) 
1267         description = self._search_regex( 
1268             r'<p[^>]+class=(["\'])subtitle\
1[^
>]*>(?P
<value
>[^
<]+)</p
>', 
1269             webpage, 'description
', fatal=False, group='value
') 
1270         return title, description 
1273 class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE): 
1274     IE_NAME = 'bbc
.co
.uk
:playlist
' 
1275     _VALID_URL = r'https?
://(?
:www\
.)?bbc\
.co\
.uk
/programmes
/(?P
<id>%s)/(?
:episodes|broadcasts|clips
)' % BBCCoUkIE._ID_REGEX 
1276     _URL_TEMPLATE = 'http
://www
.bbc
.co
.uk
/programmes
/%s' 
1277     _VIDEO_ID_TEMPLATE = r'data
-pid
=["\'](%s)' 
1279         'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/clips', 
1282             'title': 'The Disappearance - Clips - BBC Four', 
1283             'description': 'French thriller serial about a missing teenager.', 
1285         'playlist_mincount': 7, 
1287         # multipage playlist, explicit page 
1288         'url': 'http://www.bbc.co.uk/programmes/b00mfl7n/clips?page=1', 
1291             'title': 'Frozen Planet - Clips - BBC One', 
1292             'description': 'md5:65dcbf591ae628dafe32aa6c4a4a0d8c', 
1294         'playlist_mincount': 24, 
1296         # multipage playlist, all pages 
1297         'url': 'http://www.bbc.co.uk/programmes/b00mfl7n/clips', 
1300             'title': 'Frozen Planet - Clips - BBC One', 
1301             'description': 'md5:65dcbf591ae628dafe32aa6c4a4a0d8c', 
1303         'playlist_mincount': 142, 
1305         'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/broadcasts/2016/06', 
1306         'only_matching': True, 
1308         'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/clips', 
1309         'only_matching': True, 
1311         'url': 'http://www.bbc.co.uk/programmes/b055jkys/episodes/player', 
1312         'only_matching': True, 
1315     def _extract_title_and_description(self, webpage): 
1316         title = self._og_search_title(webpage, fatal=False) 
1317         description = self._og_search_description(webpage) 
1318         return title, description