2 from __future__
import unicode_literals
7 from .common
import InfoExtractor
21 class PelotonIE(InfoExtractor
):
23 _NETRC_MACHINE
= 'peloton'
24 _VALID_URL
= r
'https?://members\.onepeloton\.com/classes/player/(?P<id>[a-f0-9]+)'
26 'url': 'https://members.onepeloton.com/classes/player/0e9653eb53544eeb881298c8d7a87b86',
28 'id': '0e9653eb53544eeb881298c8d7a87b86',
29 'title': '20 min Chest & Back Strength',
31 'thumbnail': r
're:^https?://.+\.jpg',
32 'description': 'md5:fcd5be9b9eda0194b470e13219050a66',
33 'creator': 'Chase Tucker',
34 'release_timestamp': 1556141400,
35 'timestamp': 1556141400,
36 'upload_date': '20190424',
38 'categories': ['Strength'],
39 'tags': ['Workout Mat', 'Light Weights', 'Medium Weights'],
41 'chapters': 'count:1',
42 'subtitles': {'en': [{
43 'url': r
're:^https?://.+',
47 'skip_download': 'm3u8',
49 '_skip': 'Account needed'
51 'url': 'https://members.onepeloton.com/classes/player/26603d53d6bb4de1b340514864a6a6a8',
53 'id': '26603d53d6bb4de1b340514864a6a6a8',
54 'title': '30 min Earth Day Run',
56 'thumbnail': r
're:https://.+\.jpg',
57 'description': 'md5:adc065a073934d7ee0475d217afe0c3d',
58 'creator': 'Selena Samuela',
59 'release_timestamp': 1587567600,
60 'timestamp': 1587567600,
61 'upload_date': '20200422',
63 'categories': ['Running'],
67 'skip_download': 'm3u8',
69 '_skip': 'Account needed'
72 _MANIFEST_URL_TEMPLATE
= '%s?hdnea=%s'
74 def _start_session(self
, video_id
):
75 self
._download
_webpage
('https://api.onepeloton.com/api/started_client_session', video_id
, note
='Starting session')
77 def _login(self
, video_id
):
78 username
, password
= self
._get
_login
_info
()
79 if not (username
and password
):
80 self
.raise_login_required()
83 'https://api.onepeloton.com/auth/login', video_id
, note
='Logging in',
85 'username_or_email': username
,
89 headers
={'Content-Type': 'application/json', 'User-Agent': 'web'}
)
90 except ExtractorError
as e
:
91 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 401:
92 json_string
= self
._webpage
_read
_content
(e
.cause
, None, video_id
)
93 res
= self
._parse
_json
(json_string
, video_id
)
94 raise ExtractorError(res
['message'], expected
=res
['message'] == 'Login failed')
98 def _get_token(self
, video_id
):
100 subscription
= self
._download
_json
(
101 'https://api.onepeloton.com/api/subscription/stream', video_id
, note
='Downloading token',
102 data
=json
.dumps({}).encode(), headers={'Content-Type': 'application/json'}
)
103 except ExtractorError
as e
:
104 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 403:
105 json_string
= self
._webpage
_read
_content
(e
.cause
, None, video_id
)
106 res
= self
._parse
_json
(json_string
, video_id
)
107 raise ExtractorError(res
['message'], expected
=res
['message'] == 'Stream limit reached')
110 return subscription
['token']
112 def _real_extract(self
, url
):
113 video_id
= self
._match
_id
(url
)
115 self
._start
_session
(video_id
)
116 except ExtractorError
as e
:
117 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 401:
118 self
._login
(video_id
)
119 self
._start
_session
(video_id
)
123 metadata
= self
._download
_json
('https://api.onepeloton.com/api/ride/%s/details?stream_source=multichannel' % video_id
, video_id
)
124 ride_data
= metadata
.get('ride')
126 raise ExtractorError('Missing stream metadata')
127 token
= self
._get
_token
(video_id
)
130 if ride_data
.get('content_format') == 'audio':
131 url
= self
._MANIFEST
_URL
_TEMPLATE
% (ride_data
.get('vod_stream_url'), compat_urllib_parse
.quote(token
))
135 'format_id': 'audio',
140 if ride_data
.get('vod_stream_url'):
141 url
= 'https://members.onepeloton.com/.netlify/functions/m3u8-proxy?displayLanguage=en&acceptedSubtitles=%s&url=%s?hdnea=%s' % (
142 ','.join([re
.sub('^([a-z]+)-([A-Z]+)$', r
'\1', caption
) for caption
in ride_data
['captions']]),
143 ride_data
['vod_stream_url'],
144 compat_urllib_parse
.quote(compat_urllib_parse
.quote(token
)))
145 elif ride_data
.get('live_stream_url'):
146 url
= self
._MANIFEST
_URL
_TEMPLATE
% (ride_data
.get('live_stream_url'), compat_urllib_parse
.quote(token
))
149 raise ExtractorError('Missing video URL')
150 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(url
, video_id
, 'mp4')
152 if metadata
.get('instructor_cues'):
153 subtitles
['cues'] = [{
154 'data': json
.dumps(metadata
.get('instructor_cues')),
158 category
= ride_data
.get('fitness_discipline_display_name')
160 'start_time': segment
.get('start_time_offset'),
161 'end_time': segment
.get('start_time_offset') + segment
.get('length'),
162 'title': segment
.get('name')
163 } for segment
in traverse_obj(metadata
, ('segments', 'segment_list'))]
165 self
._sort
_formats
(formats
)
168 'title': ride_data
.get('title'),
170 'thumbnail': url_or_none(ride_data
.get('image_url')),
171 'description': str_or_none(ride_data
.get('description')),
172 'creator': traverse_obj(ride_data
, ('instructor', 'name')),
173 'release_timestamp': ride_data
.get('original_air_time'),
174 'timestamp': ride_data
.get('original_air_time'),
175 'subtitles': subtitles
,
176 'duration': float_or_none(ride_data
.get('length')),
177 'categories': [category
] if category
else None,
178 'tags': traverse_obj(ride_data
, ('equipment_tags', ..., 'name')),
184 class PelotonLiveIE(InfoExtractor
):
185 IE_NAME
= 'peloton:live'
186 IE_DESC
= 'Peloton Live'
187 _VALID_URL
= r
'https?://members\.onepeloton\.com/player/live/(?P<id>[a-f0-9]+)'
189 'url': 'https://members.onepeloton.com/player/live/eedee2d19f804a9788f53aa8bd38eb1b',
191 'id': '32edc92d28044be5bf6c7b6f1f8d1cbc',
192 'title': '30 min HIIT Ride: Live from Home',
194 'thumbnail': r
're:^https?://.+\.png',
195 'description': 'md5:f0d7d8ed3f901b7ee3f62c1671c15817',
196 'creator': 'Alex Toussaint',
197 'release_timestamp': 1587736620,
198 'timestamp': 1587736620,
199 'upload_date': '20200424',
201 'categories': ['Cycling'],
203 'chapters': 'count:3'
206 'skip_download': 'm3u8',
208 '_skip': 'Account needed'
211 def _real_extract(self
, url
):
212 workout_id
= self
._match
_id
(url
)
213 peloton
= self
._download
_json
(f
'https://api.onepeloton.com/api/peloton/{workout_id}', workout_id
)
215 if peloton
.get('ride_id'):
216 if not peloton
.get('is_live') or peloton
.get('is_encore') or peloton
.get('status') != 'PRE_START':
217 return self
.url_result('https://members.onepeloton.com/classes/player/%s' % peloton
['ride_id'])
219 raise ExtractorError('Ride has not started', expected
=True)
221 raise ExtractorError('Missing video ID')