fox.py (5692B)
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 }