9 from .common
import InfoExtractor
17 class GoPlayIE(InfoExtractor
):
18 _VALID_URL
= r
'https?://(www\.)?goplay\.be/video/([^/]+/[^/]+/|)(?P<display_id>[^/#]+)'
20 _NETRC_MACHINE
= 'goplay'
23 'url': 'https://www.goplay.be/video/de-container-cup/de-container-cup-s3/de-container-cup-s3-aflevering-2#autoplay',
25 'id': '9c4214b8-e55d-4e4b-a446-f015f6c6f811',
27 'title': 'S3 - Aflevering 2',
28 'series': 'De Container Cup',
31 'episode': 'Episode 2',
34 'skip': 'This video is only available for registered users',
36 'url': 'https://www.goplay.be/video/a-family-for-thr-holidays-s1-aflevering-1#autoplay',
38 'id': '74e3ed07-748c-49e4-85a0-393a93337dbf',
40 'title': 'A Family for the Holidays',
42 'skip': 'This video is only available for registered users',
44 'url': 'https://www.goplay.be/video/de-mol/de-mol-s11/de-mol-s11-aflevering-1#autoplay',
46 'id': '03eb8f2f-153e-41cb-9805-0d3a29dab656',
48 'title': 'S11 - Aflevering 1',
49 'episode': 'Episode 1',
53 'season': 'Season 11',
56 'skip_download': True,
58 'skip': 'This video is only available for registered users',
63 def _perform_login(self
, username
, password
):
65 aws
= AwsIdp(ie
=self
, pool_id
='eu-west-1_dViSsKM5Y', client_id
='6s1h851s8uplco5h6mqh1jac8m')
66 self
._id
_token
, _
= aws
.authenticate(username
=username
, password
=password
)
68 def _real_initialize(self
):
69 if not self
._id
_token
:
70 raise self
.raise_login_required(method
='password')
72 def _real_extract(self
, url
):
73 url
, display_id
= self
._match
_valid
_url
(url
).group(0, 'display_id')
74 webpage
= self
._download
_webpage
(url
, display_id
)
75 video_data_json
= self
._html
_search
_regex
(r
'<div\s+data-hero="([^"]+)"', webpage
, 'video_data')
76 video_data
= self
._parse
_json
(unescapeHTML(video_data_json
), display_id
).get('data')
78 movie
= video_data
.get('movie')
80 video_id
= movie
['videoUuid']
82 'title': movie
.get('title'),
85 episode
= traverse_obj(video_data
, ('playlists', ..., 'episodes', lambda _
, v
: v
['pageInfo']['url'] == url
), get_all
=False)
86 video_id
= episode
['videoUuid']
88 'title': episode
.get('episodeTitle'),
89 'series': traverse_obj(episode
, ('program', 'title')),
90 'season_number': episode
.get('seasonNumber'),
91 'episode_number': episode
.get('episodeNumber'),
94 api
= self
._download
_json
(
95 f
'https://api.goplay.be/web/v1/videos/long-form/{video_id}',
97 'Authorization': f
'Bearer {self._id_token}',
98 **self
.geo_verification_headers(),
101 if 'manifestUrls' in api
:
102 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(
103 api
['manifestUrls']['hls'], video_id
, ext
='mp4', m3u8_id
='HLS')
106 if 'ssai' not in api
:
107 raise ExtractorError('expecting Google SSAI stream')
109 ssai_content_source_id
= api
['ssai']['contentSourceID']
110 ssai_video_id
= api
['ssai']['videoID']
112 dai
= self
._download
_json
(
113 f
'https://dai.google.com/ondemand/dash/content/{ssai_content_source_id}/vid/{ssai_video_id}/streams',
114 video_id
, data
=b
'{"api-key":"null"}',
115 headers
={'content-type': 'application/json'}
)
117 periods
= self
._extract
_mpd
_periods
(dai
['stream_manifest'], video_id
)
119 # skip pre-roll and mid-roll ads
120 periods
= [p
for p
in periods
if '-ad-' not in p
['id']]
122 formats
, subtitles
= self
._merge
_mpd
_periods
(periods
)
127 'subtitles': subtitles
,
132 # Taken from https://github.com/add-ons/plugin.video.viervijfzes/blob/master/resources/lib/viervijfzes/auth_awsidp.py
133 # Released into Public domain by https://github.com/michaelarnauts
135 class InvalidLoginException(ExtractorError
):
136 """ The login credentials are invalid """
139 class AuthenticationException(ExtractorError
):
140 """ Something went wrong while logging in """
144 """ AWS Identity Provider """
146 def __init__(self
, ie
, pool_id
, client_id
):
148 :param InfoExtrator ie: The extractor that instantiated this class.
149 :param str pool_id: The AWS user pool to connect to (format: <region>_<poolid>).
150 E.g.: eu-west-1_aLkOfYN3T
151 :param str client_id: The client application ID (the ID of the application connecting)
156 self
.pool_id
= pool_id
157 if '_' not in self
.pool_id
:
158 raise ValueError('Invalid pool_id format. Should be <region>_<poolid>.')
160 self
.client_id
= client_id
161 self
.region
= self
.pool_id
.split('_')[0]
162 self
.url
= f
'https://cognito-idp.{self.region}.amazonaws.com/'
164 # Initialize the values
165 # https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L22
167 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1'
168 '29024E088A67CC74020BBEA63B139B22514A08798E3404DD'
169 'EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245'
170 'E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED'
171 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D'
172 'C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F'
173 '83655D23DCA3AD961C62F356208552BB9ED529077096966D'
174 '670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B'
175 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9'
176 'DE2BCBF6955817183995497CEA956AE515D2261898FA0510'
177 '15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64'
178 'ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7'
179 'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B'
180 'F12FFA06D98A0864D87602733EC86A64521F2B18177B200C'
181 'BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31'
182 '43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF')
184 # https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L49
186 self
.info_bits
= bytearray('Caldera Derived Key', 'utf-8')
188 self
.big_n
= self
.__hex
_to
_long
(self
.n_hex
)
189 self
.g
= self
.__hex
_to
_long
(self
.g_hex
)
190 self
.k
= self
.__hex
_to
_long
(self
.__hex
_hash
('00' + self
.n_hex
+ '0' + self
.g_hex
))
191 self
.small_a_value
= self
.__generate
_random
_small
_a
()
192 self
.large_a_value
= self
.__calculate
_a
()
194 def authenticate(self
, username
, password
):
195 """ Authenticate with a username and password. """
196 # Step 1: First initiate an authentication request
197 auth_data_dict
= self
.__get
_authentication
_request
(username
)
198 auth_data
= json
.dumps(auth_data_dict
).encode()
200 'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth',
201 'Accept-Encoding': 'identity',
202 'Content-Type': 'application/x-amz-json-1.1',
204 auth_response_json
= self
.ie
._download
_json
(
205 self
.url
, None, data
=auth_data
, headers
=auth_headers
,
206 note
='Authenticating username', errnote
='Invalid username')
207 challenge_parameters
= auth_response_json
.get('ChallengeParameters')
209 if auth_response_json
.get('ChallengeName') != 'PASSWORD_VERIFIER':
210 raise AuthenticationException(auth_response_json
['message'])
212 # Step 2: Respond to the Challenge with a valid ChallengeResponse
213 challenge_request
= self
.__get
_challenge
_response
_request
(challenge_parameters
, password
)
214 challenge_data
= json
.dumps(challenge_request
).encode()
215 challenge_headers
= {
216 'X-Amz-Target': 'AWSCognitoIdentityProviderService.RespondToAuthChallenge',
217 'Content-Type': 'application/x-amz-json-1.1',
219 auth_response_json
= self
.ie
._download
_json
(
220 self
.url
, None, data
=challenge_data
, headers
=challenge_headers
,
221 note
='Authenticating password', errnote
='Invalid password')
223 if 'message' in auth_response_json
:
224 raise InvalidLoginException(auth_response_json
['message'])
226 auth_response_json
['AuthenticationResult']['IdToken'],
227 auth_response_json
['AuthenticationResult']['RefreshToken'],
230 def __get_authentication_request(self
, username
):
233 :param str username: The username to use
235 :return: A full Authorization request.
240 'USERNAME': username
,
241 'SRP_A': self
.__long
_to
_hex
(self
.large_a_value
),
243 'AuthFlow': 'USER_SRP_AUTH',
244 'ClientId': self
.client_id
,
247 def __get_challenge_response_request(self
, challenge_parameters
, password
):
248 """ Create a Challenge Response Request object.
250 :param dict[str,str|imt] challenge_parameters: The parameters for the challenge.
251 :param str password: The password.
253 :return: A valid and full request data object to use as a response for a challenge.
256 user_id
= challenge_parameters
['USERNAME']
257 user_id_for_srp
= challenge_parameters
['USER_ID_FOR_SRP']
258 srp_b
= challenge_parameters
['SRP_B']
259 salt
= challenge_parameters
['SALT']
260 secret_block
= challenge_parameters
['SECRET_BLOCK']
262 timestamp
= self
.__get
_current
_timestamp
()
264 # Get a HKDF key for the password, SrpB and the Salt
265 hkdf
= self
.__get
_hkdf
_key
_for
_password
(
268 self
.__hex
_to
_long
(srp_b
),
271 secret_block_bytes
= base64
.standard_b64decode(secret_block
)
273 # the message is a combo of the pool_id, provided SRP userId, the Secret and Timestamp
275 bytearray(self
.pool_id
.split('_')[1], 'utf-8') + \
276 bytearray(user_id_for_srp
, 'utf-8') + \
277 bytearray(secret_block_bytes
) + \
278 bytearray(timestamp
, 'utf-8')
279 hmac_obj
= hmac
.new(hkdf
, msg
, digestmod
=hashlib
.sha256
)
280 signature_string
= base64
.standard_b64encode(hmac_obj
.digest()).decode('utf-8')
282 'ChallengeResponses': {
284 'TIMESTAMP': timestamp
,
285 'PASSWORD_CLAIM_SECRET_BLOCK': secret_block
,
286 'PASSWORD_CLAIM_SIGNATURE': signature_string
,
288 'ChallengeName': 'PASSWORD_VERIFIER',
289 'ClientId': self
.client_id
,
292 def __get_hkdf_key_for_password(self
, username
, password
, server_b_value
, salt
):
293 """ Calculates the final hkdf based on computed S value, and computed U value and the key.
295 :param str username: Username.
296 :param str password: Password.
297 :param int server_b_value: Server B value.
298 :param int salt: Generated salt.
300 :return Computed HKDF value.
304 u_value
= self
.__calculate
_u(self
.large_a_value
, server_b_value
)
306 raise ValueError('U cannot be zero.')
307 username_password
= '{}{}:{}'.format(self
.pool_id
.split('_')[1], username
, password
)
308 username_password_hash
= self
.__hash
_sha
256(username_password
.encode())
310 x_value
= self
.__hex
_to
_long
(self
.__hex
_hash
(self
.__pad
_hex
(salt
) + username_password_hash
))
311 g_mod_pow_xn
= pow(self
.g
, x_value
, self
.big_n
)
312 int_value2
= server_b_value
- self
.k
* g_mod_pow_xn
313 s_value
= pow(int_value2
, self
.small_a_value
+ u_value
* x_value
, self
.big_n
)
314 return self
.__compute
_hkdf
(
315 bytearray
.fromhex(self
.__pad
_hex
(s_value
)),
316 bytearray
.fromhex(self
.__pad
_hex
(self
.__long
_to
_hex
(u_value
))),
319 def __compute_hkdf(self
, ikm
, salt
):
320 """ Standard hkdf algorithm
322 :param {Buffer} ikm Input key material.
323 :param {Buffer} salt Salt value.
324 :return {Buffer} Strong key material.
327 prk
= hmac
.new(salt
, ikm
, hashlib
.sha256
).digest()
328 info_bits_update
= self
.info_bits
+ bytearray(chr(1), 'utf-8')
329 hmac_hash
= hmac
.new(prk
, info_bits_update
, hashlib
.sha256
).digest()
330 return hmac_hash
[:16]
332 def __calculate_u(self
, big_a
, big_b
):
333 """ Calculate the client's value U which is the hash of A and B
335 :param int big_a: Large A value.
336 :param int big_b: Server B value.
338 :return Computed U value.
342 u_hex_hash
= self
.__hex
_hash
(self
.__pad
_hex
(big_a
) + self
.__pad
_hex
(big_b
))
343 return self
.__hex
_to
_long
(u_hex_hash
)
345 def __generate_random_small_a(self
):
346 """ Helper function to generate a random big integer
348 :return a random value.
351 random_long_int
= self
.__get
_random
(128)
352 return random_long_int
% self
.big_n
354 def __calculate_a(self
):
355 """ Calculate the client's public value A = g^a%N with the generated random number a
357 :return Computed large A.
361 big_a
= pow(self
.g
, self
.small_a_value
, self
.big_n
)
363 if (big_a
% self
.big_n
) == 0:
364 raise ValueError('Safety check for A failed')
368 def __long_to_hex(long_num
):
369 return f
'{long_num:x}'
372 def __hex_to_long(hex_string
):
373 return int(hex_string
, 16)
376 def __hex_hash(hex_string
):
377 return AwsIdp
.__hash
_sha
256(bytearray
.fromhex(hex_string
))
380 def __hash_sha256(buf
):
381 """AuthenticationHelper.hash"""
382 digest
= hashlib
.sha256(buf
).hexdigest()
383 return (64 - len(digest
)) * '0' + digest
386 def __pad_hex(long_int
):
387 """ Converts a Long integer (or hex string) to hex format padded with zeroes for hashing
389 :param int|str long_int: Number or string to pad.
391 :return Padded hex string.
395 if not isinstance(long_int
, str):
396 hash_str
= AwsIdp
.__long
_to
_hex
(long_int
)
399 if len(hash_str
) % 2 == 1:
400 hash_str
= f
'0{hash_str}'
401 elif hash_str
[0] in '89ABCDEFabcdef':
402 hash_str
= f
'00{hash_str}'
406 def __get_random(nbytes
):
407 random_hex
= binascii
.hexlify(os
.urandom(nbytes
))
408 return AwsIdp
.__hex
_to
_long
(random_hex
)
411 def __get_current_timestamp():
412 """ Creates a timestamp with the correct English format.
414 :return: timestamp in format 'Sun Jan 27 19:00:04 UTC 2019'
418 # We need US only data, so we cannot just do a strftime:
419 # Sun Jan 27 19:00:04 UTC 2019
420 months
= [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
421 days
= ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
423 time_now
= dt
.datetime
.now(dt
.timezone
.utc
)
424 format_string
= f
'{days[time_now.weekday()]} {months[time_now.month]} {time_now.day} %H:%M:%S UTC %Y'
425 return time_now
.strftime(format_string
)
428 return 'AWS IDP Client for:\nRegion: {}\nPoolId: {}\nAppId: {}'.format(
429 self
.region
, self
.pool_id
.split('_')[1], self
.client_id
,