6 from .common
import InfoExtractor
7 from .adobepass
import AdobePassIE
8 from .once
import OnceIE
24 (?:(?:\w+\.)+)?espn\.go|
29 video/(?:clip|iframe/twitter)|
38 (?:www\.)espnfc\.(?:com|us)/(?:video/)?[^/]+/\d+/video/
44 'url': 'http://espn.go.com/video/clip?id=10365079',
48 'title': '30 for 30 Shorts: Judging Jewell',
49 'description': 'md5:39370c2e016cb4ecf498ffe75bef7f0f',
50 'timestamp': 1390936111,
51 'upload_date': '20140128',
53 'thumbnail': r
're:https://.+\.jpg',
56 'skip_download': True,
59 'url': 'https://broadband.espn.go.com/video/clip?id=18910086',
63 'title': 'Kyrie spins around defender for two',
64 'description': 'md5:2b0f5bae9616d26fba8808350f0d2b9b',
65 'timestamp': 1489539155,
66 'upload_date': '20170315',
69 'skip_download': True,
71 'expected_warnings': ['Unable to download f4m manifest'],
73 'url': 'http://nonredline.sports.espn.go.com/video/clip?id=19744672',
74 'only_matching': True,
76 'url': 'https://cdn.espn.go.com/video/clip/_/id/19771774',
77 'only_matching': True,
79 'url': 'http://www.espn.com/video/clip?id=10365079',
80 'only_matching': True,
82 'url': 'http://www.espn.com/video/clip/_/id/17989860',
83 'only_matching': True,
85 'url': 'https://espn.go.com/video/iframe/twitter/?cms=espn&id=10365079',
86 'only_matching': True,
88 'url': 'http://www.espnfc.us/video/espn-fc-tv/86/video/3319154/nashville-unveiled-as-the-newest-club-in-mls',
89 'only_matching': True,
91 'url': 'http://www.espnfc.com/english-premier-league/23/video/3324163/premier-league-in-90-seconds-golden-tweets',
92 'only_matching': True,
94 'url': 'http://www.espn.com/espnw/video/26066627/arkansas-gibson-completes-hr-cycle-four-innings',
95 'only_matching': True,
97 'url': 'http://www.espn.com/watch/player?id=19141491',
98 'only_matching': True,
100 'url': 'http://www.espn.com/watch/player?bucketId=257&id=19505875',
101 'only_matching': True,
104 def _real_extract(self
, url
):
105 video_id
= self
._match
_id
(url
)
107 clip
= self
._download
_json
(
108 'http://api-app.espn.com/v1/video/clips/%s' % video_id
,
109 video_id
)['videos'][0]
111 title
= clip
['headline']
116 def traverse_source(source
, base_source_id
=None):
117 for source_id
, source
in source
.items():
118 if source_id
== 'alert':
120 elif isinstance(source
, str):
121 extract_source(source
, base_source_id
)
122 elif isinstance(source
, dict):
125 '%s-%s' % (base_source_id
, source_id
)
126 if base_source_id
else source_id
)
128 def extract_source(source_url
, source_id
=None):
129 if source_url
in format_urls
:
131 format_urls
.add(source_url
)
132 ext
= determine_ext(source_url
)
133 if OnceIE
.suitable(source_url
):
134 formats
.extend(self
._extract
_once
_formats
(source_url
))
136 formats
.extend(self
._extract
_smil
_formats
(
137 source_url
, video_id
, fatal
=False))
139 formats
.extend(self
._extract
_f
4m
_formats
(
140 source_url
, video_id
, f4m_id
=source_id
, fatal
=False))
142 formats
.extend(self
._extract
_m
3u8_formats
(
143 source_url
, video_id
, 'mp4', entry_protocol
='m3u8_native',
144 m3u8_id
=source_id
, fatal
=False))
148 'format_id': source_id
,
150 mobj
= re
.search(r
'(\d+)p(\d+)_(\d+)k\.', source_url
)
153 'height': int(mobj
.group(1)),
154 'fps': int(mobj
.group(2)),
155 'tbr': int(mobj
.group(3)),
157 if source_id
== 'mezzanine':
161 links
= clip
.get('links', {})
162 traverse_source(links
.get('source', {}))
163 traverse_source(links
.get('mobile', {}))
164 self
._sort
_formats
(formats
)
166 description
= clip
.get('caption') or clip
.get('description')
167 thumbnail
= clip
.get('thumbnail')
168 duration
= int_or_none(clip
.get('duration'))
169 timestamp
= unified_timestamp(clip
.get('originalPublishDate'))
174 'description': description
,
175 'thumbnail': thumbnail
,
176 'timestamp': timestamp
,
177 'duration': duration
,
182 class ESPNArticleIE(InfoExtractor
):
183 _VALID_URL
= r
'https?://(?:espn\.go|(?:www\.)?espn)\.com/(?:[^/]+/)*(?P<id>[^/]+)'
185 'url': 'http://espn.go.com/nba/recap?gameId=400793786',
186 'only_matching': True,
188 'url': 'http://espn.go.com/blog/golden-state-warriors/post/_/id/593/how-warriors-rapidly-regained-a-winning-edge',
189 'only_matching': True,
191 'url': 'http://espn.go.com/sports/endurance/story/_/id/12893522/dzhokhar-tsarnaev-sentenced-role-boston-marathon-bombings',
192 'only_matching': True,
194 'url': 'http://espn.go.com/nba/playoffs/2015/story/_/id/12887571/john-wall-washington-wizards-no-swelling-left-hand-wrist-game-5-return',
195 'only_matching': True,
199 def suitable(cls
, url
):
200 return False if (ESPNIE
.suitable(url
) or WatchESPNIE
.suitable(url
)) else super(ESPNArticleIE
, cls
).suitable(url
)
202 def _real_extract(self
, url
):
203 video_id
= self
._match
_id
(url
)
205 webpage
= self
._download
_webpage
(url
, video_id
)
207 video_id
= self
._search
_regex
(
208 r
'class=(["\']).*?video
-play
-button
.*?\
1[^
>]+data
-id=["\'](?P<id>\d+)',
209 webpage, 'video id', group='id')
211 return self.url_result(
212 'http://espn.go.com/video/clip?id=%s' % video_id, ESPNIE.ie_key())
215 class FiveThirtyEightIE(InfoExtractor):
216 _VALID_URL = r'https?://(?:www\.)?fivethirtyeight\.com/features/(?P<id>[^/?#]+)'
218 'url': 'http://fivethirtyeight.com/features/how-the-6-8-raiders-can-still-make-the-playoffs/',
222 'title': 'FiveThirtyEight: The Raiders can still make the playoffs',
223 'description': 'Neil Paine breaks down the simplest scenario that will put the Raiders into the playoffs at 8-8.',
226 'skip_download': True,
230 def _real_extract(self, url):
231 video_id = self._match_id(url)
233 webpage = self._download_webpage(url, video_id)
235 embed_url = self._search_regex(
236 r'<iframe[^>]+src=["\'](https?
://fivethirtyeight\
.abcnews\
.go\
.com
/video
/embed
/\d
+/\d
+)',
237 webpage, 'embed url
')
239 return self.url_result(embed_url, 'AbcNewsVideo
')
242 class ESPNCricInfoIE(InfoExtractor):
243 _VALID_URL = r'https?
://(?
:www\
.)?espncricinfo\
.com
/video
/[^
#$&?/]+-(?P<id>\d+)'
245 'url': 'https://www.espncricinfo.com/video/finch-chasing-comes-with-risks-despite-world-cup-trend-1289135',
249 'title': 'Finch: Chasing comes with \'risks\' despite World Cup trend',
250 'description': 'md5:ea32373303e25efbb146efdfc8a37829',
251 'upload_date': '20211113',
254 'params': {'skip_download': True}
257 def _real_extract(self
, url
):
258 id = self
._match
_id
(url
)
259 data_json
= self
._download
_json
(f
'https://hs-consumer-api.espncricinfo.com/v1/pages/video/video-details?videoId={id}', id)['video']
260 formats
, subtitles
= [], {}
261 for item
in data_json
.get('playbacks') or []:
262 if item
.get('type') == 'HLS' and item
.get('url'):
263 m3u8_frmts
, m3u8_subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(item
['url'], id)
264 formats
.extend(m3u8_frmts
)
265 subtitles
= self
._merge
_subtitles
(subtitles
, m3u8_subs
)
266 elif item
.get('type') == 'AUDIO' and item
.get('url'):
271 self
._sort
_formats
(formats
)
274 'title': data_json
.get('title'),
275 'description': data_json
.get('summary'),
276 'upload_date': unified_strdate(dict_get(data_json
, ('publishedAt', 'recordedAt'))),
277 'duration': data_json
.get('duration'),
279 'subtitles': subtitles
,
283 class WatchESPNIE(AdobePassIE
):
284 _VALID_URL
= r
'https://www.espn.com/watch/player/_/id/(?P<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
286 'url': 'https://www.espn.com/watch/player/_/id/ba7d17da-453b-4697-bf92-76a99f61642b',
288 'id': 'ba7d17da-453b-4697-bf92-76a99f61642b',
290 'title': 'Serbia vs. Turkey',
291 'thumbnail': 'https://artwork.api.espn.com/artwork/collections/media/ba7d17da-453b-4697-bf92-76a99f61642b/default?width=640&apikey=1ngjw23osgcis1i1vbj96lmfqs',
294 'skip_download': True,
297 'url': 'https://www.espn.com/watch/player/_/id/4e9b5bd1-4ceb-4482-9d28-1dd5f30d2f34',
299 'id': '4e9b5bd1-4ceb-4482-9d28-1dd5f30d2f34',
301 'title': 'Real Madrid vs. Real Betis (LaLiga)',
302 'thumbnail': 'https://s.secure.espncdn.com/stitcher/artwork/collections/media/bd1f3d12-0654-47d9-852e-71b85ea695c7/16x9.jpg?timestamp=202201112217&showBadge=true&cb=12&package=ESPN_PLUS',
305 'skip_download': True,
309 _API_KEY
= 'ZXNwbiZicm93c2VyJjEuMC4w.ptUt7QxsteaRruuPmGZFaJByOoqKvDP2a5YkInHrc7c'
311 def _call_bamgrid_api(self
, path
, video_id
, payload
=None, headers
={}):
312 if 'Authorization' not in headers
:
313 headers
['Authorization'] = f
'Bearer {self._API_KEY}'
314 parse
= urllib
.parse
.urlencode
if path
== 'token' else json
.dumps
315 return self
._download
_json
(
316 f
'https://espn.api.edge.bamgrid.com/{path}', video_id
, headers
=headers
, data
=parse(payload
).encode())
318 def _real_extract(self
, url
):
319 video_id
= self
._match
_id
(url
)
320 video_data
= self
._download
_json
(
321 f
'https://watch-cdn.product.api.espn.com/api/product/v3/watchespn/web/playback/event?id={video_id}',
322 video_id
)['playbackState']
324 # ESPN+ subscription required, through cookies
325 if 'DTC' in video_data
.get('sourceId'):
326 cookie
= self
._get
_cookies
(url
).get('ESPN-ONESITE.WEB-PROD.token')
328 self
.raise_login_required(method
='cookies')
330 assertion
= self
._call
_bamgrid
_api
(
332 headers
={'Content-Type': 'application/json; charset=UTF-8'}
,
334 'deviceFamily': 'android',
335 'applicationRuntime': 'android',
336 'deviceProfile': 'tv',
339 token
= self
._call
_bamgrid
_api
(
340 'token', video_id
, payload
={
341 'subject_token': assertion
,
342 'subject_token_type': 'urn:bamtech:params:oauth:token-type:device',
343 'platform': 'android',
344 'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange'
347 assertion
= self
._call
_bamgrid
_api
(
348 'accounts/grant', video_id
, payload
={'id_token': cookie.value.split('|')[1]}
,
350 'Authorization': token
,
351 'Content-Type': 'application/json; charset=UTF-8'
353 token
= self
._call
_bamgrid
_api
(
354 'token', video_id
, payload
={
355 'subject_token': assertion
,
356 'subject_token_type': 'urn:bamtech:params:oauth:token-type:account',
357 'platform': 'android',
358 'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange'
361 playback
= self
._download
_json
(
362 video_data
['videoHref'].format(scenario
='browser~ssai'), video_id
,
364 'Accept': 'application/vnd.media-service+json; version=5',
365 'Authorization': token
367 m3u8_url
, headers
= playback
['stream']['complete'][0]['url'], {'authorization': token}
370 elif video_data
.get('sourceId') == 'ESPN_FREE':
371 asset
= self
._download
_json
(
372 f
'https://watch.auth.api.espn.com/video/auth/media/{video_id}/asset?apikey=uiqlbgzdwuru14v627vdusswb',
374 m3u8_url
, headers
= asset
['stream'], {}
376 # TV Provider required
378 resource
= self
._get
_mvpd
_resource
('ESPN', video_data
['name'], video_id
, None)
379 auth
= self
._extract
_mvpd
_auth
(url
, video_id
, 'ESPN', resource
).encode()
381 asset
= self
._download
_json
(
382 f
'https://watch.auth.api.espn.com/video/auth/media/{video_id}/asset?apikey=uiqlbgzdwuru14v627vdusswb',
383 video_id
, data
=f
'adobeToken={urllib.parse.quote_plus(base64.b64encode(auth))}&drmSupport=HLS'.encode())
384 m3u8_url
, headers
= asset
['stream'], {}
386 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(m3u8_url
, video_id
, 'mp4', m3u8_id
='hls')
387 self
._sort
_formats
(formats
)
391 'title': video_data
.get('name'),
393 'subtitles': subtitles
,
394 'thumbnail': video_data
.get('posterHref'),
395 'http_headers': headers
,