2 from __future__ 
import unicode_literals
 
   6 import xml
.etree
.ElementTree
 
   8 from .common 
import InfoExtractor
 
  13     compat_urllib_parse_urlparse
, 
  14     compat_urllib_request
, 
  27 class BrightcoveIE(InfoExtractor
): 
  28     _VALID_URL 
= r
'(?:https?://.*brightcove\.com/(services|viewer).*?\?|brightcove:)(?P<query>.*)' 
  29     _FEDERATED_URL_TEMPLATE 
= 'http://c.brightcove.com/services/viewer/htmlFederated?%s' 
  33             # From http://www.8tv.cat/8aldia/videos/xavier-sala-i-martin-aquesta-tarda-a-8-al-dia/ 
  34             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1654948606001&flashID=myExperience&%40videoPlayer=2371591881001', 
  35             'md5': '5423e113865d26e40624dce2e4b45d95', 
  36             'note': 'Test Brightcove downloads and detection in GenericIE', 
  38                 'id': '2371591881001', 
  40                 'title': 'Xavier Sala i Martín: “Un banc que no presta és un banc zombi que no serveix per a res”', 
  42                 'description': 'md5:a950cc4285c43e44d763d036710cd9cd', 
  46             # From http://medianetwork.oracle.com/video/player/1785452137001 
  47             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1217746023001&flashID=myPlayer&%40videoPlayer=1785452137001', 
  49                 'id': '1785452137001', 
  51                 'title': 'JVMLS 2012: Arrays 2.0 - Opportunities and Challenges', 
  52                 'description': 'John Rose speaks at the JVM Language Summit, August 1, 2012.', 
  57             # From http://mashable.com/2013/10/26/thermoelectric-bracelet-lets-you-control-your-body-temperature/ 
  58             'url': 'http://c.brightcove.com/services/viewer/federated_f9?&playerID=1265504713001&publisherID=AQ%7E%7E%2CAAABBzUwv1E%7E%2CxP-xFHVUstiMFlNYfvF4G9yFnNaqCw_9&videoID=2750934548001', 
  60                 'id': '2750934548001', 
  62                 'title': 'This Bracelet Acts as a Personal Thermostat', 
  63                 'description': 'md5:547b78c64f4112766ccf4e151c20b6a0', 
  64                 'uploader': 'Mashable', 
  68             # test that the default referer works 
  69             # from http://national.ballet.ca/interact/video/Lost_in_Motion_II/ 
  70             'url': 'http://link.brightcove.com/services/player/bcpid756015033001?bckey=AQ~~,AAAApYJi_Ck~,GxhXCegT1Dp39ilhXuxMJxasUhVNZiil&bctid=2878862109001', 
  72                 'id': '2878862109001', 
  74                 'title': 'Lost in Motion II', 
  75                 'description': 'md5:363109c02998fee92ec02211bd8000df', 
  76                 'uploader': 'National Ballet of Canada', 
  80             # test flv videos served by akamaihd.net 
  81             # From http://www.redbull.com/en/bike/stories/1331655643987/replay-uci-dh-world-cup-2014-from-fort-william 
  82             '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', 
  83             # The md5 checksum changes on each download 
  85                 'id': '2996102916001', 
  87                 'title': 'UCI MTB World Cup 2014: Fort William, UK - Downhill Finals', 
  88                 'uploader': 'Red Bull TV', 
  89                 'description': 'UCI MTB World Cup 2014: Fort William, UK - Downhill Finals', 
  94             # from http://support.brightcove.com/en/video-cloud/docs/playlist-support-single-video-players 
  95             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=3550052898001&playerKey=AQ%7E%7E%2CAAABmA9XpXk%7E%2C-Kp7jNgisre1fG5OdqpAFUTcs0lP_ZoL', 
  98                 'id': '3550319591001', 
 100             'playlist_mincount': 7, 
 105     def _build_brighcove_url(cls
, object_str
): 
 107         Build a Brightcove url from a xml string containing 
 108         <object class="BrightcoveExperience">{params}</object> 
 111         # Fix up some stupid HTML, see https://github.com/rg3/youtube-dl/issues/1553 
 112         object_str 
