2 from __future__ 
import unicode_literals
 
   6 import xml
.etree
.ElementTree
 
   8 from .common 
import InfoExtractor
 
  15     compat_urllib_request
, 
  17     compat_urllib_parse_urlparse
, 
  26 class BrightcoveIE(InfoExtractor
): 
  27     _VALID_URL 
= r
'https?://.*brightcove\.com/(services|viewer).*?\?(?P<query>.*)' 
  28     _FEDERATED_URL_TEMPLATE 
= 'http://c.brightcove.com/services/viewer/htmlFederated?%s' 
  32             # From http://www.8tv.cat/8aldia/videos/xavier-sala-i-martin-aquesta-tarda-a-8-al-dia/ 
  33             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1654948606001&flashID=myExperience&%40videoPlayer=2371591881001', 
  34             'md5': '5423e113865d26e40624dce2e4b45d95', 
  35             'note': 'Test Brightcove downloads and detection in GenericIE', 
  37                 'id': '2371591881001', 
  39                 'title': 'Xavier Sala i Martín: “Un banc que no presta és un banc zombi que no serveix per a res”', 
  41                 'description': 'md5:a950cc4285c43e44d763d036710cd9cd', 
  45             # From http://medianetwork.oracle.com/video/player/1785452137001 
  46             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1217746023001&flashID=myPlayer&%40videoPlayer=1785452137001', 
  48                 'id': '1785452137001', 
  50                 'title': 'JVMLS 2012: Arrays 2.0 - Opportunities and Challenges', 
  51                 'description': 'John Rose speaks at the JVM Language Summit, August 1, 2012.', 
  56             # From http://mashable.com/2013/10/26/thermoelectric-bracelet-lets-you-control-your-body-temperature/ 
  57             'url': 'http://c.brightcove.com/services/viewer/federated_f9?&playerID=1265504713001&publisherID=AQ%7E%7E%2CAAABBzUwv1E%7E%2CxP-xFHVUstiMFlNYfvF4G9yFnNaqCw_9&videoID=2750934548001', 
  59                 'id': '2750934548001', 
  61                 'title': 'This Bracelet Acts as a Personal Thermostat', 
  62                 'description': 'md5:547b78c64f4112766ccf4e151c20b6a0', 
  63                 'uploader': 'Mashable', 
  67             # test that the default referer works 
  68             # from http://national.ballet.ca/interact/video/Lost_in_Motion_II/ 
  69             'url': 'http://link.brightcove.com/services/player/bcpid756015033001?bckey=AQ~~,AAAApYJi_Ck~,GxhXCegT1Dp39ilhXuxMJxasUhVNZiil&bctid=2878862109001', 
  71                 'id': '2878862109001', 
  73                 'title': 'Lost in Motion II', 
  74                 'description': 'md5:363109c02998fee92ec02211bd8000df', 
  75                 'uploader': 'National Ballet of Canada', 
  79             # test flv videos served by akamaihd.net 
  80             # From http://www.redbull.com/en/bike/stories/1331655643987/replay-uci-dh-world-cup-2014-from-fort-william 
  81             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?%40videoPlayer=ref%3ABC2996102916001&linkBaseURL=http%3A%2F%2Fwww.redbull.com%2Fen%2Fbike%2Fvideos%2F1331655630249%2Freplay-uci-fort-william-2014-dh&playerKey=AQ%7E%7E%2CAAAApYJ7UqE%7E%2Cxqr_zXk0I-zzNndy8NlHogrCb5QdyZRf&playerID=1398061561001#__youtubedl_smuggle=%7B%22Referer%22%3A+%22http%3A%2F%2Fwww.redbull.com%2Fen%2Fbike%2Fstories%2F1331655643987%2Freplay-uci-dh-world-cup-2014-from-fort-william%22%7D', 
  82             # The md5 checksum changes on each download 
  84                 'id': '2996102916001', 
  86                 'title': 'UCI MTB World Cup 2014: Fort William, UK - Downhill Finals', 
  87                 'uploader': 'Red Bull TV', 
  88                 'description': 'UCI MTB World Cup 2014: Fort William, UK - Downhill Finals', 
  93             # from http://support.brightcove.com/en/video-cloud/docs/playlist-support-single-video-players 
  94             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=3550052898001&playerKey=AQ%7E%7E%2CAAABmA9XpXk%7E%2C-Kp7jNgisre1fG5OdqpAFUTcs0lP_ZoL', 
  98             'playlist_mincount': 7, 
 103     def _build_brighcove_url(cls
, object_str
): 
 105         Build a Brightcove url from a xml string containing 
 106         <object class="BrightcoveExperience">{params}</object> 
 109         # Fix up some stupid HTML, see https://github.com/rg3/youtube-dl/issues/1553 
 110         object_str 
