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 "".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"]