= re
.sub(r
'(<param(?:\s+[a-zA-Z0-9_]+="[^"]*")*)>', 
 113                             lambda m
: m
.group(1) + '/>', object_str
) 
 114         # Fix up some stupid XML, see https://github.com/rg3/youtube-dl/issues/1608 
 115         object_str 
= object_str
.replace('<--', '<!--') 
 116         # remove namespace to simplify extraction 
 117         object_str 
= re
.sub(r
'(<object[^>]*)(xmlns=".*?")', r
'\1', object_str
) 
 118         object_str 
= fix_xml_ampersands(object_str
) 
 120         object_doc 
= xml
.etree
.ElementTree
.fromstring(object_str
.encode('utf-8')) 
 122         fv_el 
= find_xpath_attr(object_doc
, './param', 'name', 'flashVars') 
 123         if fv_el 
is not None: 
 126                 for k
, v 
in compat_parse_qs(fv_el
.attrib
['value']).items()) 
 130         def find_param(name
): 
 131             if name 
in flashvars
: 
 132                 return flashvars
[name
] 
 133             node 
= find_xpath_attr(object_doc
, './param', 'name', name
) 
 135                 return node
.attrib
['value'] 
 140         playerID 
= find_param('playerID') 
 142             raise ExtractorError('Cannot find player ID') 
 143         params
['playerID'] = playerID
 
 145         playerKey 
= find_param('playerKey') 
 146         # Not all pages define this value 
 147         if playerKey 
is not None: 
 148             params
['playerKey'] = playerKey
 
 149         # The three fields hold the id of the video 
 150         videoPlayer 
= find_param('@videoPlayer') or find_param('videoId') or find_param('videoID') 
 151         if videoPlayer 
is not None: 
 152             params
['@videoPlayer'] = videoPlayer
 
 153         linkBase 
= find_param('linkBaseURL') 
 154         if linkBase 
is not None: 
 155             params
['linkBaseURL'] = linkBase
 
 156         data 
= compat_urllib_parse
.urlencode(params
) 
 157         return cls
