diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index 56b243e..b072572 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -1913,8 +1913,7 @@ class dl: ), licence=partial( service.get_playready_license - if (is_playready_cdm(self.cdm)) - and hasattr(service, "get_playready_license") + if is_playready_cdm(self.cdm) else service.get_widevine_license, title=title, track=track, diff --git a/unshackle/core/service.py b/unshackle/core/service.py index ae32797..bba4fdd 100644 --- a/unshackle/core/service.py +++ b/unshackle/core/service.py @@ -394,6 +394,27 @@ class Service(metaclass=ABCMeta): Decode the data, return as is to reduce unnecessary computations. """ + def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]: + """ + Get a PlayReady License message by sending a License Request (challenge). + + This License message contains the encrypted Content Decryption Keys and will be + read by the CDM and decrypted. + + This is a very important request to get correct. A bad, unexpected, or missing + value in the request can cause your key to be detected and promptly banned, + revoked, disabled, or downgraded. + + :param challenge: The license challenge from the PlayReady CDM. + :param title: The current `Title` from get_titles that is being executed. This is provided in + case it has data needed to be used, e.g. for a HTTP request. + :param track: The current `Track` needing decryption. Provided for same reason as `title`. + :return: The License response as Bytes or a Base64 string. Don't Base64 Encode or + Decode the data, return as is to reduce unnecessary computations. + """ + # Delegates license handling to the Widevine license method by default if a service-specific PlayReady implementation is not provided. + return self.get_widevine_license(challenge=challenge, title=title, track=track) + # Required Abstract functions # The following functions *must* be implemented by the Service. # The functions will be executed in shown order. diff --git a/unshackle/services/EXAMPLE/__init__.py b/unshackle/services/EXAMPLE/__init__.py index c296cd0..b7e5c3b 100644 --- a/unshackle/services/EXAMPLE/__init__.py +++ b/unshackle/services/EXAMPLE/__init__.py @@ -303,21 +303,6 @@ class EXAMPLE(Service): def get_widevine_service_certificate(self, **_: any) -> str: return self.config.get("certificate") - def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[bytes]: - license_url = self.config["endpoints"].get("playready_license") - if not license_url: - raise ValueError("PlayReady license endpoint not configured") - - response = self.session.post( - url=license_url, - data=challenge, - headers={ - "user-agent": self.config["client"][self.device]["license_user_agent"], - }, - ) - response.raise_for_status() - return response.content - def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]: license_url = self.license_data.get("url") or self.config["endpoints"].get("widevine_license") if not license_url: @@ -340,3 +325,18 @@ class EXAMPLE(Service): return response.json().get("license") except ValueError: return response.content + + def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]: + license_url = self.config["endpoints"].get("playready_license") + if not license_url: + raise ValueError("PlayReady license endpoint not configured") + + response = self.session.post( + url=license_url, + data=challenge, + headers={ + "user-agent": self.config["client"][self.device]["license_user_agent"], + }, + ) + response.raise_for_status() + return response.content \ No newline at end of file