[youtube] fix hd720 format position
[youtube-dl] / youtube_dl / extractor / itv.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import uuid
5 import xml.etree.ElementTree as etree
6 import json
7 import re
8
9 from .common import InfoExtractor
10 from .brightcove import BrightcoveNewIE
11 from ..compat import (
12     compat_str,
13     compat_etree_register_namespace,
14 )
15 from ..utils import (
16     extract_attributes,
17     xpath_with_ns,
18     xpath_element,
19     xpath_text,
20     int_or_none,
21     parse_duration,
22     smuggle_url,
23     ExtractorError,
24     determine_ext,
25 )
26
27
28 class ITVIE(InfoExtractor):
29     _VALID_URL = r'https?://(?:www\.)?itv\.com/hub/[^/]+/(?P<id>[0-9a-zA-Z]+)'
30     _GEO_COUNTRIES = ['GB']
31     _TESTS = [{
32         'url': 'http://www.itv.com/hub/mr-bean-animated-series/2a2936a0053',
33         'info_dict': {
34             'id': '2a2936a0053',
35             'ext': 'flv',
36             'title': 'Home Movie',
37         },
38         'params': {
39             # rtmp download
40             'skip_download': True,
41         },
42     }, {
43         # unavailable via data-playlist-url
44         'url': 'https://www.itv.com/hub/through-the-keyhole/2a2271a0033',
45         'only_matching': True,
46     }, {
47         # InvalidVodcrid
48         'url': 'https://www.itv.com/hub/james-martins-saturday-morning/2a5159a0034',
49         'only_matching': True,
50     }, {
51         # ContentUnavailable
52         'url': 'https://www.itv.com/hub/whos-doing-the-dishes/2a2898a0024',
53         'only_matching': True,
54     }]
55
56     def _real_extract(self, url):
57         video_id = self._match_id(url)
58         webpage = self._download_webpage(url, video_id)
59         params = extract_attributes(self._search_regex(
60             r'(?s)(<[^>]+id="video"[^>]*>)', webpage, 'params'))
61
62         ns_map = {
63             'soapenv': 'http://schemas.xmlsoap.org/soap/envelope/',
64             'tem': 'http://tempuri.org/',
65             'itv': 'http://schemas.datacontract.org/2004/07/Itv.BB.Mercury.Common.Types',
66             'com': 'http://schemas.itv.com/2009/05/Common',
67         }
68         for ns, full_ns in ns_map.items():
69             compat_etree_register_namespace(ns, full_ns)
70
71         def _add_ns(name):
72             return xpath_with_ns(name, ns_map)
73
74         def _add_sub_element(element, name):
75             return etree.SubElement(element, _add_ns(name))
76
77         production_id = (
78             params.get('data-video-autoplay-id') or
79             '%s#001' % (
80                 params.get('data-video-episode-id') or
81                 video_id.replace('a', '/')))
82
83         req_env = etree.Element(_add_ns('soapenv:Envelope'))
84         _add_sub_element(req_env, 'soapenv:Header')
85         body = _add_sub_element(req_env, 'soapenv:Body')
86         get_playlist = _add_sub_element(body, ('tem:GetPlaylist'))
87         request = _add_sub_element(get_playlist, 'tem:request')
88         _add_sub_element(request, 'itv:ProductionId').text = production_id
89         _add_sub_element(request, 'itv:RequestGuid').text = compat_str(uuid.uuid4()).upper()
90         vodcrid = _add_sub_element(request, 'itv:Vodcrid')
91         _add_sub_element(vodcrid, 'com:Id')
92         _add_sub_element(request, 'itv:Partition')
93         user_info = _add_sub_element(get_playlist, 'tem:userInfo')
94         _add_sub_element(user_info, 'itv:Broadcaster').text = 'Itv'
95         _add_sub_element(user_info, 'itv:DM')
96         _add_sub_element(user_info, 'itv:RevenueScienceValue')
97         _add_sub_element(user_info, 'itv:SessionId')
98         _add_sub_element(user_info, 'itv:SsoToken')
99         _add_sub_element(user_info, 'itv:UserToken')
100         site_info = _add_sub_element(get_playlist, 'tem:siteInfo')
101         _add_sub_element(site_info, 'itv:AdvertisingRestriction').text = 'None'
102         _add_sub_element(site_info, 'itv:AdvertisingSite').text = 'ITV'
103         _add_sub_element(site_info, 'itv:AdvertisingType').text = 'Any'
104         _add_sub_element(site_info, 'itv:Area').text = 'ITVPLAYER.VIDEO'
105         _add_sub_element(site_info, 'itv:Category')
106         _add_sub_element(site_info, 'itv:Platform').text = 'DotCom'
107         _add_sub_element(site_info, 'itv:Site').text = 'ItvCom'
108         device_info = _add_sub_element(get_playlist, 'tem:deviceInfo')
109         _add_sub_element(device_info, 'itv:ScreenSize').text = 'Big'
110         player_info = _add_sub_element(get_playlist, 'tem:playerInfo')
111         _add_sub_element(player_info, 'itv:Version').text = '2'
112
113         headers = self.geo_verification_headers()
114         headers.update({
115             'Content-Type': 'text/xml; charset=utf-8',
116             'SOAPAction': 'http://tempuri.org/PlaylistService/GetPlaylist',
117         })
118
119         info = self._search_json_ld(webpage, video_id, default={})
120         formats = []
121         subtitles = {}
122
123         def extract_subtitle(sub_url):
124             ext = determine_ext(sub_url, 'ttml')
125             subtitles.setdefault('en', []).append({
126                 'url': sub_url,
127                 'ext': 'ttml' if ext == 'xml' else ext,
128             })
129
130         resp_env = self._download_xml(
131             params['data-playlist-url'], video_id,
132             headers=headers, data=etree.tostring(req_env))
133         playlist = xpath_element(resp_env, './/Playlist')
134         if playlist is None:
135             fault_code = xpath_text(resp_env, './/faultcode')
136             fault_string = xpath_text(resp_env, './/faultstring')
137             if fault_code == 'InvalidGeoRegion':
138                 self.raise_geo_restricted(
139                     msg=fault_string, countries=self._GEO_COUNTRIES)
140             elif fault_code not in (
141                     'InvalidEntity', 'InvalidVodcrid', 'ContentUnavailable'):
142                 raise ExtractorError(
143                     '%s said: %s' % (self.IE_NAME, fault_string), expected=True)
144             info.update({
145                 'title': self._og_search_title(webpage),
146                 'episode_title': params.get('data-video-episode'),
147                 'series': params.get('data-video-title'),
148             })
149         else:
150             title = xpath_text(playlist, 'EpisodeTitle', default=None)
151             info.update({
152                 'title': title,
153                 'episode_title': title,
154                 'episode_number': int_or_none(xpath_text(playlist, 'EpisodeNumber')),
155                 'series': xpath_text(playlist, 'ProgrammeTitle'),
156                 'duration': parse_duration(xpath_text(playlist, 'Duration')),
157             })
158             video_element = xpath_element(playlist, 'VideoEntries/Video', fatal=True)
159             media_files = xpath_element(video_element, 'MediaFiles', fatal=True)
160             rtmp_url = media_files.attrib['base']
161
162             for media_file in media_files.findall('MediaFile'):
163                 play_path = xpath_text(media_file, 'URL')
164                 if not play_path:
165                     continue
166                 tbr = int_or_none(media_file.get('bitrate'), 1000)
167                 f = {
168                     'format_id': 'rtmp' + ('-%d' % tbr if tbr else ''),
169                     'play_path': play_path,
170                     # Providing this swfVfy allows to avoid truncated downloads
171                     'player_url': 'http://www.itv.com/mercury/Mercury_VideoPlayer.swf',
172                     'page_url': url,
173                     'tbr': tbr,
174                     'ext': 'flv',
175                 }
176                 app = self._search_regex(
177                     'rtmpe?://[^/]+/(.+)$', rtmp_url, 'app', default=None)
178                 if app:
179                     f.update({
180                         'url': rtmp_url.split('?', 1)[0],
181                         'app': app,
182                     })
183                 else:
184                     f['url'] = rtmp_url
185                 formats.append(f)
186
187             for caption_url in video_element.findall('ClosedCaptioningURIs/URL'):
188                 if caption_url.text:
189                     extract_subtitle(caption_url.text)
190
191         ios_playlist_url = params.get('data-video-playlist') or params.get('data-video-id')
192         hmac = params.get('data-video-hmac')
193         if ios_playlist_url and hmac and re.match(r'https?://', ios_playlist_url):
194             headers = self.geo_verification_headers()
195             headers.update({
196                 'Accept': 'application/vnd.itv.vod.playlist.v2+json',
197                 'Content-Type': 'application/json',
198                 'hmac': hmac.upper(),
199             })
200             ios_playlist = self._download_json(
201                 ios_playlist_url, video_id, data=json.dumps({
202                     'user': {
203                         'itvUserId': '',
204                         'entitlements': [],
205                         'token': ''
206                     },
207                     'device': {
208                         'manufacturer': 'Safari',
209                         'model': '5',
210                         'os': {
211                             'name': 'Windows NT',
212                             'version': '6.1',
213                             'type': 'desktop'
214                         }
215                     },
216                     'client': {
217                         'version': '4.1',
218                         'id': 'browser'
219                     },
220                     'variantAvailability': {
221                         'featureset': {
222                             'min': ['hls', 'aes', 'outband-webvtt'],
223                             'max': ['hls', 'aes', 'outband-webvtt']
224                         },
225                         'platformTag': 'dotcom'
226                     }
227                 }).encode(), headers=headers, fatal=False)
228             if ios_playlist:
229                 video_data = ios_playlist.get('Playlist', {}).get('Video', {})
230                 ios_base_url = video_data.get('Base')
231                 for media_file in video_data.get('MediaFiles', []):
232                     href = media_file.get('Href')
233                     if not href:
234                         continue
235                     if ios_base_url:
236                         href = ios_base_url + href
237                     ext = determine_ext(href)
238                     if ext == 'm3u8':
239                         formats.extend(self._extract_m3u8_formats(
240                             href, video_id, 'mp4', entry_protocol='m3u8_native',
241                             m3u8_id='hls', fatal=False))
242                     else:
243                         formats.append({
244                             'url': href,
245                         })
246                 subs = video_data.get('Subtitles')
247                 if isinstance(subs, list):
248                     for sub in subs:
249                         if not isinstance(sub, dict):
250                             continue
251                         href = sub.get('Href')
252                         if isinstance(href, compat_str):
253                             extract_subtitle(href)
254                 if not info.get('duration'):
255                     info['duration'] = parse_duration(video_data.get('Duration'))
256
257         self._sort_formats(formats)
258
259         info.update({
260             'id': video_id,
261             'formats': formats,
262             'subtitles': subtitles,
263         })
264         return info
265
266
267 class ITVBTCCIE(InfoExtractor):
268     _VALID_URL = r'https?://(?:www\.)?itv\.com/btcc/(?:[^/]+/)*(?P<id>[^/?#&]+)'
269     _TEST = {
270         'url': 'http://www.itv.com/btcc/races/btcc-2018-all-the-action-from-brands-hatch',
271         'info_dict': {
272             'id': 'btcc-2018-all-the-action-from-brands-hatch',
273             'title': 'BTCC 2018: All the action from Brands Hatch',
274         },
275         'playlist_mincount': 9,
276     }
277     BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/1582188683001/HkiHLnNRx_default/index.html?videoId=%s'
278
279     def _real_extract(self, url):
280         playlist_id = self._match_id(url)
281
282         webpage = self._download_webpage(url, playlist_id)
283
284         entries = [
285             self.url_result(
286                 smuggle_url(self.BRIGHTCOVE_URL_TEMPLATE % video_id, {
287                     # ITV does not like some GB IP ranges, so here are some
288                     # IP blocks it accepts
289                     'geo_ip_blocks': [
290                         '193.113.0.0/16', '54.36.162.0/23', '159.65.16.0/21'
291                     ],
292                     'referrer': url,
293                 }),
294                 ie=BrightcoveNewIE.ie_key(), video_id=video_id)
295             for video_id in re.findall(r'data-video-id=["\'](\d+)', webpage)]
296
297         title = self._og_search_title(webpage, fatal=False)
298
299         return self.playlist_result(entries, playlist_id, title)