._FEDERATED
_URL
_TEMPLATE 
% data
 
 160     def _extract_brightcove_url(cls
, webpage
): 
 161         """Try to extract the brightcove url from the webpage, returns None 
 164         urls 
= cls
._extract
_brightcove
_urls
(webpage
) 
 165         return urls
[0] if urls 
else None 
 168     def _extract_brightcove_urls(cls
, webpage
): 
 169         """Return a list of all Brightcove URLs from the webpage """ 
 172             r
'<meta\s+property="og:video"\s+content="(https?://(?:secure|c)\.brightcove.com/[^"]+)"', 
 175             url 
= unescapeHTML(url_m
.group(1)) 
 176             # Some sites don't add it, we can't download with this url, for example: 
 177             # http://www.ktvu.com/videos/news/raw-video-caltrain-releases-video-of-man-almost/vCTZdY/ 
 178             if 'playerKey' in url 
or 'videoId' in url
: 
 181         matches 
= re
.findall( 
 184                 [^>]+?class=[\'"][^>]*?BrightcoveExperience.*?[\'"] |
 
 185                 [^
>]*?
>\s
*<param\s
+name
="movie"\s
+value
="https?://[^/]*brightcove\.com/ 
 188         return [cls._build_brighcove_url(m) for m in matches] 
 190     def _real_extract(self, url): 
 191         url, smuggled_data = unsmuggle_url(url, {}) 
 193         # Change the 'videoId' and others field to '@videoPlayer' 
 194         url = re.sub(r'(?<=[?&])(videoI(d|D)|bctid)', '%40videoPlayer', url) 
 195         # Change bckey (used by bcove.me urls) to playerKey 
 196         url = re.sub(r'(?<=[?&])bckey', 'playerKey', url) 
 197         mobj = re.match(self._VALID_URL, url) 
 198         query_str = mobj.group('query') 
 199         query = compat_urlparse.parse_qs(query_str) 
 201         videoPlayer = query.get('@videoPlayer') 
 203             # We set the original url as the default 'Referer' header 
 204             referer = smuggled_data.get('Referer', url) 
 205             return self._get_video_info( 
 206                 videoPlayer[0], query_str, query, referer=referer) 
 207         elif 'playerKey' in query: 
 208             player_key = query['playerKey'] 
 209             return self._get_playlist_info(player_key[0]) 
 211             raise ExtractorError( 
 212                 'Cannot find playerKey= variable. Did you forget quotes in a shell invocation?', 
 215     def _get_video_info(self, video_id, query_str, query, referer=None): 
 216         request_url = self._FEDERATED_URL_TEMPLATE % query_str 
 217         req = compat_urllib_request.Request(request_url) 
 218         linkBase = query.get('linkBaseURL') 
 219         if linkBase is not None: 
 220             referer = linkBase[0] 
 221         if referer is not None: 
 222             req.add_header('Referer', referer) 
 223         webpage = self._download_webpage(req, video_id) 
 225         error_msg = self._html_search_regex( 
 226             r"<h1
>We
're sorry.</h1>([\s\n]*<p>.*?</p>)+", webpage, 
 227             'error message
', default=None) 
 228         if error_msg is not None: 
 229             raise ExtractorError( 
 230                 'brightcove said
: %s' % error_msg, expected=True) 
 232         self.report_extraction(video_id) 
 233         info = self._search_regex(r'var experienceJSON 
= ({.*});', webpage, 'json
') 
 234         info = json.loads(info)['data
'] 
 235         video_info = info['programmedContent
']['videoPlayer
']['mediaDTO
'] 
 236         video_info['_youtubedl_adServerURL
'] = info.get('adServerURL
') 
 238         return self._extract_video_info(video_info) 
 240     def _get_playlist_info(self, player_key): 
 241         info_url = 'http
://c
.brightcove
.com
/services
/json
/experience
/runtime
/?command
=get_programming_for_experience
&playerKey
=%s' % player_key 
 242         playlist_info = self._download_webpage( 
 243             info_url, player_key, 'Downloading playlist information
') 
 245         json_data = json.loads(playlist_info) 
 246         if 'videoList
' not in json_data: 
 247             raise ExtractorError('Empty playlist
') 
 248         playlist_info = json_data['videoList
'] 
 249         videos = [self._extract_video_info(video_info) for video_info in playlist_info['mediaCollectionDTO
']['videoDTOs
']] 
 251         return self.playlist_result(videos, playlist_id='%s' % playlist_info['id'], 
 252                                     playlist_title=playlist_info['mediaCollectionDTO
']['displayName
']) 
 254     def _extract_video_info(self, video_info): 
 256             'id': compat_str(video_info['id']), 
 257             'title
': video_info['displayName
'].strip(), 
 258             'description
': video_info.get('shortDescription
'), 
 259             'thumbnail
': video_info.get('videoStillURL
') or video_info.get('thumbnailURL
'), 
 260             'uploader
': video_info.get('publisherName
'), 
 263         renditions = video_info.get('renditions
') 
 266             for rend in renditions: 
 267                 url = rend['defaultURL
'] 
 272                     url_comp = compat_urllib_parse_urlparse(url) 
 273                     if url_comp.path.endswith('.m3u8
'): 
 275                             self._extract_m3u8_formats(url, info['id'], 'mp4
')) 
 277                     elif 'akamaihd
.net
' in url_comp.netloc: 
 278                         # This type of renditions are served through 
 279                         # akamaihd.net, but they don't use f4m manifests
 
 280                         url 
= url
.replace('control/', '') + '?&v=3.3.0&fp=13&r=FEEFJ&g=RTSJIMBMPFPB' 
 283                     ext 
= determine_ext(url
) 
 284                 size 
= rend
.get('size') 
 288                     'height': rend
.get('frameHeight'), 
 289                     'width': rend
.get('frameWidth'), 
 290                     'filesize': size 
if size 
!= 0 else None, 
 292             self
._sort
_formats
(formats
) 
 293             info
['formats'] = formats
 
 294         elif video_info
.get('FLVFullLengthURL') is not None: 
 296                 'url': video_info
['FLVFullLengthURL'], 
 299         if self
._downloader
.params
.get('include_ads', False): 
 300             adServerURL 
= video_info
.get('_youtubedl_adServerURL') 
 309                         'title': info
['title'], 
 310                         'entries': [ad_info
, info
], 
 315         if 'url' not in info 
and not info
.get('formats'): 
 316             raise ExtractorError('Unable to extract video url for %s' % info
['id'])