105 lines
3.3 KiB
Python
105 lines
3.3 KiB
Python
|
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"]
|