= re
.sub(r
'(<param name="[^"]+" value="[^"]+")>', 
 111                             lambda m
: m
.group(1) + '/>', object_str
) 
 112         # Fix up some stupid XML, see https://github.com/rg3/youtube-dl/issues/1608 
 113         object_str 
= object_str
.replace('<--', '<!--') 
 114         # remove namespace to simplify extraction 
 115         object_str 
= re
.sub(r
'(<object[^>]*)(xmlns=".*?")', r
'\1', object_str
) 
 116         object_str 
= fix_xml_ampersands(object_str
) 
 118         object_doc 
= xml
.etree
.ElementTree
.fromstring(object_str
.encode('utf-8')) 
 120         fv_el 
= find_xpath_attr(object_doc
, './param', 'name', 'flashVars') 
 121         if fv_el 
is not None: 
 124                 for k
, v 
in compat_parse_qs(fv_el
.attrib
['value']).items()) 
 128         def find_param(name
): 
 129             if name 
in flashvars
: 
 130                 return flashvars
[name
] 
 131             node 
= find_xpath_attr(object_doc
, './param', 'name', name
) 
 133                 return node
.attrib
['value'] 
 138         playerID 
= find_param('playerID') 
 140             raise ExtractorError('Cannot find player ID') 
 141         params
['playerID'] = playerID
 
 143         playerKey 
= find_param('playerKey') 
 144         # Not all pages define this value 
 145         if playerKey 
is not None: 
 146             params
['playerKey'] = playerKey
 
 147         # The three fields hold the id of the video 
 148         videoPlayer 
= find_param('@videoPlayer') or find_param('videoId') or find_param('videoID') 
 149         if videoPlayer 
is not None: 
 150             params
['@videoPlayer'] = videoPlayer
 
 151         linkBase 
= find_param('linkBaseURL') 
 152         if linkBase 
is not None: 
 153             params
['linkBaseURL'] = linkBase
 
 154         data 
= compat_urllib_parse
.urlencode(params
) 
 155         return cls
