]>
Commit | Line | Data |
---|---|---|
1 | # coding: utf-8 | |
2 | from __future__ import unicode_literals | |
3 | ||
4 | import json | |
5 | import uuid | |
6 | ||
7 | from .adobepass import AdobePassIE | |
8 | from ..compat import ( | |
9 | compat_HTTPError, | |
10 | compat_str, | |
11 | compat_urllib_parse_unquote, | |
12 | ) | |
13 | from ..utils import ( | |
14 | ExtractorError, | |
15 | int_or_none, | |
16 | parse_age_limit, | |
17 | parse_duration, | |
18 | try_get, | |
19 | unified_timestamp, | |
20 | ) | |
21 | ||
22 | ||
23 | class FOXIE(AdobePassIE): | |
24 | _VALID_URL = r'https?://(?:www\.)?fox\.com/watch/(?P<id>[\da-fA-F]+)' | |
25 | _TESTS = [{ | |
26 | # clip | |
27 | 'url': 'https://www.fox.com/watch/4b765a60490325103ea69888fb2bd4e8/', | |
28 | 'md5': 'ebd296fcc41dd4b19f8115d8461a3165', | |
29 | 'info_dict': { | |
30 | 'id': '4b765a60490325103ea69888fb2bd4e8', | |
31 | 'ext': 'mp4', | |
32 | 'title': 'Aftermath: Bruce Wayne Develops Into The Dark Knight', | |
33 | 'description': 'md5:549cd9c70d413adb32ce2a779b53b486', | |
34 | 'duration': 102, | |
35 | 'timestamp': 1504291893, | |
36 | 'upload_date': '20170901', | |
37 | 'creator': 'FOX', | |
38 | 'series': 'Gotham', | |
39 | 'age_limit': 14, | |
40 | }, | |
41 | 'params': { | |
42 | 'skip_download': True, | |
43 | }, | |
44 | }, { | |
45 | # episode, geo-restricted | |
46 | 'url': 'https://www.fox.com/watch/087036ca7f33c8eb79b08152b4dd75c1/', | |
47 | 'only_matching': True, | |
48 | }, { | |
49 | # episode, geo-restricted, tv provided required | |
50 | 'url': 'https://www.fox.com/watch/30056b295fb57f7452aeeb4920bc3024/', | |
51 | 'only_matching': True, | |
52 | }] | |
53 | _GEO_BYPASS = False | |
54 | _HOME_PAGE_URL = 'https://www.fox.com/' | |
55 | _API_KEY = 'abdcbed02c124d393b39e818a4312055' | |
56 | _access_token = None | |
57 | ||
58 | def _call_api(self, path, video_id, data=None): | |
59 | headers = { | |
60 | 'X-Api-Key': self._API_KEY, | |
61 | } | |
62 | if self._access_token: | |
63 | headers['Authorization'] = 'Bearer ' + self._access_token | |
64 | try: | |
65 | return self._download_json( | |
66 | 'https://api2.fox.com/v2.0/' + path, | |
67 | video_id, data=data, headers=headers) | |
68 | except ExtractorError as e: | |
69 | if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403: | |
70 | entitlement_issues = self._parse_json( | |
71 | e.cause.read().decode(), video_id)['entitlementIssues'] | |
72 | for e in entitlement_issues: | |
73 | if e.get('errorCode') == 1005: | |
74 | raise ExtractorError( | |
75 | 'This video is only available via cable service provider ' | |
76 | 'subscription. You may want to use --cookies.', expected=True) | |
77 | messages = ', '.join([e['message'] for e in entitlement_issues]) | |
78 | raise ExtractorError(messages, expected=True) | |
79 | raise | |
80 | ||
81 | def _real_initialize(self): | |
82 | if not self._access_token: | |
83 | mvpd_auth = self._get_cookies(self._HOME_PAGE_URL).get('mvpd-auth') | |
84 | if mvpd_auth: | |
85 | self._access_token = (self._parse_json(compat_urllib_parse_unquote( | |
86 | mvpd_auth.value), None, fatal=False) or {}).get('accessToken') | |
87 | if not self._access_token: | |
88 | self._access_token = self._call_api( | |
89 | 'login', None, json.dumps({ | |
90 | 'deviceId': compat_str(uuid.uuid4()), | |
91 | }).encode())['accessToken'] | |
92 | ||
93 | def _real_extract(self, url): | |
94 | video_id = self._match_id(url) | |
95 | ||
96 | video = self._call_api('vodplayer/' + video_id, video_id) | |
97 | ||
98 | title = video['name'] | |
99 | release_url = video['url'] | |
100 | try: | |
101 | m3u8_url = self._download_json(release_url, video_id)['playURL'] | |
102 | except ExtractorError as e: | |
103 | if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403: | |
104 | error = self._parse_json(e.cause.read().decode(), video_id) | |
105 | if error.get('exception') == 'GeoLocationBlocked': | |
106 | self.raise_geo_restricted(countries=['US']) | |
107 | raise ExtractorError(error['description'], expected=True) | |
108 | raise | |
109 | formats = self._extract_m3u8_formats( | |
110 | m3u8_url, video_id, 'mp4', | |
111 | entry_protocol='m3u8_native', m3u8_id='hls') | |
112 | self._sort_formats(formats) | |
113 | ||
114 | data = try_get( | |
115 | video, lambda x: x['trackingData']['properties'], dict) or {} | |
116 | ||
117 | duration = int_or_none(video.get('durationInSeconds')) or int_or_none( | |
118 | video.get('duration')) or parse_duration(video.get('duration')) | |
119 | timestamp = unified_timestamp(video.get('datePublished')) | |
120 | creator = data.get('brand') or data.get('network') or video.get('network') | |
121 | series = video.get('seriesName') or data.get( | |
122 | 'seriesName') or data.get('show') | |
123 | ||
124 | subtitles = {} | |
125 | for doc_rel in video.get('documentReleases', []): | |
126 | rel_url = doc_rel.get('url') | |
127 | if not url or doc_rel.get('format') != 'SCC': | |
128 | continue | |
129 | subtitles['en'] = [{ | |
130 | 'url': rel_url, | |
131 | 'ext': 'scc', | |
132 | }] | |
133 | break | |
134 | ||
135 | return { | |
136 | 'id': video_id, | |
137 | 'title': title, | |
138 | 'formats': formats, | |
139 | 'description': video.get('description'), | |
140 | 'duration': duration, | |
141 | 'timestamp': timestamp, | |
142 | 'age_limit': parse_age_limit(video.get('contentRating')), | |
143 | 'creator': creator, | |
144 | 'series': series, | |
145 | 'season_number': int_or_none(video.get('seasonNumber')), | |
146 | 'episode': video.get('name'), | |
147 | 'episode_number': int_or_none(video.get('episodeNumber')), | |
148 | 'release_year': int_or_none(video.get('releaseYear')), | |
149 | 'subtitles': subtitles, | |
150 | } |