105 lines
3.3 KiB
Python
Raw Permalink Normal View History

2025-03-18 00:17:27 +05:30
import base64
import hashlib
import logging
import random
import pyhulu
class Device: # pylint: disable=too-few-public-methods
"""Data class used for containing device attributes."""
def __init__(self, device_code, device_key):
self.device_code = str(device_code)
if isinstance(device_key, str):
self.device_key = bytes.fromhex(device_key)
else:
self.device_key = device_key
if len(self.device_code) != 3:
raise ValueError("Invalid device code length")
if len(self.device_key) != 16:
raise ValueError("Invalid device key length")
def __repr__(self):
return "<Device device_code={}, device_key={}>".format(
self.device_code,
base64.b64encode(self.device_key).decode("utf-8")
)
class HuluClient(pyhulu.HuluClient):
def __init__(self, device, session, version=1, **kwargs):
self.logger = logging.getLogger(__name__)
self.device = device
self.session = session
self.version = version or 1
self.extra_playlist_params = kwargs
self.session_key, self.server_key = self.get_session_key()
def load_playlist(self, video_id):
"""
load_playlist()
Method to get a playlist containing the MPD
and license URL for the provided video ID and return it
@param video_id: String of the video ID to get a playlist for
@return: Dict of decrypted playlist response
"""
params = {
"device_identifier": hashlib.md5().hexdigest().upper(),
"deejay_device_id": int(self.device.device_code),
"version": self.version,
"content_eab_id": video_id,
"rv": random.randrange(100000, 1000000),
"kv": self.server_key
}
params.update(self.extra_playlist_params)
r = self.session.post("https://play.hulu.com/v6/playlist", json=params)
ciphertext = self.__get_ciphertext(r.text, params)
return self.decrypt_response(self.session_key, ciphertext)
def get_session_key(self):
"""
get_session_key()
Method to do a Hulu config request and calculate
the session key against device key and current server key
@return: Session key in bytes, and the config key ID.
"""
random_value = random.randrange(100000, 1000000)
nonce = hashlib.md5(",".join([
self.device.device_key.hex(),
self.device.device_code,
str(self.version),
str(random_value)
]).encode("utf-8")).hexdigest()
payload = {
"rv": random_value,
"mozart_version": "1",
"region": "US",
"version": self.version,
"device": self.device.device_code,
"encrypted_nonce": nonce
}
r = self.session.post("https://play.hulu.com/config", data=payload)
ciphertext = self.__get_ciphertext(r.text, payload)
config = self.decrypt_response(self.device.device_key, ciphertext)
derived_key_array = bytearray()
for device_byte, server_byte in zip(self.device.device_key, bytes.fromhex(config["key"])):
derived_key_array.append(device_byte ^ server_byte)
return bytes(derived_key_array), config["key_id"]