._FEDERATED
_URL
_TEMPLATE 
% data
 
 158     def _extract_brightcove_url(cls
, webpage
): 
 159         """Try to extract the brightcove url from the webpage, returns None 
 162         urls 
= cls
._extract
_brightcove
_urls
(webpage
) 
 163         return urls
[0] if urls 
else None 
 166     def _extract_brightcove_urls(cls
, webpage
): 
 167         """Return a list of all Brightcove URLs from the webpage """ 
 170             r
'<meta\s+property="og:video"\s+content="(https?://(?:secure|c)\.brightcove.com/[^"]+)"', 
 173             url 
= unescapeHTML(url_m
.group(1)) 
 174             # Some sites don't add it, we can't download with this url, for example: 
 175             # http://www.ktvu.com/videos/news/raw-video-caltrain-releases-video-of-man-almost/vCTZdY/ 
 176             if 'playerKey' in url 
or 'videoId' in url
: 
 179         matches 
= re
.findall( 
 182                 [^>]+?class=[\'"][^>]*?BrightcoveExperience.*?[\'"] |
 
 183                 [^
>]*?
>\s
*<param\s
+name
="movie"\s
+value
="https?://[^/]*brightcove\.com/ 
 186         return [cls._build_brighcove_url(m) for m in matches] 
 188     def _real_extract(self, url): 
 189         url, smuggled_data = unsmuggle_url(url, {}) 
 191         # Change the 'videoId' and others field to '@videoPlayer' 
 192         url = re.sub(r'(?<=[?&])(videoI(d|D)|bctid)', '%40videoPlayer', url) 
 193         # Change bckey (used by bcove.me urls) to playerKey 
 194         url = re.sub(r'(?<=[?&])bckey', 'playerKey', url) 
 195         mobj = re.match(self._VALID_URL, url) 
 196         query_str = mobj.group('query') 
 197         query = compat_urlparse.parse_qs(query_str) 
 199         videoPlayer = query.get('@videoPlayer') 
 201             # We set the original url as the default 'Referer' header 
 202             referer = smuggled_data.get('Referer', url) 
 203             return self._get_video_info( 
 204                 videoPlayer[0], query_str, query, referer=referer) 
 205         elif 'playerKey' in query: 
 206             player_key = query['playerKey'] 
 207             return self._get_playlist_info(player_key[0]) 
 209             raise ExtractorError( 
 210                 'Cannot find playerKey= variable. Did you forget quotes in a shell invocation?', 
 213     def _get_video_info(self, video_id, query_str, query, referer=None): 
 214         request_url = self._FEDERATED_URL_TEMPLATE % query_str 
 215         req = compat_urllib_request.Request(request_url) 
 216         linkBase = query.get('linkBaseURL') 
 217         if linkBase is not None: 
 218             referer = linkBase[0] 
 219         if referer is not None: 
 220             req.add_header('Referer', referer) 
 221         webpage = self._download_webpage(req, video_id) 
 223         error_msg = self._html_search_regex( 
 224             r"<h1
>We
're sorry.</h1>([\s\n]*<p>.*?</p>)+", webpage, 
 225             'error message
', default=None) 
 226         if error_msg is not None: 
 227             raise ExtractorError( 
 228                 'brightcove said
: %s' % error_msg, expected=True) 
 230         self.report_extraction(video_id) 
 231         info = self._search_regex(r'var experienceJSON 
= ({.*});', webpage, 'json
') 
 232         info = json.loads(info)['data
'] 
 233         video_info = info['programmedContent
']['videoPlayer
']['mediaDTO
'] 
 234         video_info['_youtubedl_adServerURL
'] = info.get('adServerURL
') 
 236         return self._extract_video_info(video_info) 
 238     def _get_playlist_info(self, player_key): 
 239         info_url = 'http
://c
.brightcove
.com
/services
/json
/experience
/runtime
/?command
=get_programming_for_experience
&playerKey
=%s' % player_key 
 240         playlist_info = self._download_webpage( 
 241             info_url, player_key, 'Downloading playlist information
') 
 243         json_data = json.loads(playlist_info) 
 244         if 'videoList
' not in json_data: 
 245             raise ExtractorError('Empty playlist
') 
 246         playlist_info = json_data['videoList
'] 
 247         videos = [self._extract_video_info(video_info) for video_info in playlist_info['mediaCollectionDTO
']['videoDTOs
']] 
 249         return self.playlist_result(videos, playlist_id=playlist_info['id'], 
 250                                     playlist_title=playlist_info['mediaCollectionDTO
']['displayName
']) 
 252     def _extract_video_info(self, video_info): 
 254             'id': compat_str(video_info['id']), 
 255             'title
': video_info['displayName
'].strip(), 
 256             'description
': video_info.get('shortDescription
'), 
 257             'thumbnail
': video_info.get('videoStillURL
') or video_info.get('thumbnailURL
'), 
 258             'uploader
': video_info.get('publisherName
'), 
 261         renditions = video_info.get('renditions
') 
 264             for rend in renditions: 
 265                 url = rend['defaultURL
'] 
 269                     url_comp = compat_urllib_parse_urlparse(url) 
 270                     if url_comp.path.endswith('.m3u8
'): 
 272                             self._extract_m3u8_formats(url, info['id'], 'mp4
')) 
 274                     elif 'akamaihd
.net
' in url_comp.netloc: 
 275                         # This type of renditions are served through 
 276                         # akamaihd.net, but they don't use f4m manifests
 
 277                         url 
= url
.replace('control/', '') + '?&v=3.3.0&fp=13&r=FEEFJ&g=RTSJIMBMPFPB' 
 280                     ext 
= determine_ext(url
) 
 281                 size 
= rend
.get('size') 
 285                     'height': rend
.get('frameHeight'), 
 286                     'width': rend
.get('frameWidth'), 
 287                     'filesize': size 
if size 
!= 0 else None, 
 289             self
._sort
_formats
(formats
) 
 290             info
['formats'] = formats
 
 291         elif video_info
.get('FLVFullLengthURL') is not None: 
 293                 'url': video_info
['FLVFullLengthURL'], 
 296         if self
._downloader
.params
.get('include_ads', False): 
 297             adServerURL 
= video_info
.get('_youtubedl_adServerURL') 
 306                         'title': info
['title'], 
 307                         'entries': [ad_info
, info
], 
 312         if 'url' not in info 
and not info
.get('formats'): 
 313             raise ExtractorError('Unable to extract video url for %s' % info
['id'])