mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-12-22 06:00:00 +00:00
[ie/crunchyroll] Fix auth and remove cookies support (#9749)
Closes #9745 Authored by: bashonly
This commit is contained in:
parent
8056a3026e
commit
ff38a011d5
1 changed files with 72 additions and 62 deletions
|
@ -24,11 +24,15 @@ class CrunchyrollBaseIE(InfoExtractor):
|
||||||
_BASE_URL = 'https://www.crunchyroll.com'
|
_BASE_URL = 'https://www.crunchyroll.com'
|
||||||
_API_BASE = 'https://api.crunchyroll.com'
|
_API_BASE = 'https://api.crunchyroll.com'
|
||||||
_NETRC_MACHINE = 'crunchyroll'
|
_NETRC_MACHINE = 'crunchyroll'
|
||||||
|
_REFRESH_TOKEN = None
|
||||||
_AUTH_HEADERS = None
|
_AUTH_HEADERS = None
|
||||||
|
_AUTH_EXPIRY = None
|
||||||
_API_ENDPOINT = None
|
_API_ENDPOINT = None
|
||||||
_BASIC_AUTH = None
|
_BASIC_AUTH = 'Basic ' + base64.b64encode(':'.join((
|
||||||
|
't-kdgp2h8c3jub8fn0fq',
|
||||||
|
'yfLDfMfrYvKXh4JXS1LEI2cCqu1v5Wan',
|
||||||
|
)).encode()).decode()
|
||||||
_IS_PREMIUM = None
|
_IS_PREMIUM = None
|
||||||
_CLIENT_ID = ('cr_web', 'noaihdevm_6iyg0a8l0q')
|
|
||||||
_LOCALE_LOOKUP = {
|
_LOCALE_LOOKUP = {
|
||||||
'ar': 'ar-SA',
|
'ar': 'ar-SA',
|
||||||
'de': 'de-DE',
|
'de': 'de-DE',
|
||||||
|
@ -43,69 +47,74 @@ class CrunchyrollBaseIE(InfoExtractor):
|
||||||
'hi': 'hi-IN',
|
'hi': 'hi-IN',
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
def _set_auth_info(self, response):
|
||||||
def is_logged_in(self):
|
CrunchyrollBaseIE._IS_PREMIUM = 'cr_premium' in traverse_obj(response, ('access_token', {jwt_decode_hs256}, 'benefits', ...))
|
||||||
return bool(self._get_cookies(self._BASE_URL).get('etp_rt'))
|
CrunchyrollBaseIE._AUTH_HEADERS = {'Authorization': response['token_type'] + ' ' + response['access_token']}
|
||||||
|
CrunchyrollBaseIE._AUTH_EXPIRY = time_seconds(seconds=traverse_obj(response, ('expires_in', {float_or_none}), default=300) - 10)
|
||||||
|
|
||||||
|
def _request_token(self, headers, data, note='Requesting token', errnote='Failed to request token'):
|
||||||
|
try: # TODO: Add impersonation support here
|
||||||
|
return self._download_json(
|
||||||
|
f'{self._BASE_URL}/auth/v1/token', None, note=note, errnote=errnote,
|
||||||
|
headers=headers, data=urlencode_postdata(data))
|
||||||
|
except ExtractorError as error:
|
||||||
|
if not isinstance(error.cause, HTTPError) or error.cause.status != 403:
|
||||||
|
raise
|
||||||
|
raise ExtractorError(
|
||||||
|
'Request blocked by Cloudflare; navigate to Crunchyroll in your browser, '
|
||||||
|
'then pass the fresh cookies (with --cookies-from-browser or --cookies) '
|
||||||
|
'and your browser\'s User-Agent (with --user-agent)', expected=True)
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
if self.is_logged_in:
|
if not CrunchyrollBaseIE._REFRESH_TOKEN:
|
||||||
|
CrunchyrollBaseIE._REFRESH_TOKEN = self.cache.load(self._NETRC_MACHINE, username)
|
||||||
|
if CrunchyrollBaseIE._REFRESH_TOKEN:
|
||||||
return
|
return
|
||||||
|
|
||||||
upsell_response = self._download_json(
|
|
||||||
f'{self._API_BASE}/get_upsell_data.0.json', None, 'Getting session id',
|
|
||||||
query={
|
|
||||||
'sess_id': 1,
|
|
||||||
'device_id': 'whatvalueshouldbeforweb',
|
|
||||||
'device_type': 'com.crunchyroll.static',
|
|
||||||
'access_token': 'giKq5eY27ny3cqz',
|
|
||||||
'referer': f'{self._BASE_URL}/welcome/login'
|
|
||||||
})
|
|
||||||
if upsell_response['code'] != 'ok':
|
|
||||||
raise ExtractorError('Could not get session id')
|
|
||||||
session_id = upsell_response['data']['session_id']
|
|
||||||
|
|
||||||
login_response = self._download_json(
|
|
||||||
f'{self._API_BASE}/login.1.json', None, 'Logging in',
|
|
||||||
data=urlencode_postdata({
|
|
||||||
'account': username,
|
|
||||||
'password': password,
|
|
||||||
'session_id': session_id
|
|
||||||
}))
|
|
||||||
if login_response['code'] != 'ok':
|
|
||||||
raise ExtractorError('Login failed. Server message: %s' % login_response['message'], expected=True)
|
|
||||||
if not self.is_logged_in:
|
|
||||||
raise ExtractorError('Login succeeded but did not set etp_rt cookie')
|
|
||||||
|
|
||||||
def _update_auth(self):
|
|
||||||
if CrunchyrollBaseIE._AUTH_HEADERS and CrunchyrollBaseIE._AUTH_REFRESH > time_seconds():
|
|
||||||
return
|
|
||||||
|
|
||||||
if not CrunchyrollBaseIE._BASIC_AUTH:
|
|
||||||
cx_api_param = self._CLIENT_ID[self.is_logged_in]
|
|
||||||
self.write_debug(f'Using cxApiParam={cx_api_param}')
|
|
||||||
CrunchyrollBaseIE._BASIC_AUTH = 'Basic ' + base64.b64encode(f'{cx_api_param}:'.encode()).decode()
|
|
||||||
|
|
||||||
auth_headers = {'Authorization': CrunchyrollBaseIE._BASIC_AUTH}
|
|
||||||
if self.is_logged_in:
|
|
||||||
grant_type = 'etp_rt_cookie'
|
|
||||||
else:
|
|
||||||
grant_type = 'client_id'
|
|
||||||
auth_headers['ETP-Anonymous-ID'] = uuid.uuid4()
|
|
||||||
try:
|
try:
|
||||||
auth_response = self._download_json(
|
login_response = self._request_token(
|
||||||
f'{self._BASE_URL}/auth/v1/token', None, note=f'Authenticating with grant_type={grant_type}',
|
headers={'Authorization': self._BASIC_AUTH}, data={
|
||||||
headers=auth_headers, data=f'grant_type={grant_type}'.encode())
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'grant_type': 'password',
|
||||||
|
'scope': 'offline_access',
|
||||||
|
}, note='Logging in', errnote='Failed to log in')
|
||||||
except ExtractorError as error:
|
except ExtractorError as error:
|
||||||
if isinstance(error.cause, HTTPError) and error.cause.status == 403:
|
if isinstance(error.cause, HTTPError) and error.cause.status == 401:
|
||||||
raise ExtractorError(
|
raise ExtractorError('Invalid username and/or password', expected=True)
|
||||||
'Request blocked by Cloudflare; navigate to Crunchyroll in your browser, '
|
|
||||||
'then pass the fresh cookies (with --cookies-from-browser or --cookies) '
|
|
||||||
'and your browser\'s User-Agent (with --user-agent)', expected=True)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
CrunchyrollBaseIE._IS_PREMIUM = 'cr_premium' in traverse_obj(auth_response, ('access_token', {jwt_decode_hs256}, 'benefits', ...))
|
CrunchyrollBaseIE._REFRESH_TOKEN = login_response['refresh_token']
|
||||||
CrunchyrollBaseIE._AUTH_HEADERS = {'Authorization': auth_response['token_type'] + ' ' + auth_response['access_token']}
|
self.cache.store(self._NETRC_MACHINE, username, CrunchyrollBaseIE._REFRESH_TOKEN)
|
||||||
CrunchyrollBaseIE._AUTH_REFRESH = time_seconds(seconds=traverse_obj(auth_response, ('expires_in', {float_or_none}), default=300) - 10)
|
self._set_auth_info(login_response)
|
||||||
|
|
||||||
|
def _update_auth(self):
|
||||||
|
if CrunchyrollBaseIE._AUTH_HEADERS and CrunchyrollBaseIE._AUTH_EXPIRY > time_seconds():
|
||||||
|
return
|
||||||
|
|
||||||
|
auth_headers = {'Authorization': self._BASIC_AUTH}
|
||||||
|
if CrunchyrollBaseIE._REFRESH_TOKEN:
|
||||||
|
data = {
|
||||||
|
'refresh_token': CrunchyrollBaseIE._REFRESH_TOKEN,
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'scope': 'offline_access',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data = {'grant_type': 'client_id'}
|
||||||
|
auth_headers['ETP-Anonymous-ID'] = uuid.uuid4()
|
||||||
|
try:
|
||||||
|
auth_response = self._request_token(auth_headers, data)
|
||||||
|
except ExtractorError as error:
|
||||||
|
username, password = self._get_login_info()
|
||||||
|
if not username or not isinstance(error.cause, HTTPError) or error.cause.status != 400:
|
||||||
|
raise
|
||||||
|
self.to_screen('Refresh token has expired. Re-logging in')
|
||||||
|
CrunchyrollBaseIE._REFRESH_TOKEN = None
|
||||||
|
self.cache.store(self._NETRC_MACHINE, username, None)
|
||||||
|
self._perform_login(username, password)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_auth_info(auth_response)
|
||||||
|
|
||||||
def _locale_from_language(self, language):
|
def _locale_from_language(self, language):
|
||||||
config_locale = self._configuration_arg('metadata', ie_key=CrunchyrollBetaIE, casesense=True)
|
config_locale = self._configuration_arg('metadata', ie_key=CrunchyrollBetaIE, casesense=True)
|
||||||
|
@ -168,7 +177,8 @@ def _extract_stream(self, identifier, display_id=None):
|
||||||
self._update_auth()
|
self._update_auth()
|
||||||
stream_response = self._download_json(
|
stream_response = self._download_json(
|
||||||
f'https://cr-play-service.prd.crunchyrollsvc.com/v1/{identifier}/console/switch/play',
|
f'https://cr-play-service.prd.crunchyrollsvc.com/v1/{identifier}/console/switch/play',
|
||||||
display_id, note='Downloading stream info', headers=CrunchyrollBaseIE._AUTH_HEADERS)
|
display_id, note='Downloading stream info', errnote='Failed to download stream info',
|
||||||
|
headers=CrunchyrollBaseIE._AUTH_HEADERS)
|
||||||
|
|
||||||
available_formats = {'': ('', '', stream_response['url'])}
|
available_formats = {'': ('', '', stream_response['url'])}
|
||||||
for hardsub_lang, stream in traverse_obj(stream_response, ('hardSubs', {dict.items}, lambda _, v: v[1]['url'])):
|
for hardsub_lang, stream in traverse_obj(stream_response, ('hardSubs', {dict.items}, lambda _, v: v[1]['url'])):
|
||||||
|
@ -383,9 +393,9 @@ def entries():
|
||||||
|
|
||||||
if not self._IS_PREMIUM and traverse_obj(response, (f'{object_type}_metadata', 'is_premium_only')):
|
if not self._IS_PREMIUM and traverse_obj(response, (f'{object_type}_metadata', 'is_premium_only')):
|
||||||
message = f'This {object_type} is for premium members only'
|
message = f'This {object_type} is for premium members only'
|
||||||
if self.is_logged_in:
|
if CrunchyrollBaseIE._REFRESH_TOKEN:
|
||||||
raise ExtractorError(message, expected=True)
|
raise ExtractorError(message, expected=True)
|
||||||
self.raise_login_required(message)
|
self.raise_login_required(message, method='password')
|
||||||
|
|
||||||
result['formats'], result['subtitles'] = self._extract_stream(internal_id)
|
result['formats'], result['subtitles'] = self._extract_stream(internal_id)
|
||||||
|
|
||||||
|
@ -575,9 +585,9 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
if not self._IS_PREMIUM and response.get('isPremiumOnly'):
|
if not self._IS_PREMIUM and response.get('isPremiumOnly'):
|
||||||
message = f'This {response.get("type") or "media"} is for premium members only'
|
message = f'This {response.get("type") or "media"} is for premium members only'
|
||||||
if self.is_logged_in:
|
if CrunchyrollBaseIE._REFRESH_TOKEN:
|
||||||
raise ExtractorError(message, expected=True)
|
raise ExtractorError(message, expected=True)
|
||||||
self.raise_login_required(message)
|
self.raise_login_required(message, method='password')
|
||||||
|
|
||||||
result = self._transform_music_response(response)
|
result = self._transform_music_response(response)
|
||||||
result['formats'], _ = self._extract_stream(f'music/{internal_id}', internal_id)
|
result['formats'], _ = self._extract_stream(f'music/{internal_id}', internal_id)
|
||||||
|
|
Loading…
Reference in a new issue