6 from .adobepass
import AdobePassIE
7 from .common
import InfoExtractor
8 from .once
import OnceIE
25 (?:(?:\w+\.)+)?espn\.go|
30 video/(?:clip|iframe/twitter)|
39 (?:www\.)espnfc\.(?:com|us)/(?:video/)?[^/]+/\d+/video/
45 'url': 'http://espn.go.com/video/clip?id=10365079',
49 'title': '30 for 30 Shorts: Judging Jewell',
50 'description': 'md5:39370c2e016cb4ecf498ffe75bef7f0f',
51 'timestamp': 1390936111,
52 'upload_date': '20140128',
54 'thumbnail': r
're:https://.+\.jpg',
57 'skip_download': True,
60 'url': 'https://broadband.espn.go.com/video/clip?id=18910086',
64 'title': 'Kyrie spins around defender for two',
65 'description': 'md5:2b0f5bae9616d26fba8808350f0d2b9b',
66 'timestamp': 1489539155,
67 'upload_date': '20170315',
70 'skip_download': True,
72 'expected_warnings': ['Unable to download f4m manifest'],
74 'url': 'http://nonredline.sports.espn.go.com/video/clip?id=19744672',
75 'only_matching': True,
77 'url': 'https://cdn.espn.go.com/video/clip/_/id/19771774',
78 'only_matching': True,
80 'url': 'http://www.espn.com/video/clip?id=10365079',
81 'only_matching': True,
83 'url': 'http://www.espn.com/video/clip/_/id/17989860',
84 'only_matching': True,
86 'url': 'https://espn.go.com/video/iframe/twitter/?cms=espn&id=10365079',
87 'only_matching': True,
89 'url': 'http://www.espnfc.us/video/espn-fc-tv/86/video/3319154/nashville-unveiled-as-the-newest-club-in-mls',
90 'only_matching': True,
92 'url': 'http://www.espnfc.com/english-premier-league/23/video/3324163/premier-league-in-90-seconds-golden-tweets',
93 'only_matching': True,
95 'url': 'http://www.espn.com/espnw/video/26066627/arkansas-gibson-completes-hr-cycle-four-innings',
96 'only_matching': True,
98 'url': 'http://www.espn.com/watch/player?id=19141491',
99 'only_matching': True,
101 'url': 'http://www.espn.com/watch/player?bucketId=257&id=19505875',
102 'only_matching': True,
105 def _real_extract(self
, url
):
106 video_id
= self
._match
_id
(url
)
108 clip
= self
._download
_json
(
109 f
'http://api-app.espn.com/v1/video/clips/{video_id}',
110 video_id
)['videos'][0]
112 title
= clip
['headline']
117 def traverse_source(source
, base_source_id
=None):
118 for src_id
, src_item
in source
.items():
119 if src_id
== 'alert':
121 elif isinstance(src_item
, str):
122 extract_source(src_item
, base_source_id
)
123 elif isinstance(src_item
, dict):
126 f
'{base_source_id}-{src_id}'
127 if base_source_id
else src_id
)
129 def extract_source(source_url
, source_id
=None):
130 if source_url
in format_urls
:
132 format_urls
.add(source_url
)
133 ext
= determine_ext(source_url
)
134 if OnceIE
.suitable(source_url
):
135 formats
.extend(self
._extract
_once
_formats
(source_url
))
137 formats
.extend(self
._extract
_smil
_formats
(
138 source_url
, video_id
, fatal
=False))
140 formats
.extend(self
._extract
_f
4m
_formats
(
141 source_url
, video_id
, f4m_id
=source_id
, fatal
=False))
143 formats
.extend(self
._extract
_m
3u8_formats
(
144 source_url
, video_id
, 'mp4', entry_protocol
='m3u8_native',
145 m3u8_id
=source_id
, fatal
=False))
149 'format_id': source_id
,
151 mobj
= re
.search(r
'(\d+)p(\d+)_(\d+)k\.', source_url
)
154 'height': int(mobj
.group(1)),
155 'fps': int(mobj
.group(2)),
156 'tbr': int(mobj
.group(3)),
158 if source_id
== 'mezzanine':
162 links
= clip
.get('links', {})
163 traverse_source(links
.get('source', {}))
164 traverse_source(links
.get('mobile', {}))
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().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 f'http://espn.go.com/video/clip?id={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
/(?
:cricket
-)?videos?
/[^
#$&?/]+-(?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},
256 'url': 'https://www.espncricinfo.com/cricket-videos/daryl-mitchell-mitchell-santner-is-one-of-the-best-white-ball-spinners-india-vs-new-zealand-1356225',
260 'description': '"Santner has done it for a long time for New Zealand - we\'re lucky to have him"',
261 'upload_date': '20230128',
262 'title': 'Mitchell: \'Santner is one of the best white-ball spinners at the moment\'',
265 'params': {'skip_download': 'm3u8'},
268 def _real_extract(self
, url
):
269 video_id
= self
._match
_id
(url
)
270 data_json
= self
._download
_json
(
271 f
'https://hs-consumer-api.espncricinfo.com/v1/pages/video/video-details?videoId={video_id}', video_id
)['video']
272 formats
, subtitles
= [], {}
273 for item
in data_json
.get('playbacks') or []:
274 if item
.get('type') == 'HLS' and item
.get('url'):
275 m3u8_frmts
, m3u8_subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(item
['url'], video_id
)
276 formats
.extend(m3u8_frmts
)
277 subtitles
= self
._merge
_subtitles
(subtitles
, m3u8_subs
)
278 elif item
.get('type') == 'AUDIO' and item
.get('url'):
285 'title': data_json
.get('title'),
286 'description': data_json
.get('summary'),
287 'upload_date': unified_strdate(dict_get(data_json
, ('publishedAt', 'recordedAt'))),
288 'duration': data_json
.get('duration'),
290 'subtitles': subtitles
,
294 class WatchESPNIE(AdobePassIE
):
295 _VALID_URL
= r
'https?://(?:www\.)?espn\.com/(?:watch|espnplus)/player/_/id/(?P<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
297 'url': 'https://www.espn.com/watch/player/_/id/dbbc6b1d-c084-4b47-9878-5f13c56ce309',
299 'id': 'dbbc6b1d-c084-4b47-9878-5f13c56ce309',
301 'title': 'Huddersfield vs. Burnley',
303 'thumbnail': 'https://artwork.api.espn.com/artwork/collections/media/dbbc6b1d-c084-4b47-9878-5f13c56ce309/default?width=640&apikey=1ngjw23osgcis1i1vbj96lmfqs',
306 'skip_download': True,
309 'url': 'https://www.espn.com/watch/player/_/id/a049a56e-a7ce-477e-aef3-c7e48ef8221c',
311 'id': 'a049a56e-a7ce-477e-aef3-c7e48ef8221c',
313 'title': 'Dynamo Dresden vs. VfB Stuttgart (Round #1) (German Cup)',
315 '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',
318 'skip_download': True,
321 'url': 'https://www.espn.com/espnplus/player/_/id/317f5fd1-c78a-4ebe-824a-129e0d348421',
323 'id': '317f5fd1-c78a-4ebe-824a-129e0d348421',
325 'title': 'The Wheel - Episode 10',
327 'thumbnail': 'https://s.secure.espncdn.com/stitcher/artwork/collections/media/317f5fd1-c78a-4ebe-824a-129e0d348421/16x9.jpg?timestamp=202205031523&showBadge=true&cb=12&package=ESPN_PLUS',
330 'skip_download': True,
334 _API_KEY
= 'ZXNwbiZicm93c2VyJjEuMC4w.ptUt7QxsteaRruuPmGZFaJByOoqKvDP2a5YkInHrc7c'
336 def _call_bamgrid_api(self
, path
, video_id
, payload
=None, headers
={}):
337 if 'Authorization' not in headers
:
338 headers
['Authorization'] = f
'Bearer {self._API_KEY}'
339 parse
= urllib
.parse
.urlencode
if path
== 'token' else json
.dumps
340 return self
._download
_json
(
341 f
'https://espn.api.edge.bamgrid.com/{path}', video_id
, headers
=headers
, data
=parse(payload
).encode())
343 def _real_extract(self
, url
):
344 video_id
= self
._match
_id
(url
)
345 cdn_data
= self
._download
_json
(
346 f
'https://watch-cdn.product.api.espn.com/api/product/v3/watchespn/web/playback/event?id={video_id}',
348 video_data
= cdn_data
['playbackState']
350 # ESPN+ subscription required, through cookies
351 if 'DTC' in video_data
.get('sourceId'):
352 cookie
= self
._get
_cookies
(url
).get('ESPN-ONESITE.WEB-PROD.token')
354 self
.raise_login_required(method
='cookies')
356 assertion
= self
._call
_bamgrid
_api
(
358 headers
={'Content-Type': 'application/json; charset=UTF-8'},
360 'deviceFamily': 'android',
361 'applicationRuntime': 'android',
362 'deviceProfile': 'tv',
365 token
= self
._call
_bamgrid
_api
(
366 'token', video_id
, payload
={
367 'subject_token': assertion
,
368 'subject_token_type': 'urn:bamtech:params:oauth:token-type:device',
369 'platform': 'android',
370 'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
373 assertion
= self
._call
_bamgrid
_api
(
374 'accounts/grant', video_id
, payload
={'id_token': cookie
.value
.split('|')[1]},
376 'Authorization': token
,
377 'Content-Type': 'application/json; charset=UTF-8',
379 token
= self
._call
_bamgrid
_api
(
380 'token', video_id
, payload
={
381 'subject_token': assertion
,
382 'subject_token_type': 'urn:bamtech:params:oauth:token-type:account',
383 'platform': 'android',
384 'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
387 playback
= self
._download
_json
(
388 video_data
['videoHref'].format(scenario
='browser~ssai'), video_id
,
390 'Accept': 'application/vnd.media-service+json; version=5',
391 'Authorization': token
,
393 m3u8_url
, headers
= playback
['stream']['complete'][0]['url'], {'authorization': token
}
396 elif video_data
.get('sourceId') == 'ESPN_FREE':
397 asset
= self
._download
_json
(
398 f
'https://watch.auth.api.espn.com/video/auth/media/{video_id}/asset?apikey=uiqlbgzdwuru14v627vdusswb',
400 m3u8_url
, headers
= asset
['stream'], {}
402 # TV Provider required
404 resource
= self
._get
_mvpd
_resource
('ESPN', video_data
['name'], video_id
, None)
405 auth
= self
._extract
_mvpd
_auth
(url
, video_id
, 'ESPN', resource
).encode()
407 asset
= self
._download
_json
(
408 f
'https://watch.auth.api.espn.com/video/auth/media/{video_id}/asset?apikey=uiqlbgzdwuru14v627vdusswb',
409 video_id
, data
=f
'adobeToken={urllib.parse.quote_plus(base64.b64encode(auth))}&drmSupport=HLS'.encode())
410 m3u8_url
, headers
= asset
['stream'], {}
412 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(m3u8_url
, video_id
, 'mp4', m3u8_id
='hls')
416 'duration': traverse_obj(cdn_data
, ('tracking', 'duration')),
417 'title': video_data
.get('name'),
419 'subtitles': subtitles
,
420 'thumbnail': video_data
.get('posterHref'),
421 'http_headers': headers
,