Skip to content

Commit

Permalink
Fix crunpyroll/types/manifest.py parse function, remove async, replac…
Browse files Browse the repository at this point in the history
…e httpx with requests
  • Loading branch information
justin025 committed Jan 18, 2025
1 parent 3989102 commit c671c82
Show file tree
Hide file tree
Showing 40 changed files with 147 additions and 195 deletions.
33 changes: 0 additions & 33 deletions .github/workflows/python.yml

This file was deleted.

21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
---

#### Features 🔥
- Fully async ([httpx](https://www.python-httpx.org/))
- Python 3.7+ support
- Clean and modern code
- Updated to latest Crunchyroll API
Expand Down Expand Up @@ -40,18 +39,18 @@ client = crunpyroll.Client(
password="password",
locale="it-IT"
)
async def main():
def main():
# Start client and login
await client.start()
client.start()
# Search for Attack on Titan
query = await client.search("Attack On Titan")
query = client.search("Attack On Titan")
series_id = query.items[0].id
print(series_id)
# Retrieve all seasons of the series
seasons = await client.get_seasons(series_id)
seasons = client.get_seasons(series_id)
print(seasons)

asyncio.run(main())
main()
```

---
Expand All @@ -68,15 +67,15 @@ from pywidevine.device import Device
device = Device.load("l3cdm.wvd")
cdm = Cdm.from_device(device)
# Get streams of the episode/movie
streams = await client.get_streams("GRVDQ1G4R")
streams = client.get_streams("GRVDQ1G4R")
# Get manifest of the format you prefer
manifest = await client.get_manifest(streams.hardsubs[0].url)
manifest = client.get_manifest(streams.hardsubs[0].url)
# print(manifest)
# Get Widevine PSSH from manifest
pssh = PSSH(manifest.content_protection.widevine.pssh)
session_id = cdm.open()
challenge = cdm.get_license_challenge(session_id, pssh)
license = await client.get_license(
license = client.get_license(
streams.media_id,
challenge=challenge,
token=streams.token
Expand All @@ -86,7 +85,7 @@ for key in cdm.get_keys(session_id, "CONTENT"):
print(f"{key.kid.hex}:{key.key.hex()}")
cdm.close(session_id)
# Deleting active streams will prevent Crunchyroll HTTP 420 (too_many_queued_streams) error.
await client.delete_active_stream(
client.delete_active_stream(
streams.media_id,
token=streams.token
)
Expand All @@ -96,4 +95,4 @@ Output:
056ec1ca849e350181753cacc9bd404b:2307a188ecd8de3859b71b30791f171d
```
> [!TIP]
> Decryption keys are universally applicable to both video and audio streams, maintaining consistency across all available formats.
> Decryption keys are universally applicable to both video and audio streams, maintaining consistency across all available formats.
2 changes: 1 addition & 1 deletion crunpyroll/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .client import Client
from .client import Client
29 changes: 16 additions & 13 deletions crunpyroll/client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

from .methods import Methods
from .utils import (
get_api_headers,
DEVICE_ID,
DEVICE_NAME,
DEVICE_TYPE
)

from .session import Session
from .errors import CrunpyrollException
from .enums import APIHost
Expand All @@ -16,12 +16,12 @@
Dict
)

import httpx
import requests
import json

class Client(Object, Methods):
"""Initialize Crunchyroll Client
Parameters:
email (``str``):
Email or username of the account.
Expand Down Expand Up @@ -64,17 +64,20 @@ def __init__(
self.device_name: str = device_name
self.device_type: str = device_type

self.http = httpx.AsyncClient(proxies=proxies, timeout=15)
self.http = requests.Session()
if proxies:
self.http.proxies = proxies
self.http.timeout = 15
self.session = Session(self)

async def start(self):
def start(self):
if self.session.is_authorized:
raise CrunpyrollException("Client is already authorized and started.")
return await self.session.authorize()
return self.session.authorize()

@staticmethod
def parse_response(
response: httpx.Response,
response: requests.Response,
*,
method: str = "GET",
) -> Optional[Union[Dict, str]]:
Expand All @@ -91,7 +94,7 @@ def parse_response(
return content
raise CrunpyrollException(message)

async def api_request(
def api_request(
self,
method: str,
endpoint: str,
Expand All @@ -107,26 +110,26 @@ async def api_request(
api_headers = get_api_headers(headers)
if self.session.is_authorized and include_session:
api_headers.update(self.session.authorization_header)
response = await self.http.request(
response = self.http.request(
method=method,
url=url,
params=params,
headers=api_headers,
data=payload
)
return Client.parse_response(response, method=method)
async def manifest_request(

def manifest_request(
self,
url: str,
headers: Dict = None,
) -> str:
api_headers = get_api_headers(headers)
if self.session.is_authorized:
api_headers.update(self.session.authorization_header)
response = await self.http.request(
response = self.http.request(
method="GET",
url=url,
headers=api_headers,
)
return Client.parse_response(response)
return Client.parse_response(response)
2 changes: 1 addition & 1 deletion crunpyroll/enums/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .content_type import ContentType
from .image_type import ImageType
from .api_host import APIHost
from .api_host import APIHost
4 changes: 2 additions & 2 deletions crunpyroll/enums/api_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class APIHost(Enum):
"""API hosts enumeration."""

WEB = "www.crunchyroll.com"
"Universal WWW host. Mostly used on web applications."

Expand All @@ -13,4 +13,4 @@ class APIHost(Enum):
"SVC host used for video players."

LICENSE = "cr-license-proxy.prd.crunchyrollsvc.com"
"SVC host used for license."
"SVC host used for license."
2 changes: 1 addition & 1 deletion crunpyroll/enums/image_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ class ImageType(Enum):
"Promotional image."

THUMBNAIL = "thumbnail"
"Thumbnail used for episodes."
"Thumbnail used for episodes."
2 changes: 1 addition & 1 deletion crunpyroll/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ class CrunpyrollException(Exception):
pass

class ClientNotAuthorized(CrunpyrollException):
pass
pass
11 changes: 5 additions & 6 deletions crunpyroll/methods/delete_active_stream.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
from crunpyroll import enums

import crunpyroll

class DeleteActiveStream:
async def delete_active_stream(
def delete_active_stream(
self: "crunpyroll.Client",
media_id: str,
*,
token: str,
) -> bool:
"""
Delete an active stream.
Parameters:
media_id (``str``):
Unique identifier of the media.
Expand All @@ -21,10 +20,10 @@ async def delete_active_stream(
``bool``:
On success, True is returned.
"""
await self.session.retrieve()
await self.api_request(
self.session.retrieve()
self.api_request(
method="DELETE",
endpoint="v1/token/" + media_id + "/" + token,
host=enums.APIHost.PLAY_SERVICE
)
return True
return True
11 changes: 5 additions & 6 deletions crunpyroll/methods/get_episodes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from crunpyroll import types

import crunpyroll

class GetEpisodes:
async def get_episodes(
def get_episodes(
self: "crunpyroll.Client",
season_id: str,
*,
Expand All @@ -18,17 +17,17 @@ async def get_episodes(
locale (``str``, *optional*):
Localize request for different results.
Default to the one used in Client.
Returns:
:obj:`~crunpyroll.types.EpisodesQuery`:
On success, query of episodes is returned.
"""
await self.session.retrieve()
response = await self.api_request(
self.session.retrieve()
response = self.api_request(
method="GET",
endpoint="content/v2/cms/seasons/" + season_id + "/episodes",
params={
"locale": locale or self.locale
}
)
return types.EpisodesQuery.parse(response)
return types.EpisodesQuery.parse(response)
9 changes: 4 additions & 5 deletions crunpyroll/methods/get_index.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from crunpyroll import types

import crunpyroll

class GetIndex:
async def get_index(
def get_index(
self: "crunpyroll.Client",
) -> "types.SessionIndex":
"""
Expand All @@ -13,9 +12,9 @@ async def get_index(
:obj:`~crunpyroll.types.SessionIndex`:
On success, informations about session index are returned.
"""
await self.session.retrieve()
response = await self.api_request(
self.session.retrieve()
response = self.api_request(
method="GET",
endpoint="index/v2",
)
return types.SessionIndex(response)
return types.SessionIndex(response)
11 changes: 5 additions & 6 deletions crunpyroll/methods/get_license.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from crunpyroll import types
from crunpyroll import enums

import crunpyroll

class GetLicense:
async def get_license(
def get_license(
self: "crunpyroll.Client",
media_id: str,
*,
Expand All @@ -15,7 +14,7 @@ async def get_license(
Get DRM license. Useful to obtain decryption keys.
.. todo::
Add support for PlayReady DRM
Parameters:
Expand All @@ -30,8 +29,8 @@ async def get_license(
``str``:
On success, license is returned.
"""
await self.session.retrieve()
response = await self.api_request(
self.session.retrieve()
response = self.api_request(
method="POST",
endpoint="v1/license/widevine",
params={"specConform": True},
Expand All @@ -43,4 +42,4 @@ async def get_license(
host=enums.APIHost.LICENSE,
payload=challenge,
)
return response
return response
10 changes: 4 additions & 6 deletions crunpyroll/methods/get_manifest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from crunpyroll import types

import crunpyroll

class GetManifest:
async def get_manifest(
def get_manifest(
self: "crunpyroll.Client",
url: str
) -> "types.Manifest":
Expand All @@ -17,8 +16,7 @@ async def get_manifest(
Returns:
:obj:`~crunpyroll.types.Manifest`:
On success, parsed manifest is returned.
"""
await self.session.retrieve()
response = await self.manifest_request(url)
return types.Manifest.parse(response)
self.session.retrieve()
response = self.manifest_request(url)
return types.Manifest.parse(response)
Loading

0 comments on commit c671c82

Please sign in to comment.