]>
Commit | Line | Data |
---|---|---|
41c2c254 RA |
1 | import json |
2 | import uuid | |
96c186e1 | 3 | |
443f8de8 | 4 | from .common import InfoExtractor |
6df196f3 | 5 | from ..compat import ( |
0d08bcdb | 6 | compat_HTTPError, |
6df196f3 RA |
7 | compat_str, |
8 | compat_urllib_parse_unquote, | |
9 | ) | |
e37b54b1 | 10 | from ..utils import ( |
0d08bcdb | 11 | ExtractorError, |
bf6ec2fe S |
12 | int_or_none, |
13 | parse_age_limit, | |
14 | parse_duration, | |
15 | try_get, | |
16 | unified_timestamp, | |
e37b54b1 | 17 | ) |
9787c5f4 | 18 | |
19 | ||
443f8de8 | 20 | class FOXIE(InfoExtractor): |
6df196f3 | 21 | _VALID_URL = r'https?://(?:www\.)?fox\.com/watch/(?P<id>[\da-fA-F]+)' |
bf6ec2fe S |
22 | _TESTS = [{ |
23 | # clip | |
24 | 'url': 'https://www.fox.com/watch/4b765a60490325103ea69888fb2bd4e8/', | |
5e3a6fec | 25 | 'md5': 'ebd296fcc41dd4b19f8115d8461a3165', |
9787c5f4 | 26 | 'info_dict': { |
bf6ec2fe | 27 | 'id': '4b765a60490325103ea69888fb2bd4e8', |
9787c5f4 | 28 | 'ext': 'mp4', |
bf6ec2fe S |
29 | 'title': 'Aftermath: Bruce Wayne Develops Into The Dark Knight', |
30 | 'description': 'md5:549cd9c70d413adb32ce2a779b53b486', | |
31 | 'duration': 102, | |
32 | 'timestamp': 1504291893, | |
33 | 'upload_date': '20170901', | |
34 | 'creator': 'FOX', | |
35 | 'series': 'Gotham', | |
6df196f3 | 36 | 'age_limit': 14, |
443f8de8 | 37 | 'episode': 'Aftermath: Bruce Wayne Develops Into The Dark Knight' |
9787c5f4 | 38 | }, |
bf6ec2fe S |
39 | 'params': { |
40 | 'skip_download': True, | |
41 | }, | |
42 | }, { | |
43 | # episode, geo-restricted | |
44 | 'url': 'https://www.fox.com/watch/087036ca7f33c8eb79b08152b4dd75c1/', | |
45 | 'only_matching': True, | |
46 | }, { | |
443f8de8 | 47 | # sports event, geo-restricted |
48 | 'url': 'https://www.fox.com/watch/b057484dade738d1f373b3e46216fa2c/', | |
bf6ec2fe S |
49 | 'only_matching': True, |
50 | }] | |
0d08bcdb | 51 | _GEO_BYPASS = False |
6df196f3 | 52 | _HOME_PAGE_URL = 'https://www.fox.com/' |
443f8de8 | 53 | _API_KEY = '6E9S4bmcoNnZwVLOHywOv8PJEdu76cM9' |
41c2c254 | 54 | _access_token = None |
443f8de8 | 55 | _device_id = compat_str(uuid.uuid4()) |
96c186e1 | 56 | |
41c2c254 RA |
57 | def _call_api(self, path, video_id, data=None): |
58 | headers = { | |
6df196f3 | 59 | 'X-Api-Key': self._API_KEY, |
41c2c254 RA |
60 | } |
61 | if self._access_token: | |
62 | headers['Authorization'] = 'Bearer ' + self._access_token | |
0d08bcdb RA |
63 | try: |
64 | return self._download_json( | |
443f8de8 | 65 | 'https://api3.fox.com/v2.0/' + path, |
0d08bcdb RA |
66 | video_id, data=data, headers=headers) |
67 | except ExtractorError as e: | |
62d10f0d | 68 | if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403: |
0d08bcdb RA |
69 | entitlement_issues = self._parse_json( |
70 | e.cause.read().decode(), video_id)['entitlementIssues'] | |
71 | for e in entitlement_issues: | |
72 | if e.get('errorCode') == 1005: | |
73 | raise ExtractorError( | |
74 | 'This video is only available via cable service provider ' | |
75 | 'subscription. You may want to use --cookies.', expected=True) | |
76 | messages = ', '.join([e['message'] for e in entitlement_issues]) | |
77 | raise ExtractorError(messages, expected=True) | |
78 | raise | |
96c186e1 | 79 | |
41c2c254 | 80 | def _real_initialize(self): |
6df196f3 RA |
81 | if not self._access_token: |
82 | mvpd_auth = self._get_cookies(self._HOME_PAGE_URL).get('mvpd-auth') | |
83 | if mvpd_auth: | |
84 | self._access_token = (self._parse_json(compat_urllib_parse_unquote( | |
85 | mvpd_auth.value), None, fatal=False) or {}).get('accessToken') | |
86 | if not self._access_token: | |
87 | self._access_token = self._call_api( | |
88 | 'login', None, json.dumps({ | |
443f8de8 | 89 | 'deviceId': self._device_id, |
6df196f3 | 90 | }).encode())['accessToken'] |
9787c5f4 | 91 | |
92 | def _real_extract(self, url): | |
93 | video_id = self._match_id(url) | |
7aa0ee32 | 94 | |
443f8de8 | 95 | self._access_token = self._call_api( |
96 | 'previewpassmvpd?device_id=%s&mvpd_id=TempPass_fbcfox_60min' % self._device_id, | |
97 | video_id)['accessToken'] | |
98 | ||
99 | video = self._call_api('watch', video_id, data=json.dumps({ | |
100 | 'capabilities': ['drm/widevine', 'fsdk/yo'], | |
101 | 'deviceWidth': 1280, | |
102 | 'deviceHeight': 720, | |
103 | 'maxRes': '720p', | |
104 | 'os': 'macos', | |
105 | 'osv': '', | |
106 | 'provider': { | |
107 | 'freewheel': {'did': self._device_id}, | |
108 | 'vdms': {'rays': ''}, | |
109 | 'dmp': {'kuid': '', 'seg': ''} | |
110 | }, | |
111 | 'playlist': '', | |
112 | 'privacy': {'us': '1---'}, | |
113 | 'siteSection': '', | |
114 | 'streamType': 'vod', | |
115 | 'streamId': video_id}).encode('utf-8')) | |
bf6ec2fe S |
116 | |
117 | title = video['name'] | |
41c2c254 | 118 | release_url = video['url'] |
443f8de8 | 119 | |
0d08bcdb RA |
120 | try: |
121 | m3u8_url = self._download_json(release_url, video_id)['playURL'] | |
122 | except ExtractorError as e: | |
e0dde1d8 | 123 | if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403: |
0d08bcdb RA |
124 | error = self._parse_json(e.cause.read().decode(), video_id) |
125 | if error.get('exception') == 'GeoLocationBlocked': | |
126 | self.raise_geo_restricted(countries=['US']) | |
127 | raise ExtractorError(error['description'], expected=True) | |
128 | raise | |
96c186e1 RA |
129 | formats = self._extract_m3u8_formats( |
130 | m3u8_url, video_id, 'mp4', | |
131 | entry_protocol='m3u8_native', m3u8_id='hls') | |
132 | self._sort_formats(formats) | |
133 | ||
6df196f3 RA |
134 | data = try_get( |
135 | video, lambda x: x['trackingData']['properties'], dict) or {} | |
136 | ||
96c186e1 RA |
137 | duration = int_or_none(video.get('durationInSeconds')) or int_or_none( |
138 | video.get('duration')) or parse_duration(video.get('duration')) | |
139 | timestamp = unified_timestamp(video.get('datePublished')) | |
140 | creator = data.get('brand') or data.get('network') or video.get('network') | |
141 | series = video.get('seriesName') or data.get( | |
142 | 'seriesName') or data.get('show') | |
684ae102 RA |
143 | |
144 | subtitles = {} | |
145 | for doc_rel in video.get('documentReleases', []): | |
146 | rel_url = doc_rel.get('url') | |
147 | if not url or doc_rel.get('format') != 'SCC': | |
148 | continue | |
149 | subtitles['en'] = [{ | |
150 | 'url': rel_url, | |
151 | 'ext': 'scc', | |
152 | }] | |
153 | break | |
bf6ec2fe | 154 | |
96c186e1 | 155 | return { |
bf6ec2fe S |
156 | 'id': video_id, |
157 | 'title': title, | |
96c186e1 RA |
158 | 'formats': formats, |
159 | 'description': video.get('description'), | |
bf6ec2fe S |
160 | 'duration': duration, |
161 | 'timestamp': timestamp, | |
6df196f3 | 162 | 'age_limit': parse_age_limit(video.get('contentRating')), |
bf6ec2fe S |
163 | 'creator': creator, |
164 | 'series': series, | |
96c186e1 RA |
165 | 'season_number': int_or_none(video.get('seasonNumber')), |
166 | 'episode': video.get('name'), | |
167 | 'episode_number': int_or_none(video.get('episodeNumber')), | |
168 | 'release_year': int_or_none(video.get('releaseYear')), | |
684ae102 | 169 | 'subtitles': subtitles, |
bf6ec2fe | 170 | } |