Version 1.0.2

- Update version
- Add initial login variable in class to track initial login
- added logic to only `super().authenticate` on first authentication
- Store credentials in class for refresh tokens
- Store cookies in class for later possible use
- Only raise cookie error on first login
- Dropped token expiry from 5 minutes to 4
- Update season number to reflect first season if it is the only season for the series
- added authenticate method on each license request
This commit is contained in:
TPD94 2025-10-05 22:30:48 -04:00
parent 192a915834
commit 9349d46546
2 changed files with 30 additions and 24 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.pyc *.pyc
/EXAMPLE

View File

@ -25,7 +25,7 @@ class CR(Service):
""" """
Service code for Crunchyroll Service code for Crunchyroll
Author: TPD94 Author: TPD94
Version: 1.0.1 Version: 1.0.2
Authorization: Cookies for web endpoints, Credentials for TV endpoints, Cookies/Credentials for both. Cookies required. Authorization: Cookies for web endpoints, Credentials for TV endpoints, Cookies/Credentials for both. Cookies required.
Security: FHD@L3 Security: FHD@L3
Use Series ID/URL (for example - https://www.crunchyroll.com/series/GG5H5XQ7D/kaiju-no-8) or Series ID (for example - GG5H5XQ7D). Use Series ID/URL (for example - https://www.crunchyroll.com/series/GG5H5XQ7D/kaiju-no-8) or Series ID (for example - GG5H5XQ7D).
@ -36,7 +36,7 @@ class CR(Service):
help=""" help="""
Service code for Crunchyroll\n Service code for Crunchyroll\n
Author: TPD94\n Author: TPD94\n
Version: 1.0.1\n Version: 1.0.2\n
Authorization: Cookies for web endpoints, Credentials for TV endpoints, Cookies/Credentials for both. Cookies required.\n Authorization: Cookies for web endpoints, Credentials for TV endpoints, Cookies/Credentials for both. Cookies required.\n
Security: FHD@L3\n Security: FHD@L3\n
Use Series ID/URL (for example - https://www.crunchyroll.com/series/GG5H5XQ7D/kaiju-no-8) or Series ID (for example - GG5H5XQ7D). Use Series ID/URL (for example - https://www.crunchyroll.com/series/GG5H5XQ7D/kaiju-no-8) or Series ID (for example - GG5H5XQ7D).
@ -73,12 +73,17 @@ class CR(Service):
self.credential = None self.credential = None
self.initial_login = False
def get_session(self): def get_session(self):
# Create a session using curl_cffi as it can impersonate browsers and avoid bot detection by Crunchyroll # Create a session using curl_cffi as it can impersonate browsers and avoid bot detection by Crunchyroll
return session("chrome124") return session("chrome124")
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None: def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
# Run the super method to load the cookies without writing redundant code
if not self.initial_login:
super().authenticate(cookies, credential)
if cookies: if cookies:
self.cookies = cookies self.cookies = cookies
elif hasattr(self, 'cookies'): elif hasattr(self, 'cookies'):
@ -89,11 +94,10 @@ class CR(Service):
elif hasattr(self, 'credential'): elif hasattr(self, 'credential'):
credential = self.credential credential = self.credential
# Run the super method to load the cookies without writing redundant code self.initial_login = True
super().authenticate(cookies, credential)
# Raise error if no cookies, Crunchyroll has implemented recaptcha, so authorization via credentials is not implemented # Raise error if no cookies, Crunchyroll has implemented recaptcha, so authorization via credentials is not implemented
if not cookies: if not cookies and not self.initial_login:
raise EnvironmentError("Service requires cookies for authentication.") raise EnvironmentError("Service requires cookies for authentication.")
# If authenticate is being called for the first time and cookies are present, retrieve an authorization token # If authenticate is being called for the first time and cookies are present, retrieve an authorization token
@ -115,10 +119,10 @@ class CR(Service):
} }
).json()['access_token'] ).json()['access_token']
# Update the token expiry, Crunchyroll offers 15 minutes between expiry. # Update the token expiry, Crunchyroll offers 5 minutes between expiry.
self.auth_token_expiry_web = datetime.now() + timedelta(minutes=5) self.auth_token_expiry_web = datetime.now() + timedelta(minutes=4)
if not credential: if not credential and not self.initial_login:
console.log("Only cookies detected, can only fetch web manifests") console.log("Only cookies detected, can only fetch web manifests")
if credential and self.auth_token_tv is None: if credential and self.auth_token_tv is None:
@ -141,8 +145,8 @@ class CR(Service):
} }
).json()['access_token'] ).json()['access_token']
# Update the token expiry, Crunchyroll offers 15 minutes between expiry. # Update the token expiry, Crunchyroll offers 5 minutes between expiry.
self.auth_token_expiry_tv = datetime.now() + timedelta(minutes=5) self.auth_token_expiry_tv = datetime.now() + timedelta(minutes=4)
# If there is already an authorization token for web, and it is expired, get a new one # If there is already an authorization token for web, and it is expired, get a new one
@ -165,7 +169,7 @@ class CR(Service):
self.auth_token_web = refresh_response.json()['access_token'] self.auth_token_web = refresh_response.json()['access_token']
# Update the token expiry time # Update the token expiry time
self.auth_token_expiry_web = datetime.now() + timedelta(minutes=5) self.auth_token_expiry_web = datetime.now() + timedelta(minutes=4)
# If there is already an authorization token for TV, and it is expired, get a new one # If there is already an authorization token for TV, and it is expired, get a new one
if self.auth_token_tv is not None and datetime.now() > self.auth_token_expiry_tv: if self.auth_token_tv is not None and datetime.now() > self.auth_token_expiry_tv:
@ -178,8 +182,8 @@ class CR(Service):
}, },
data={ data={
'grant_type': 'password', 'grant_type': 'password',
'username': credential.username, 'username': self.credential.username,
'password': credential.password, 'password': self.credential.password,
'scope': 'offline_access', 'scope': 'offline_access',
'client_id': 'anydazwaxclrocanwho3', 'client_id': 'anydazwaxclrocanwho3',
'client_secret': '88gnIsucV-Q7sYrY29uOW_JGlMqx1mBN', 'client_secret': '88gnIsucV-Q7sYrY29uOW_JGlMqx1mBN',
@ -193,7 +197,7 @@ class CR(Service):
self.auth_token_tv = refresh_response.json()['access_token'] self.auth_token_tv = refresh_response.json()['access_token']
# Update the token expiry time # Update the token expiry time
self.auth_token_expiry_tv = datetime.now() + timedelta(minutes=5) self.auth_token_expiry_tv = datetime.now() + timedelta(minutes=4)
def get_titles(self) -> Titles_T: def get_titles(self) -> Titles_T:
@ -241,7 +245,7 @@ class CR(Service):
id_=episode['id'], id_=episode['id'],
service=self.__class__, service=self.__class__,
title=episode['series_title'], title=episode['series_title'],
season=int(episode['season_display_number']) if episode['season_display_number'] != '' else episode['season_sequence_number'] if episode['season_display_number'] == '' and episode['season_sequence_number'] == 1 else 0, season=int(episode['season_display_number']) if episode['season_display_number'] != '' else episode['season_sequence_number'] if episode['season_display_number'] == '' and episode['season_sequence_number'] == 1 else 1 if episode['season_sequence_number'] == 0 else 0,
number = episode['episode_number'] if isinstance(episode['episode_number'], int) else special_counter, number = episode['episode_number'] if isinstance(episode['episode_number'], int) else special_counter,
name=episode['title'] if episode['title'] else episode['season_title'], name=episode['title'] if episode['title'] else episode['season_title'],
year=episode['episode_air_date'][:4], year=episode['episode_air_date'][:4],
@ -592,6 +596,7 @@ class CR(Service):
return chapters return chapters
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]: def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
self.authenticate()
if track.data['endpoint_type'] == 'tv': if track.data['endpoint_type'] == 'tv':
# Get the episode response # Get the episode response
episode_response = self.session.get( episode_response = self.session.get(